我如何从零构建一个生产级 React SSR 框架

作者:

🇬🇧 English

前言

在 React 生态系统中,Next.js 作为 SSR 框架的代表,以其开箱即用的体验和强大的功能深受开发者喜爱。但对于想要完全掌控构建流程深度定制部署方案、或者只是想学习 SSR 底层原理的开发者来说,Next.js 的"黑盒"特性反而成为了一种限制。

最近,我花了几个月时间,从零开始构建了一个轻量级但功能完整的 React SSR 框架——NSBP (Node React SSR by Webpack)。今天,我想分享一下这个项目的开发历程、技术选型、核心特性以及从中获得的实战经验。

在线演示: https://nsbp.erishen.cn/

GitHub: https://github.com/erishen/nsbp


一、为什么选择自建而不是 Next.js?

1.1 完全掌控构建流程

Next.js 的强大来自于它的自动化,但这也意味着你失去了一些控制权:

Next.js 的构建过程(黑盒)

next build

→ 自动路由

→ 自动代码分割

→ 自动图片优化

→ … 你看不到的几百个步骤

而在 NSBP 中,你可以精确控制每一个构建步骤:

// config/webpack.base.js module.exports = { entry: { client: './src/client/index.tsx', server: './src/server/index.ts' }, plugins: [ new MiniCssExtractPlugin(), new TerserPlugin(), // … 你可以添加任何 Webpack 插件 ] }

1.2 深度定制部署方案

Next.js 自带的 Vercel 部署确实方便,但如果你想:

  • 在自己的 Linux 服务器上部署

  • 使用 Docker Compose 编排多个服务

  • 配置反向代理和负载均衡

  • 实现特定的安全策略

那么自己掌控 Express 服务器会更有优势。

1.3 学习 SSR 的底层原理

通过手写 SSR 渲染逻辑,你将深入理解:

// SSR 渲染流程 const sheet = new ServerStyleSheet() const jsx = sheet.collectStyles( {/* … */} ) const content = renderToString(jsx) const styleTags = sheet.getStyleTags()

这些知识对你在任何 React SSR 项目中都非常有价值。


二、技术选型与架构设计

2.1 核心技术栈

| 类别 | 技术选型 | 版本 | 理由

| React | React 19.2.3 | 最新版本,并发特性,更好的类型支持 |

| 状态管理 | Redux Toolkit | 简化 Redux 开发,内置 Thunk 支持 |

| 路由 | React Router DOM 7.12.0 | 稳定的客户端路由 |

| 代码分割 | @loadable/component | 5.15.0,成熟的代码分割方案 |

| 构建工具 | Webpack 5.96.0 | 模块化、可扩展、生态丰富 |

| 服务端 | Express 5.2.1 | 轻量级、中间件生态完善 |

| 样式方案 | Styled-components + Sass/Less | 多种选择,灵活切换 |

| TypeScript | 5.x | 类型安全,开发体验优秀 |

2.2 SSR 渲染架构

┌─────────────────────────────────────────┐ │ HTTP 请求 │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ Express 中间件层 │ │ ┌──────────────┬──────────────┐ │ │ │ Helmet (安全) │ Rate Limit │ │ │ └──────────────┴──────────────┘ │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ SSR 渲染层 │ │ 1. 创建 Redux Store │ │ 2. 预取路由数据 (loadData) │ │ 3. renderToString 渲染 HTML │ │ 4. 提取 Styled-components 样式 │ │ 5. 序列化状态到 window.context │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ 返回 HTML + 初始状态 │ │ │ │ │ │ ${styleTags} │ │ │ │ │ │

${content}
│ │ │ │ │ │ │ └─────────────────────────────────────────┘

2.3 客户端 Hydration 流程

// src/client/index.tsx const App = () => { const store = useMemo(() => { // 读取服务端传递的状态 if (isSEO() && window?.context?.state) { return getStore(window.context.state) } return getStore() }, [])

return ( {/* … */} ) }

// 等待代码分割组件加载完成 loadableReady(() => { hydrateRoot(document.getElementById('root')!, ) })


三、核心特性详解

3.1 三种渲染模式(灵活切换)

