前三篇文章我们从源码层面剖析了 vue-router、react-router、tanstack-router 的底层实现。

这篇文章换个角度——从造工具的人的视角,聊聊我在构建 create-lve 这个前端脚手架 CLI 时,路由选型踩过的坑、走过的弯路,以及最终的取舍。


一、起点:React Router + glob 自动发现

create-lve 的第一个 React 模板用的是 React Router 的 library modecreateBrowserRouter),搭配 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 frameworkTanStack 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 RouterReact Router v8
布局路由_auth/route.tsx_auth/route.tsx
动态参数$userId.tsx$userId.tsx
类型生成routeTree.gen.ts+types/home.ts
索引路由index.tsxindex()

连命名都一样。这不是巧合——行业共识正在形成:文件系统就是最好的路由配置。

过去我们手写路由数组,后来用 import.meta.glob 自动发现(我旧模板的做法),现在两个框架都把它内置了。造轮子的时代结束了。


四、做减法:删掉 Astryx 模板

create-lve 最初有三个模板:

  • template-react-tanstackrouter:React + TanStack Router + Tailwind
  • template-react-astxy:React + Astryx 设计系统 + StyleX
  • template-vue-rekaui:Vue 3 + Reka UI

后来我删掉了 Astryx 模板。原因很简单:

  • Astryx 和 StyleX 都比较小众,用户基数太小
  • 维护三个模板的成本 > 收益
  • 精简模板数量对用户来说反而更好选

最终保留两个

模板定位
template-reactReact 主力模板(TanStack Router + Tailwind)
template-vueVue 生态覆盖(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/reactjotai 这些重依赖,全都混在 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 设计系统很酷,但用户太少了。

最终留下的是:能跑、够用、好维护的方案。

这大概就是工程的本质——不是追求最优解,而是在约束条件下找到最不坏的解

工具是用来解决问题的,不是用来折腾工具本身的。


系列目录

  1. 三套路由器,同一个 History
  2. vue-router 源码剖析
  3. react-router 源码剖析
  4. tanstack-router 源码剖析
  5. 从 glob 扫描到文件路由:我在 create-lve 中的路由选型之路(本文)