React 第六十六节Router中 StaticRouter使用详解及注意事项
前言
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 () => {// 实际项目中会调用 APIconst 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>`);// 发送完整 HTMLres.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;// 实际项目中会调用 APIreturn {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
表现。