TanStack Router 插件实战指南:文件路由生成与自动代码分割(@tanstack/router-plugin)
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
本文以本仓库中的@tanstack/router-plugin(v1.168.37)为核心,系统讲解 TanStack Router 官方打包器插件的安装接入、全部配置项、自动代码分割原理、路由重构工作流与高频踩坑点。读完本文,你将能够在 Vite、Webpack、Rspack、esbuild 四种构建体系中正确接入该插件,理解routeTree.gen.ts的生成机制与三个子插件的协作关系,并掌握autoCodeSplitting下按路由精细控制分割粒度的实战能力。
一、插件定位与核心职责
@tanstack/router-plugin是 TanStack Router 的打包器插件,通过 unplugin 统一封装,为 Vite、Webpack、Rspack、esbuild 提供两种核心能力:
- 文件路由生成(Route Generation):监听
routesDirectory下的路由文件,自动生成类型安全的routeTree.gen.ts; - 自动代码分割(Automatic Code Splitting):在构建期把路由文件按可配置的分组拆成懒加载 chunk,无需手写
createLazyRoute。
从仓库的 package.json 可以看到,插件通过./vite、./webpack、./rspack、./esbuild、./context等多个子路径导出入口,依赖@tanstack/router-generator(路由树生成器)、@tanstack/router-core(类型与路由核心)、chokidar(文件监听)与zod(配置校验),并可选 peer 依赖各框架插件(@tanstack/react-router、vite-plugin-solid等)。
CRITICAL:在 Vite 配置中,router 插件必须位于框架插件(React、Solid、Vue)之前。顺序错误会导致路由生成与代码分割静默失败——这一点在后文「工作原理」与「常见错误」中会给出源码级验证。
二、安装
在项目根目录安装为开发依赖:
npm install -D @tanstack/router-plugin本仓库使用 pnpm workspace,包名版本为1.168.37(见 packages/router-plugin/package.json),要求 Node>=20.19,支持 Vite>=5.0.0(含 6/7/8)与 Webpack>=5.92.0。
三、四种构建体系的接入配置
3.1 Vite(最常见)
// vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { tanstackRouter } from '@tanstack/router-plugin/vite' export default defineConfig({ plugins: [ // MUST come before react() tanstackRouter({ target: 'react', autoCodeSplitting: true, }), react(), ], })仓库中的真实 e2e 工程也遵循这一顺序,例如 e2e/react-router/basic-file-based/vite.config.js:
plugins: [ tailwindcss(), tanstackRouter({ target: 'react' }), react(), ],3.2 Webpack
// webpack.config.js const { tanstackRouter } = require('@tanstack/router-plugin/webpack') module.exports = { plugins: [ tanstackRouter({ target: 'react', autoCodeSplitting: true, }), ], }3.3 Rspack
// rspack.config.js const { tanstackRouter } = require('@tanstack/router-plugin/rspack') module.exports = { plugins: [ tanstackRouter({ target: 'react', autoCodeSplitting: true, }), ], }3.4 esbuild
import { tanstackRouter } from '@tanstack/router-plugin/esbuild' import esbuild from 'esbuild' esbuild.build({ plugins: [ tanstackRouter({ target: 'react', autoCodeSplitting: true, }), ], })说明:各入口都是同一份 unplugin 工厂函数的打包器适配,导出路径与 package.json 的
exports字段一一对应。
四、配置项全解
插件的配置由 core/config.ts 中的 zod schema 统一校验,分为核心、文件约定、代码分割、输出四组。
4.1 核心选项(Core Options)
| Option | Type | Default | Description |
|---|---|---|---|
target | 'react' \| 'solid' \| 'vue' | 'react' | 目标框架 |
routesDirectory | string | './src/routes' | 路由文件所在目录 |
generatedRouteTree | string | './src/routeTree.gen.ts' | 生成的路由树文件路径 |
autoCodeSplitting | boolean | undefined | 是否启用自动代码分割 |
enableRouteGeneration | boolean | true | 设为false可关闭路由生成 |
其中target会直接决定插件编译时使用哪个框架的标识符与产物。源码 code-splitter/framework-options.ts 中维护了三套映射:React 对应@tanstack/react-router、Solid 对应@tanstack/solid-router、Vue 对应@tanstack/vue-router,标识符均为createFileRoute、lazyFn、lazyRouteComponent;传入不支持的框架会直接抛出Unsupported framework错误。
routesDirectory支持绝对路径,相对路径会以构建根目录(process.cwd()或 Vite 的config.root)为基准拼接,见 router-generator-plugin.ts。
4.2 文件约定选项(File Convention Options)
| Option | Type | Default | Description |
|---|---|---|---|
routeFilePrefix | string | undefined | 路由文件前缀过滤 |
routeFileIgnorePrefix | string | '-' | 以该前缀开头的文件被排除出路由 |
routeFileIgnorePattern | string | undefined | 按正则模式排除文件 |
indexToken | string \| RegExp \| { regex: string; flags?: string } | 'index' | 标识 index 路由的 token |
routeToken | string \| RegExp \| { regex: string; flags?: string } | 'route' | 标识路由配置文件的 token |
routeFileIgnorePrefix默认值为'-',即形如-component.tsx这类以连字符开头的文件不会被当作路由;indexToken/routeToken支持字符串、正则或{ regex, flags }对象三种形态,用于自定义index路由与.route.tsx配置文件的识别规则。
4.3 输出选项(Output Options)
| Option | Type | Default | Description |
|---|---|---|---|
quoteStyle | 'single' \| 'double' | 'single' | 生成代码的引号风格 |
semicolons | boolean | false | 生成代码是否使用分号 |
disableTypes | boolean | false | 关闭生成的 TypeScript 类型 |
disableLogging | boolean | false | 关闭插件日志 |
addExtensions | boolean \| string | false | 为 import 添加文件扩展名 |
enableRouteTreeFormatting | boolean | true | 是否格式化生成的路由树 |
quoteStyle、semicolons等选项直接控制routeTree.gen.ts的生成风格,便于与团队 lint/prettier 规则对齐;disableTypes适用于纯 JS 工程(仓库js-only-file-based类 e2e 场景即属此类)。
五、自动代码分割:codeSplittingOptions
开启autoCodeSplitting: true后,可用codeSplittingOptions精细控制分割策略:
tanstackRouter({ target: 'react', autoCodeSplitting: true, codeSplittingOptions: { // 所有路由的默认分组 defaultBehavior: [['component'], ['errorComponent'], ['notFoundComponent']], // 按路由定制分割 splitBehavior: ({ routeId }) => { if (routeId === '/dashboard') { // 对 dashboard 路由,把 loader 和 component 放在同一个 chunk return [['loader', 'component'], ['errorComponent']] } // 返回 undefined 则回退到 defaultBehavior }, }, })5.1 分组(Groupings)的合法取值
源码 config.ts 中的splitGroupingsSchema约束了分组必须是「数组的数组」,内部元素只能取自五个路由节点:
loadercomponentpendingComponenterrorComponentnotFoundComponent
且元素在整个分组中不得重复(例如[['component'], ['component', 'loader']]会直接校验失败并给出错误信息)。同一分组内的节点会打进同一个 chunk,不同分组则各自独立懒加载。
5.2 默认分组与 loader 的取舍
源码 constants.ts 定义的默认分组为:
export const defaultCodeSplitGroupings = [ ['component'], ['errorComponent'], ['notFoundComponent'], ]即默认把component、errorComponent、notFoundComponent各自拆成独立 chunk,而loader默认不分割——loader 本身是异步函数,再拆一个 chunk 会造成「先拉 chunk、再执行 loader」的双重异步开销。只有确实需要时才把loader放入分组(如上例 dashboard)。
5.3 其他子选项
splitBehavior: ({ routeId }) => CodeSplitGroupings | undefined:按routeId编程式决定每个路由的分割方式,返回值会被 router-code-splitter-plugin.ts 用同一套 schema 校验,非法分组会抛错;deleteNodes: Array<'loader' | 'component' | ...>:从路由中删除指定节点(用于某些特殊场景);addHmr: boolean(默认true):非生产环境是否为分割后的路由注入 HMR 处理。
5.4 关键提醒:不要混用手动懒加载
开启autoCodeSplitting后,插件会在构建期自动改写你的路由文件,不要再手写createLazyRoute或lazyRouteComponent:
// WRONG —— autoCodeSplitting 开启时手动懒加载 const LazyAbout = lazyRouteComponent(() => import('./about')) // CORRECT —— 正常写路由文件即可,插件负责分割 // src/routes/about.tsx export const Route = createFileRoute('/about')({ component: AboutPage, }) function AboutPage() { return <h1>About</h1> }六、虚拟路由配置:virtualRouteConfig
除文件路由外,插件还支持以编程方式传入虚拟路由树:
import { routes } from './routes' tanstackRouter({ target: 'react', virtualRouteConfig: routes, // 或直接传 './routes.ts' 字符串路径 })virtualRouteConfig可接受一个路由配置数组或指向配置文件的路径字符串,适合路由由代码/服务端动态产出的场景。更完整的编程式路由树用法可参考 virtual-file-routes 技能文档。
七、工作原理:三个子插件的协作
组合插件(composed plugin)的装配逻辑位于 router-composed-plugin.ts:
const result = [ { name: 'tanstack:router-inline-css-defaults', ... }, // 内联 CSS 默认 define ...routerGenerator, // ① 路由生成器(始终存在) ] if (userConfig.autoCodeSplitting) { result.push(...routerCodeSplitter) // ② 代码分割器(可选) } if (!isProduction && !userConfig.autoCodeSplitting) { result.push(...routerHmr) // ③ HMR(开发态、分割关闭时) }Route Generator(始终启用):由 router-generator-plugin.ts 实现,基于
@tanstack/router-generator的Generator实例监听路由目录并生成routeTree.gen.ts。Vite 下在configResolved阶段初始化并首次生成,之后通过watchChange钩子按create/update/delete事件增量生成;Webpack/Rspack 下额外用chokidar监听路由目录——注释明确说明「webpack/rspack 的 watcher 不会注册新建文件」,因此必须自己补一个文件监听来处理新增路由。插件在入口处声明enforce: 'pre',确保在框架插件之前运行。Code Splitter(
autoCodeSplitting: true时启用):由 router-code-splitter-plugin.ts 实现,内部再拆成三个 transform 插件:compile-reference-file:编译原始路由文件,检测分组并产出引用文件;compile-virtual-file:处理以?tsr-split为标识的虚拟模块,按分组生成懒加载 chunk;compile-shared-file:处理?tsr-shared标识的共享模块,抽出多个分组共用的绑定(sharedBindingsMap在编译间传递)。
虚拟模块标识常量定义于 constants.ts:
tsrSplit = 'tsr-split'、tsrShared = 'tsr-shared'。HMR(开发态且未开启分割时启用):由 router-hmr-plugin.ts 实现,向路由文件注入
createRouteHmrStatement的 HMR 处理语句;开启分割后 HMR 由 code splitter 自身接管(addHmr默认开启)。
7.1 插件顺序的源码级校验
值得注意的是,code splitter 并不只依赖「约定」,还会在 ViteconfigResolved阶段主动校验插件顺序:若检测到@vitejs/plugin-react、@vitejs/plugin-react-swc、@vitejs/plugin-react-oxc或vite-plugin-solid排在 router 插件之前,会直接抛出Plugin order error并给出修正后的配置示例(见 router-code-splitter-plugin.ts)。因此在较新版本中,「顺序错误静默失败」已升级为「构建期显式报错」,更易排查。
八、按需使用独立子插件
Vite 入口(vite.ts)除组合插件外,还导出各子插件,适合只想用其中一部分能力的进阶场景:
import { tanstackRouter, // 组合插件(默认,推荐) tanstackRouterGenerator, // 仅路由生成 tanStackRouterCodeSplitter, // 仅代码分割 } from '@tanstack/router-plugin/vite'注意:tanStackRouterCodeSplitter依赖生成器产出的routesByFile映射(routerPluginContext),独立使用时需保证生成器同时运行;TanStackRouterVite为已弃用的旧名称,请使用tanstackRouter。
九、路由重构工作流(Route Refactor Workflow)
移动、重命名、新增或删除文件路由时,按以下流程操作:
- 只改源文件:在
routesDirectory下修改路由文件,保持导出的路由标识符名为Route; - 重新生成:让插件自动重新生成路由树;若项目使用 CLI,可运行
pnpm exec tsr generate手动触发; - 审查生成 diff:检查生成的
routeTree.gen.tsdiff 是否符合预期的 route ID、父路由、路径与 import。永远不要手工修复routeTree.gen.ts; - 同步外围引用:更新指向旧路由的链接、redirect、
from类型收窄、params、preload 调用及相关测试; - 跑完整验证:运行路由生成测试、类型测试与生产构建。编辑器类型检查通过 ≠ 插件正确生成了新路由,必须以构建产物为准。
routeTree.gen.ts是运行时实际使用的生成源码,应当提交进版本库。
十、常见错误与排查
10.1 CRITICAL:Vite 配置中插件顺序错误
router 插件必须位于框架插件之前,否则路由生成与代码分割会失败(新版会在构建期显式抛错,见 7.1):
// WRONG —— react() 在 tanstackRouter() 之前 plugins: [react(), tanstackRouter({ target: 'react' })] // CORRECT —— tanstackRouter() 在前 plugins: [tanstackRouter({ target: 'react' }), react()]10.2 HIGH:非 React 框架未指定 target
target默认是'react',Solid 或 Vue 必须显式指定,否则生成的 import 与分割逻辑全部错乱:
// WRONG for Solid —— 会生成 React 的 import tanstackRouter({ autoCodeSplitting: true }) // CORRECT for Solid tanstackRouter({ target: 'solid', autoCodeSplitting: true })10.3 MEDIUM:混淆 autoCodeSplitting 与手动懒加载
开启autoCodeSplitting后插件会在构建期自动改写路由文件,不需要(也不应该)手动createLazyRoute/lazyRouteComponent,详见 5.4 的示例对比。
10.4 HIGH:手动编辑生成的路由树
对routeTree.gen.ts的任何手改都会在下次生成时被覆盖,并可能导致源路由、生成类型与运行时路由三者失步。正确做法是:修正路由文件名或插件配置 → 重新生成 → 审查 diff。
十一、延伸阅读
- router-core/code-splitting 技能文档:手动代码分割概念、
.lazy.tsx约定与getRouteApi用法,与本文的autoCodeSplitting互为补充; - virtual-file-routes 技能文档:编程式路由树与
virtualRouteConfig的完整用法; - 源码参考:组合插件装配、配置 schema、代码分割实现、路由生成实现、HMR 实现、Vite 入口与独立导出。
【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考