前端路由设置通常分为 2 类:
集中配置式路由
这也是早些时候用得最多的方式,比如:
{
"routes": [
{"path": "/", "component": "./a"},
{"path": "/list", "component": "./b", "Routes": ["./routes/PrivateRoute.js"]},
{
"path": "/users",
"component": "./users/_layout",
"routes": [
{"path": "/users/detail", "component": "./users/detail"},
{"path": "/users/:id", "component": "./users/id"}
]
}
]
}
甚至有的框架把路由和请求数据
融合到一起,比如:
const Routes = [
{
path: '/',
exact: true,
component: Home,
},
{
path: '/posts',
component: Posts,
loadData: () => loadData('posts'),
},
{
path: '/todos',
component: Todos,
loadData: () => loadData('todos'),
},
{
component: NotFound,
},
];
上面都是比较简单的例子,在真实项目中路由往往还和业务逻辑息息相关,获取数据还有前置依赖和冲突
,再考虑到路由权限、逻辑重用、模块化、路由守护、条件判断等等,让集中配置式路由不堪重负,到后面写出来到配置文件谁都看不明白了。
动态组件式路由
典型到案例就是 react-router,它将路由逻辑分散在各个组件中变成一种特殊的路由组件
,组件在生命周期中一边运行一边动态生成路由规则。例如:
<Switch>
<Route exact path="/admin/home" component="{AdminHome}" />
<Route exact path="/admin/role/:listView" component="{AdminRole}" />
<Route path="/admin/member/:listView" component="{AdminMember}" />
</Switch>
这样一来路由及业务逻辑被分散在各个相关组件中,有效的分解了集中配置式的复杂度,看上去很优雅,但也有不少新问题:
- 首先路由变得隐晦、不直观了,修改变化起来比较麻烦
- 将路由绑定到组件,render 不再纯粹,包含了外部环境的副作用
- 将 path 硬编码到组件中,不利于后期修改
- path 作为一个 string 类型,失去了类型推断与检查
- 对于重定向等等,非得动态执行到后面才知到,等于前面但渲染白白浪费了
- 最后这种方式难以跨平台与框架,比如无法应用到服务器渲染 SSR 中,也无法应用到不支持动态组件到某些环境中比如:小程序、VUE 等
探索更好路由方案
既然集中配置式
和动态组件式
路由各有优势和劣势,那有没有更好等办法来融合 2 者呢?其实它们的最大的问题都在于路由承担了过多的职能(路由、取数据、业务逻辑、权限、守护、组件生命周期...)
回归到路由的本质:
-
从内部来看:路由只不过就是应用提供一种方法,用来保存记录应用的某个视角切片。 对于没有配置路由的单页应用来说,整个应用就只有一个切片,路由规则定义得越多越细,系统可展示的内部切片也越多越细。所以路由就是一种记录应用状态的状态。
-
从外界来看:路由只不过就是应用提供一种方法,让外界(用户)可以申请访问哪些视图。 那么从这个角度说,路由无非就是携带了 2 个信息:
- 申请展示哪些视图。也就是视图名称
- 如何展示它们。也就是展示它们需要提供的参数
我们在路由中提取出这 2 笔信息后,直接将它们注入到我们的状态管理,至此路由的职能也就算完成了,接下来的事情就是状态管理的事情,与路由无关了。
比如对于一个URL:
/admin/role/list?q={adminRole: {title: "medux", page: 1, pageSize: 20}}#q={adminRole: {_random: 34532324}}
我们可以从中间提取到这样到信息,并将它注入redux中:
{
"views": {
"app": {"Main": true},
"adminLayout": {"Main": true},
"adminRole": {"List": true}
},
"paths": ["app.Main", "adminLayout.Main", "adminRole.List"],
"params": {
"app": {},
"adminLayout": {},
"adminRole": {
"listView": "list",
"title": "medux",
"page": 1,
"pageSize": 20,
"sortBy": "createTime",
"_random": 34532324
}
}
}
路由状态化
将路由当成是一种跟 Redux 一样记录着应用的状态。只不过 Redux 是记录在内存中由用户通过界面交互触发维护,而 Route 是记录在浏览器地址栏中,由用户手工输入维护,它们本质都是一样的。
所以在将路由转换为状态之后就没有路由什么事了,在组件中你直接用状态控制视图的显示即可:
<Switch>
{routeViews.adminHome?.Main && <AdminHome />}
{routeViews.adminRole?.List && <AdminRole />}
{routeViews.adminMember?.List && <AdminMember />}
</Switch>
结论
经过以上对路由职能的单一化,路由逻辑变成很薄的一层,那我们还需要 react-router 吗?还需要路由组件吗?还要啥自行车?没那么复杂的心智负担了,回归到简单的 MVVM 即可,用个抽象的公式来表示:
- State = Combine(Route)
- UI = Render(State)
其实路由逻辑还是存在,只是由router转移到了状态管理中,切断了router与component的直接联系,变成router与状态管理直接联系,因为状态比UI更简练、没有那么多生命周期,而我们又直接有成熟的MVVM方案可以使用,所以可以降低复杂度
最后问题又来了,虽然路由职能变得单一,但还是要履行职能啊,怎么将路由变成为状态呢?也就是怎么从一串 URL 字符中提取到前面所说的:展示哪些视图
和传递哪些参数
,这 2 笔信息呢?
这个你当然可以自己去设计你的路由方案,我也设计了一种:@medux/route-plan-a,可以看看我的开源框架:Medux
以上仅代表个人探索,欢迎拍砖交流
【6/16】补充一点:
路由并不是一开始就定好的,很可能一开始业务什么路由都不要求,你做一个单页就行了,后面发现用得不爽(怎么一刷新就回首页了?我能把当前URL发给客户看吗?我能保持当前的搜索条件吗?)突然之间让你加入一个路由控制,这个时候如果你用了路由状态管理,你只要把控制参数由状态管理放入路由中即可,下层可以不变。
比如,我原来弹窗是通过状态管理来控制的,业务说能不能通过URL就能主动弹出来,以下是Demo,可以在线预览: medux-react-admin.80zp.com/admin/membe…