当前位置: 首页 > news >正文

NextJs - 服务端/客户端组件之架构多样性设计

NextJs - 服务端/客户端组件之架构多样性设计

  • 前言
  • 一. 架构设计
    • 1.1 SSR+流式渲染常见错误设计之 - 根页面同步阻塞
    • 1.2 架构设计之 - 客户端组件依赖于服务端组件数据
      • ① 使用 Redux 完成数据共享
    • 1.3 架构设计之 - 单页内的分步骤跳转
      • ① 如何做到服务端组件和客户端组件之间的切换
      • ② 进行UI切换的时候如何做到状态保持

前言

本篇文章主要讲解不同场景下,我们怎样去设计客户端和服务端组件的交互,或者是怎么去写代码。本篇文章建立于:使用SSR渲染+Suspense流式渲染,并且服务端/客户端组件混合使用的基础上讲解的。

一. 架构设计

我们知道,NextJsAPP路由模式下,在对应目录下创建一个page.tsx文件,他就会生成对应的路由,我们可以称page.tsx为根页面。

在此基础上,我们说下基本准则:

  1. 根页面(page.tsx)一般作为服务端组件,我们常用于获取一些上下文变量。
  2. 切记不可让根页面作为同步请求获取数据的地方,否则整个页面就会同步阻塞,等待请求返回才能开始渲染。

我们接下来先做个简单的讲解。

1.1 SSR+流式渲染常见错误设计之 - 根页面同步阻塞

在刚开始接触Nextjs这类具备SSR渲染的框架的时候,可能容易写出这样的代码:

  1. 我们在page.tsx根页面中同步阻塞获取接口数据,然后将数据通过Props的形式传递给子组件
  2. 子组件可能是服务端组件、客户端组件。如图:
    在这里插入图片描述

这种写法,从逻辑上它并没有任何问题,但是在Suspense流式渲染的场景下,就没有任何意义。因为阻塞的动作发生在服务端,也就是说:

  1. 必须阻塞所有的异步接口返回,我们的服务器才会开始渲染组件。
  2. 哪怕我们的子组件使用Suspense包装,也没有任何作用。
  3. 我们的页面打开来就会白屏阻塞,阻塞时间取决于这个异步接口的等待返回时间。

正确设计如下:

  1. 我们让异步请求的逻辑,封装在一个粒度尽可能小的服务端组件中,然后使用Suspense包装这个服务端组件。
  2. 这样我们的页面,就不会因为这个请求发生阻塞。就会从上到下,依次渲染相关的组件,而使用Suspense包装的,就会返回对应的fallback效果。
    在这里插入图片描述

倘若在此基础上,我们的客户端组件,需要用到服务端组件中获取的数据,怎么交互?

1.2 架构设计之 - 客户端组件依赖于服务端组件数据

在上述架构图中,我们可以发现,我们的服务端组件是和客户端组件同一层级的。那么同一层级的就无法采用Props的方式传递数据。

那么就可能有读者想:那如果我的客户端组件封装到服务端组件中不就好啦?如图:
在这里插入图片描述
如果这么做:我们的客户端组件就会随着服务端组件同时具备Suspense效果,也就是客户端组件必须等待异步请求返回后才能完成渲染。 但是这样的设计是不合理的,因为我们的客户端组件的渲染不应该等待数据返回再完成渲染。

大家别忘了,我们的客户端组件是可以具备State动态效果的,也就是可以使用useState这样的勾子函数。因此我们可以做到立刻渲染客户端组件,让相关的数据通过State来传递,完成动态渲染。

那么我们如何做到服务端和客户端组件的数据共享呢?

① 使用 Redux 完成数据共享

我们服务端组件,拿到接口数据后,可以将它丢给一个专门的用于存储State的客户端组件,这里我们称之为Context Compoent。它的作用就是:

  • 接收服务端传递的接口数据。
  • 将接口数据保存在Redux中。

在这里插入图片描述

这么做的好处:

  1. 服务端组件的内部渲染,可以直接依赖于接口数据编译为HTML,但是切记服务端组件往往只用来做展示,不具备任何的交互(onChange事件),同时服务端组件一般又通过Suspense封装,可以完成loading效果。
  2. 客户端组件几乎不受服务端组件影响,可以立刻完成渲染,将最基本的UI呈现给用户,而页面相关的数据来自于Redux。当ContextComponent将服务端数据存储到Redux中后,客户端组件自动完成动态渲染。

备注:这样的架构设计一般能满足大多数的开发需求,当然可能有更好的设计,这里只不过提供一种思路。

1.3 架构设计之 - 单页内的分步骤跳转

