React 第六十六节Router中 StaticRouter使用详解及注意事项

发布于:2025-06-30 ⋅ 阅读:(22) ⋅ 点赞:(0)

前言

StaticRouterReact Router 为服务器端渲染(SSR)提供的专用路由组件。它允许在服务器环境中处理路由逻辑,确保服务器和客户端渲染结果一致。下面我将详细解释其用途、原理并提供完整的代码示例。

一、StaticRouter 的核心用途

  1. 服务器端渲染(SSR):在 Node.js 服务器上预渲染 React 应用
  2. SEO 优化:生成可被搜索引擎索引的完整 HTML
  3. 性能提升:加速首屏加载时间
  4. 路由状态同步:确保服务器和客户端渲染结果一致
  5. HTTP 状态码控制:根据路由返回正确的状态码(如 404)

二、StaticRouter与客户端路由器的区别

在这里插入图片描述

三、StaticRouter工作原理

StaticRouter 的核心机制:

  1. 接收请求 URL 作为 location 属性
  2. 使用 context 对象收集渲染过程中的路由信息
  3. 根据路由配置渲染对应的组件树
  4. 将渲染结果和 context 信息返回给服务器
  5. 服务器根据 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 性能优化技巧

  1. 组件缓存:使用 react-ssr-prepass 进行组件级缓存
  2. 流式渲染:使用 renderToNodeStream 替代 renderToString
  3. 代码分割:配合 React.lazySuspense 实现代码分割
  4. 数据缓存:在服务器端缓存 API 响应
  5. HTTP/2 推送:推送关键资源加速加载

八、常见问题解决方案

8.1、 客户端-服务器渲染不匹配

解决方案:

  1. 确保服务器和客户端使用相同的路由配置
  2. 避免在渲染中使用浏览器特定 API
  3. 使用 React.StrictMode 检测问题

8.2、 数据预取复杂

解决方案:

  1. 使用 React Routerloader 函数(v6.4+)
  2. 采用 ReduxReact Query 管理数据状态
  3. 实现统一的数据获取层

8.3、 样式闪烁

解决方案:

  1. 使用 CSS-in-JS 库(如 styled-components)提取关键 CSS
  2. 实现服务器端样式提取
  3. 使用 CSS 模块避免类名冲突

九、总结

StaticRouter 是 React Router 为服务器端渲染提供的核心工具,它解决了 SSR 中的关键问题:

  1. 路由匹配:根据请求 URL 确定渲染内容
  2. 状态同步:通过 context 对象在服务器和客户端传递状态
  3. HTTP 控制:设置正确的状态码和重定向
  4. 性能优化:加速首屏渲染,提升用户体验

服务器端渲染是现代 Web 应用的重要技术,它能显著提升应用的性能SEO 表现。


网站公告

今日签到

点亮在社区的每一天
去签到