vite之基于文件目录的路由模式

2024-03-0416 min 教程

最近因为毕设需要,又重温了一下前端的屎山,毕设是做个前后端分离的商城类项目,前台我是准备用 SolidJS 写,后台管理界面用 vue3。一开始是觉得前台业务逻辑一般不会太过复杂,忘了这是商城了,用户购物车、订单管理什么的,逻辑其实也挺复杂的。不过无所谓了,冲!!!

为什么要基于文件系统路由

其实也是在 vite 找 SolidJS 的社区模板的时候看到了这个说法,第一反应就是之前学 vue3 看的一个后台管理的开源项目的源码,里面用到了 import.meta.glob 来读取目录下的路由文件。但仔细看了一下,区别很大。

这里 SolidJS 的文件路由,主要指的是 solid-start 这个库里的基于文件的路由(详细可见:Routing (solidjs.com)),简单来说,就是我们在创建路由时,只需要在 routes 文件夹下新建 view 模板,系统会根据文件名和路劲来确定路由:

  • hogwarts.com/routes/index.tsx
  • hogwarts.com/blog/routes/blog.tsx
  • hogwarts.com/admin/edit-settings/routes/admin/edit-settings.tsx

并且也提供了通过在 tsx 文件了 export route 对象的方式来对路由进行其他配置,可以看到,这种做法使得项目的路由配置非常清晰高效。solid-start 的源码进去看了一下,没找到具体实现的代码在哪里,于是就尝试用 import.meta.glob 来实现该功能

import.meta.glob 介绍

import.meta.glob 是 vite 提供的一个从文件系统导入多个模块的函数,如果你不设置 {eager: true} 的话,它会自动转译成异步加载代码,我们的路由业务肯定是需要异步加载来进行分包的,所以这里就不设置 eager 了。

TypeScript
// 获取 routes 目录下的所有模板对象
const routeViews = import.meta.glob<any>('./routes/**/*.tsx')
console.log(routeViews)

// 会生成这样的东西
{
    "./routes/index.tsx": () => import("/src/routes/index.tsx")
	"./routes/shop/index.tsx": () => import("/src/routes/shop/index.tsx")
}

预处理获取到的数据

我们得到了 routeViews 之后,就需要对其进行预处理了,主要是将文件路径转化为路由的 path

注意这里我没有考虑 param 的情况,你可以对照 solid-start 的规则,用 [id] 之类的命名方式来识别路径参数,实现起来也很简单

TypeScript
function handleRouteViews() {
  const routes: RouteDefinition[] = []

  for (const path in routeViews) {
    // 这里的 component 就是上面的 import 异步函数
    const component = routeViews[path]

    // 替换路径
    let pathNew = path.replace('./routes/', '').replace('.tsx', '')
    if (pathNew.endsWith('index')) {
      pathNew = pathNew.replace('index', '')
    }
    const route = {
      path: pathNew,
      // 这里使用 SolidJS 的 lazy 函数来包装我们的异步导入
      component: lazy(component),
    }
    routes.push(route)
  }

  return routes
}

这样我们就根据文件路径获取到了路由路径和组件本身,但这显然还不够,因为实际业务中,我们在路由跳转的时候可能还需要进行一些工作,比如设置标题,准备数据之类的,SolidJS 的路由提供了 load 参数来实现跳转的时候进行数据获取和其它操作。

获取组件的路由配置

我们模仿 solid-start ,通过在组件中导出配置对象的方法来获取对应组件的配置(这里统一 export const route),按照以上的代码,不难想到通过 component.route 来获取目标对象,但 component 是一个异步函数,那我们又该如何在保证组件不会预先加载的情况下获取到对象呢?

一开始我想到的使用 import.meta.globeagerimport 配置了具名导出该对象,但这种做法会导致 vite 不再将组件分包,也就是失去按需加载的能力,这无疑是致命的。后来又想到 vue 的那些开源的项目多是将路由配置与模板文件分开存放,就也想那么做,但感觉很鸡肋,又回到了老的路由配置模式。

想了半天,突然想到,load 本身就是一个函数,而且是支持异步的,直接将 component 放进 load 里不就好了?

TypeScript
{
    load: () => {
        component().then((mod: any) => {
          const routeConf = mod.route
          if (routeConf?.title) {
            document.title = `${routeConf.title} - 特色农产品团购平台`
          }
          routeConf?.load?.()
        })
      },
}

测试,成功!

总结

其实后面我还是很担心这种写法是不是会导致分包问题,经过测试,正确的做到了分包和懒加载。理论上,只要是 vite 应该就能这么做,无非是导出的路由配置项有差异,当然由于我没找到相关资料,啃源代码的能力也不太行,不清楚我的写法是否规范,有没有隐患。

评论
正在加载评论组件...