前言

react-router 的路由是是基于 history 实现的,而我们可以根据不同的使用场景,使用不同的包,如浏览器上使用react-router-dom,而react-router是一个 monorepo 项目,使用 yarn 的工作空间管理 node_modules 的包,所以 package 目录下的包是没有 node_modules 的,在scripts\build.js里有它的打包方式,是进入每个 package 的包目录下进行打包,而且是使用了 roullup 打包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const path = require("path");
const execSync = require("child_process").execSync;

function exec(cmd) {
execSync(cmd, { stdio: "inherit", env: process.env });
}

const cwd = process.cwd();

["react-router", "react-router-dom", "react-router-config"].forEach(
(packageName) => {
process.chdir(path.resolve(__dirname, "../packages/" + packageName));
exec("yarn build");
}
);

process.chdir(cwd);

react-router包是其他包的基础包,就是其他包都会去引用它,就是其他包都在基础包的功能上新增功能来实现到符合不同场景的使用,所以有部分的功能都是直接包基础包的功能进行导出或增强而已。

react-router只是进行了组件层的实现,真正的路由逻辑都在 history 这个包里面,如 Prompt、Route、Router 等组件的实现,而纯粹地址变化的路由实现是 history 实现的,包含三种模式createBrowserHistory H5 的 history 模式、createHashHistory URL 的 hash 模式、createMemoryHistory 无输入地址栏的内存记录模式,这是对 Native 场景很有用。

react-router组件间数据的传递使用了React.createContext,而Router组件是数据提供方,而其他组件基本上都是数据消费方,所以我们都要使用Router放置在组件的顶层,让其他的组件都包含在它之下使用。

Router

我们在使用的react-router-dom导出的 BrowserRouter 与 HashRouter 其实在实现上是没有很大的区别的,只是在使用 createBrowserHistory 与 createHashHistory 的区别,它们都是简单的封装了 Router。

如 HashRouter:

1
2
3
4
5
6
7
class HashRouter extends React.Component {
history = createHashHistory(this.props);

render() {
return <Router history={this.history} children={this.props.children} />;
}
}

就调用了 createHashHistory 让路由设置为 hash 模式的路由,而 Router 的使用方式都是一样的。

Route

route 组件实现关键在match,因为这是路由匹配决定是否加载组件。

1
2
3
4
5
const match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;

从以下可以知道 route 组件渲染路由匹配组件,而第一层三元表达式就是决定是否加载组件,而其他的三元表达式是根据在 route 组件传入组件方式进行组件加载,从中可以看出 props 传入的加载组件顺序是 children、component、render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>

Switch组件的子组件要的是 route 组件,因为它主要的作用使用只要匹配中后就不再匹配,就只渲染当前的 route 组件,而它实现是React.Children.forEach遍历所有的子组件,所以要实现只匹配一次,那么就要使用它包囊,否则无法生效。

1
2
3
4
5
6
7
8
9
10
11
React.Children.forEach(this.props.children, (child) => {
if (match == null && React.isValidElement(child)) {
element = child;

const path = child.props.path || child.props.from;

match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});

element 当前组件,match 为匹配情况,如果匹配中,从判断match == null可知就再也无法进入遍历的赋值逻辑。

Prompt

Prompt 是 react-router 唯一的路由守卫,不像 vue-router 钩子特别丰富,它的作为就是路由离开时的操作,需要匹配 history 的 getUserConfirmation 使用,因为真正拦截路由的时 getUserConfirmation,而 Prompt 只是一个离开时是否渲染的组件,它的实现很简单,就是基于 Lifecycle 生命周期组件做了一些添加信息,而它的作用就是在全局的路由里进行一些离开是否需要提示或二次确认是否离开的操作,如我们实现一个有大量填写内容的页面,如果误操作直接路由离开,那么可以会因为丢失数据,这样的用户体验是很差的,所以我们可以使用这个功能就是二次确认,来提示用户是否要放弃这些数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from "react";

class Lifecycle extends React.Component {
componentDidMount() {
if (this.props.onMount) this.props.onMount.call(this, this);
}

componentDidUpdate(prevProps) {
if (this.props.onUpdate) this.props.onUpdate.call(this, this, prevProps);
}

componentWillUnmount() {
if (this.props.onUnmount) this.props.onUnmount.call(this, this);
}

render() {
return null;
}
}

export default Lifecycle;

而添加信息,它调用的是 history 的 block,block 会接受一个入参 prompt,然后会把 prompt 保存起来,当调用 getUserConfirmation 会当一个参数传入,而它的第二参数为 callback,当 callback 传入为 true 时就跳转路由,传入为 false 则停止跳转,而 history 内部路由跳转实现也是基于 getUserConfirmation 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
transitionManager.confirmTransitionTo(
location,
action,
getUserConfirmation,
(ok) => {
if (ok) {
setState({ action, location });
} else {
revertPop(location);
}
}
);

confirmTransitionTo 的第四个参数就是决定了是否跳转路由。

history 的属性了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
length, // 历史堆栈高度
action, // 当前导航动作有 pushpopreplace 三种
location: {
pathname, // 当前 url
search, // queryString
hash, // url hash
},
push(path[state]), // 将一个新的历史推入堆栈 (可以理解为正常跳转)
replace(path[state]), // 替换当前栈区的内容 (可以理解为重定向)
go(number), // 移动堆栈指针
goBack(number), // 返回上一历史堆栈指针 -1
goForward(number), // 前进到下一历史堆栈指针 +1
block(string | (location, action) => {}) // 传入提示信息,监听并阻止路由变化
}

react-router 其实它的实现没有想象那么复杂,可以说只是实现了路由该有的功能,而一些扩展功能,它完全没有实现,保留最大的灵活性给使用者,让使用者根据需要自己完成所需的扩展实现,如鉴权,我们要根据二次封装 Route 组件实现一个鉴权路由,没有使用那些所谓的路由守卫的功能,因此我们可以通俗的理解为 react-router 就是一个普通的组件,在我们需要的时候就加载使用,不需要时就不加载,只是它比我们的组件多了一个根据路由匹配出需要加载的组件而已。

而实现 keep-alive,我们可以考虑使用react-activation,这个可以查看react-组件缓存的方式

而 hooks 方面可以使用 useHistory、useLocation、useParams、useRouteMatch 获取数据或方法,就不必使用 withRouter。

basename 设置路由的默认前缀路径,就是基础路径 basePath。