浅谈 ssr,ssg,csr,rsc
前言
在文章正式开始之前我想澄清一个概念,我们所写的程序有开发,构建,生产等阶段,所谓的 ssr,ssg, csr,rsc,只是在这些特定的阶段做了特定的事情,进而形成的一种开发模式,澄清了这个概念,让我们正式进入正文
csr
csr
(client side render),客户端渲染,它是前后端分离的经典产物,通过 spa 开发,实现在客户端的局部数据请求,让用户告别了点击刷新整页网页的时代,不仅优化了用户体验,同时对服务器的压力也更小,但伴随的是 seo 效果差,和首屏渲染速度过慢的问题
ssr
ssr
(server side render),服务端渲染,它的出现解决了 seo 优化和首屏渲染过慢的问题,这里我通过 vite
和 react
实现 ssr
的过程简单讲讲为什么它可以解决上述问题。
- index.html
- server.js # main application server
- src/
- main.js # 导出环境无关的(通用的)应用代码
- entry-client.js # 将应用挂载到一个 DOM 元素上
- entry-server.js # 使用某框架的 SSR API 渲染该应用
这是
vite
官方实现的ssr
的文件目录,我们ssr
的主要逻辑其实都是在server.js
中。
// entry-client.ts
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";
import { fetchData } from "./entry-server";
import axios from "axios";
const rootDOM = document.getElementById("root");
if (rootDOM) {
hydrateRoot(
rootDOM,
<React.StrictMode>
<App data={data} />
</React.StrictMode>
);
} else {
throw new Error("hydrate error");
}
// entry-server.ts
import App from "./App";
import "./index.css";
export async function fetchData() {
return {
user: "xxx",
};
}
export function ServerEntry(props: any) {
return <App data={props.data} />;
}
// server.ts
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
...
return async (req, res, next) => {
// SSR 的逻辑
// 1. 加载服务端入口模块
// 2. 数据预取
// 3. 「核心」渲染组件
// 4. 拼接 HTML,返回响应
try {
...
const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
const data = await fetchData();
// 获取组件的html字符串
const appHtml = renderToString(
React.createElement(ServerEntry, { data })
);
// 解析template.html文件
const templatePath = resolveTemplatePath();
let template = await memoryFsRead(templatePath);
const html = template
.replace("<!-- SSR_APP -->", appHtml)
// 注入数据标签,用于客户端 hydrate
.replace(
"<!-- SSR_DATA -->",
`<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
);
res.status(200).setHeader("Content-Type", "text/html").end(html);
} catch (e: any) {
vite?.ssrFixStacktrace(e);
console.error(e);
res.status(500).end(e.message);
}
};
}
async function createServer() {
const app = express();
// 加入 Vite SSR 中间件
app.use(await createSsrMiddleware(app));
app.listen(3000, () => {
console.log("Node 服务器已启动~");
console.log("http://localhost:3000");
});
}
createServer();
这里就是对于
ssr
的一个简单实现,中间只包含了重要代码,具体实现的话可以参考源码,我们不难发现,其实ssr
的实现就是在生产阶段,通过中间件来对我们返回到客户端端代码进行处理,动态生成 html 框架,同时将要水合的 js 代码发送到客户端进行水合(因为有些js
运行在浏览器的代码是不能在nodejs
的环境中运行的,所以需要发送到客户端水合),当然如果服务器出现问题,也可以在中间件中进行 csr 降级,这里就不展开讨论,其实对于ssr
来说,它解决首屏渲染过慢的方式其实是将渲染压力放到了服务端,通过在服务端提前组装html
来减少客户端通过js
生成dom
节点的压力,同时这样生成的html
结构也有助于seo
优化
ssg
ssg
(static site generation),静态站点生成,它总结一下其实就是构建阶段的 ssr
,它生成 html
的时机是在 build 打包阶段,进行 html 的拼接,这样能减少服务器的渲染压力,让服务器有更快的响应速度,但同时 ssg
开发的网站一般都是静态的,不能有大范围的页面更改,所以 ssg
一般用于博客网站,以下是 ssg
构建的核心代码
export async function build(root: string = process.cwd(), config: SiteConfig) {
// 1. bundle - client 端 + server 端
const [clientBundle] = await bundle(root, config);
// 2. 引入 server-entry 模块
const serverEntryPath = join(root, ".temp", "ssr-entry.js");
const { render, routes } = await import(serverEntryPath);
// 3. 服务端渲染,产出 HTML
try {
await renderPages(render, routes, root, clientBundle);
} catch (e) {
console.log("Render page error.\n", e);
}
}
其实对于
ssg
来说,开发的重点主要是插件,通过对插件进行开发,对build
阶段的各个模块进行定制化处理,来提ssg
的性能,和功能的扩展,同样,这里的具体代码也能参考源码,在ssg
框架中,有一个相对来说比较重要的概念,就是island
架构,我们通过对在客户端需要进行水合的组件进行标记,在打包的阶段单独打包出来bundle
,在向客户端发送js
代码的时候可以分包处理,达到渐进式增强的效果,而不是在客户端进行全量水合,不仅可以细粒度的控制水合,还可以减少TTI
的时间,那么,ssr
有没有类似的升级呢,答案是有的,虽然没有island
架构这样性能这么高,但是对比原始的ssr
有了不少的提升。
rsc
rsc
(react server component),服务端组件,这是 next.js
提出来的一个重要理念,相对于传统的 ssr
,next.js
将组件区分为服务端组件和客户端组件,对于服务端组件来说,是不需要进行水合的组件,而对于客户端组件来说,则是需要水合的,next.js
会将需要水合的 js
流式传输,渐进式增强的水合,以下是对客户端发送请求之后客户端返回的部分代码
1d:["$","div",null,{"className":"note-preview","children":["$","div",null,{"className":"text-with-markdown","dangerouslySetInnerHTML":{"__html":"<p>et iusto sed quo iure</p>\n"}},"$1e"]},"$1e"]
这个就是
rsc payload
的一部分,对于 rsc 来说,服务端组件是在服务端直接生成的,所以在payload
中直接包含,而客户端组件则是通过一个占位符"$"来表示,等到了客户端再进行水合,同时这里的rsc payload
的Transfer-Encoding
是chunked
,通过分片上传来渐进式的水合客户端组件,细粒度的控制js
水合的逻辑,当然,对于next.js
来说,向服务端发送请求的时候还可以带上next-router-state-tree
等参数,这样rsc payload
可以记住之前的状态,达到状态保存的效果。
再谈 ssg
通过对比,我们不难发现,island
架构和 rsc
都是强调只为交互部分加载 js
文件,都是渐进式增强的水合,不同的是 rsc
是将客户端逻辑和服务端逻辑进行分离达到部分水合的效果,而 island
架构则是在框架层面通过特殊标识来控制水合效果
小结
好了,说了这么多,其实最重要的不是哪种开发模式最好,而是要看具体的使用场景,面对不同的业务使用不同的开发模式,甚至可以混合使用,例如 rsc
何尝不是 ssr
和 csr
的结合呢,我们在开发过程中也要有自己独立的思考,敢于创新,才是正解,而不是一味的遵循某种开发模式,一头走到黑。
😉