我爱读源码系列之飞冰配置式路由

react-router 升级到 4 或 5 之后,路由配置方式发生极大的改变。

背景

v2/v3 的版本采用的方式是将路由看成是一个整体的单元,与别的组件是分离的,一般会单独放到一个 router 文件中,对其进行集中式管理;并且,布局和页面的嵌套由路由的嵌套所决定。
v4 的版本则将路由进行了拆分,将其放到了各自的模块中,不再有单独的 router 模块,充分体现了组件化的思想;另外, 的使用与之前作为 history 属性传入的方式也不同了。

但是,v4 组件化路由,不便于对入口文件的整体理解,不能做到心里有 b 数,需要一层一层去查找。
对于庞大且路由结构复杂的项目来说,不仅改造成本高,而且很容易让人摸不着头脑,对于刚接触项目的同学极度不友好,快速熟悉项目难度大。

所以在构建项目或做项目依赖升级时,还是希望可以集中管理式的配置路由。
在公司项目做 react 及 react-router 升级时,我们选取了react-router-config,这个库,它也是 react-router 团队提供的,使用了之后,虽然可以支持集中管理式的配置路由,但是其功能非常简陋,只是做一个简单的递归 render,还得手动调用,对复杂嵌套路由及懒加载什么的支持的都很差,且还需要调用renderRoutes,与 v2/v3 的this.props.children的使用,有习惯上的变化。
总之,体验一般,还是得自己造轮子,对 react-router 进行二次封装。
在研究 阿里 飞冰 时,发现它可以支持配置式路由和约定式路由,就去研究了一下它对于路由部分处理的源码,大佬的轮子就是好。

飞冰是个啥

阿里开源的一个基于 React.js 的通用框架。

随着 React 生态的快速发展,基于 React 的方案数不胜数,在 v16.8 版本发布 Hooks 这一重大改进之后,社区基于 Hooks 的状态管理也是层出不穷,这意味着很多方案开发者依然要做很多选择,没有约定的团队,沟通成本和跨团队协作成本,以及长期的维护是非常高的,这时候统一一套开发模式就显得尤为重要。

而 icejs 正是在这种背景下诞生的,通过框架提供完整的标准化的 React 应用开发模式和最佳实践。目的是简化应用的开发,以及收敛技术栈、屏蔽底层差异和统一开发体验,帮助开发团队和开发人员降低开发和维护成本。

有点类似 umi(乌米),但是是蚂蚁金服的底层前端框架,而 ice(飞冰)是阿里巴巴的。就像 antd 和 fusion 一样。

虽然这些都不重要,但是我觉得前边那堆说统一一套开发模式尤为重要,我是极为赞同的。

进入正题

react-router-config

贴上 react-router-config 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from "react";
import { Switch, Route } from "react-router";

function renderRoutes(routes, extraProps = {}, switchProps = {}) {
return routes ? (
<Switch {...switchProps}>
{routes.map((route, i) => (
<Route
key={route.key || i}
path={route.path}
exact={route.exact}
strict={route.strict}
render={props =>
route.render ? (
route.render({ ...props, ...extraProps, route: route })
) : (
<route.component {...props} {...extraProps} route={route} />
)
}
/>
))}
</Switch>
) : null;
}

export default renderRoutes;

可以看到react-router-config的源码极其简单,虽然支持了集中管理式的路由配置,但就相当于是做了一次递归渲染,而且 children 的渲染还得自己手动调用 renderRoutes 方法,将子路由 render。不仅工程改造成本高,对于一些懒加载的组件,重定向等功能,都不太友好。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { lazy, Suspense } from "react";
import { BrowserRouter } from "react-router-dom";
import { renderRoutes } from "react-router-config";

import App from "./containers/App";
const Organizationlazy = lazy(() => import("./containers/App"));

const routes = [
{
path: ["/headhunters/:orgId", "/headhunters"],
component: App,
routes: [
{
path: "/headhunters/:orgId/org",
component: Organization
},
{
path: "/headhunters/:orgId/org",
render: () => <Redirect to="/headhunters" />
}
]
}
];

export default (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
{renderRoutes(routes)}
// 这里只render了第一层routes,子组件要在container组件里边再次调用renderRoutes才能render
// 手动递归。。。
</Suspense>
</BrowserRouter>
);

像上边这种就写的会比较复杂

  1. 嵌套路由写的比较复杂,要把全部路径都写出来,显得杂乱,不好看。
  2. 对于重定向来说,在一个看似 json 数据的配置里边又加入了 jsx,也显得格格不入。
  3. 对于子组件的懒加载很不友好,如果在这里写 Suspense,那么 fallback 会影响全局,如果在子组件 renderRoutes 外边包裹,那么每个子组件需要懒加载的时候又要都写一次,很难受。

icejs

贴上 icejs plugin-router 源码 其实大佬是 ts 写的,我给改成 js 了。。。

有点长,咱们一行一行来看,看看大佬是如何封装的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import * as React from "react";
import {
HashRouter,
BrowserRouter,
// (<MemoryRouter> 和 <StaticRouter> 分别是非浏览器环境和服务器端渲染用的,在此不做讨论。)
MemoryRouter,
StaticRouter,
Switch,
Route,
Redirect
} from "react-router-dom";