那么在这个架构设计基础上,倘若我的页面有这样的功能:

  1. 页面加载完毕之后,呈现第一页。
  2. 第一页可以点击:“下一步”,跳转到第二页(同一个URL
  3. 第二页还能够返回到:第一页。同时保持第一页的状态(例如Checkbox的勾选、Input框的内容)

这个功能也就是单页内的分步骤跳转,说白了就是使用同一个URL,但是具有多页效果。下一页的时候,上一页的状态还要保持。只不过UI呈现的是第二页。

但是想要实现单页内的分步骤跳转,有好几个问题需要解决:

  1. 我的首屏UI(第一页)是通过SSR渲染的,怎么做到下一步的时候,把第一页UI切换到第二页的UI?(别忘了,服务端组件是不具备State效果的)
  2. 如何控制Redux的初始化动作只做一次?

① 如何做到服务端组件和客户端组件之间的切换

1.我们在根页面下引入一个RoutePage页面(客户端组件),然后将服务端组件通过Props传递下去:

import ServerComponent from "./ServerComponent";
import RoutePage from "./RoutePage";const Parent = () => {return <><RoutePage slot={<ServerComponent/>}/></>
}export default Parent

RoutePage组件专门用来做UI切换的,也就是控制渲染第一页还是第二页,然后使用Redux来获取全局的状态,我们用一个变量来代表当前是第几页(因为本案例只有两页,就用isServer来表达了)

'use client';
import ClientComponent from "./ClientComponent";
import { ReactNode } from "react";const RoutePage = ({ slot }: { slot: ReactNode }) => {// 假代码const context = useRedux(testState);return <>{/* 如果当前是第一页,就渲染服务端组件,否则渲染客户端组件 */}{context.isServer ? { slot } : <ClientComponent />}</>
}export default RoutePage;

那么isServer的初始值我们设定为true,就做到首屏渲染服务端组件了。我们只要在客户端组件和服务端组件中维护这个State即可完成UI的切换。
设计结构如下:
在这里插入图片描述

备注:

  1. 服务端组件中需要引入额外的一个客户端组件,专门用来控制State。不能在服务端组件中控制State哦。

② 进行UI切换的时候如何做到状态保持

试想一下,第一页首屏加载的时候,数据必定来自于服务端服务端组件里面会引用一个ContextComponent组件,每次渲染的时候都会初始化一遍数据。 假设这里是数据A

倘若第一页有个按钮:加载更多数据。它会发送请求,拉取更多的数据然后呈现在页面上,假设这里获取的数据是:数据B

那么此时第一页呈现的数据是 数据A数据B 的一个并集数据C。那么问题来了:当我们点击下一步,呈现第二页,再次返回第一页的时候,会做什么操作?

  1. 第一页重新触发渲染(但是这里不会触发服务器的SSR渲染),此时服务端组件通过Props传递的初始数据:数据A 还在,会重新赋值给Redux。即导致 数据A 会覆盖 数据C
  2. 那么回到第一页后,之前的数据就被覆盖了,状态也就被刷掉了。

因此我们需要控制,Redux的初始化赋值动作只执行一次。

这个就比较好解决了,我们只需要在Redux中增加一个变量:hasLoadedSSR 一类的标识,代表我们已经SSR渲染过一次了,在Redux赋值的时候加个判断即可,以下是ContextComponent伪代码:

'use client';const ContextComponent = (props)=>{const context = useRedux(testState)const dispatch = useDispatch();const {data} = props;// Redux初始化,如果没有经历过SSR,就完成初始化赋值if(!context.hasLoadedSSR){dispatch({context : {...data,// 再将标识赋值为truehasLoadedSSR: true}})}
}

这样就能防止每次UI切换的时候,初始化状态覆盖当前状态的问题了。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • function call使用基础
  • 手把手教你手写单例,六种实现方式一网打尽!
  • 【MySQL进阶之路】oracle 9i的经典测试雇员信息表案例——多表查询
  • WPF Mvvm
  • MySQL集群+Keepalived实现高可用部署
  • Hooks 「 useImperativeHandle 」子组件向父组件暴露方法
  • Dockerfile常用指令详解
  • 在NVIDIA jetson中使用jetson-ffmpeg调用硬件编解码加速处理
  • TCP的连接建立及报文段首部格式
  • ESP32-IDF 在 Ubuntu 下的配置
  • 【xilinx】Vivado 成功运行Ubuntu需要哪些 文件?
  • 微软RDL远程代码执行超高危漏洞(CVE-2024-38077)漏洞检测排查方式
  • JavaSE基础(12)——文件、递归、IO流
  • 未知单播泛洪原因
  • 日志审计Graylog 使用教程-kafka收取消息
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • JavaScript 一些 DOM 的知识点
  • LeetCode18.四数之和 JavaScript
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • PHP变量
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 从setTimeout-setInterval看JS线程
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 海量大数据大屏分析展示一步到位:DataWorks数据服务+MaxCompute Lightning对接DataV最佳实践...
  • 延迟脚本的方式
  • 正则表达式小结
  • 【云吞铺子】性能抖动剖析(二)
  • Salesforce和SAP Netweaver里数据库表的元数据设计
  • 你学不懂C语言,是因为不懂编写C程序的7个步骤 ...
  • 数据可视化之下发图实践
  • ​​​​​​​STM32通过SPI硬件读写W25Q64
  • (20050108)又读《平凡的世界》
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (4.10~4.16)
  • (C语言)fread与fwrite详解
  • (delphi11最新学习资料) Object Pascal 学习笔记---第14章泛型第2节(泛型类的类构造函数)
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (Matlab)使用竞争神经网络实现数据聚类
  • (MonoGame从入门到放弃-1) MonoGame环境搭建
  • (第三期)书生大模型实战营——InternVL(冷笑话大师)部署微调实践
  • (附源码)小程序儿童艺术培训机构教育管理小程序 毕业设计 201740
  • (免费领源码)Java#ssm#MySQL 创意商城03663-计算机毕业设计项目选题推荐
  • (全注解开发)学习Spring-MVC的第三天
  • (十六)Flask之蓝图
  • (四)软件性能测试
  • (转)平衡树
  • **PHP分步表单提交思路(分页表单提交)
  • .Net CoreRabbitMQ消息存储可靠机制
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET/C# 检测电脑上安装的 .NET Framework 的版本
  • .Net环境下的缓存技术介绍
  • .NET开源、简单、实用的数据库文档生成工具
  • .NET上SQLite的连接
  • .NET与java的MVC模式(2):struts2核心工作流程与原理
  • @Autowired和@Resource的区别