浅谈 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 的结合呢,我们在开发过程中也要有自己独立的思考,敢于创新,才是正解,而不是一味的遵循某种开发模式,一头走到黑。
😉