路由Next.js想必大家都不陌生,其中最为人熟知的就是ConventionalRouting(基于文件系统)。现在让我们巧妙地在Vite中实现这个省心的功能。本文使用React结合React-Router实现。vue的实现思路基本一致,只是后缀名和vue-router的区别,需要的可以照搬这个方案。路由形式首先,让我们看看Next.js的基于文件的基于约定的路由是什么样的。Next.js在pages目录下添加文件时,会自动生成对应的路由。开发时节省大量模板代码,提高开发效率。特性一:将index文件名js|jsx|ts|tsx结尾的文件映射到当前目录的根路由:pages/index.js→/pages/blog/index.js→/blog特性二:支持嵌套目录文件。如果您创建嵌套文件夹结构,文件将自动以相同方式路由:pages/about.js→/aboutpages/blog/first-post.js→/blog/first-postpages/dashboard/settings/username.js→/dashboard/settings/username特点三:使用括号语法。匹配动态命名参数:pages/blog/[slug].js→/blog/:slug(/blog/hello-world)pages/[username]/settings.js→/:username/settings(/foo/settings)this这种路由方式看起来很清晰,创建路由就像写组件一样简单。umijs也支持常规路由,形式基本一样。使用过它的人也一定会从中受益。而Vite作为脚手架,提供了更通用的功能来支持vue和react,自然不会耦合这种路由方案。灵感来自于Vite官方文档https://cn.vitejs.dev/guide/features.html#glob-importGlob导入介绍如下:Vite支持使用特殊的import.meta.glob函数从文件系统中导入多个模块:constmodules=import.meta.glob('./dir/*.js');上面会被翻译成下面的:constmodules={'./dir/foo.js':()=>import('./dir/foo.js'),'./dir/bar.js':()=>import('./dir/bar.js'),};这个API类似于Webpack的require.context()。好的。你可以想出一个大胆的想法,使用React.lazy结合React-Routerv6做一个文件约定路由。去做就对了!我们只需要做一件事,那就是将从文件中读取的JSON转换为React-Router配置。我们看一下React-Routerv6的结构:}>}/>}>}/>}/>}/>还有useRoutes配置JSON形式的路由:constroutes=[{element:,path:'/',children:[{index:true,element:,},{path:'teams',element:,children:[{index:true,element:,},{路径:':teamId',element:,},{path:'new',element:,},],},],},];//导出路由组件exportfunctionPageRoutes(){returnuseRoutes(routes);}这样只需要转成上面的json结构即可。路由规则生成方式尽可能接近next。js是一致的,以umijs的形式实现常规布局。但要避免一个问题:避免将不必要的组件映射到路由中。此时next.js必须把非路由相关的文件放在pages目录外。umijs的排除规则如下:.开头的文件或目录。或者_,以d.ts结尾的类型定义文件,以test.ts、spec.ts、e2e.ts结尾的测试文件(适用于.js、.jsx和.tsx文件)组件和组件目录utils和util目录不是.js、.jsx、.ts或.tsx文件,文件内容不包含JSX元素。umijs确实有点复杂和冗余,一大堆规则很容易让开发者头晕。在组件化项目中,路由文件往往远远少于页面组件。我们可以使用一些特殊的标识符来表示它是一个路由:我们暂定以$开头的文件作为路由pages/$index.tsx→/pages/blog/$index.tsx→/blogpages/$生成的规则about.tsx→/aboutpages/blog/$[foo].tsx→/blog/:foo(/blog/hello-world)使用$.tsx作为布局,而不是umijs中的_layout.tsx。更多用法在fast-globhttps://github.com/mrmlnc/fast-glob#pattern-syntax详细文档中支持,我们需要读取pages目录下的所有ts和tsx文件,通配符可以这样写:constmodules=import.meta.glob('/src/pages/**/$*.{ts,tsx}');我们有这样一个目录├─pages││$.tsx││$index.tsx│││└─demo││$index.tsx│││└─demo-child│$hello-world.tsx│$index。tsx│$[name].tsxprintmodules结果如下:要实现,我们可以先将modules变量转换为嵌套的JSON结构很容易理解(先忽略$.tsx):import{set}from'lodash-es';/***根据pages目录生成路径配置*/functiongeneratePathConfig():Record{//扫描src/pages下的所有路由文件constmodules=import.meta.glob('/src/pages/**/$*.{ts,tsx}');常量路径配置={};Object.keys(modules).forEach((filePath)=>{constroutePath=filePath//去掉src/pages中不相关的字符。replace('/src/pages/','')//去掉文件名后缀。replace(/.tsx?/,'')//变换动态路由$[foo].tsx=>:foo.replace(/\$\[([\w-]+)]/,':$1')//转换以$.replace(/\$([\w-]+)/,'$1')//以目录分隔。分裂('/');//使用lodash.set组合成一个对象set(pathConfig,routePath,modules[filePath]);});返回路径配置;}打印出来的generatePathConfig()目录结构结果如下:现在已经很接近React-Router的配置了我们只需要在import()语法的基础上稍微包装一下()=>import('./demo/index.tsx')一层React.lazy就可以转化为组件:/***Wraplazy用于动态导入和Suspense*/functionwrapSuspense(importer:()=>Promise<{default:ComponentType}>){if(!importer){returnundefined;}//使用React.lazywrap()=>import()语法constComponent=lazy(importer);//结合Suspense,这里可以自定义加载组件return();}我们递归地将pathConfig转换为React-Router的Configuration/***映射文件pathconfigurationtoareact-routerroute*/functionmapPathConfigToRoute(cfg:Record):RouteObject[]{//路由的子节点是一个数组returnObject.entries(cfg).map(([routePath,child])=>{//()=>import()语法判断if(typeofchild==='function'){//等于index,则映射到当前根路由constisIndex=routePath==='index';return{index:isIndex,path:isIndex?undefined:routePath,//转换成组件元素:wrapSuspense(c希尔),};}//否则为目录,则寻找下一层const{$,...rest}=child;return{path:routePath,//布局处理元素:wrapSuspense($),//递归子children:mapPathConfigToRoute(rest),};});}最后组装这个配置:functiongenerateRouteConfig():RouteObject[]{const{$,...pathConfig}=generatePathConfig();//提取和路由布局打印这个routeConfig配置试试:最后将封装好的组件插入到App中去exportfunctionPageRoutes(){returnuseRoutes(routeConfig);}至于为什么要把PageRoutes做成一个单独的组件,因为useRoutes需要BrowserRouter的Context,否则会报错functionApp(){return();}大功告成!Preview:Epilogue我还记得几年前写React-Routerv2配置JSON的惨痛经历。现在有了基于文件的路由使用,你可以愉快地在Vite上早点下班。