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 类型确定了,params
和query
就能有字段的提示和检查了, - 如果内部有子组件
<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… .