Vite SSR 与服务端渲染实战:从模块图到全栈框架生态

系统讲解 Vite 的 SSR 能力:SSR 与传统 SPA 的差异、Vite SSR 架构与模块图、服务端渲染流程(entry-server/entry-client)、数据预取与流式渲染、开发期 SSR 的 HMR、以及基于 Vite 的全栈框架生态(Nuxt/Astro/SolidStart/SvelteKit)。

引言

SPA 的「白屏等待 JS」让首屏体验与 SEO 始终受制于客户端渲染。Vite 之所以成为新一代全栈框架的地基,正是因为它原生支持 SSR(服务端渲染)——开发期以模块图为驱动提供即时的 SSR + HMR,生产期把同一套源码编译为服务端可执行的 bundle。

本文从 SPA 的困境讲起,拆解 Vite SSR 的架构(模块图、entry-server/entry-client 双入口、ssrLoadModule),深入数据预取与流式渲染,再给出一个手写 Vite SSR 的最小示例,最后梳理基于 Vite 的全栈框架生态(Nuxt、Astro、SolidStart、SvelteKit)及选型建议。

前置:https://plumephp.com/vite-plugin-development/(了解插件钩子可辅助理解 SSR 中间件)与 https://plumephp.com/frontend-vite-deep-dive/(模块图基础)。


目录


1. SSR 解决什么问题

1.1 SPA 的三个痛点

痛点表现SSR 的解法
首屏白屏等待 JS 下载执行服务端直接输出 HTML
SEO 差爬虫看不到内容渲染出的 HTML 含正文
首屏慢大量客户端请求关键内容随首响应返回

1.2 SSR 的代价

  • 服务器成本:每次请求都要跑一遍渲染。
  • 双端同构复杂度:一套代码跑在 Node 与浏览器。
  • 数据一致:服务端预取的数据需水合(hydration)给客户端。

1.3 什么时候值得 SSR

内容型站点(博客/文档/电商)-> 强烈建议
高交互后台 -> 价值有限,可仅用 SSG/SPA

2. Vite SSR 架构概览

2.1 核心思路

开发期:
  Node 服务器 import Vite 中间件 -> ssrLoadModule 按需加载模块(HMR 生效)
生产期:
  vite build 产出 server bundle -> Node 服务器 require 运行

2.2 Vite 与 SSR 的独特优势

  • 开发期零构建:ssrLoadModule 用同一套模块图直接执行源码。
  • 框架无关:Vite 提供底层 SSR 能力,UI 框架自行决定渲染函数。
  • 同一套插件:源码转换插件在 SSR 与客户端复用。

2.3 关键区分

SSR 构建 = vite build --ssr          # 只产出服务端 bundle
SSG 构建 = 预渲染为静态 HTML        # 无运行时服务器

3. 双入口:entry-server 与 entry-client

3.1 为什么需要两个入口

entry-client.ts   -> 浏览器:挂载、水合
entry-server.ts   -> Node:把组件渲染成 HTML 字符串

3.2 entry-client(浏览器侧)

// entry-client.tsx
import { hydrateRoot } from 'react-dom/client'
import { App } from './App'

hydrateRoot(document.getElementById('root')!, <App />)

3.3 entry-server(服务端侧)

// entry-server.tsx
import { renderToString } from 'react-dom/server'
import { App } from './App'

export async function render(url: string) {
  // 可在此处做数据预取
  return renderToString(<App />)
}

3.4 双入口在构建中的体现

// vite.config.ts(SSR 模式示意)
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        client: 'entry-client.tsx',
        server: 'entry-server.tsx',
      },
      output: {
        // server bundle 输出为 CJS,便于 Node require
      },
    },
  },
})

4. ssrLoadModule 与开发期 SSR

4.1 ssrLoadModule 的作用

// server.ts(Node 端)
import { createServer } from 'vite'

const vite = await createServer({
  server: { middlewareMode: true },
})

// 开发期:按需加载入口模块(走完整插件管线 + HMR)
const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')

4.2 好处

  • 修改源码后无需重启 Node 服务器。
  • 与客户端共享同一套模块图与转换。
  • 生产环境替换为 require('./dist/server/entry-server.js')。

4.3 中间件挂载

app.use(vite.middlewares)
app.get('*', async (req, res) => {
  const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
  const html = await render(req.url)
  res.send(html)
})

5. 数据预取与流式渲染

5.1 数据预取

