前言
StaticRouter
是 React Router
为服务器端渲染(SSR)
提供的专用路由组件。它允许在服务器环境中处理路由逻辑,确保服务器和客户端渲染结果一致。下面我将详细解释其用途、原理并提供完整的代码示例。
一、StaticRouter
的核心用途
- 服务器端渲染(SSR):在 Node.js 服务器上预渲染 React 应用
- SEO 优化:生成可被搜索引擎索引的完整 HTML
- 性能提升:加速首屏加载时间
- 路由状态同步:确保服务器和客户端渲染结果一致
- HTTP 状态码控制:根据路由返回正确的状态码(如 404)
二、StaticRouter与客户端路由器的区别
三、StaticRouter
工作原理
StaticRouter
的核心机制:
- 接收请求 URL 作为 location 属性
- 使用 context 对象收集渲染过程中的路由信息
- 根据路由配置渲染对应的组件树
- 将渲染结果和 context 信息返回给服务器
- 服务器根据 context 设置 HTTP 状态码等响应信息
四、StaticRouter
完整代码示例
项目结构
text
project/
├── client/
│ ├── App.js
│ ├── index.js # 客户端入口
│ └── routes.js
├── server/
│ └── server.js # Express 服务器
└── shared/
└── components/ # 共享组件
4.1、 客户端应用 (client/App.js
)
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import Home from './Home';
import About from './About';
import User from './User';
import NotFound from './NotFound';
function App() {
return (
<div className="app">
<header>
<h1>SSR 示例应用</h1>
<nav>
<ul>
<li><Link to="/">首页</Link></li>
<li><Link to="/about">关于</Link></li>
<li><Link to="/user/123">用户123</Link></li>
<li><Link to="/invalid">无效链接</Link></li>
</ul>
</nav>
</header>
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<User />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
<footer>
<p>服务器端渲染 (SSR) 示例</p>
</footer>
</div>
);
}
export default App;
4.2、 页面组件 (client/Home.js
)
import React from 'react';
const Home = () => (
<div className="page home">
<h2>🏠 欢迎来到首页</h2>
<p>这是一个服务器端渲染的 React 应用示例</p>
<div className="features">
<div className="feature-card">
<h3>SEO 友好</h3>
<p>完整的 HTML 内容可被搜索引擎索引</p>
</div>
<div className="feature-card">
<h3>性能优化</h3>
<p>加速首屏加载时间</p>
</div>
<div className="feature-card">
<h3>用户体验</h3>
<p>更快的交互响应</p>
</div>
</div>
</div>
);
export default Home;
4.3、 用户页面组件 (client/User.js
)
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
const User = () => {
const { id } = useParams();
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
// 模拟数据获取
useEffect(() => {
const fetchData = async () => {
// 实际项目中会调用 API
const data = {
id,
name: `用户 ${id}`,
email: `user${id}@example.com`,
joinDate: '2023-01-15'
};
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
setUserData(data);
setLoading(false);
};
fetchData();
}, [id]);
if (loading) {
return <div className="loading">加载中...</div>;
}
return (
<div className="page user">
<h2>👤 用户信息</h2>
<div className="user-info">
<div className="info-row">
<span className="label">用户ID:</span>
<span className="value">{userData.id}</span>
</div>
<div className="info-row">
<span className="label">用户名:</span>
<span className="value">{userData.name}</span>
</div>
<div className="info-row">
<span className="label">邮箱:</span>
<span className="value">{userData.email}</span>
</div>
<div className="info-row">
<span className="label">加入日期:</span>
<span className="value">{userData.joinDate}</span>
</div>
</div>
</div>
);
};
export default User;
4.4、 404 页面组件 (client/NotFound.js
)
import React from 'react';
import { useNavigate } from 'react-router-dom';
const NotFound = ({ staticContext }) => {
const navigate = useNavigate();
// 在服务器端渲染时设置状态码
if (staticContext) {
staticContext.status = 404;
staticContext.message = "页面未找到";
}
return (
<div className="page not-found">
<h2>🔍 404 - 页面未找到</h2>
<p>抱歉,您访问的页面不存在</p>
<button
onClick={() => navigate('/')}
className="btn"
>
返回首页
</button>
</div>
);
};
export default NotFound;
4.5、 服务器端代码 (server/server.js
)
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom/server';
import App from '../client/App';
import fs from 'fs';
import path from 'path';
const app = express();
const port = 3000;
// 静态文件服务
app.use(express.static('build'));
// 读取客户端构建的 HTML 模板
const indexFile = path.resolve('./build/index.html');
const htmlTemplate = fs.readFileSync(indexFile, 'utf8');
// 服务器端渲染中间件
app.get('*', (req, res) => {
// 创建 context 对象收集渲染信息
const context = {};
// 使用 StaticRouter 渲染应用
const appMarkup = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
// 如果遇到重定向,处理重定向
if (context.url) {
return res.redirect(301, context.url);
}
// 设置 HTTP 状态码(来自 NotFound 组件)
const status = context.status || 200;
res.status(status);
// 注入渲染结果到 HTML 模板
const html = htmlTemplate
.replace('<!-- SSR_APP -->', appMarkup)
.replace('<!-- SSR_STATE -->', `<script>window.__PRELOADED_STATE__ = ${JSON.stringify(context)}</script>`);
// 发送完整 HTML
res.send(html);
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
4.6、 客户端入口 (client/index.js
)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
// 客户端渲染入口
const hydrateApp = () => {
const root = ReactDOM.hydrateRoot(
document.getElementById('root'),
<BrowserRouter>
<App />
</BrowserRouter>
);
// 开发模式下启用热模块替换
if (module.hot) {
module.hot.accept('./App', () => {
const NextApp = require('./App').default;
root.render(
<BrowserRouter>
<NextApp />
</BrowserRouter>
);
});
}
};
// 检查是否已存在服务器渲染的内容
if (document.getElementById('root').hasChildNodes()) {
hydrateApp();
} else {
// 如果没有 SSR 内容,则进行客户端渲染
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
}
4.7、 HTML 模板 (build/index.html
)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React SSR 示例</title>
<style>
/* 基础样式 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f7fa;
}
.app {
max-width: 1200px;
margin: 0 auto;
background: white;
box-shadow: 0 5px 25px rgba(0,0,0,0.1);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: #2c3e50;
color: white;
padding: 20px;
}
header h1 {
margin-bottom: 15px;
}
nav ul {
display: flex;
list-style: none;
gap: 15px;
}
nav a {
color: rgba(255,255,255,0.9);
text-decoration: none;
padding: 8px 15px;
border-radius: 4px;
transition: background 0.3s;
}
nav a:hover {
background: rgba(255,255,255,0.1);
}
main {
padding: 30px;
flex: 1;
}
.page {
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 30px;
}
.feature-card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0,0,0,0.08);
}
.user-info {
max-width: 500px;
margin-top: 20px;
}
.info-row {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.label {
font-weight: bold;
width: 120px;
color: #555;
}
.not-found {
text-align: center;
padding: 50px 20px;
}
.btn {
display: inline-block;
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-top: 20px;
font-size: 1rem;
transition: background 0.3s;
}
.btn:hover {
background: #2980b9;
}
.loading {
padding: 30px;
text-align: center;
font-size: 1.2rem;
color: #777;
}
footer {
background: #2c3e50;
color: white;
padding: 20px;
text-align: center;
}
</style>
</head>
<body>
<div id="root">
<!-- SSR_APP -->
</div>
<!-- SSR_STATE -->
<!-- 客户端脚本 -->
<script src="/client_bundle.js"></script>
</body>
</html>
五、StaticRouter
关键特性详解
5.1、 核心组件使用
// 服务器端
const context = {};
const appMarkup = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
// 客户端
const root = ReactDOM.hydrateRoot(
document.getElementById('root'),
<BrowserRouter>
<App />
</BrowserRouter>
);
5.2、 状态码处理
// NotFound 组件中设置状态码
const NotFound = ({ staticContext }) => {
if (staticContext) {
staticContext.status = 404;
}
// ...
};
// 服务器端处理
res.status(context.status || 200);
5.3、 重定向处理
// 在路由组件中执行重定向
import { Navigate } from 'react-router-dom';
const ProtectedRoute = () => {
const isAuthenticated = false;
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Dashboard />;
};
// 服务器端处理重定向
if (context.url) {
return res.redirect(301, context.url);
}
5.4、 数据预取(高级用法)
// 在路由组件上添加静态方法
User.fetchData = async ({ params }) => {
const { id } = params;
// 实际项目中会调用 API
return {
id,
name: `用户 ${id}`,
email: `user${id}@example.com`,
joinDate: '2023-01-15'
};
};
// 服务器端数据预取
const matchRoutes = matchRoutes(routes, req.url);
const promises = matchRoutes.map(({ route }) => {
return route.element.type.fetchData
? route.element.type.fetchData({ params: match.params })
: Promise.resolve(null);
});
const data = await Promise.all(promises);
六、StaticRouter
部署配置
6.1、 构建脚本 (package.json)
json
{
"scripts": {
"build:client": "webpack --config webpack.client.config.js",
"build:server": "webpack --config webpack.server.config.js",
"start": "node build/server.js",
"dev": "nodemon --watch server --exec babel-node server/server.js"
}
}
6.2、 Webpack 客户端配置 (webpack.client.config.js
)
const path = require('path');
module.exports = {
entry: './client/index.js',
output: {
path: path.resolve(__dirname, 'build'),
filename: 'client_bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
}
]
},
resolve: {
extensions: ['.js', '.jsx']
}
};
6.3、 Webpack 服务器配置 (webpack.server.config.js
)
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
entry: './server/server.js',
target: 'node',
externals: [nodeExternals()],
output: {
path: path.resolve(__dirname, 'build'),
filename: 'server.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react']
}
}
}
]
},
resolve: {
extensions: ['.js', '.jsx']
}
};
七、StaticRouter 性能优化技巧
- 组件缓存:使用
react-ssr-prepass
进行组件级缓存 - 流式渲染:使用
renderToNodeStream
替代renderToString
- 代码分割:配合
React.lazy
和Suspense
实现代码分割 - 数据缓存:在服务器端缓存
API
响应 - HTTP/2 推送:推送关键资源加速加载
八、常见问题解决方案
8.1、 客户端-服务器渲染不匹配
解决方案:
- 确保服务器和客户端使用相同的路由配置
- 避免在渲染中使用浏览器特定 API
- 使用
React.StrictMode
检测问题
8.2、 数据预取复杂
解决方案:
- 使用
React Router
的loader
函数(v6.4+) - 采用
Redux
或React Query
管理数据状态 - 实现统一的数据获取层
8.3、 样式闪烁
解决方案:
- 使用 CSS-in-JS 库(如 styled-components)提取关键 CSS
- 实现服务器端样式提取
- 使用 CSS 模块避免类名冲突
九、总结
StaticRouter
是 React Router 为服务器端渲染提供的核心工具,它解决了 SSR
中的关键问题:
- 路由匹配:根据请求 URL 确定渲染内容
- 状态同步:通过 context 对象在服务器和客户端传递状态
- HTTP 控制:设置正确的状态码和重定向
- 性能优化:加速首屏渲染,提升用户体验
服务器端渲染是现代 Web 应用的重要技术,它能显著提升应用的性能和 SEO
表现。