前言:从“白屏”到“秒开”的救赎
最近在整理面试题时,发现**SSR(服务端渲染)和CSR(客户端渲染)**简直是前端面试的“重灾区”。很多候选人(包括我)只会干巴巴地背:“SSR首屏快、SEO好,CSR交互好、服务器压力小”。
面试官听完内心毫无波澜,甚至想给你倒杯水。
今天,咱们不整那些虚的。结合我最近复盘的一套手写SSR简易服务器的代码,带大家从代码层面、原理层面以及业务场景层面,把这两个概念彻底吃透。看完这篇,下次面试官再问,你直接把代码甩在桌子上(心里想想就好),气场全开!
CSR的痛:为什么我们需要SSR?
在SSR登场之前,CSR(Client-Side Rendering)是React/Vue时代的宠儿。它的模式很简单:服务器返回一个空的<div id="root"></div>,剩下的交给浏览器去下载JS、执行JS、请求数据、渲染页面。
这就好比你去餐厅吃饭:
- CSR模式:服务员(服务器)给你端上来一个空盘子(空HTML),然后告诉你:“菜(JS)还在厨房,您稍等,还得等厨师现炒(浏览器执行JS),炒好了您自己盛(渲染)。”
- 结果:你看着空盘子发呆(白屏),心里很慌。如果网不好,你可能等半天都吃不上饭。而且,如果来个美食评论家(搜索引擎爬虫),看到空盘子直接就走了,根本不知道你这有好吃的。
这就是CSR的两大死穴:首屏白屏时间长和SEO不友好。
SSR的救赎:代码里见真章
为了解决这个问题,SSR(Server-Side Rendering)站了出来。它的逻辑是:服务器直接把菜炒好,装在盘子里端给你。你拿到手就能吃(看到内容),至于后续的加菜(交互),再让厨房慢慢弄。
光说不练假把式,我们来看看怎么用Node.js + Express + Vite手写一个最基础的SSR服务器。这段代码虽然简单,但涵盖了SSR的核心流程。
核心代码解析(server.js):
你可以把整个 server.js 想象成一个餐厅的后厨流水线,我们来看看每个环节都在干什么:
搭建厨房流水线(初始化服务器)
const app = express();
async function start(){
// 1. 创建一个 Vite 服务器实例
const vite = await createViteServer({
server: {middlewareMode:true}, // 中间件模式:Vite 不自己起服务,而是作为 Express 的插件运行
appType:'custom' // 自定义应用类型
});
// 2. 把 Vite 的能力挂载到 Express 上
app.use(vite.middlewares);
// ...
}
代码解读:
- 这里最核心的是
middlewareMode: true。 - 为什么要这样做? 如果不加这个,Vite 会自己启动一个服务器(通常是 5173 端口)。但我们要用 Express(3000 端口)来统一接管。
app.use(vite.middlewares):这句话的意思是,“Express 经理,以后遇到.js,.css这些静态资源请求,或者需要转换代码时,直接交给 Vite 大厨处理,别来问我”。这样我们就拥有了热更新(HMR)和 ES Module 支持。
核心烹饪逻辑(手写 SSR 中间件)
这是最关键的部分,也是面试官最想看的逻辑。
app.use(async(req, res) => {
try {
// 1. 读取 HTML 模板(拿到空盘子)
let template = fs.readFileSync(
path.resolve(__dirname, './index.html'),
'utf-8'
);
// 2. 核心魔法:注入内容(把菜装进盘子)
// vite.transformIndexHtml 会做两件事:
// A. 处理 HTML 里的标签(如处理 import map)
// B. (在完整SSR中) 调用 render 函数生成 React 组件的 HTML 字符串并注入
const { render } = await vite.transformIndexHtml(req.url, template);
// 3. 返回给浏览器(上菜)
res.status(200).set({"Content-Type":"text/html"}).end(template);
} catch(err) {
console.log(err);
}
})
代码逻辑深度拆解:
-
fs.readFileSync(...):- 每次请求进来,我们先去硬盘里把
index.html读出来。注意,这时候的 HTML 还是那个只有<div id="root"></div>的空壳。
- 每次请求进来,我们先去硬盘里把
-
vite.transformIndexHtml(req.url, template):- 这是 Vite 提供的“黑科技”。它不仅仅是返回 HTML,它会根据你的路由
req.url和模板,动态地把 React/Vue 组件渲染成字符串,塞进root容器里。 - 注:你提供的代码片段里省略了具体的
render调用细节(通常在entry-server.js中),但transformIndexHtml是 Vite SSR 的入口点,它负责把“死”的 HTML 变成包含 React 内容的 HTML 字符串。
- 这是 Vite 提供的“黑科技”。它不仅仅是返回 HTML,它会根据你的路由
-
res.end(template):- 把组装好的、有内容的 HTML 字符串直接发给浏览器。浏览器一收到,立马就能画出页面(首屏完成)。
水合(Hydration)的伏笔
虽然这段代码主要在讲服务端,但别忘了 index.html 里的这一行:
<script type="module" src="/src/entry-client.jsx"></script>
代码解读:
- 这就是水合的关键。
- 服务端返回的 HTML 虽然能看到,但不能点。
- 浏览器解析到这个
<script>标签时,会去下载客户端的 JS 包(entry-client.jsx)。 - 这个客户端 JS 运行后,会“接管”现有的 DOM,把点击事件、状态绑定上去。
- 如果没有这一步,你的页面就是纯静态的,点啥都没反应。
数据流向
配合代码,脑海里要有这张图:
- 用户访问
localhost:3000 - Express 拦截请求 -> 调用 SSR 中间件
- fs 读取
index.html(空壳) - Vite 介入 -> 运行 React 组件 -> 生成 HTML 字符串 -> 塞入
index.html - Express 返回完整 HTML (有内容) -> 浏览器渲染 (看到画面)
- 浏览器 下载
entry-client.jsx-> 水合 (恢复交互)
这样拆分下来,是不是觉得 SSR 的逻辑其实就是一条清晰的“加工流水线”?面试的时候,你可以指着代码说:“这里是为了读模板,这里是为了注入内容,这里是为了返回响应”,条理瞬间清晰!
这段代码告诉我们什么?
- 服务器变累了:以前服务器只负责发静态文件,现在它要运行React/Vue代码,生成HTML字符串。这就是为什么SSR服务器CPU和内存压力大。
- 浏览器变爽了:浏览器收到的不再是空壳,而是有内容的HTML。
- 流程变化:请求 -> 服务器渲染 -> 返回HTML -> 浏览器展示 -> 客户端JS加载。
灵魂拷问:Hydration(水合)是什么?
既然服务器已经返回了HTML,为什么还需要下载客户端的JS(entry-client.jsx)?
这就引出了**Hydration(水合)**的概念。
服务器返回的HTML是“死”的。比如你有个按钮,虽然它显示在页面上,但你点它没反应,因为它没有绑定onclick事件。
Hydration就是“注水”的过程:
客户端JS下载完成后,React/Vue会“捡起”服务器生成的HTML DOM结构,并在上面重新绑定事件监听器、恢复状态。
- 形象比喻:服务器给你送来了一具完美的人体模型(静态HTML) ,虽然看着像人,但不会动。客户端JS(水)注入后,模型变成了活人(可交互页面) 。
- 注意:水合期间,用户可能会遇到“点击无反应”的情况,因为JS还没加载完。所以现代框架(如Next.js, React 18)都在搞流式SSR、选择性水合,就是为了减少这个尴尬期。
终极对决:业务场景怎么选?
面试最后,面试官通常会问:“那你在项目中怎么选?”这时候千万别只说理论,要结合场景。
CSR(客户端渲染)的主场:
- 后台管理系统:这种系统通常不需要SEO(百度搜不到你的后台),而且用户登录后停留时间长,首屏慢那一两秒可以接受,后续的无刷新跳转体验极佳。
- 强交互应用:比如在线文档、Canvas画图工具、复杂的工作流软件。这些应用逻辑都在前端,用SSR反而增加服务器负担,没必要。
- 混合开发App:正如笔记里提到的,很多App用原生做壳(调用相机、蓝牙),里面嵌WebView。这种场景下,流量入口是App本身,不是搜索引擎,且为了复用代码,CSR是首选。
SSR(服务端渲染)的主场:
- 电商网站(淘宝/京东) :商品详情页必须秒开,而且必须被搜索引擎搜到。
- 资讯/博客/新闻:内容驱动型网站,SEO是命脉。
- 营销活动页:需要极致的首屏速度,减少用户流失。
总结一张表
为了方便记忆,我把这些知识点浓缩成了一张表:
| 维度 | CSR (客户端渲染) | SSR (服务端渲染) |
|---|---|---|
| 首屏速度 | 慢 (需下载JS+执行+请求数据) | 快 (直接返回HTML) |
| SEO友好度 | 差 (爬虫看到的是空壳) | 优 (爬虫直接抓取内容) |
| 服务器压力 | 低 (只托管静态文件) | 高 (需实时渲染HTML) |
| 交互体验 | 优 (SPA无刷新切换) | 一般 (需水合,可能有卡顿) |
| 适用场景 | 后台、SaaS、App内嵌页 | 电商、新闻、博客、官网 |
最后的小建议:
现在的趋势是混合渲染。比如Next.js,你可以让首页用SSR保证SEO,进入后台后用CSR保证交互。技术没有绝对的好坏,只有适不适合。
希望这篇笔记能帮你在面试中“杀”出一条血路!如果觉得有用,记得点赞收藏哦~