TanStack Router 代码分割实战指南:从 autoCodeSplitting 到 .lazy 手动拆分与加载器分割
TanStack Router 代码分割实战指南从 autoCodeSplitting 到 .lazy 手动拆分与加载器分割【免费下载链接】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代码分割Code Splitting与懒加载Lazy Loading是提升 Web 应用包体积与加载性能的关键技术。TanStack Router 围绕关键路由配置与非关键路由配置做了精细的切分设计并提供了从全自动 AST 变换、.lazy.tsx文件后缀到虚拟路由Virtual Routes的多种落地方式。读完本篇你将掌握如何在vite.config.ts中一行配置开启自动代码分割、理解插件在 dev/build 阶段的 Reference File / Virtual File 双产物机制、通过defaultBehavior/splitBehavior/codeSplitGroupings三级优先级自定义 chunk 分组以及在代码路由code-based routing场景下用createLazyRoute、Route.lazy()、lazyFn、getRouteApi完成完整的类型安全拆分。一、为什么要做代码分割TanStack Router 文档将代码分割与懒加载的收益归纳为三点减少首屏加载量降低初始页面加载所需代码的体积按需加载代码在其真正被需要时才加载更小的缓存单元切分后产生更多更小的 chunk浏览器更容易增量缓存更新时只需重新下载变化的部分。二、TanStack Router 如何切分路由代码TanStack Router 将每个路由文件的配置划分为两大类1. 关键路由配置Critical Route Configuration——渲染当前路由、尽早启动数据加载流程所必需的代码路径解析/序列化Path Parsing/Serialization搜索参数校验Search Param Validation加载器Loaders与beforeLoad路由上下文Route Context静态数据Static Data链接Links、脚本Scripts、样式Styles上述未列出的所有其他路由配置2. 非关键/懒加载路由配置Non-Critical/Lazy Route Configuration——路由匹配不需要、可以按需加载的代码component路由组件errorComponent错误组件pendingComponent等待/加载组件notFoundComponent未找到组件从源码结构看这两类配置恰好对应插件中可切分的节点白名单。在 常量定义 中splitRouteIdentNodes明确了只有五个属性参与自动分割export const splitRouteIdentNodes [ loader, component, pendingComponent, errorComponent, notFoundComponent, ] as const export type SplitRouteIdentNodes (typeof splitRouteIdentNodes)[number] export type CodeSplitGroupings ArrayArraySplitRouteIdentNodes export const defaultCodeSplitGroupings: CodeSplitGroupings [ [component], [errorComponent], [notFoundComponent], ]注意默认分组里不包含loader——这正是官方立场的体现。为什么默认不分割 loaderWhy is the loader not split?loader 本身就是一个异步边界。如果再把 loader 切成独立 chunk你既要等待下载 chunk、又要等待 loader 执行等于双重付出pay double先拿 chunk再等 loader 跑完。从类别上讲loader 对包体积的贡献通常小于组件代码。loader 是路由最重要的可预加载preloadable资产之一——尤其当你使用默认的预加载意图例如鼠标悬停链接时loader 必须没有任何额外异步开销就能就位。知道分割 loader 的代价后如果你仍然想这么做可以继续阅读下文 Data Loader Splitting 章节。三、把路由文件封装进目录TanStack Router 的文件路由file-based routing同时支持扁平与嵌套结构因此可以把一个路由的所有文件封装进同一个目录而无需任何额外配置把路由文件本身移动到与路由文件同名的目录中并改名为route.tsx.route文件。例如把posts.tsx封装为目录后BeforeBefore 状态posts.tsxAfterAfter 状态posts/route.tsx这个能力是后续代码分割的基础分割出来的.lazy文件、loader 文件、组件文件都可以放在同一个路由目录内保持结构整洁。四、代码分割的四种途径文件路由场景下TanStack Router 提供以下方案代码路由场景请跳至 Code-Based Splitting自动代码分割autoCodeSplitting——最省事、最强大.lazy.tsx后缀——手动两文件拆分Virtual Routes虚拟路由——路由文件被拆空后的自动生成锚点。五、自动代码分割autoCodeSplitting这是最简单、也最强大的代码分割方式。开启后TanStack Router 会按照上文非关键路由配置自动切分你的路由文件。重要限制自动代码分割仅在你使用文件路由 受支持打包器bundlersVite / esbuild / Rspack / webpack时可用如果只使用 CLItanstack/router-cli该功能不生效。相关前提见 文件路由指南。5.1 启用方式在 TanStack Router Bundler Plugintanstack/router-plugin的配置中加入autoCodeSplitting: true// vite.config.ts import { defineConfig } from vite import react from vitejs/plugin-react import { tanstackRouter } from tanstack/router-plugin/vite export default defineConfig({ plugins: [ tanstackRouter({ // ... autoCodeSplitting: true, }), react(), // 务必把该插件放在 TanStack Router Bundler 插件之后 ], })注意 React 插件或 Solid 的solid()插件必须排在 TanStack Router 插件之后——从源码看插件会在 框架 JSX 变换插件清单 中按框架react / solid声明这一顺序约束用于保证路由文件先完成切分变换、再交由框架做 JSX 编译。5.2 工作原理Reference File Virtual File自动代码分割在开发期development与构建期build都会对路由文件做 AST 变换插件通过静态代码分析把component、pendingComponent等属性改写成指向虚拟文件的懒加载包装器让打包器把这些属性归入独立 chunk。每个路由文件被处理后产生两个关键产物Reference File引用文件插件改写你的原始路由文件如posts.route.tsx把component等属性的取值替换为稍后才会拉取真实代码的懒加载包装器指向一个稍后由打包器解析的虚拟文件Virtual File虚拟文件当打包器请求该虚拟文件例如posts.route.tsx?tsr-splitcomponent时插件拦截请求并即时生成一个最小化文件只包含所请求属性的代码例如仅有PostsComponent。这一机制在源码中有直接对应查询参数常量tsrSplit tsr-split、tsrShared tsr-shared定义于 constants.ts拼接/剥离查询参数的逻辑见 addSplitSearchParamToFilename。而每个属性如何被包装则由 SPLIT_NODES_CONFIG 精确指定——loader采用lazyFn策略生成$$splitLoaderImporter动态 import组件类属性采用lazyRouteComponent策略二者分别导出为SplitLoader/SplitComponent等标识符。整体变换由 compileCodeSplitReferenceRoute 完成并在 RouterCodeSplitter 插件 中串联起分组检测、校验与虚拟文件编译。5.3 默认分割分组Split Groupings插件用分割分组Split Groupings决定路由各部分如何归组每个分组是若干属性名的数组会被打包进同一个懒加载 chunk。可参与分割的属性为component、errorComponent、pendingComponent、notFoundComponent、loader。默认分组为[ [component], [errorComponent], [notFoundComponent] ]即每个路由默认产生三个懒加载 chunk组件、错误组件、未找到组件各一个loader保留在初始 bundle 中。这与 defaultCodeSplitGroupings 的定义完全一致。5.4 分割规则不要导出路由属性自动分割有一条硬性规则component、loader等路由属性对应的符号不要从路由文件中导出。导出的属性会直接进入主 bundle从而无法被分割export const Route createRoute(/posts)({ // ... notFoundComponent: PostsNotFoundComponent, }) // ❌ 不要这样做导出该组件会阻止其被代码分割 // 它会被打进主 bundle。 export function PostsNotFoundComponent() { // ❌ // ... } function PostsNotFoundComponent() { // ✅ // ... }除此之外没有其他限制路由文件中可以使用任意的 JavaScript / TypeScript 特性。5.5 细粒度控制三级配置与优先级自动代码分割还提供多层次的自定义选项详见 Automatic Code Splitting 指南1全局行为defaultBehavior——定义不同属性如何归组。例如把所有 UI 组件合进一个 chunkimport { defineConfig } from vite import { tanstackRouter } from tanstack/router-plugin/vite export default defineConfig({ plugins: [ tanstackRouter({ autoCodeSplitting: true, codeSplittingOptions: { defaultBehavior: [ [ component, pendingComponent, errorComponent, notFoundComponent, ], // 把所有 UI 组件打进同一个 chunk ], }, }), ], })2程序化控制splitBehavior——针对复杂规则集可按routeId编程式决定分组import { defineConfig } from vite import { tanstackRouter } from tanstack/router-plugin/vite export default defineConfig({ plugins: [ tanstackRouter({ autoCodeSplitting: true, codeSplittingOptions: { splitBehavior: ({ routeId }) { // /posts 下的所有路由loader 与 component 打进同一 chunk if (routeId.startsWith(/posts)) { return [[loader, component]] } // 其余路由回落到 defaultBehavior }, }, }), ], })3路由级覆写codeSplitGroupings——在路由文件内直接覆写全局配置适合有特殊优化需求的个别路由import { loadPostsData } from ./-heavy-posts-utils export const Route createFileRoute(/posts)({ // 对该特定路由把 loader 与 component 打进同一个 chunk codeSplitGroupings: [[loader, component]], loader: () loadPostsData(), component: PostsComponent, }) function PostsComponent() { // ... }这会为该路由生成一个同时包含loader与component的单一 chunk覆盖默认行为与插件配置中的程序化行为。配置优先级从高到低路由级覆写路由文件中的codeSplitGroupings优先级最高程序化分割行为插件配置中的splitBehavior默认行为defaultBehavior作为兜底。从源码结构看这一优先级在 handleCompilingReferenceFile 中落实插件先通过detectCodeSplitGroupingsFromRoute检测路由代码内声明的分组再依次回落到splitBehavior与defaultCodeSplitGroupings且每一级都会经过splitGroupingsSchema的 Zod 校验非法分组会直接抛出带错误详情的异常。5.6 用 e2e 用例验证分割行为仓库中提供了现成的端到端工程用于验证自动代码分割在文件路由下的行为例如 e2e/react-router/basic-file-based-code-splittingReact 版与 e2e/solid-router/basic-file-based-code-splittingSolid 版可用于对照真实工程确认 chunk 输出。六、.lazy.tsx后缀手动拆分若无法使用自动代码分割可以用.lazy.tsx后缀手动完成分割——把代码移入带.lazy.tsx后缀的单独文件并用createLazyFileRoute替代createFileRoute即可。重要__root.tsx根路由文件无论用createRootRoute还是createRootRouteWithContext不支持代码分割因为它在当前路由无关的情况下总是被渲染。这一点在插件源码中同样有对应compilers.ts 将createRootRoute、createRootRouteWithContext列为unsplittableCreateRouteFns只有createFileRoute属于可分割工厂函数。createLazyFileRoute只支持以下选项导出名Export Name说明Descriptioncomponent路由要渲染的组件。errorComponent路由加载过程中发生错误时渲染的组件。pendingComponent路由加载期间渲染的组件。notFoundComponent抛出 not-found 错误时渲染的组件。6.1 前后对照示例Before单文件import { createFileRoute } from tanstack/react-router import { fetchPosts } from ./api export const Route createFileRoute(/posts)({ loader: fetchPosts, component: Posts, }) function Posts() { // ... }After拆为两个文件。此文件承载关键路由配置loader 等import { createFileRoute } from tanstack/react-router import { fetchPosts } from ./api export const Route createFileRoute(/posts)({ loader: fetchPosts, })非关键路由配置组件放入.lazy.tsx文件import { createLazyFileRoute } from tanstack/react-router export const Route createLazyFileRoute(/posts)({ component: Posts, }) function Posts() { // ... }Solid 用户只需把 import 来源换成tanstack/solid-routercreateFileRoute/createLazyFileRoute文件结构与 API 形态完全相同。七、Virtual Routes虚拟路由如果你把所有东西都从路由文件里拆分出去、路由文件变成空壳此时直接把该路由文件删掉即可文件路由系统会自动生成一个虚拟路由作为代码分割文件的锚点该虚拟路由直接生成在 route tree 文件中。Beforesrc/routes/posts.tsx只剩一个空配置src/routes/posts.lazy.tsximport { createFileRoute } from tanstack/react-router export const Route createFileRoute(/posts)({ // Hello? })import { createLazyFileRoute } from tanstack/react-router export const Route createLazyFileRoute(/posts)({ component: Posts, }) function Posts() { // ... }After删除空的posts.tsx只保留import { createLazyFileRoute } from tanstack/react-router export const Route createLazyFileRoute(/posts)({ component: Posts, }) function Posts() { // ... }这样路由树依然完整/posts匹配与数据加载不受影响而组件代码被隔离在独立的懒加载 chunk 中。仓库中的 e2e/react-router/basic-virtual-file-based 与 e2e/react-start/virtual-routes 工程覆盖了虚拟路由在不同构建链路下的行为。八、Code-Based Splitting代码路由场景如果你使用的是代码路由code-based routing可以用createLazyRoute函数与Route.lazy()方法完成分割需要把路由配置拆成两部分。第 1 步用createLazyRoute创建懒路由export const Route createLazyRoute(/posts)({ component: MyComponent, }) function MyComponent() { return divMy Component/div }第 2 步在app.tsx中调用路由定义的.lazy方法导入携带非关键配置组件的分割路由const postsRoute createRoute({ getParentRoute: () rootRoute, path: /posts, }).lazy(() import(./posts.lazy).then((d) d.Route))createRoute的关键配置路径、loader 等仍留在主 bundle仅component等通过动态import()按需加载。九、Data Loader Splitting加载器分割警告分割路由 loader 是一场危险的游戏。它是降低包体积的有力工具但代价如前文所述chunk 下载 loader 执行的双重等待。你可以利用路由的loader选项分割数据加载逻辑。虽然这会让传给 loader 的参数难以维持类型安全但泛型LoaderContext类型能帮你覆盖大部分场景import { lazyFn } from tanstack/react-router const route createRoute({ path: /my-route, component: MyComponent, loader: lazyFn(() import(./loader), loader), }) // 在另一个文件中... export const loader async (context: LoaderContext) { // ... }lazyFn接收一个返回 Promise 的函数和一个导出名返回的加载器会在首次调用时才触发import(./loader)。Solid 侧对应导入tanstack/solid-router。这与自动分割内部对loader属性采用的lazyFn拆分策略见 SPLIT_NODES_CONFIG在机制上是一致的。文件路由的额外限制在文件路由中只有当你使用自动代码分割autoCodeSplitting并自定义了分割选项把loader放入某个分组时才能分割loader。再次强调官方立场除非有明确的体积收益需求否则强烈不建议把loader拆出初始 bundle。十、在其他文件中用getRouteApi访问路由的类型安全 API把组件代码放进独立文件后消费路由本身会变得更麻烦。为此TanStack Router 导出getRouteApi函数允许你在不导入路由本身的文件中访问该路由的类型安全 APIimport { createRoute } from tanstack/react-router import { MyComponent } from ./MyComponent const route createRoute({ path: /my-route, loader: () ({ foo: bar, }), component: MyComponent, })import { getRouteApi } from tanstack/react-router const route getRouteApi(/my-route) export function MyComponent() { const loaderData route.useLoaderData() // ^? { foo: string } return div.../div }由于路径字面量/my-route作为类型参数参与推导useLoaderData()的返回类型自动收敛为{ foo: string }——组件文件与路由文件之间不需要任何相互 import类型链依然完整。Solid 版本同样只需将 import 来源换成tanstack/solid-router。getRouteApi还可用于访问其他类型安全 API包括useLoaderDatauseLoaderDepsuseMatchuseParamsuseRouteContextuseSearch十一、方案选型小结场景推荐方案关键前提文件路由 Vite/esbuild/Rspack/webpackautoCodeSplitting: true默认分组即三个组件 chunk需要 bundler 插件纯 CLI 不支持需要合并/合并 loader 与 componentcodeSplittingOptions.defaultBehavior/splitBehavior/ 路由内codeSplitGroupings按路由级 splitBehavior defaultBehavior优先级生效无法使用自动分割createLazyFileRoute.lazy.tsx后缀懒文件仅支持 4 个组件类选项根路由不可分割路由文件被拆空删除路由文件使用虚拟路由自动生成于 route tree 文件代码路由createLazyRoutecreateRoute(...).lazy(import)—组件文件需要消费路由数据getRouteApi(/path)无需 import 路由保持类型安全综合来看TanStack Router 的代码分割体系以关键/非关键路由配置二分法为理论骨架以 Babel AST 变换插件router-code-splitter-plugin.ts、compilers.ts为实现核心再辅以文档化的手动路径.lazy后缀、虚拟路由、createLazyRoute、lazyFn、getRouteApi让开发者在零配置自动分割与逐 chunk 精细控制之间平滑过渡。【免费下载链接】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),仅供参考