You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Router v6+Express SSR问题求助:仅首屏可渲染

问题:Express + React Router v6 SSR 页面渲染异常

用Express实现React服务端渲染(SSR)时,仅首页能正常渲染,其他页面均无法显示。排查后确定问题出在初始渲染阶段未正确注入路由,目前卡在这一步。技术栈是React Router v6(客户端路由)+ Express Router(服务端路由)。


服务端路由代码

//imports

//-Initial Render-//
router.get('*', initial_render);
router.get("^/$", initial_render);

//-Public-//
router.get('/', public_home);
router.get('/About', about);
router.get('/GrowYourBusiness', grow_your_business);
router.get('/Contact', contact);
router.get('/Login', login)
router.get('/NewUser', new_user)
//logout

//-Client-//  
router.get('/api', client_view);
router.get('/api/Account', account_info);
router.get('/api/SubmitNewLoan', submit_new_loan);
router.get('/api/ActiveLoans', active_loan);
router.get('/api/ActiveLoans/ActiveLoanOverview', active_Loverview);
router.get('/api/InactiveLoans', inactive_loan);
router.get('/api/InactiveLoans/InactiveLoanOverview', inactive_Loverview);

客户端路由代码

export const Router = createBrowserRouter([
    {
        path: '/',
        exact: true,
        element: <Public />,
        errorElement : <Error />,
        children: [
            {
                path: '/About', 
                element: <About />
            }, 
            {
                path: '/GrowYourBusiness', 
                element: <GrowYourBusiness />
            }, 
            {
                path: '/Contact', 
                element: <Contact />
            },
            {
                path: '/Login',
                element: <Login />
            },
            {
                path: '/NewUser',
                element: <NewUser />
            } 

        ],
        path: '/api', 
        exact: true,
        element: <Client />,
        errorElement : <Error />,
        children: [
            {
                path: '/api/SubmitNewLoan',
                element: <SubmitNewLoan />
            }, 
            {
                path: '/api/ActiveLoans', 
                element: <ActiveLoans />
            },
            {
                path: '/api/Account', 
                element: <Account />
            } 
        ] 
    }
])

const domContainer = document.querySelector('#root');
ReactDOM.hydrateRoot(domContainer,(
    <StrictMode>
        <RouterProvider router = {Router} /> 
    </StrictMode>
));

初始渲染代码

export const initial_render = (req, res, next) => {
    fs.readFile(path.resolve('./public/index.html'),(err, data) => {
            const context = {Error};
            if(err) {
                console.log(err.message);
                return res.send(500).end('Internal Server Error');
            }
            if (context.url) {
                redirect(301, context.url);
            } else {
                const matches = router.reduce((matches, route) => {
                const match = matchPath(req.url, route.path, route) 
                if (match) {
                    matches.push({
                        route, match,
                        promose: route.element.fetchData ?
                        route.element.fetchData(match) : Promise.resolve(null)
                    })
                }
                return matches  
                },[])
                if (matches.length === 0 ) {
                    res.status(404)
                }
                const promise = matches.map((match) => match.promise)
                Promise.all(promise).then((...data) => {
                    content = {}
                    return res.send(data.toString().replace(
                        '<div id="root"></div>', 
                        `<div id="root">${ReactDOMServer.renderToString(
                                <StaticRouter location={req.url} context ={content}>
                                   <App routes={router} initialData={data}/>
                                </StaticRouter>
                        )}</div>`))
                }), 
                (error) => {
                    handleError(res, error)
                }
            }
        }
    )}
        

export default initial_render

当前遇到的错误

  • 使用客户端路由时,报错document is not defined——服务端渲染无浏览器环境,直接执行客户端路由代码会访问不存在的document对象。
  • 使用服务端路由时,报错reduce is not a function——Express Router实例不是数组,无法直接调用reduce方法,转数组尝试无效。

解决方向与步骤

1. 统一路由配置,避免双路由分离

React Router v6的SSR核心是共享路由配置数组,不能分别用Express Router和createBrowserRouter写两套。先创建通用路由文件routes.js:

// routes.js
import Public from './components/Public';
import About from './components/About';
// 其他组件导入...

export const routes = [
  {
    path: '/',
    element: <Public />,
    errorElement: <Error />,
    children: [
      { path: 'About', element: <About /> },
      { path: 'GrowYourBusiness', element: <GrowYourBusiness /> },
      // 其他公共子路由...
    ]
  },
  {
    path: '/api',
    element: <Client />,
    errorElement: <Error />,
    children: [
      { path: 'SubmitNewLoan', element: <SubmitNewLoan /> },
      { path: 'ActiveLoans', element: <ActiveLoans /> },
      // 其他客户端子路由...
    ]
  }
];

2. 精简服务端路由逻辑

Express只保留API接口路由,所有页面请求统一交给SSR处理:

// 保留API路由
router.get('/api/Account', account_info);
router.get('/api/SubmitNewLoan', submit_new_loan);
// 其他API路由...

// 所有页面请求交给SSR处理
router.get('*', initial_render);

3. 修复初始渲染代码

用共享路由数组替换Express Router,使用React Router v6官方服务端API处理路由匹配:

import { routes } from './routes';
import { createStaticHandler, StaticRouterProvider } from 'react-router-dom/server';

export const initial_render = (req, res, next) => {
  fs.readFile(path.resolve('./public/index.html'), async (err, data) => {
    if (err) {
      console.log(err.message);
      return res.status(500).end('Internal Server Error');
    }

    // 创建服务端路由处理器,匹配当前请求
    const handler = createStaticHandler(routes);
    const context = await handler.query(req.url);

    // 处理重定向
    if (context.status === 301 || context.status === 302) {
      return res.redirect(context.status, context.headers.get('Location'));
    }

    // 处理404
    if (context.status === 404) {
      res.status(404);
    }

    // 渲染React组件为字符串
    const html = ReactDOMServer.renderToString(
      <StaticRouterProvider handler={handler} context={context}>
        <App initialData={context.data} />
      </StaticRouterProvider>
    );

    // 替换index.html中的root节点
    const finalHtml = data.toString().replace(
      '<div id="root"></div>',
      `<div id="root">${html}</div>`
    );

    res.send(finalHtml);
  });
};

4. 同步客户端路由配置

客户端直接使用共享路由数组创建BrowserRouter:

import { routes } from './routes';

export const Router = createBrowserRouter(routes);

const domContainer = document.querySelector('#root');
ReactDOM.hydrateRoot(domContainer,(
  <StrictMode>
    <RouterProvider router={Router} /> 
  </StrictMode>
));

5. 处理组件的浏览器API依赖

组件中若有document、window调用,需包裹在环境判断或useEffect中:

// 用useEffect延迟执行浏览器环境代码
useEffect(() => {
  document.title = 'About Page';
}, []);

// 或直接判断环境
if (typeof window !== 'undefined') {
  // 仅浏览器环境执行的代码
}

内容的提问来源于stack exchange,提问作者user123

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 12:05:38