紧跟上一篇的步伐, 这一章主要学习react-router-dom的Router的思想, 和实现自己的Router组件

一、更新


[2019-4-21]

Fixed

  • 修复图片链接失效问题

Changed

  • 完善文章格式

二、前言


Router是整个react-router-dom体系中最重要的一环, BrowserRouterHashRouter都依赖其, Router组件中做的事情主要有以下几个方面:

  • context创建
  • 路由监听

三、细说


3.1 context创建

首先看一下Router.js的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Router extends React.Component {
render() {
return (
<RouterContext.Provider
children={this.props.children || null}
value={{
history: this.props.history,
loaction: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
staticContext: this.props.staticContext, // 这个可以不用理, 好像没多大用
}}
/>
);
}
}

由于Router.js中用到了RouterContext这个小东西, 再把目光转向同目录下的RouterContext.js文件, 大致是这样的:

1
const context = createNamedContext('Router');

通过React提供的createContextAPI, 在RouterContext中轻松的创建了context, context静态函数, 分别是ProviderConsumer, 将我们常用的三大API: history & location & match作为value传递给该context的Provider, 这也是Router为什么要作为最外层组件的原因.

3.2 路由监听

如果说上面讲的context是整个react-router-dom体系的基石的话, 那么history监听则是整个体系的powered by, 也就是动力来源.

先来看一下主要的源码:

1
2
3
4
5
6
7
this.unlisten = props.history.listen((location) => {
if(this._isMounted) {
this.setState({ location });
} else {
this._pendingLocation = location;
}
});

这里的listen函数并不稀奇, 这是history组件官方为开发者提供的钩子函数, 其本质上是对window.onpopstate做了一层封装. 因此, react应用中的所有url变化, 反射到Router组件中的监听函数, 从而将需要被渲染的components传递给Route组件, Route组件通过:

1
props.path === props.location.pathname

判断是否渲染该组件…

四、实践


多说无益, 但做无妨. 了解了react-router-dom的基本思想, 我们继续完善自己的库.

首先, 就是完成我们的Router.tsx:

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
// src/yyg-react-router-dom/components/Router.tsx
import * as React from 'react';
import {
History,
Location,
} from 'history';

import RouterContext from './RouterContext';
import {
IStaticMatchParams,
} from '../types';

export interface IRouterProps {
children: React.ReactNode;
history: History;
};
interface IRouterState {
match: IStaticMatchParams,
location: Location,
};


const Router = React.memo<IRouterProps>((
props: IRouterProps,
): JSX.Element => {
const [state, setState] = React.useState<IRouterState>({
match: {
params: {},
path: '/',
url: '/',
isExact: false,
},
location: {
key: '',
pathname: '/',
search: '',
hash: '',
state: {},
},
});

// ** 设置初始的location & match **
React.useEffect(() => {
setState({
match: {
...state.match,
isExact: props.history.location.pathname === '/',
},
location: props.history.location,
});
}, [props.history]);

React.useEffect(() => {
// ** 监听url **
const unListen = props.history.listen((location) => {
// ** 设置新的location **
setState({
...state,
location,
})
});

return () => {
unListen();
};
}, []);

return (
<RouterContext.Provider
children={props.children}
value={{
...state,
history: props.history,
}}
/>
);
});

export default Router;

接着创建我们的RouterContext, 值得注意的是:

PS: react-router-dom官方使用的是create-react-context这个库, 我们了解一下就行了, 我们可以自己创建.

1
2
3
4
5
6
7
8
9
10
// src/yyg-react-router-dom/components/RouterContext.tsx
import * as React from 'react';

function createRouterContext(
defaultValue = {},
) {
return React.createContext(defaultValue);
}

export default createRouterContext();

另外, 值得注意的是:

PS: 在书写.tsx的时候, 最好将公共的types定义提取出来, 这样将可以将上述match对象的接口定义在types.tsx中, 方便后续融合到*.d.ts中发布…

1
2
3
4
5
6
7
8
9
10
11
// src/yyg-react-router-dom/types.tsx
...
export interface IStaticMatchParams {
params: {
[key: string]: string,
};
isExact: boolean;
path: string;
url: string;
}
...

基本上到这里, Router组件的相关内容已经搞定了, 下面该做一下测试来验证我们的组件..

五、测试


实践完自己的react-router-dom库之后, 需要作一个简单的测试, 验证是否能预期工作…

这里, 由于Route组件需要直接使用context.consumer, 所以, 我们就地取材, 使用同目录下的Route.tsx来test, 在Route组件中写入我们自己的测试内容:

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
// src/yyg-react-router-dom/components/Route.tsx
import * as React from 'react';

import RouterContext from './RouterContext';


export interface IRouteProps { };

const Route = ((
props: IRouteProps,
): JSX.Element => {
return (
<RouterContext.Consumer>
{
(context) => {
console.log(context); // 输出

return <div />
}
}
</RouterContext.Consumer>
);
});

export default Route;

完成之后, 在App.tsx中引入我们的Route组件, 像这样:

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
// src/App.tsx

import * as React from 'react';

import {
BrowserRouter,
Route,
} from './yyg-react-router-dom/index';


export interface IAppProps { };

const App = React.memo<IAppProps>((
props: IAppProps,
): JSX.Element => {
return (
<div>
<BrowserRouter>
Route
</BrowserRouter>

// 这里会出现一个问题, 马上会解决
<Route />
</div>
);

});

export default App;

完成上述步骤之后, 我们npm start启动服务器, console栏中可以看到如下输出:

测试context输出

我们可以看到, 这里输出了一个空对象, 当然这也就是我们为RouterContext设置的默认值了. 出现这一步, 代表我们已经打通了context传递的流程. 接下来, 我们还得测试url的变化, 是否能反映到Router中的context, 然后又能否通过context的Provider分发给对应的Consumer

这里, 有个小方法, 我们可以将history对象挂在到window上, 这样可以方便的在chrome中调试了. 打开BrowserRouter.tsx组件, 在创建好browserHistory对象之后, 同时将其挂载到window上:

1
2
3
4
5
6
7
// src/yyg-react-router-dom/components/BrowserRouter.tsx

...
const browserHistory: History = createBrowserHistory(props);
// 挂在到window
Reflect.set(window, 'browserHistory', browserHistory);
...

接着, 打开chrome的console栏, 输入browserRouter, 出现如图所示就代表已经成功了…

console栏browserRouter测试挂载

然后, 命令行中输入browserRouter.push('/duan'), 可以看到url地址栏已经变化了, 但是….. console栏没有输出我们的Route.tsx中的context, 按理说, 我们已经在Router中监听了url的变化, 而这种变化会即时反映到Context.Provider中, 但是这里并不是我们预期的那样…原因在哪里呢???

带着这个问题, 我们再次打开App.tsx, 我们发现, 此时的

Route和BrowserRouter并不是父子关系

RouteBrowserRouter并不是父子关系, 也就是说ConsumeProvider是分开的, 这也是问题所在. 将<Route>组件放置在<BrowserRouter>内. 保存打开chrome, console中输入browserRouter.push('/zhaoyang'), 这时候, 可以清楚的看到:

测试成功

每一次url的变化, Route组件都能正常的接收到这种改变, 到这里, 我们的测试步骤就完成了. 当然, 还有一些错误处理, 后续可以再完善…

六、源码


源码地址: 点我

七、总结


这一篇主要学习了Router组件的工作流程, 下一篇将会看看Route, 同样也是非常重要的一个组件. 最后, 附上一张流程图, 对这一节作一个总结:

流程图