我用 BFF 实现了一个 SSR
本文介绍如何使用 Node.js 作为中间层(BFF),结合 Vue 3 和 Vite 实现服务端渲染(SSR)。

为什么需要 SSR?
在传统的单页应用(SPA)中,浏览器首先加载一个空白的 HTML,然后通过 JavaScript 动态渲染页面内容。这种方式存在两个明显的问题:
| 问题 | 影响 |
|---|---|
| 首屏加载慢 | 用户需要等待 JS 下载、解析、执行后才能看到内容 |
| SEO 不友好 | 搜索引擎爬虫可能无法正确索引动态生成的内容 |
SSR(Server-Side Rendering) 可以很好地解决这些问题------在服务端就把页面渲染成完整的 HTML,浏览器拿到后直接展示。
什么是 BFF?
BFF(Backend For Frontend) 是一种架构模式,指的是专门为前端服务的后端层。在 SSR 场景中,Node.js 作为 BFF 承担两个核心职责:
css
┌─────────────────────────────────────────────────────────┐
│ Node BFF 职责 │
├─────────────────────────────────────────────────────────┤
│ 1️⃣ SSR 渲染:将 Vue 组件渲染为 HTML 字符串 │
│ 2️⃣ API 代理:提供前端需要的接口,聚合后端服务 │
└─────────────────────────────────────────────────────────┘
项目架构
我实现的这个 SSR 项目整体架构如下:
bash
浏览器请求
│
▼
┌───────────────────────────────────────┐
│ Node.js (Express) │
│ BFF 中间层 │
├───────────────────────────────────────┤
│ • 处理 /api/* 请求 → 返回 JSON │
│ • 处理页面请求 → SSR 渲染 HTML │
└───────────────────────────────────────┘
│
▼
浏览器显示 → JS 加载 → Hydration 激活
核心实现
1. 项目入口分离
SSR 项目需要两个入口文件:
| 文件 | 运行环境 | 职责 |
|---|---|---|
entry-server.js |
Node.js | 渲染 Vue 组件为 HTML 字符串 |
entry-client.js |
浏览器 | 激活静态 HTML,恢复交互能力 |
服务端入口 entry-server.js:
javascript
import { renderToString } from "vue/server-renderer";
import { createApp } from "./main.js";
export async function render(url) {
const { app, router, pinia } = createApp();
// 设置服务端路由
router.push(url);
await router.isReady();
// 渲染为 HTML 字符串
const html = await renderToString(app);
// 返回 HTML 和状态(用于客户端还原)
const state = pinia.state.value;
return { html, state };
}
客户端入口 entry-client.js:
javascript
import { createApp } from "./main.js";
async function hydrate() {
const { app, router, pinia } = createApp();
await router.isReady();
// 还原服务端状态
if (window.__PINIA_STATE__) {
pinia.state.value = window.__PINIA_STATE__;
}
// 激活服务端渲染的 HTML
app.mount("#app");
}
hydrate();
2. BFF 服务器实现
这是整个 SSR 的核心------server.js:
javascript
import express from "express";
import { renderToString } from "vue/server-renderer";
async function createServer() {
const app = express();
// ========== BFF API 接口 ==========
app.get("/api/hello", (req, res) => {
res.json({
message: "你好,这是来自 Node BFF 的响应!",
timestamp: new Date().toISOString(),
});
});
// ========== SSR 渲染 ==========
app.use("*", async (req, res) => {
const url = req.originalUrl;
// 1. 读取 HTML 模板
let template = fs.readFileSync("index.html", "utf-8");
// 2. 加载服务端入口
const { render } = await import("./src/entry-server.js");
// 3. 执行渲染
const { html: appHtml, state } = await render(url);
// 4. 注入渲染结果
let finalHtml = template.replace("<!--ssr-outlet-->", appHtml);
// 5. 注入初始状态
finalHtml = finalHtml.replace(
"</head>",
`<script>window.__PINIA_STATE__ = ${JSON.stringify(
state
)}</script></head>`
);
// 6. 返回完整 HTML
res.status(200).set({ "Content-Type": "text/html" }).end(finalHtml);
});
app.listen(3000);
}
3. 状态管理与 Hydration
SSR 最关键的一步是 状态同步:
css
服务端渲染时 客户端激活时
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Pinia 状态 │ ──序列化注入HTML──→ │ 还原状态 │
└─────────────┘ └─────────────┘
│ │
▼ ▼
渲染 HTML Hydration
服务端会将 Pinia 状态序列化后注入到 HTML 中:
xml
<script>
window.__PINIA_STATE__ = { counter: { count: 5 } };
</script>
客户端加载后读取这个状态,确保 Hydration 时状态一致,避免"闪烁"问题。
SSR 完整流程
整个 SSR 的工作流程可以总结为:
css
┌──────────────────────────────────────────────────────────────┐
│ SSR 完整流程 │
└──────────────────────────────────────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
【服务端阶段】 【客户端阶段】
│ │
1. 接收请求 4. 浏览器显示 HTML
│ │
2. Vue 组件渲染为 HTML 5. 加载客户端 JS
│ │
3. 注入状态并返回 6. Hydration 激活
│
7. 应用可交互 ✨
开发与生产环境
项目支持两种运行模式:
| 模式 | 特点 |
|---|---|
| 开发模式 | 集成 Vite,支持 HMR 热更新 |
| 生产模式 | 预构建产物,性能更优 |
arduino
# 开发模式
npm run dev
# 生产构建 + 启动
npm run build
npm run serve
功能演示

演示内容:
- • ✅ 计数器状态管理(Pinia)
- • ✅ BFF API 调用(Express)
- • ✅ 路由切换(Vue Router)
- • ✅ 客户端 Hydration
总结
通过这个项目,我实现了一个完整的 Vue 3 SSR 应用,核心要点:
-
- BFF 架构:Node.js 同时承担 API 服务和 SSR 渲染职责
-
- 入口分离:服务端和客户端各自有独立的入口文件
-
- 状态同步:服务端状态序列化注入 HTML,客户端还原后 Hydration
-
- Vite 加持:开发体验极佳,HMR 秒级刷新
如果你也想了解 SSR,欢迎 Clone 这个项目学习:
bash
git clone https://github.com/xiaogao007/vue3-ssr-demo
作者:小高
项目地址: vue3-ssr-demo