function wrapperRoute(component, routerWrappers) {
// 这块着实没看懂 猜测是nextjs的部分功能,因为飞冰还支持ssr
// 但是我们好像不需要
return (routerWrappers || []).reduce((acc, curr) => {
const compose = curr(acc);
if (acc.pageConfig) {
compose.pageConfig = acc.pageConfig;
}
if (acc.getInitialProps) {
compose.getInitialProps = acc.getInitialProps;
}
return compose;
}, component);
}

function getRouteComponent(component) {
const { __LAZY__, dynamicImport } = component || {};
return __LAZY__
? React.lazy(() =>
// 如果是 懒加载 的组件
dynamicImport().then(m => {
if (routerWrappers && routerWrappers.length) {
return { ...m, default: wrapperRoute(m.default, routerWrappers) };
}
return m;
})
)
: wrapperRoute(component, routerWrappers);
}

function parseRoutes(routes) {
// 递归解析 return 解析过后的 parsedRoute
// children 子路由
// component 当前路由组件
// routeWrappers ?这个没看懂。。。不知道是要干啥
return routes.map(route => {
const { children, component, routeWrappers, ...others } = route;
const parsedRoute = { ...others };
if (component) {
parsedRoute.component = getRouteComponent(
component,
children ? [] : routeWrappers
);
}
if (children) {
parsedRoute.children = parseRoutes(children);
}
return parsedRoute;
});
}

function Routes({ routes, fallback }) {
return (
<Switch>
{routes.map((route, id) => {
if (!route.children) {
// 如果没有 children
if (route.redirect) {
// 如果是重定向 则渲染Redirect
const { redirect, ...others } = route;
return (
<Redirect key={id} from={route.path} to={redirect} {...others} />
);
} else {
// 否则默认添加 带有 React.Suspense 包裹的组件
// 看样子是都会单独包裹 React.Suspense
const { component: RouteComponent, ...others } = route;
const RenderComponent = props => {
return (
<React.Suspense fallback={fallback || <div>loading</div>}>
<RouteComponent {...props} />
</React.Suspense>
);
};

return <Route key={id} {...others} render={RenderComponent} />;
}
} else {
// 如果有 children
// 则 用 当前父路由的组件 即 LayoutComponent,包裹 Routes 组件 并将 子路由传入
// 进行 递归 render
const { component: LayoutComponent, children, ...others } = route;
const RenderComponent = props => (
<LayoutComponent {...props}>
<Routes routes={children} />
</LayoutComponent>
);
return <Route key={id} {...others} component={RenderComponent} />;
}
})}
</Switch>
);
}

export function Router(props) {
const { type = "hash", routes, fallback, ...others } = props;
// 判断路由模式
const typeToComponent = {
hash: HashRouter,
browser: BrowserRouter,
static: StaticRouter,
// for test case
memory: MemoryRouter
};

const RouterComponent = typeToComponent[type];
// parse routes before render
const parsedRoutes = parseRoutes(routes);
return (
<RouterComponent {...others}>
<Routes routes={parsedRoutes} fallback={fallback} />
</RouterComponent>
);
}

那么它的使用就变成了酱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// routes.js
import { Router } from 'xxx';
import UserLayout from "@/layouts/UserLayout";
import Login from "@/pages/Login";

const routes = [
{
path: "/user",
component: UserLayout,
children: [
{
path: "/login",
component: Login,
extract: true
},
{
path: "/",
// 但是好像还是没有解决 嵌套路由 必须写全部路径的问题
// 我们要站在巨人的肩膀上更上一层楼
redirect: "/user/login"
}
]
}
];
export default (
<Router
type="browser"
routes={routes}
// 这里是一个统一的fallback 不太好
// 按 route 进行配置应该会比较方便
fallback={fallback}
basename="/moka"
...others
>
);

// UserLayout.js
export default function UserLayout({ children }) {
return (
<div className={styles.container}>
<div className={styles.content}>{children}</div>
</div>
);
}

总结

这样看下来,飞冰的路由配置,更简洁好看,且对重定向,懒加载什么的支持的更好,但是仍然有很多缺陷,比如并没有解决我们子路由也要写完整路径问题,且没有像react-router-config可以传 extraProps 的功能,那么像我们项目中的那些
React.cloneElement(children, {})的写法,就不能用了,因为在飞冰中 children 其实是 route,而我们想把参数传给 route.component 或者 render 函数里边,即而传到下层组件,在路由配置处是实现不了的,只能在实际的 route 的渲染时给其 render 的组件传入额外的参数。

如果用 render 这种方式,那么改造成本会比较大。放弃。

本来以为大佬的轮子会好用一些,但是看来还是要自己写,大佬的轮子只供参考。
所以我决定还是自己造轮子吧。
结合以上两种路由配置方式,仍然使用react-router-config的 renderRoutes 方法递归 render 子组件,这样才能传 extraProps,但是内部重定向和懒加载参考飞冰,另外去支持子路由不需要写全部路径的功能。
总结一下,大佬的轮子都是按照人家自己的开发模式造的,而自己的项目也有自己的开发模式,不能盲目使用别人的轮子,只有特色社会主义才最适合自己,多看源码多参考,造出适合自己的轮子。

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. 背景
  2. 2. 飞冰是个啥
  3. 3. 进入正题
    1. 3.1. react-router-config
    2. 3.2. icejs
  4. 4. 总结
,