引言 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 全栈、目录约定、自动 import Vue 大型应用 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 SSR Nuxt Astro 定制自由度 极高 中 中 上手成本 高 中 低 内容型站点 可 可 最佳 复杂交互 需自建 内置 需岛屿 生态成熟度 依赖手搭 高 中高
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 关键要点 开发期零构建 :ssrLoadModule + 中间件让 SSR 与 HMR 共存。双入口 是 SSR 工程的核心结构。数据预取 + 水合 保证两端状态一致。框架生态 把 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 全栈框架实践继续阅读
探索更多技术文章 浏览归档,发现更多关于系统设计、工具链和工程实践的内容。