React 六种服务端渲染与现代全栈框架思考
在 React 发展过程中,服务端渲染技术也发生一些变化。现代前端框架对 React 服务端显然都有良好的支持,React 自己也演化出了 server-component 来对服务端渲染组件进行原生支持。
一、服务端渲染本质 SSR
同构技术。
为了服务端代码有良好的 seo 等特性,将写的 React 代码在服务端渲染,让搜索引擎拿到更加友好的 HTML 文档。
然后再将 HTML 交给给浏览器进行 水合
,然后完整个同构过程。
为什么要这么做?前端 React、Vue 有来良好的交互能力,服务端渲染能够提供良好的 SEO 能力。
服务端渲染的几种方式
流式渲染性能好于字符串渲染。
二、服务端渲染需要水合 API 配合
- hydrateRoot
- hydrate
后端渲染出 html 开始通过工作,给到浏览器其实只有 html 文档,没有渲染脚本。浏览器加载 html 文档后开始于 React 水合 API 进行水合,同构完成,同构完成之后 React 接管前端任务。
// react 18 之前
import { hydrate } from 'react-dom';
hydrate(<App />, rootEl, cb?) // 从 React 18 开始,hydrate 被 hydrateRoot 替代。
// react 18 之后
import { hydrateRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, <App />, options?)
三、suppressHydrationWarning
如果你使用过服务端渲染的框架,在水合的时候经常可能会遇到 hydrate 报不匹配的错误,React 提供了 suppressHydrationWarning 属性来消除报错的警告,但是 suppressHydrationWarning 使用是有条件的。一般的使用场景:
- 如果一个单独元素属性或文本内容在服务器和客户端之间是不可避免地不同的(例如,时间戳),则可以抑制 hydrate 处理不匹配警告。
四、React 的六种服务端渲染
- renderToNodeStream/renderToStaticNodeStream
- renderToPipeableStream/renderToReadableStream
- renderToStaticMarkup/renderToString
这六个 API 很好区分,其实就是正对不同的场景和环境进行:
- Node.js 环境。
- 是否支持 Suspense。
- 是否纯 HTML 输出,如果是纯 HTML 输出不能水合带有 static 标记。
五、renderToNodeStream
5.1)用法
import { renderToNodeStream } from 'react-dom/server';
import express from 'express';
app.use('/', (request, response) => {
const stream = renderToNodeStream(<App />);
stream.pipe(response);
});
renderToNodeStream 将 React 组件读取为一个流,流的 pipe 方法传入到 response 中。
- 将 tsx/jsx 编译,使用 esbuild 等直接支持的此文件类型的运行,如果引入的其他类型的文件,可能就需要打包工具了。
5.2)缺点
看这个 API 的命名 NodeStream, 渲染成一个 Node 的流,一个将要废弃的 api。那么它为什么会被废弃呢?
- 与 Suspense API 配合并不好
- 缓冲所有的输出类型,没有起到流式输出的好处
六、renderToPipeableStream
由于分析的 renderToNodeStream 的缺点,React 提供了 renderToNodeStream API,需要注意的是renderToPipeableStream 是为 Node.js 环境准备的。
renderToPipeableStream 的功能比较丰富:
6.1)基本用法
renderToPipeableStream 返回值是一个对象,pipe 管道函数和 abort 取消函数。
import { renderToPipeableStream } from 'react-dom/server';
const { pipe, abort } = renderToPipeableStream(<App />, options?)
七、renderToReadableStream
renderToReadableStream` 将React树渲染为可读的Web流, renderToReadableStream 返回的是一个 promise。
const stream = await renderToReadableStream(reactNode, options?)
配合 Response 返回流,将整个流渲染封装到一个函数中:
async function handler(request) {
const stream = await renderToReadableStream(<App />});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
八、renderToStaticMarkup
不能水合的 api, 作用是生成一些完全静态的 HTML。
九、renderToString
这个 API 是早起 React 服务端渲染主要的 API。最大的特点就是不支持流式。将整个组件渲染为 HTML,与静态输出不同的是 renderToString 是能够水合的。
9.1)基本用法
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
从代码层面我们看到 renderToString 是一个同步代码。渲染之后就输出 html 字符串。
9.2)renderToString
缺点
- 不支持 Suspense, 如果你使用了 Suspense 组件在服务端渲染,Suspense 部分一直被挂起。
renderToString
在浏览器环境中使用,不推荐在客户端中使用。
十、Next.js 中的服务端渲染
渲染模式 | 特点 | 优点 | 缺点 |
---|---|---|---|
SSR (Server-Side Rendering) | 每个页面请求都在服务器上生成一个新的 HTML 页面 | - 更好的 SEO- 初次加载时用户可以快速看到内容 | - 服务器负载高- 可能增加延迟 |
SSG (Static Site Generation) | 在构建时生成静态 HTML 文件,请求时直接提供这些文件 | - 性能极佳- 服务器负载低- 安全性高 | - 对于频繁更新的数据不太适合- 生成静态页面需要重新构建整个站点 |
CSR (Client-Side Rendering) | 初始页面仅包含基本的 HTML 结构和 JavaScript,内容通过 JavaScript 在客户端加载 | - 用户交互体验好- 初次加载后页面切换速度快 | - SEO 较差- 初次加载时间长 |
ISR (Incremental Static Regeneration) | 结合 SSG 和 SSR 的优点,允许在构建时生成静态页面,并在后台重新生成部分页面 | - 具有 SSG 的高性能和低服务器负载- 支持对部分页面的增量更新- 用户访问时可以看到最新的数据 | - 实现相对复杂- 需要对构建和缓存策略有深入理解 |
当然含有边缘函数和Node.js 运行时,这里就深究了。
10.1)Next.js 中服务端渲染开发体验
Next.js 在服务端渲染方面有两种模式:
- Page 模式
- App 模式
这两种模式都是在文件路由方面的创新,App 文件路由创新较大,对文件路由感兴的小伙伴可以尝试,Next.js 强大的文件路由模式。当然它们获取数据方式也会有所不同。
10.2)服务端组件不再需要初始化组件数据
- 默认组件在服务端渲染,意味着我们可以在服务端获取到数据之后,数据随页面一起在前端 hydrate。不在需要对
async function getData() {
const res = await prisma.blog.findMany()
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
这也意味着 Page 组中,可以是 async 组件,全栈开发者看课是不是特别方便。
10.3)表单 action
Next.js 中表单 action 可以标记为一个服务端函数 use server
这个函数仅仅在服务端运行。如果你有 GraphQL 的知道,数据操作被抽象为两个部分
- query
- mutation
这里的表单的 action 本质就是要一些 mutation 操作,下面是一个个示例:
import { actionSomething } from '@/actions/something';
export default function Page() {
async function createSomeThing(formData: FormData) {
actionSomething(formData)
}
return <form action={createInvoice}>...</form>
}
'use server';
export const actionSomething = (formData: FormData) => {
const data = FormData.entries(formData)
// your server handle
}
10.4)React 中增强对服务端和表单的增强
- useActionState: 根据表单操作的结果更新状态
传递一个 action 函数,初始值 state, 创建出 form 的 action, action 返回 下一个 值。(这个和 reducer 的思想差不多,函数式编程思想的精髓)
function action(currentState, formData) {
return 'next state';
}
import { useActionState } from 'react';
import { action } from './actions.js';
function MyComponent() {
const [state, formAction] = useActionState(action, null);
// ...
return (
<form action={formAction}>
{/* ... */}
</form>
);
- userFormStatus: 提供上次表单提交的状态信息
const { pending, data, method, action } = useFormStatus();
- pending 表单的提交过程,处理表单的禁用禁用特别好用,在不用单独的定义一个 state 来处理它了。
- data 继承了表单的接口,能使用表单的方法 get 等获取表单中的数据
十一、Remix 中的服务端渲染
11.1)Remix 中各种端渲染模式
Remix 中基于 vite 支持 ssr 和 non-ssr 模式,当然也能预渲染 html 页面。
11.2)Remix 中数据
Remix 中有自己数据渲染流,在全站中已经形成了一个闭环,开发者不必在操心数据的流向问题。
11.3)SSR 模式
-
- Remix loader 函数在服务端,准备好数据。
-
- Remix 在页面中
useLoaderData/
记载 loader 的数据,loader 函数处理 HTTP GET 方法。
- Remix 在页面中
-
- Remix 中表单 action 修改之后,直接对应到当前路由 action 函数。
-
- action 函数中接收到本路由的表单处理,然后
useActionData
监听表单的变化。
- action 函数中接收到本路由的表单处理,然后
-
- action 处理非 GET 方法, 如果你习惯 RestFul API,可能会不太习惯这种方案,需要自己 action 中判断不同的 HTTP 请求的类型, 这里我们提供两种方案:一种之前的是 HTTP 方法方案,另外一种是 form Action name 方法,站在 action name 的角度给不同的请求一个命名 action。
- 6.当 action 响应时候可以获取状态变化和数据变化。loader重新获取新的数据。
-
- 形成一个闭环。
11.4)SPA 模式
首先,需要配置一下:
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
remix({
ssr: false,
}),
],
});
与 SSR 不同的是,非 SSR 模式不再使用 loader 而是 clientLoader, 在 clientLoader 中获取api接口中的数据。然后使用 useLoaderData 钩子获取。
十二、vite ssr
vite ssr 基于 vite 可以轻松的创建出我们想要的 ssr 服务,vite 开始支持各种框架。
十三、SSR 构建优化:外部化依赖
为什么会有外部化依赖?原因很简单包的依赖问题,使用 Vite 构建经常后遇到输出的包是 esm/cjs 问题。使用外部依赖能很好的解决这个问题,但是也带来了新的问题,构建之后的代码对 node_modules 有依赖。
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
// other
ssr: {
noExternal: ["@ant-design/icons", "@ant-design/pro-chat"],
},
});
由于存在依赖问题, Next.js 中有一点就做的非常好提供了 out.standalone
配置,复制生产所需要 node_modules 文件夹,整个项目可以单独部署 standalone 文件。
十四、小结
本文主要介绍了 React 中多种 SSR 服务端渲染,目前React中有六种服务渲染模式。服务端渲染在现代框架中得到普遍支持,在表单方面的创新和数据方面的创新,体现现代前端框架思考与实践。最后希望能够,希望可以帮助到读者。
转载自:https://juejin.cn/post/7371273982200184859