NSBP 支持通过 URL 参数控制渲染模式:

| 模式 | URL | 场景

| SSR(默认) | //?nsbp=1 | SEO 优化、首屏加载快

| CSR(纯客户端) | /?nsbp=0 | 调试、开发体验

| SSR 回退 | /?nsbp=1&from=link | 内部链接,允许客户端更新

实现逻辑

// src/utils/index.ts export const isSEO = () => { if (typeof window !== 'undefined') { const nsbp = getLocationParams('nsbp') if (nsbp !== '') { return parseInt(nsbp, 10) } return 1 // 默认 SSR } return 1 }

3.2 智能 SSR 数据预取

服务端预取数据,避免客户端二次请求:

// src/Routers.tsx export default [ { path: '/', element: , loadData: homeLoadData, // ← SSR 时预取数据 key: 'home' } ]

// src/services/home.ts export const loadData = (resolve: any, query: any) => { return async (dispatch: any) => { try { const response = await fetch('/getPhotoMenu') const data = await response.json()

  // Redux Thunk action
  dispatch({
    type: FETCH_PHOTO_MENU_SUCCESS,
    payload: data.data
  })

  resolve()
} catch (error) {
  console.error('Data loading error:', error)
  resolve()
}

} }

效果对比

传统 CSR 方式:

  1. 客户端请求页面 → 返回空 HTML
  2. 加载 JS bundle
  3. 执行 JS
  4. 发起 API 请求
  5. 渲染内容 ⏱️ 总耗时: ~2-3 秒

NSBP SSR 方式:

  1. 服务端预取数据
  2. 渲染完整 HTML
  3. 返回给客户端
  4. 客户端直接 hydrate ⏱️ 总耗时: ~0.5-1 秒

3.3 代码分割与懒加载

使用 @loadable/component 实现路由级别的代码分割:

import loadable from '@loadable/component'

const Home = loadable(() => import('@containers/Home'), { fallback: })

const Photo = loadable(() => import('@containers/Photo'), { fallback: })

Webpack 配置

// config/webpack.client.js const { ChunkExtractor } = require('@loadable/webpack-plugin')

module.exports = merge(baseConfig, { mode: 'production', plugins: [ new ChunkExtractor({ statsFile: path.resolve(__dirname, '../public/loadable-stats.json') }) ] })

效果

首次加载:

  • 主 bundle: ~200KB
  • 只加载首页代码

访问 /photo 页面:

  • 按需加载 Photo 组件
  • 不加载 Login 组件

3.4 多层安全防护体系

Helmet 安全头部 // src/server/index.ts app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:', 'https:'], connectSrc: ["'self'", 'https:'], fontSrc: ["'self'", 'data:'], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] } }, crossOriginEmbedderPolicy: false, crossOriginOpenerPolicy: false }))

安全效果

| 攻击类型 | 防护措施

| XSS 攻击 | CSP script-src 策略

| 点击劫持 | X-Frame-Options: SAMEORIGIN

| MIME 嗅探 | X-Content-Type-Options: nosniff

| 不安全的 HTTPS | Strict-Transport-Security

