您现在的位置是:亿华云 > 应用开发
Vite 约定式路由的优秀实践
亿华云2025-10-04 03:22:07【应用开发】6人已围观
简介路由Next.js 想必大家不陌生吧,其中最为熟知的就是约定式路由(基于文件系统)。现在我们来在Vite 中巧妙地实现这一项省心的功能。本文是以 React 结合 React-Router 实现,vu
路由Next.js 想必大家不陌生吧,约定由的优秀其中最为熟知的式路实践就是约定式路由(基于文件系统)。现在我们来在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)这种路由方式看起来非常清晰,创建一个路由就如同写组件一样简单。umijs 也支持约定式路由,形式基本一致,用过的想必也因此受益。然而 Vite作为一个脚手架提供更加通用的功能以支持 vue和 react,自然不会耦合这种路由方案。
启发
在 Vite 官方文档中 https://cn.vitejs.dev/guide/features.html#glob-import Glob 导入是这样介绍的:
Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块:
const modules = import.meta.glob(./dir/*.js);以上将会被转译为下面的样子:
const modules = {
./dir/foo.js: () => import(./dir/foo.js),
./dir/bar.js: () => import(./dir/bar.js),
};这个 API 就类似 Webpack 的require.context()。Nice. 可以来个大胆的想法,用 React.lazy 结合 React-Router v6 做个文件约定式路由。说做就做!我们需要做的事情只有一件,那就是将这个从文件读取出来的 JSON 转换为 React-Router 配置。
先看一下 React-Router v6 的结构长这样:
还有个 useRoutes 以 JSON 的形式来配置路由:
const routes = [
{
element: ,
path: /,
children: [
{
index: true,
element:
},
{
path: teams,
element:
children: [
{
index: true,
element:
},
{
path: :teamId,
element:
},
{
path: new,
element:
},
],
},
],
},
];
// 导出路由组件
export function PageRoutes() {
return useRoutes(routes);
}这样只需要转换成以上 JSON 结构就可以了。
路由规则
生成的方式,我们尽量与 next.js 保持一致, 并实现 umijs 形式的约定式 layout。但避免一个问题:避免将不需要的高防服务器组件映射成路由。这点 next.js 必须将非路由相关的文件放到 pages 目录之外。而 umijs 的排除规则是这样的:
以 . 或 _ 开头的文件或目录以 d.ts 结尾的类型定义文件以 test.ts、spec.ts、e2e.ts 结尾的测试文件(适用于 .js、.jsx 和 .tsx 文件)components 和 component 目录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 作为 layout 而不是 umijs 中的 _layout.tsx。
在 fast-glob https://github.com/mrmlnc/fast-glob#pattern-syntax 详细文档中支持更多用法,我们则需要读取 pages 目录下的所有 ts、tsx 文件,通配符可以这样写:
const modules = import.meta.glob(/src/pages/**/$*.{ ts,tsx});我们有这样一个目录
├─pages
│ │ $.tsx
│ │ $index.tsx
│ │
│ └─demo
│ │ $index.tsx
│ │
│ └─demo-child
│ $hello-world.tsx
│ $index.tsx
│ $[name].tsx
打印 modules 结果如下:
实现
我们可以先将 modules 变量转换为嵌套结构的 JSON 便于理解(先忽略 $.tsx):
import { set } from lodash-es;
/
*** 根据 pages 目录生成路径配置
*/
function generatePathConfig(): Record
// 扫描 src/pages 下的源码下载所有具有路由文件
const modules = import.meta.glob(/src/pages/**/$*.{ ts,tsx});
const pathConfig = { };
Object.keys(modules).forEach((filePath) => {
const routePath = filePath
// 去除 src/pages 不相关的字符
.replace(/src/pages/, )
// 去除文件名后缀
.replace(/.tsx?/, )
// 转换动态路由 $[foo].tsx => :foo
.replace(/\$\[([\w-]+)]/, :$1)
// 转换以 $ 开头的文件
.replace(/\$([\w-]+)/, $1)
// 以目录分隔
.split(/);
// 使用 lodash.set 合并为一个对象
set(pathConfig, routePath, modules[filePath]);
});
return pathConfig;
}打印的 generatePathConfig() 目录结构结果如下:
现在已经很接近 React-Router 的配置了。
我们只需要将 import() 语法稍微封装一下 () => import(./demo/index.tsx) 基础上包一层 React.lazy 将其转换为组件:
/
*** 为动态 import 包裹 lazy 和 Suspense
*/
function wrapSuspense(importer: () => Promise<{ default: ComponentType }>) {
if (!importer) {
return undefined;
}
// 使用 React.lazy 包裹 () => import() 语法
const Component = lazy(importer);
// 结合 Suspense,这里可以自定义 loading 组件
return (
);
}我们将 pathConfig 递归将其转换为 React-Router 的配置
/
*** 将文件路径配置映射为 react-router 路由
*/
function mapPathConfigToRoute(cfg: Record
// route 的子节点为数组
return Object.entries(cfg).map(([routePath, child]) => {
// () => import() 语法判断
if (typeof child === function) {
// 等于 index 则映射为当前根路由
const isIndex = routePath === index;
return {
index: isIndex,
path: isIndex ? undefined : routePath,
// 转换为组件
element: wrapSuspense(child),
};
}
// 否则为目录,则查找下一层级
const { $, ...rest } = child;
return {
path: routePath,
// layout 处理
element: wrapSuspense($),
// 递归 children
children: mapPathConfigToRoute(rest),
};
});
}最后组装这个配置:
function generateRouteConfig(): RouteObject[] {
const { $, ...pathConfig } = generatePathConfig();
// 提取跟路由的 layout
return [
{
path: /,
element: wrapSuspense($),
children: mapPathConfigToRoute(pathConfig),
},
];
}
const routeConfig = generateRouteConfig();打印这个 routeConfig 配置试试:
最后将封装的组件插入到 App 中
export function PageRoutes() {
return useRoutes(routeConfig);
}至于为什么要将 PageRoutes 单独做成个组件,因为 useRoutes 需要 BrowserRouter 的 Context,否则会报错。
function App() {
return (
);
}大功告成!预览一下:
结语
想起几年前写 React-Router v2 配置 JSON 的痛苦经历历历在目。现在有了基于文件式路由用法,在Vite 上面也能愉快地早点下班了。
很赞哦!(11318)
相关文章
- 这个不用多说,不同平台的注册价格不同,且不同平台对域名释放交易的把控与曝光不同,当然价格相对便宜且平台渠道广操作便利的平台最好。
- Golagn 四种配置方式实现
- Ccat – 使用语法突出显示输出内容
- 必须了解的 MySQL 三大日志
- 如果你的潜在终端必须是这个米(域名),那么潜在终端并不多,也没有硬通货,那么你的域名应该在终端有兴趣购买时出售。否则,你可能得自己留着吃。
- .top域名怎么样?
- 搭建桥梁:残奥会作为全球团结的催化剂, .bond域名如何强化连接?
- MySQL中的SQL Mode及其作用
- 旧域名的外链是否会对新建站点产生影响?
- 阿里二面:RocketMQ同一个消费组内的消费者订阅量不同tag,会有问题吗?