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

⏱️ 预计阅读时间: 6 分钟

前言

在 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(
  <Provider store={store}>
    <StaticRouter location={reqPath}>
      <Routes>{/* ... */}</Routes>
    </StaticRouter>
  </Provider>
)
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 + 初始状态            │
│  <html>                            │
│    <head>                          │
│      ${styleTags}                    │
│    </head>                          │
│    <body>                          │
│      <div id="root">${content}</div>   │
│      <script>                        │
│        window.context = { state: ... } │
│      </script>                        │
│    </body>                          │
│  </html>                            │
└─────────────────────────────────────────┘

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 (
    <Provider store={store}>
      <BrowserRouter>
        <Routes>{/* ... */}</Routes>
      </BrowserRouter>
    </Provider>
  )
}

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

三、核心特性详解

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: <Home />,
    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: <Loading />
})

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

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 的场景

  1. 个人项目和快速原型

    • 需要快速搭建 SSR 应用
    • 想要学习 SSR 原理
    • 对构建流程有特殊需求
  2. 教育和学习目的

    • 深入理解 React SSR
    • 学习 Webpack 配置
    • 掌握 Redux 状态管理
  3. 需要高度定制的项目

    • 自定义构建流程
    • 特殊的安全策略
    • 自定义部署方案
  4. 低资源环境部署

    • 小型 VPS 服务器
    • 需要优化镜像大小
    • 精确控制资源占用

❌ 不适合使用 NSBP 的场景

  1. 企业级大型应用

    • Next.js 的自动优化能节省大量时间
    • 需要 Image Optimization、API Routes 等高级功能
    • 团队规模大,需要统一的开发规范
  2. 快速迭代的项目

    • 开发速度 > 定制能力
    • 不想深入理解底层原理
  3. 需要 AI/ML 功能

    • Next.js 的 AI SDK 集成
    • Vercel AI Platform 支持

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

7.1 React 19 Hydration 错误 #418

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

Minified React error #418

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

// ❌ 错误写法
<script
  dangerouslySetInnerHTML={{
    __html: `setTimeout(() => { ... }, 800)`
  }}
/>

解决方案:将脚本逻辑移到 useEffect 中。

// ✅ 正确写法
useEffect(() => {
  const timer = setTimeout(() => {
    // 客户端执行
  }, 300)
  return () => clearTimeout(timer)
}, [])

7.2 Docker 端口配置问题

问题.env 文件在根目录,docker-compose.ymldocker/ 目录,导致环境变量无法读取。

# 目录结构
nsbp/
├── .env              # 环境变量
└── docker/
    └── docker-compose.yml  # 配置文件

解决方案

  1. docker-compose.yml 移到根目录
  2. 更新 Makefile 移除 docker/ 路径前缀
  3. 更新 CLI 模板保持一致
// docker-compose.yml (现在在根目录)
services:
  app:
    build:
      context: .  // ← 直接使用根目录
      dockerfile: docker/Dockerfile

7.3 Webpack Dev Server 热重载失效

问题:修改样式文件后,页面没有自动刷新。

原因:Browser Sync 配置不正确。

解决方案:正确配置 Browser Sync Webpack Plugin。

const BrowserSyncPlugin = require('browser-sync-webpack-plugin')

module.exports = {
  plugins: [
    new BrowserSyncPlugin({
      host: 'localhost',
      port: 3000,
      proxy: {
        target: 'http://localhost:3001',
        ws: true
      }
    })
  ]
}

八、未来规划与改进方向

8.1 技术升级

  • 升级到 React Server Components(React 19 新特性)
  • 集成 tRPC 替代 Axios(类型安全的 API)
  • 使用 Vitest 替代 Jest(更快的测试框架)
  • 添加 Storybook 组件文档

8.2 性能优化

  • 实现 Service Worker 缓存
  • 添加 CDN 支持静态资源
  • 图片懒加载 + WebP 格式转换
  • 实现 ISR (Incremental Static Regeneration)

8.3 开发体验

  • 添加 TypeScript 路径自动导入
  • 实现代码生成工具(CRUD 模板)
  • 集成 e2e 测试(Playwright)
  • 添加性能监控和分析

九、总结

收获与经验

技术层面

  • 深入理解了 React SSR 的工作原理
  • 掌握了 Webpack 的复杂配置
  • 熟练使用 Redux Toolkit 状态管理
  • 学会了 Docker 容器化部署

工程层面

  • 建立了完整的代码质量工具链
  • 实现了自动化部署流程
  • 掌握了 CLI 工具开发
  • 理解了软件架构设计原则

对比 Next.js

特性 NSBP Next.js
学习曲线 陡峭(需要 Webpack 知识) 平缓(开箱即用)
定制能力 完全可控 有限(受框架限制)
部署灵活性 高(可自定义任何方案) 中(优先 Vercel)
开发体验 需要配置 优秀(零配置)
性能优化 手动控制 自动优化
适用场景 学习、个人项目、高度定制 企业应用、快速开发

给新手的建议

  1. 先学 Next.js:理解 SSR 的基本概念和最佳实践
  2. 再学 Webpack:掌握构建工具的底层原理
  3. 尝试 NSBP:理解一个完整的 SSR 框架是如何工作的
  4. 动手实践:自己构建一些小项目,深入理解每个环节

十、快速开始

# 1. 安装依赖
pnpm install

# 2. 配置环境
cp .env.example .env

# 3. 启动开发
pnpm run dev

# 4. 访问应用
open http://localhost:3001

# 5. Docker 部署
make prod

结语

构建 NSBP 的过程是一次非常有价值的学习经历。虽然最终产品可能不会成为 Next.js 的替代品,但通过这个项目,我:

  • 深入理解了现代前端工程化的每个环节
  • 掌握了从零构建一个完整应用的能力
  • 培养了解决复杂问题的思维模式

技术框架的选择没有绝对的对错,只有适不适合。Next.js 适合快速开发和大多数场景,但如果你想要完全掌控、深入学习、或者有特殊需求,那么从零构建也是一种选择。

希望这篇文章能对正在学习 React SSR 的你有所帮助!


项目链接

相关文章

  • [如何从零开始构建一个 React SSR 应用(待写)]
  • [Webpack 5 最佳实践指南(待写)]
  • [TypeScript + Redux Toolkit 实战(待写)]

本文首发于 Erishen 的技术博客

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

更多文章