速率限制 if (process.env.ENABLE_RATE_LIMIT === '1') { const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 分钟 max: 100, // 最多 100 次请求 message: 'Too many requests from this IP' }) app.use('/api', limiter) }

3.5 Docker 一键部署

生产环境 (docker-compose.yml):

services: app: build: context: . dockerfile: docker/Dockerfile container_name: nsbp-app ports: – "8081:3001" environment: – NODE_ENV=production – ENABLE_RATE_LIMIT=1 restart: unless-stopped healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"] interval: 30s timeout: 3s retries: 3

开发环境 (docker-compose.dev.yml):

services: app: build: context: . dockerfile: docker/Dockerfile.dev container_name: nsbp-app-dev ports: – "3001:3001" – "9229:9229" # Node.js debug port volumes: # 代码挂载(热重载) – ./src:/app/src – ./public:/app/public – ./config:/app/config command: ["dumb-init", "–", "pnpm", "run", "dev"]

一键启动

生产环境

make prod

docker-compose up -d

开发环境

make dev

docker-compose -f docker-compose.dev.yml up –build


四、开发体验优化

4.1 完整的代码质量工具链

// package.json scripts { "scripts": { "lint": "eslint src –ext .ts,.tsx,.js,.jsx", "lint:fix": "eslint src –ext .ts,.tsx,.js,.jsx –fix", "format": "prettier –write **/*.{js,css,less,scss,ts,tsx}" } }

Git Hooks (Husky + lint-staged):

// .husky/pre-commit #!/usr/bin/env sh . "$(dirname — "$0")/_/husky.sh"

lint-staged –partial-staged

效果

$ git add . $ git commit -m "feat: add new component" ✔ Pre-commit checks → ESLint auto-fixed 3 files → Prettier formatted 5 files [master 8a3f9d] feat: add new component

4.2 CLI 工具快速创建项目

安装 CLI

npm install -g nsbp-cli

创建新项目

npx nsbp-cli create my-app

项目结构

my-app/ ├── src/ │ ├── client/ │ ├── server/ │ ├── containers/ │ ├── component/ │ ├── store/ │ └── utils/ ├── docker/ │ ├── Dockerfile │ ├── Dockerfile.dev │ ├── docker-compose.yml │ └── docker-compose.dev.yml ├── config/ │ ├── webpack.base.js │ ├── webpack.client.js │ └── webpack.server.js ├── .env.example ├── Makefile └── README.md

4.3 Make 命令简化操作

查看所有命令

make help

Available targets: help Show this help message build Build Docker images for production dev Start development environment prod Start production environment down Stop and remove containers logs View logs restart Restart production containers rebuild Rebuild and restart containers shell Open shell in container


五、性能优化实践

5.1 Tree Shaking

// config/webpack.base.js module.exports = { optimization: { usedExports: true, // 只导出使用的导出 sideEffects: false, // 没有副作用 } }

5.2 代码压缩与混淆

// config/webpack.client.js (production) const TerserPlugin = require('terser-webpack-plugin')

module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: true, // 多进程压缩 terserOptions: { compress: { drop_console: true, // 移除 console.log pure_funcs: ['console.log', 'console.info'] } } }) ] } }

5.3 静态资源缓存优化

// src/server/index.ts app.use(express.static('public', { dotfiles: 'ignore', setHeaders: (res, filePath) => { // 缓存 1 年 if (filePath.match(/.(js|css|png|jpg|jpeg|gif|svg|ico)$/)) { res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') } } }))

5.4 图片尺寸探测

// 使用 probe-image-size 在 SSR 阶段获取图片尺寸 import { getPhotoWH } from '@server/photo'

// 客户端直接使用,避免二次请求 const [width, height, filename] = photos[0]


六、适用场景分析

✅ 适合使用 NSBP 的场景

个人项目和快速原型

  • 需要快速搭建 SSR 应用

  • 想要学习 SSR 原理

  • 对构建流程有特殊需求

教育和学习目的

  • 深入理解 React SSR

  • 学习 Webpack 配置

  • 掌握 Redux 状态管理

需要高度定制的项目

  • 自定义构建流程

  • 特殊的安全策略

  • 自定义部署方案

低资源环境部署

  • 小型 VPS 服务器

  • 需要优化镜像大小

  • 精确控制资源占用

❌ 不适合使用 NSBP 的场景

企业级大型应用

  • Next.js 的自动优化能节省大量时间

  • 需要 Image Optimization、API Routes 等高级功能

  • 团队规模大,需要统一的开发规范

快速迭代的项目

  • 开发速度 > 定制能力

  • 不想深入理解底层原理

需要 AI/ML 功能

  • Next.js 的 AI SDK 集成

  • Vercel AI Platform 支持


七、遇到的问题与解决方案

7.1 React 19 Hydration 错误 #418

问题:在 HTTPS 环境下,Home 组件报错:

Minified React error #418

原因:组件中使用了 dangerouslySetInnerHTML<script> 标签,导致服务端和客户端 HTML 不匹配。

// ❌ 错误写法

首页 简历 商店 Web Chat Nsbp 关于 隐私政策

@ 2026 ESN
沪ICP备2024079226号-1   沪公网安备31010502007082号