// entry-server.tsx
export async function render(url: string) {
  // 服务端预取数据
  const data = await fetchData(url)
  const html = renderToString(<App data={data} />)

  // 把数据序列化到 HTML,供客户端水合
  return {
    html,
    state: JSON.stringify(data),
  }
}
<!-- 输出中嵌入 -->
<script>window.__INITIAL_STATE__ = { ... }</script>

5.2 流式渲染(React 18+)

import { renderToPipeableStream } from 'react-dom/server'

const stream = renderToPipeableStream(<App />, {
  onAllReady() {
    stream.pipe(res)
  },
})

5.3 何时用流式

场景建议
首屏依赖慢接口流式输出关键 HTML,避免整页等待
页面体积大流式分段渲染提升 TTFB
简单静态页非流式足够

6. 手写一个最小 Vite SSR

6.1 完整示例

// server.ts
import express from 'express'
import { createServer } from 'vite'

const app = express()
const vite = await createServer({
  server: { middlewareMode: true },
  appType: 'custom',   // 不使用默认 HTML 回退
})

app.use(vite.middlewares)

app.get('*', async (req, res) => {
  const { render } = await vite.ssrLoadModule('/src/entry-server.tsx')
  const appHtml = await render(req.url)

  const template = await vite.transformIndexHtml(req.url, `
    <!doctype html>
    <html>
      <head><title>Vite SSR</title></head>
      <body>
        <div id="root">${appHtml}</div>
        <script type="module" src="/entry-client.tsx"></script>
      </body>
    </html>
  `)
  res.send(template)
})

app.listen(3000)

6.2 依赖说明

npm install express @vitejs/plugin-react react react-dom

6.3 运行

node server.ts
# 打开 http://localhost:3000 可见服务端渲染的 HTML

7. 基于 Vite 的全栈框架生态

7.1 框架地图

框架基于 Vite特点适用
Nuxt✅Vue 全栈、目录约定、自动 importVue 大型应用
Astro✅岛屿架构、默认零 JS内容型站点
SolidStart✅Solid 全栈、细粒度响应式Solid 团队
SvelteKit✅Svelte 全栈、SSR/SSG 灵活Svelte 团队
react-router 手动 SSR✅完全自控深度定制

7.2 它们如何利用 Vite

共享:插件管线、模块图、HMR、dev middleware
扩展:框架自有路由/数据层/渲染函数

7.3 何时选择框架 vs 手动 SSR

需求选择
团队已有框架技术栈对应全栈框架
高度定制渲染流程手动 SSR(如第 6 节)
内容/文档为主Astro
Vue 业务系统Nuxt

8. 框架选型建议

8.1 决策矩阵

维度手动 Vite SSRNuxtAstro
定制自由度极高中中
上手成本高中低
内容型站点可可最佳
复杂交互需自建内置需岛屿
生态成熟度依赖手搭高中高

8.2 生产建议

内部中后台 + 少量 SSR -> Nuxt(Vue)/ 手动 SSR
内容营销站           -> Astro(零 JS 优先)
追求极致控制         -> 手动 Vite SSR

8.3 性能与部署

SSR 需要 Node 运行时(Node 服务器 / Serverless 函数)
SSG 可纯静态部署(CDN)
两者可混合:部分页面 SSG、部分 SSR

9. 总结:Vite SSR 的心智模型

9.1 一句话框架

一套源码 -> 双入口(server 渲染 + client 水合)-> 模块图驱动开发期 -> 双 bundle 生产

9.2 关键要点

  1. 开发期零构建:ssrLoadModule + 中间件让 SSR 与 HMR 共存。
  2. 双入口是 SSR 工程的核心结构。
  3. 数据预取 + 水合保证两端状态一致。
  4. 框架生态把 Vite SSR 能力封装为开箱即用的全栈方案。

9.3 自检清单

检查项是否掌握
能解释 SPA 三个痛点☐
能说清 entry-server/client 职责☐
能实现数据预取与水合☐
能运行手写 Vite SSR☐
能根据场景选择全栈框架☐

延伸阅读

  • https://plumephp.com/frontend-vite-deep-dive/ — 模块图与 HMR 底层
  • https://plumephp.com/vite-plugin-development/ — SSR 相关的插件钩子
  • https://plumephp.com/vite-build-optimization/ — server bundle 的构建优化
  • Vite SSR 官方指南 — 完整 SSR API 参考
  • Nuxt.js 文档 — Vue 全栈框架实践

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「vite」更多文章

  1. Vite 静态资源与媒体资产处理:图片、字体、SVG 与 Worker
  2. Vite 环境变量与生产构建最佳实践:import.meta.env、构建模式与产物优化
  3. Vite 浏览器兼容与 Legacy 构建:build.target、Polyfill 与兼容插件