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

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

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

typescript 复制代码
import ServerComponent from "./ServerComponent";
import RoutePage from "./RoutePage";

const Parent = () => {
    return <>
        <RoutePage slot={<ServerComponent/>}/>
    </>
}

export default Parent

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

typescript 复制代码
'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伪代码:

typescript 复制代码
'use client';

const ContextComponent = (props)=>{
    const context = useRedux(testState)
    const dispatch = useDispatch();

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

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

相关推荐
堂铭6 天前
NextJS多语言
前端·i18n·nextjs
Mebius19162 个月前
开源全站第一个Nextron(NextJS+electron)项目--NextTalk:一款集成chatgpt的实时聊天工具
前端·react.js·typescript·开源·github·tailwindcss·nextjs
tangfuling19913 个月前
用 nextjs 创建 Node+React Demo
前端框架·react·nextjs
A洛3 个月前
Cloudflare Pages 部署 Next.js 应用教程
开发语言·github·nextjs·cloudflare·cloudflarepages
hawk2014bj4 个月前
NextJS 使用 Docker 发布
docker·nextjs
hawk2014bj4 个月前
NextJS 服务器端代码调试
react·nextjs·ssr
loong_XL5 个月前
nextjs 实现TodoList网页应用案例
开发语言·前端·javascript·nextjs
Zong_09156 个月前
NextJs 系列文章
nextjs
Ygria_6 个月前
详解Next Auth:自定义邮箱密码登录注册、Github、Notion授权 & Convex集成
github·nextjs·notion