是时候放弃react-router,拥抱route状态化了

3,451 阅读5分钟

前端路由设置通常分为 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

Medux 框架介绍

以上仅代表个人探索,欢迎拍砖交流

【6/16】补充一点:

路由并不是一开始就定好的,很可能一开始业务什么路由都不要求,你做一个单页就行了,后面发现用得不爽(怎么一刷新就回首页了?我能把当前URL发给客户看吗?我能保持当前的搜索条件吗?)突然之间让你加入一个路由控制,这个时候如果你用了路由状态管理,你只要把控制参数由状态管理放入路由中即可,下层可以不变。

比如,我原来弹窗是通过状态管理来控制的,业务说能不能通过URL就能主动弹出来,以下是Demo,可以在线预览: medux-react-admin.80zp.com/admin/membe…