1

ruled-router 是我们(积梦前端)定制的路由方案, 另外强化了类型方面,
之前的介绍可以看文章: 积梦前端的路由方案 ruled-router.

关于跳转方法的类型

路由生成部分, 大致上就是对于规则:

[
  {
    "name": "a",
    "path": "a",
    "next": [
      {
        "name": "b",
        "path": "b/:id"
      }
    ]
  }
]

会通过脚本生成路由的调用方法, 现在的生成结果是:

export let genRouter = {
  a: {
    name: "a",
    raw: "a",
    path: () => `/a`,
    go: () => switchPath(`/a`),
    b: {
      name: "b",
      raw: "b",
      path: (id: string) => `/a/b/${id}`,
      go: (id: string) => switchPath(`/a/b/${id}`),
    },
  },
};

这样可以通过调用方法来进行路由跳转,

genRouter.a.b.go(id)

这个步骤, 是有类型支持的. TypeScript 会检查整个结构, 不会有错误的调用.
也就是说, 所有的调用, 按照这个写法, 不会导致出现不符合路由规则的路径.
整个实现模块维护在 https://github.com/jimengio/r... .

解析结果的类型问题

现在的短板是在解析解析结果的类型上面, 回顾一下 ruled-router 解析的结果,
对于路径:

/home/plant/123/shop/456/789

按照路由规则做一次解析,

let pageRules = [
  {
    path: "home",
    next: [
      {
        path: "plant/:plantId",
        next: [
          {
            path: "shop/:shopId/:corner"
          }
        ]
      }
    ]
  }
];

会得到一个 JSON 结构,

{
  "raw": "home",
  "name": "home",
  "matches": true,
  "restPath": ["plant", "123", "shop", "456", "789"],
  "params": {},
  "data": {},
  "next": {
    "raw": "plant/:plantId",
    "name": "plant",
    "matches": true,
    "restPath": ["shop", "456", "789"],
    "params": {
      "plantId": "123"
    },
    "data": {
      "plantId": "123"
    },
    "next": {
      "raw": "shop/:shopId/:corner",
      "name": "shop",
      "matches": true,
      "next": null,
      "restPath": [],
      "data": {
        "shopId": "456",
        "corner": "789"
      },
      "params": {
        "plantId": "123",
        "shopId": "456",
        "corner": "789"
      }
    }
  }
}

这个 JSON 结构当中部分字段是固定的, 部分是按照规则定义的参数,
如果用一个类型来表示, 就是:

interface IParsedResult<IParams, IQuery>

这也是我们以往的写法. 这个写法比较稳妥, 但是问题就是书写麻烦,
路由比较多, 需要手写的 IParams IQuery 比较多, 也难以维护.

当前尝试生成路由的方案

对于这个问题, 我想到的方案, 主要是能不能像前面一样把类型都生成出来,
大致想到的是这样一个方案, 生成一棵嵌套的路由的树,
https://gist.github.com/cheny...
我需要这棵树满足两个需求,

  • 能得到一个完整的路由, 其中的 next: A | B | C 能罗列所有子路由类型,
  • 我能通过 x.y.z.$type 来获取其中一棵子树, 因为子组件需要具体一个类型,

这个方案最重要的地方就是需要 VS Code 能推断出类型进行提示,
经过调整以后, 得到一个可用的方案, 基于这样的规则,

[
  {
    "path": "a",
    "queries": ["a"],
    "next": [
      {
        "path": "b",
        "queries": ["a", "b"]
      },
      {
        "path": "d"
      }
    ]
  }
]

生成的类型文件的是这样:

export type GenRouterTypeMain = GenRouterTypeTree["a"];

export interface GenRouterTypeTree {
  a: {
    name: "a";
    params: {};
    query: { a: string };
    next: GenRouterTypeTree["a"]["b"] | GenRouterTypeTree["a"]["d"];
    b: {
      name: "b";
      params: {};
      query: { a: string; b: string };
      next: null;
    };
    d: {
      name: "d";
      params: {};
      query: { a: string };
      next: null;
    };
  };
}
  • 顶层的路由

页面首先会被解析, 得到一个 router 对象

let router: GenRouterTypeMain = parseRoutePath(this.props.location.pathname, pageRules);

router 的类型是 GenRouterTypeMain, 这个类型是顶层的类型,
这个例子当中只有一个顶级路由,

export type GenRouterTypeMain = GenRouterTypeTree["a"];

实际当中更可能是多个可选值, 就像这样

type GenRouterTypeMain = GenRouterTypeTree["a"] | GenRouterTypeTree["b"] | GenRouterTypeTree["c"];
  • 组件使用的子路由

子组件当中, props.router 的类型对应的是子树的某一个位置,
这里的 next 因为用了 Union Type, 不能直接引用其中某个 case,
就需要通过另一个写法, 从数据的路径上直接通过类型访问, 比如:

GenRouterTypeTree["a"]

更深层的子组件的类型, 比如嵌套的第二层, 就需要用:

GenRouterTypeTree["a"]["b"]

不过这个在组件定义当中并不直接是拿到, 因为在 props 可能无法确定类型,
就需要通过父级的 next 来访问, 具体是一个 Union Type:

let InformationIndex: FC<{
  router: GenRouterTypeTree["a"]["next"] }
  // next type
  // GenRouterTypeTree["a"]["b"] | GenRouterTypeTree["a"]["d"]

> = (props) => {
  // TODO
}
  • 配合 VS Code 做类型推断

为了能让 VS Code 从 next 推断出类型, 需要同 switch 语句判断,

if (props.router) {
  switch (props.router.name) {
  case "b": // TODO, router: GenRouterTypeTree["a"]["b"]
  case "d": // TODO, router: GenRouterTypeTree["a"]["d"]
  }
}

效果大致上,

  • case 后面的字符串在一定程度上可以自动补全和类型检查,
  • case 后面, router 类型确定了, paramsquery 就能有字段的提示和检查了,
  • 如果内部有子组件 <A router={router.next} />, router.next 会被类型检查.

当然这些主要还是提示的作用, 并不是完全跟 router 对应的类型, 不然结构会更复杂,
我试着在已有的组件项目当中做了尝试, 包括比链接更大的项目, 基本是可用的,
https://github.com/jimengio/m...

其他

目前来说, 能对项目路由进行检查, 就算是达到了最初的类型的目标,
至少能够保证, 开发当中, 使用生成的路由, 能提示和检查 params query 中的字段,
并且提交到仓库的代码, CI 当中能检查到参数, 做一些质量的保证.

case 当中能够提示字符串, 算是意料之外的一个好处吧.
不过这个也要注意, VS Code 推断的能力有限, 只能用 switch 这个简单的写法,
再复杂一些, 比如嵌套了表达式, 或者往子路由数据再判断, 就推断不出来了.

当前比较担心的是项目当中出现深度嵌套的路由, 加上字段名称长, 整体会非常长:

GenRouterTypeTree["a"]["d"]["e"]["f"]["g"]

由于我们最大的项目当中曾在深达 6 层的路由, 不能不担心会出现超长的单行路由...
后面再想想有没有什么办法继续做优化..


其他关于积梦前端的模块和工具可以查看我们的 GitHub 主页 https://github.com/jimengio .
目前团队正在扩充, 招聘文档见 GitHub 仓库 https://github.com/jimengio/h... .


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者