如何使用 react-router 动态构建 Routes
结论
不要让 API 返回前端组件的文件路径,也不要通过 require(PathToComponent) 加载任意路径。API 应返回稳定的 feature 标识,例如 paymentReconciliation。前端维护一份“feature 标识 → 动态 import”的注册表,并根据 API 配置生成 <Route>。
Webpack、Vite 等构建工具需要在构建阶段确定可能加载的模块,无法可靠分析 API 在运行时返回的任意字符串路径。
推荐实现:组件注册表配合 React.lazy
可以将 API 返回的数据调整为:
{
"links": [
{
"label": "Payment Reconciliation",
"feature": "paymentReconciliation",
"url": "/paymentreconciliation"
},
{
"label": "Device Integration",
"feature": "deviceIntegration",
"url": "/deviceintegration"
}
]
}
然后在前端建立注册表:
import React, { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
const featureRegistry = {
paymentReconciliation: lazy(() =>
import('./components/_paymentReconciliation/paymentReconciliationPage')
),
deviceIntegration: lazy(() =>
import('./components/_deviceIntegration/deviceIntegrationPage')
),
};
再根据 API 返回的配置生成路由:
export default function AppRoutes({ links }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{links.map((item) => {
const FeatureComponent = featureRegistry[item.feature];
if (!FeatureComponent) {
console.warn(`Unknown feature: ${item.feature}`);
return null;
}
return (
<Route
key={item.feature}
path={item.url}
element={<FeatureComponent />}
/>
);
})}
<Route path="*" element={<div>Page not found</div>} />
</Routes>
</Suspense>
);
}
对应的组件通常使用默认导出:
export default function PaymentReconciliationPage() {
return <div>Payment Reconciliation</div>;
}
如果组件使用命名导出,可以在动态导入时进行转换:
const featureRegistry = {
paymentReconciliation: lazy(() =>
import('./components/_paymentReconciliation/paymentReconciliationPage')
.then((module) => ({
default: module.PaymentReconciliationPage,
}))
),
};
在 React 启动前获取配置
如果必须在 bootstrapping React 之前取得配置,可以先请求 API,成功后再创建 React 根节点:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import AppRoutes from './AppRoutes';
async function bootstrap() {
const response = await fetch('/api/features');
if (!response.ok) {
throw new Error(`Failed to load features: ${response.status}`);
}
const config = await response.json();
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<AppRoutes links={config.links ?? []} />
</BrowserRouter>
);
}
bootstrap().catch((error) => {
console.error(error);
ReactDOM.createRoot(document.getElementById('root')).render(
<div>Unable to load application configuration.</div>
);
});
另一种做法是先渲染应用外壳,等请求完成后再更新路由配置。具体选择取决于加载期间是否需要显示登录页、骨架屏或错误页面。
为什么不能直接使用 API 返回的路径
下面这种写法通常不可取:
const Component = lazy(() => import(menuItem.component));
menuItem.component 是运行时变量,构建工具无法提前判断它可能指向哪些文件,因此目标模块可能不会被打包进产物。即使某些构建配置支持有限的动态路径,通常也只允许静态分析范围内的目录,例如:
import(`./pages/${pageName}.jsx`);
这种写法仍受构建工具的规则限制,还可能把目录中的大量模块都打进构建结果。它并不意味着 API 可以返回任意本地路径。
handler={require(PathToComponent)} 是旧式 React Router 或 CommonJS 场景中的写法。现代 React Router 不再使用 handler。React Router v6 的写法是:
<Route path="/example" element={<ExamplePage />} />
require() 同样受模块打包阶段的静态分析限制,不能安全地把服务器返回的任意字符串当作模块地址。
使用 React Router 数据路由
如果项目使用 React Router 的数据路由 API,也可以通过路由对象的 lazy 属性加载模块,但仍建议保留注册表:
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const routeLoaders = {
paymentReconciliation: () =>
import('./routes/paymentReconciliationRoute'),
deviceIntegration: () =>
import('./routes/deviceIntegrationRoute'),
};
function createFeatureRouter(links) {
return createBrowserRouter([
...links.flatMap((item) => {
const loader = routeLoaders[item.feature];
if (!loader) {
return [];
}
return [
{
path: item.url,
lazy: loader,
},
];
}),
{
path: '*',
element: <div>Page not found</div>,
},
]);
}
export default function App({ links }) {
const router = createFeatureRouter(links);
return <RouterProvider router={router} />;
}
路由模块可以这样导出:
export function Component() {
return <div>Payment Reconciliation</div>;
}
具体的导出约定取决于项目使用的 React Router 版本,应以当前版本的数据路由文档为准。
如果 feature 数量很多
Vite 项目可以使用 import.meta.glob 收集指定目录中的模块:
import React, { lazy } from 'react';
const pageModules = import.meta.glob('./components/**/*Page.jsx');
function getLazyComponent(modulePath) {
const loader = pageModules[modulePath];
return loader ? lazy(loader) : null;
}
不过,后端仍不应直接控制 modulePath。更稳妥的方式是由前端将 feature 标识转换为路径:
const featurePaths = {
paymentReconciliation:
'./components/_paymentReconciliation/paymentReconciliationPage.jsx',
deviceIntegration:
'./components/_deviceIntegration/deviceIntegrationPage.jsx',
};
function resolveFeature(feature) {
const path = featurePaths[feature];
const loader = path ? pageModules[path] : null;
return loader ? lazy(loader) : null;
}
import.meta.glob 是 Vite 提供的构建能力,不是标准 JavaScript 或 React Router API。Webpack 项目需要使用对应的机制,或者继续采用显式注册表。
注意事项
- API 最好只返回
feature、url、label等业务配置,不要暴露前端源代码的目录结构。 - 动态 import 只负责按需加载代码块,这些模块仍须在构建时包含在部署产物中。
- 遇到无法识别的 feature 时,应跳过并记录错误,或者显示受控的错误页面,不能直接尝试执行服务器返回的路径。
- subscription 配置只能控制界面和路由是否显示,不能替代后端权限校验。相关 API 仍需在服务端验证用户权限。
url也需要校验,防止出现重复路由、无效格式或意外覆盖固定路由。- 如果希望在部署后不重新构建前端也能添加全新的功能模块,普通动态 import 无法满足要求,需要采用 Module Federation、远程模块或插件系统。这是另一套架构,还要处理模块签名、版本兼容和远程代码信任问题。
对于常规的订阅功能开关,组件注册表简单而可靠:后端决定启用哪些 feature,前端决定每个 feature 对应哪段可执行代码。
备注:内容仅供参考。
