Skip to content

Repository files navigation

React 服务端渲染 (SSR) 实践项目

一个完整的 React 服务端渲染(Server-Side Rendering)教学项目,展示了如何构建一个生产级的 React SSR 应用,包含路由同构、数据预取、样式处理、热更新等完整功能。

目录

核心特性

  • 完整的 SSR 实现 - 服务端渲染 HTML,提升首屏加载速度和 SEO
  • 路由同构 - 客户端和服务端共享路由配置
  • 数据预取 - 服务端预先获取数据,实现真正的同构
  • 样式同构 - 支持 CSS Modules 和 Material-UI 的 SSR
  • 热更新 - 客户端和服务端代码热更新,提升开发效率
  • 代码分割 - 基于路由的代码分割和异步加载
  • SEO 优化 - 使用 react-helmet 管理页面 TDK
  • 双渲染模式 - 支持 SSR 和 CSR 模式切换
  • Redux 集成 - 完整的状态管理和同构支持
  • Material-UI - 集成 Material-UI 组件库

技术栈

前端框架

  • React 16.13.1 - UI 框架
  • React Router 5.1.2 - 路由管理
  • Redux 4.0.5 - 状态管理
  • Material-UI 4.9.12 - UI 组件库
  • React Helmet 6.0.0 - HTML 头部管理

服务端

  • Express 4.17.1 - Node.js Web 框架
  • React-DOM Server - 服务端渲染

构建工具

  • Webpack 4.43.0 - 模块打包
  • Babel 7 - JavaScript 编译
  • Stylus - CSS 预处理器

开发工具

  • webpack-dev-middleware - Webpack 开发中间件
  • webpack-hot-middleware - 客户端热更新
  • webpack-hot-server-middleware - 服务端热更新
  • react-hot-loader - React 组件热更新

快速开始

环境要求

  • Node.js >= 10.0.0
  • npm >= 6.0.0

安装依赖

npm install

开发模式

npm run dev

访问 http://localhost:3000

生产构建

npm run start

这将先清理构建目录,然后进行生产构建并启动服务器。

清理构建文件

npm run clean

项目结构

react-service-render/
├── client/                     # 客户端代码
│   ├── compoents/             # 通用组件
│   │   ├── header.js          # 头部组件
│   │   └── sider.js           # 侧边栏组件
│   ├── containers/            # 页面容器
│   │   ├── a.js               # 页面 A
│   │   ├── b.js               # 页面 B (代码分割示例)
│   │   ├── c.js               # 页面 C
│   │   └── d.js               # 页面 D
│   ├── lib/                   # 工具库
│   │   └── index.js           # 高阶组件(数据预取 HOC)
│   ├── store/                 # Redux 状态管理
│   │   └── index.js           # Store 配置
│   ├── app.js                 # 应用根组件
│   ├── client.js              # 客户端入口 (BrowserRouter)
│   ├── server.js              # 服务端入口 (StaticRouter)
│   ├── Index.js               # 客户端渲染入口 (hydrate)
│   ├── routes.js              # 路由配置
│   └── theme.js               # Material-UI 主题配置
├── server/                    # 服务端代码
│   ├── index.js               # Express 服务器
│   └── render.js              # SSR 渲染逻辑
├── webpack/                   # Webpack 配置
│   ├── client.dev.js          # 客户端开发配置
│   ├── client.prod.js         # 客户端生产配置
│   ├── server.dev.js          # 服务端开发配置
│   └── server.prod.js         # 服务端生产配置
├── .babelrc                   # Babel 配置
├── config.js                  # 全局配置 (SSR/CSR 模式切换)
├── package.json               # 项目依赖
└── README.md                  # 项目文档

核心概念

1. 什么是 SSR?

服务端渲染(Server-Side Rendering)是指在服务器端将 React 组件渲染成 HTML 字符串,然后发送给客户端。相比传统的客户端渲染(CSR),SSR 具有以下优势:

  • 更快的首屏加载 - 用户可以更快看到页面内容
  • 更好的 SEO - 搜索引擎可以直接抓取到页面内容
  • 更好的用户体验 - 特别是在移动设备和弱网环境下

