前三篇文章我们从源码层面剖析了 vue-router、react-router、tanstack-router 的底层实现。
这篇文章换个角度——从造工具的人的视角,聊聊我在构建 create-lve 这个前端脚手架 CLI 时,路由选型踩过的坑、走过的弯路,以及最终的取舍。
一、起点:React Router + glob 自动发现
create-lve 的第一个 React 模板用的是 React Router 的 library mode(createBrowserRouter),搭配 vite-plus 作为构建工具。
路由注册方式是这样的:
// router.tsx
const routeModules = import.meta.glob<{ default: FeatureRoute[] }>('../features/*/routes.ts')
async function loadFeatureRoutes(): Promise<FeatureRoute[]> {
const routes: FeatureRoute[] = []
for (const [path, loader] of Object.entries(routeModules)) {
const mod = await loader()
if (mod.default) {
routes.push(...mod.default)
}
}
return routes
}
每个 feature 目录自包含路由定义:
src/features/
├── home/
│ ├── routes.ts ← 路由定义
│ └── pages/index.tsx ← 页面组件
└── settings/
├── routes.ts
└── pages/
设计初衷:加一个 feature 目录就自动注册路由,零配置,零改动中心文件。
实际效果:确实能跑,feature 内聚性也很高——路由和组件放在一起,不用在 routes/ 和 features/ 之间跳来跳去。
但问题也来了:
import.meta.glob是运行时解析,没有类型安全——路径拼错了只有跑起来才知道- vite-plus 基于 rolldown,当时缺少 macOS ARM64 原生绑定,我的开发机装不上
- 自己造的文件路由轮子,不如框架内置的成熟
二、分岔口:React Router v8 还是 TanStack Router?
vite-plus 跑不起来之后,我面临两个选择:
选项 A:React Router v8 framework mode
// react-router.config.ts
export default { ssr: true } satisfies Config
// app/routes.ts
export default [index('routes/home.tsx')] satisfies RouteConfig
React Router v8 的 framework mode 已经内置了文件路由、类型生成(Route.MetaArgs)、SSR 支持。官方帮你造好了轮子。
选项 B:TanStack Router
// src/routes/dashboard/users/$userId.tsx
// 文件即路由,类型自动生成到 routeTree.gen.ts
TanStack Router 的文件路由同样内置了类型安全,而且专注于客户端 SPA。
我的考量
| 维度 | React Router v8 framework | TanStack Router |
|---|---|---|
| SSR | ✅ 开箱即用 | ❌ 需要 TanStack Start |
| 类型安全 | 中等(Route.MetaArgs) | 极致(routeTree.gen.ts) |
| 轻量程度 | 重(@react-router/node 等) | 轻 |
| 适用场景 | 全栈框架 | 客户端 SPA |
| 学习成本 | 低 | 中等 |
我的场景:脚手架模板,主要是 SPA,不需要 SSR。
结论:TanStack Router 更合适。
但说实话,当时做这个决定的时候我没想那么多——主要是 vite-plus 跑不起来,而 TanStack Router 的模板已经在跑了,能跑的方案就是好方案。
三、后来发现:framework mode 都在趋同
后来看了 React Router v8 的 framework mode,我发现一个有意思的现象:
两套框架的文件路由约定几乎一样了。
| 约定 | TanStack Router | React Router v8 |
|---|---|---|
| 布局路由 | _auth/route.tsx | _auth/route.tsx |
| 动态参数 | $userId.tsx | $userId.tsx |
| 类型生成 | routeTree.gen.ts | +types/home.ts |
| 索引路由 | index.tsx | index() |
连命名都一样。这不是巧合——行业共识正在形成:文件系统就是最好的路由配置。
过去我们手写路由数组,后来用 import.meta.glob 自动发现(我旧模板的做法),现在两个框架都把它内置了。造轮子的时代结束了。
四、做减法:删掉 Astryx 模板
create-lve 最初有三个模板:
template-react-tanstackrouter:React + TanStack Router + Tailwindtemplate-react-astxy:React + Astryx 设计系统 + StyleXtemplate-vue-rekaui:Vue 3 + Reka UI
后来我删掉了 Astryx 模板。原因很简单:
- Astryx 和 StyleX 都比较小众,用户基数太小
- 维护三个模板的成本 > 收益
- 精简模板数量对用户来说反而更好选
最终保留两个:
| 模板 | 定位 |
|---|---|
template-react | React 主力模板(TanStack Router + Tailwind) |
template-vue | Vue 生态覆盖(Reka UI + Pinia) |
一 React 一 Vue,覆盖主流场景,够了。
五、优化:chunk 分组要和依赖对应
模板精简之后,我又检查了 vite.config.ts 的 build 配置。
原来的 chunk 分组只覆盖了核心依赖:
groups: [
{ name: 'vendor-react-dom', test: /node_modules[\/]react-dom/, priority: 40 },
{ name: 'vendor-react', test: /node_modules[\/]react(?!-dom)/, priority: 35 },
{ name: 'vendor-router', test: /node_modules[\/]@tanstack[\/]react-router/, priority: 30 },
{ name: 'vendor-query', test: /node_modules[\/]@tanstack[\/]react-query/, priority: 25 },
{ name: 'vendor', test: /node_modules/, priority: 10 },
]
但模板还有 @tanstack/react-virtual、@base-ui/react、jotai 这些重依赖,全都混在 vendor 里。补上之后:
groups: [
{ name: 'vendor-react-dom', test: /node_modules[\/]react-dom/, priority: 40 },
{ name: 'vendor-react', test: /node_modules[\/]react(?!-dom)/, priority: 35 },
{ name: 'vendor-router', test: /node_modules[\/]@tanstack[\/]react-router/, priority: 30 },
{ name: 'vendor-query', test: /node_modules[\/]@tanstack[\/]react-query/, priority: 25 },
{ name: 'vendor-tanstack', test: /node_modules[\/]@tanstack/, priority: 22 }, // 新增
{ name: 'vendor-ui', test: /node_modules[\/]@base-ui/, priority: 20 }, // 新增
{ name: 'vendor-state', test: /node_modules[\/]jotai/, priority: 18 }, // 新增
{ name: 'vendor', test: /node_modules/, priority: 10 },
]
原则:重依赖单独分包,小工具库兜底在 vendor 里。小库单独拎出来反而增加 HTTP 请求数,得不偿失。
六、复盘:选型的本质是做减法
回头看这一路:
React Router lib mode + glob 自动发现
↓ vite-plus 跑不起来
TanStack Router 文件路由
↓ 发现 framework mode 也在趋同
确认 TanStack Router 是 SPA 最优解
↓ 精简模板
删掉 Astryx,保留 React + Vue 两个模板
↓ 优化构建
chunk 分组和依赖对齐
每一步都是在做减法。
选型不是选"最好的",而是选"最适合当前场景的"。 React Router v8 framework mode 很好,但我用不上 SSR;glob 自动发现很灵活,但没有类型安全;Astryx 设计系统很酷,但用户太少了。
最终留下的是:能跑、够用、好维护的方案。
这大概就是工程的本质——不是追求最优解,而是在约束条件下找到最不坏的解。
工具是用来解决问题的,不是用来折腾工具本身的。
系列目录
- 三套路由器,同一个 History
- vue-router 源码剖析
- react-router 源码剖析
- tanstack-router 源码剖析
- 从 glob 扫描到文件路由:我在 create-lve 中的路由选型之路(本文)