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
相关产品推荐
相关产品推荐

