「从零到一」我用 Node BFF 手撸一个 Vue3 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 承担两个核心职责:

复制代码
┌─────────────────────────────────────────────────────────┐
│                     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 应用,核心要点:

    1. BFF 架构:Node.js 同时承担 API 服务和 SSR 渲染职责
    1. 入口分离:服务端和客户端各自有独立的入口文件
    1. 状态同步:服务端状态序列化注入 HTML,客户端还原后 Hydration
    1. Vite 加持:开发体验极佳,HMR 秒级刷新

如果你也想了解 SSR,欢迎 Clone 这个项目学习:

复制代码
  git clone https://github.com/xiaogao007/vue3-ssr-demo

作者:小高
项目地址: vue3-ssr-demo

相关推荐
IT_陈寒1 天前
Python 3.12性能优化实战:5个让你的代码提速30%的新特性
前端·人工智能·后端
爱写程序的小高1 天前
npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree
前端·npm·node.js
loonggg1 天前
竖屏,其实是程序员的一个集体误解
前端·后端·程序员
程序员爱钓鱼1 天前
Node.js 编程实战:测试与调试 - 单元测试与集成测试
前端·后端·node.js
码界奇点1 天前
基于Vue.js与Element UI的后台管理系统设计与实现
前端·vue.js·ui·毕业设计·源代码管理
时光少年1 天前
Android KeyEvent传递与焦点拦截
前端
踢球的打工仔1 天前
typescript-引用和const常量
前端·javascript·typescript
OEC小胖胖1 天前
03|从 `ensureRootIsScheduled` 到 `commitRoot`:React 工作循环(WorkLoop)全景
前端·react.js·前端框架
时光少年1 天前
ExoPlayer MediaCodec视频解码Buffer模式GPU渲染加速
前端