约定式路由

约定式路由即根据前端文件夹结构来自动的生成前端路由配置。

本框架同时支持约定式路由和声明式路由,当检测到 web/route.ts 文件存在时会使用该文件来作为前端路由结构, 但我们不支持你这么做。因为框架会根据你当前的不同配置来生成不同的路由结构,并且也不保证这些结构在之后的版本中是一成不变的。如果手动编写工作量过大且容易出错。在没有特殊需求的情况下建议直接使用约定式路由。

注:在最新的版本中我们支持约定式路由和声明式路由同时存在,并以声明式路由为更高优先级

路由规则

下面来介绍我们详细的路由映射规则, 以下为一个基础的 web 文件夹的结构,我们主要关注 web/pages 文件夹,根据该文件夹来 parse 前端路由结构

$ tree ./ -I node_modules -L 3
├── pages
│   ├── detail
│   │   ├── fetch.ts
│   │   └── render$id.vue
│   └── index
│       ├── fetch.ts
│       └── render.vue

页面组件

pages 文件夹下的每个文件夹,我们都会认为它是一个页面。上述结构包含 index, detail 两个页面。

同样我们定义 render 文件代表一个页面的渲染组件。render 文件支持多种格式来应对不同类型的前端路由

普通路由

最常见的普通路由即 /, /detail, /user 这种我们只需要创建同名文件夹即可。这里我们特殊针对根路由,来将 index 文件夹进行映射

  • /index/render.vue 映射为 /
  • /detail/render.vue 映射为 /detail
  • /user/render.vue 映射为 /user

动态路由

动态路由即携带参数的路由,例如 /user/:id 这种

  • /user/render$id.vue 映射为 /user/:id
  • /user/render$foo$bar.vue 多参数的情况下映射为 /user/:foo/:bar

可选参数路由

React|Vue 场景下均可使用。由于 ? 符号无法作为文件名使用,所以这里我们需要用 # 号代替

  • /index/render$id#.vue 映射为 /:id?

通配符路由

用来匹配所有符合要求的文件, 综合考虑 path-to-regexpvue-router 文档,使用如下结构

由于 * 符号在 Windows 下无法作为文件名使用,所以这里我们需要用 & 号代替

  • /detail/render$params&.vue 映射为 /detail/:params*,本质上对应所有来自 /detail/* 的请求

React 场景同理生效

多级路由

尽管在大多数情况下我们用不到多级路由,但这里我们仍然提供了对应的解析策略。如果你的应用所有路由 path 前面都需要加上一个统一的前缀,那么你应该通过 config.prefix 来实现,而不是多级路由。参考应用配置

  • /user/detail/render$id 映射为 /user/detail/:id
  • /user/detail/render$foo$bar 映射为 /user/detail/:foo/:bar

嵌套路由

约定式路由不支持生成嵌套路由也就是 children 子结构。虽然支持嵌套路由并不难,但这会让规范变得复杂。特别是获取数据这一块,且嵌套路由用业务代码实现是非常简单的事情。在 React 中直接手动引入 Router 来实现即可。在 Vue 中需要手动填写 children 字段。如果不支持嵌套路由的 fetch, 那么非常容易实现,但是意义不大开发者直接在业务代码中实现即可,如果要支持嵌套路由的 fetch 那么会让规范变得复杂。例如需要在框架层面让 render$child$foo.vue 对应 fetch$child$foo.ts 文件。这非常的 dirty,所以并不打算支持嵌套路由。

手动编写嵌套路由

Vue2/3 场景下由于底层的 vue-router 支持了嵌套路由的书写规范。所以在这里 ssr 框架层也支持了手动编写嵌套路由并且能够正确的在服务端匹配渲染的能力。这里的 children 字段与 vue-router 官方的用法保持一致。webpackChunkName 的编写规范与下文提到的手动路由编写规范的注意事项保持一致

// web/route.ts
export const FeRoutes = [
  {
    fetch: async () => await import(/* webpackChunkName: "detail-id-fetch" */ '@/pages/detail/fetch'),
    path: '/detail',
    component: async () => await import(/* webpackChunkName: "detail-id" */ '@/pages/detail/detail.vue'),
    webpackChunkName: 'detail',
    children: [
      {
        path: 'foo', // 将会匹配 /detail/foo 的请求地址
        fetch: async () => await import(/* webpackChunkName: "detail-foo-fetch" */ '@/pages/detail/detail-fetch'),
        component: async () => await import(/* webpackChunkName: "detail-foo" */ '@/pages/detail/foo.vue'),
        webpackChunkName: 'detail-foo'
      }
    ]
  }
]

实现代码

具体的实现代码可以查看该文件

手动编写路由结构

尽管我们不建议开发者来手动编写路由结构,但如果你一定要这么做的话,我们提供以下示例。

注意事项

  • web/route.ts 将会被编译为 build/ssr-manual-route.js 文件,所以不要在路由文件中使用相对路径引入其他模块,否则将会无法正确识别路径
  • webpackChunkName 字段和 import(/* webpackChunkName: "detail-id" */)Webpack/Vite 构建工具打包时所必须要使用的标识符。所以在手动编写路由结构时请务必正确的填写它们。保证不同页面组件的 webpackChunkName 不会重复。
// web/route.ts
export const FeRoutes = [
    {   
        "fetch": () => import(/* webpackChunkName: "detail-id-fetch" */ '@/pages/detail/fetch'),
        "path": "/detail/:id",
        "component": () => import(/* webpackChunkName: "detail-id" */ '@/pages/detail/render$id'), // vue 场景用此写法
        "component": async function dynamicComponent () { return await import(/* webpackChunkName: "detail-id" */ '@/pages/detail/render$id') }, // react 场景需要固定函数名称为 dynamicComponent
        "webpackChunkName": "detail-id"
    },
    {
        "fetch": () => import(/* webpackChunkName: "index-fetch" */ '@/pages/index/fetch'),
        "path": "/",
        "component": () => import(/* webpackChunkName: "index" */ '@/pages/index/render'), // vue 场景用此写法
        "component": async function dynamicComponent () { return await import(/* webpackChunkName: "index" */ '@/pages/index/render') }, // react 场景需要固定函数名称为 dynamicComponent
        "webpackChunkName": "index"
    }
]

优先级覆盖

覆盖规则如下

  • FeRoutes 在声明式路由存在于约定式路由相同的 path 时,取声明式路由文件为最高优先级覆盖默认的约定式路由规则,并且会额外添加声明式路由新增的路由配置