本文介绍如何使用 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 承担两个核心职责:
┌─────────────────────────────────────────────────────────┐ │ Node BFF 职责 │ ├─────────────────────────────────────────────────────────┤ │ 1️⃣ SSR 渲染:将 Vue 组件渲染为 HTML 字符串 │ │ 2️⃣ API 代理:提供前端需要的接口,聚合后端服务 │ └─────────────────────────────────────────────────────────┘项目架构
我实现的这个 SSR 项目整体架构如下:
浏览器请求 │ ▼ ┌───────────────────────────────────────┐ │ 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:
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:
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:
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 最关键的一步是状态同步:
服务端渲染时 客户端激活时 │ │ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ Pinia 状态 │ ──序列化注入HTML──→ │ 还原状态 │ └─────────────┘ └─────────────┘ │ │ ▼ ▼ 渲染 HTML Hydration服务端会将 Pinia 状态序列化后注入到 HTML 中:
<script> window.__PINIA_STATE__ = { counter: { count: 5 } }; </script>客户端加载后读取这个状态,确保 Hydration 时状态一致,避免"闪烁"问题。
SSR 完整流程
整个 SSR 的工作流程可以总结为:
┌──────────────────────────────────────────────────────────────┐ │ SSR 完整流程 │ └──────────────────────────────────────────────────────────────┘ │ ┌─────────────────────┴─────────────────────┐ ▼ ▼ 【服务端阶段】 【客户端阶段】 │ │ 1. 接收请求 4. 浏览器显示 HTML │ │ 2. Vue 组件渲染为 HTML 5. 加载客户端 JS │ │ 3. 注入状态并返回 6. Hydration 激活 │ 7. 应用可交互 ✨开发与生产环境
项目支持两种运行模式:
| 模式 | 特点 |
|---|---|
| 开发模式 | 集成 Vite,支持 HMR 热更新 |
| 生产模式 | 预构建产物,性能更优 |
# 开发模式 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 这个项目学习:
git clone https://github.com/xiaogao007/vue3-ssr-demo作者:小高
项目地址:vue3-ssr-demo