面试官问SSR和CSR,别只会背概念!手写代码+场景分析,这篇让你彻底拿捏

11 阅读8分钟

前言:从“白屏”到“秒开”的救赎

最近在整理面试题时,发现**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);
    }
})

代码逻辑深度拆解:

  1. fs.readFileSync(...)

    • 每次请求进来,我们先去硬盘里把 index.html 读出来。注意,这时候的 HTML 还是那个只有 <div id="root"></div> 的空壳。
  2. vite.transformIndexHtml(req.url, template)

    • 这是 Vite 提供的“黑科技”。它不仅仅是返回 HTML,它会根据你的路由 req.url 和模板,动态地把 React/Vue 组件渲染成字符串,塞进 root 容器里。
    • 注:你提供的代码片段里省略了具体的 render 调用细节(通常在 entry-server.js 中),但 transformIndexHtml 是 Vite SSR 的入口点,它负责把“死”的 HTML 变成包含 React 内容的 HTML 字符串。
  3. res.end(template)

    • 把组装好的、有内容的 HTML 字符串直接发给浏览器。浏览器一收到,立马就能画出页面(首屏完成)。

水合(Hydration)的伏笔

虽然这段代码主要在讲服务端,但别忘了 index.html 里的这一行:

<script type="module" src="/src/entry-client.jsx"></script>

代码解读:

  • 这就是水合的关键。
  • 服务端返回的 HTML 虽然能看到,但不能点。
  • 浏览器解析到这个 <script> 标签时,会去下载客户端的 JS 包(entry-client.jsx)。
  • 这个客户端 JS 运行后,会“接管”现有的 DOM,把点击事件、状态绑定上去。
  • 如果没有这一步,你的页面就是纯静态的,点啥都没反应。

数据流向

配合代码,脑海里要有这张图:

  1. 用户访问 localhost:3000
  2. Express 拦截请求 -> 调用 SSR 中间件
  3. fs 读取 index.html (空壳)
  4. Vite 介入 -> 运行 React 组件 -> 生成 HTML 字符串 -> 塞入 index.html
  5. Express 返回完整 HTML (有内容) -> 浏览器渲染 (看到画面)
  6. 浏览器 下载 entry-client.jsx -> 水合 (恢复交互)

这样拆分下来,是不是觉得 SSR 的逻辑其实就是一条清晰的“加工流水线”?面试的时候,你可以指着代码说:“这里是为了读模板,这里是为了注入内容,这里是为了返回响应”,条理瞬间清晰!

这段代码告诉我们什么?

  1. 服务器变累了:以前服务器只负责发静态文件,现在它要运行React/Vue代码,生成HTML字符串。这就是为什么SSR服务器CPU和内存压力大。
  2. 浏览器变爽了:浏览器收到的不再是空壳,而是有内容的HTML。
  3. 流程变化:请求 -> 服务器渲染 -> 返回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保证交互。技术没有绝对的好坏,只有适不适合。

希望这篇笔记能帮你在面试中“杀”出一条血路!如果觉得有用,记得点赞收藏哦~