2. 同构原理

同构(Isomorphic)是指同一套代码既可以在服务端运行,也可以在客户端运行。React 通过虚拟 DOM 实现了同构:

// 服务端:将虚拟 DOM 转换为 HTML 字符串
const html = ReactDOMServer.renderToString(<App />);

// 客户端:复用服务端渲染的 HTML,只绑定事件
ReactDOM.hydrate(<App />, document.getElementById('root'));

3. 核心 API

renderToString

import { renderToString } from 'react-dom/server';

// 将 React 组件渲染为 HTML 字符串
const html = renderToString(<App />);

hydrate

import { hydrate } from 'react-dom';

// 复用服务端渲染的 HTML,只绑定事件处理
hydrate(<App />, document.getElementById('root'));

4. 路由同构

使用 react-router-config 实现静态路由配置,客户端使用 BrowserRouter,服务端使用 StaticRouter

客户端

import { BrowserRouter } from 'react-router-dom';

const App = () => (
  <BrowserRouter>
    {renderRoutes(routes)}
  </BrowserRouter>
);

服务端

import { StaticRouter } from 'react-router-dom';

const App = ({ req, context }) => (
  <StaticRouter location={req.path} context={context}>
    {renderRoutes(routes)}
  </StaticRouter>
);

5. 数据预取

本项目参考 Next.js 的 getInitialProps 方法实现数据预取:

// 在组件上定义静态方法
class HomePage extends React.Component {
  static getInitialProps(store, req, res) {
    // 在服务端获取数据
    return store.dispatch(fetchData());
  }

  render() {
    return <div>{this.props.data}</div>;
  }
}

服务端流程

  1. 使用 matchRoutes 匹配当前路径的组件
  2. 调用匹配组件的 getInitialProps 方法
  3. 等待所有数据获取完成
  4. 将数据注入 Redux Store
  5. 渲染组件为 HTML
  6. 将 Store 状态序列化到页面(数据注水)

客户端流程

  1. window.context 读取服务端注入的数据
  2. 初始化 Redux Store
  3. 使用 hydrate 复用服务端 HTML

6. 样式处理

CSS Modules

import styles from './index.styl';

<div className={styles.container}>内容</div>

Material-UI SSR

服务端收集样式:

import { ServerStyleSheets } from '@material-ui/core/styles';

const sheets = new ServerStyleSheets();
const html = renderToString(sheets.collect(<App />));
const css = sheets.toString();

客户端移除服务端样式:

const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
  jssStyles.parentElement.removeChild(jssStyles);
}

7. SEO 优化

使用 react-helmet 管理页面的 TDK(Title、Description、Keywords):

import { Helmet } from 'react-helmet';

<Helmet>
  <title>页面标题</title>
  <meta name="description" content="页面描述" />
  <meta name="keywords" content="关键词1,关键词2" />
</Helmet>

服务端获取 Helmet 数据:

import { Helmet } from 'react-helmet';

const helmet = Helmet.renderStatic();

const html = `
  <!DOCTYPE html>
  <html ${helmet.htmlAttributes.toString()}>
    <head>
      ${helmet.title.toString()}
      ${helmet.meta.toString()}
    </head>
    <body>
      <div id="root">${markup}</div>
    </body>
  </html>
`;

开发指南

配置渲染模式

config.js 中可以切换 SSR/CSR 模式:

module.exports = {
  __IS_SSR__: false  // true: SSR 模式, false: CSR 模式
};

添加新页面

  1. client/containers/ 创建页面组件
  2. client/routes.js 添加路由配置:
{
  path: '/new-page',
  component: NewPage,
  exact: true,
  key: 'new-page'
}
  1. 如果需要数据预取,添加 getInitialProps 静态方法:
class NewPage extends React.Component {
  static getInitialProps(store) {
    return store.dispatch(fetchPageData());
  }

  render() {
    // ...
  }
}

代码分割

使用 react-loadable 实现路由级别的代码分割:

import Loadable from 'react-loadable';

const AsyncPage = Loadable({
  loader: () => import('./containers/page'),
  loading: () => <span>Loading...</span>
});

// 在路由配置中使用
{
  path: '/async',
  component: AsyncPage,
  exact: true,
  key: 'async'
}

状态管理

本项目使用 Redux 进行状态管理,Store 配置在 client/store/index.js

import { createStore } from 'redux';
import reducer from './reducer';

// 创建 Store(每个请求创建新的 Store,避免单例问题)
export default (initialState) => {
  return createStore(reducer, initialState);
};

热更新

项目支持双端热更新:

  • 客户端热更新:使用 webpack-hot-middlewarereact-hot-loader
  • 服务端热更新:使用 webpack-hot-server-middleware

修改代码后自动刷新,无需手动重启服务器。

构建与部署

开发环境

开发环境使用 Webpack Dev Middleware 实现热更新:

const compiler = webpack([clientConfig, serverConfig]);

app.use(webpackDevMiddleware(compiler, options));
app.use(webpackHotMiddleware(clientCompiler));      // 客户端热更新
app.use(webpackHotServerMiddleware(compiler));      // 服务端热更新

生产环境

生产环境构建优化:

  1. 代码压缩 - 使用 UglifyJS 压缩 JavaScript
  2. 代码分割 - 将第三方库分离到 libs.js
  3. 文件 Hash - 为文件名添加 Hash,利于缓存
  4. CSS 提取 - 将 CSS 提取到独立文件
  5. Tree Shaking - 移除未使用的代码

构建产物

  • buildClient/ - 客户端构建产物
  • buildServer/ - 服务端构建产物

部署建议

  1. 使用 PM2 管理 Node.js 进程
  2. 使用 Nginx 作为反向代理
  3. 启用 GZIP 压缩
  4. 配置 CDN 加速静态资源
  5. 添加监控 和日志系统

SSR 架构详解

请求流程

用户请求 → Nginx → Express Server
                      ↓
              路由匹配 (matchRoutes)
                      ↓
              数据预取 (getInitialProps)
                      ↓
              渲染组件 (renderToString)
                      ↓
              生成 HTML (包含数据注水)
                      ↓
              返回给客户端
                      ↓
              客户端 hydrate
                      ↓
              接管页面交互

Webpack 构建流程

开发环境

webpack(configs) → Dev Middleware → Hot Middleware
                                   ↓
                              浏览器接收更新

生产环境

webpack(configs) → 构建客户端和服务端代码
                   ↓
              生成构建产物
                   ↓
              启动 Express 服务器

数据流转

服务端:
  请求 → matchRoutes → getInitialProps → dispatch actions
       → Store 更新 → renderToString → 序列化 Store 状态
       → 注入到 HTML (window.context)

客户端:
  读取 window.context → 初始化 Store → hydrate
       → 后续交互走客户端渲染

注意事项

  1. 避免单例 - 每个请求应创建新的 Store 实例
  2. 生命周期 - componentWillMount 在服务端也会执行
  3. 浏览器 API - 服务端无法使用 windowdocument 等 API
  4. 样式闪烁 - 确保正确处理 CSS 的服务端渲染
  5. 数据预取 - 处理好异步数据的加载状态
  6. 错误处理 - 添加完善的错误边界和降级方案

常见问题

Q: 如何调试服务端代码?

A: 使用 Node.js 的 --inspect 参数:

node --inspect server/index.js

Q: 如何处理服务端渲染和客户端渲染的差异?

A: 使用条件判断:

if (typeof window !== 'undefined') {
  // 客户端代码
} else {
  // 服务端代码
}

Q: 如何优化 SSR 性能?

A:

  • 使用缓存(Redis)
  • 流式渲染(renderToNodeStream)
  • 减少数据预取
  • 使用 CDN
  • 启用 HTTP/2

Q: 为什么要使用 hydrate 而不是 render?

A: hydrate 会复用服务端渲染的 HTML,只添加事件监听,性能更好。render 会完全重新渲染,导致闪烁。

参考资源

License

MIT

贡献

欢迎提交 Issue 和 Pull Request!


注意:本项目主要用于学习和教学目的,展示 React SSR 的核心原理和实现方式。在生产环境中,建议使用成熟的框架如 Next.js 或 Remix。

About

react-service-render

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages