Hi, Everyone, 好久不见, 其实想了很久, 到底要不要出一篇
react-router
的源码级博客, 怕自己的理解不够深, 误导了大家, 但是本着书写学习笔记的习惯, 笔者还是写下来了自己对react路由的理解, 如果有问题之处还请大牛指教, 也希望这篇博客可以帮助到正在学习router
原理的你
本博客不会过度的去分析react
自身的源码, 因为这些我相信大家从git
上可以很轻易的拿到, 笔者是通过从0书写一个自己的react-router
来实现跟react
同样功能的方式来分享整个路由的思想, 这样也更好的让初入源码学习的同学更好的适应
react-router
团队本身在实现router
的时候引用了两个比较小的库, 一个叫做path-to-regexp
, 一个叫做history
, 所以笔者这里也将会直接引用这两个库, 当然势必对帮助大家对这两个库进行一个全面的熟悉和了解, 如果想了解这两个库是怎么写的, 可以移步笔者的另外的博客进行了解
目录结构
-
path-to-regexp
的了解
-
history
的了解
-
Router
的实现
-
Route
的实现
-
Switch
的实现
-
withRouter
的实现
-
Link
和NavLink
的实现
在前期的Router
编写中, 或许没办法直接演示, 如果小伙伴有看不太明白的地方可以直接提问或者等到Route
组件写完然后看笔者的例子再回头看Router
可能就醍醐灌顶了, 坚持到最会你就会发现大名鼎鼎的react-router
也不过如此
1. path-to-regexp的使用
在书写一个自己的router
之前, 笔者必须做一些铺垫, 首当其冲的就是认识path-to-regexp
这个库
该库用于将一个字符串正则(路径正则, path regexp), React Router中用到了这个库, 笔者这里不再手写
// 我们书写的Route组件中的path属性, 有时候会写成下面这种形式
<Route path='/news/:year/:month/:day' component={
news}/>
// 其中的path属性看着像正则却不是正则, 而path-to-regexp这个库就是帮助我们将
// /news/:year/:month/:day 转化为正儿八经的正则表达式, 然后router才会拿去比对和校验
// 如果不进行转化成真正的正则表达式, js是不认识的
这哥们接受三个参数, 并在执行调用完毕以后返回一个正则表达式, 我们可以用返回的正则表达式
参数 | 功能 |
---|---|
path | 要匹配的校验规则 |
keys | path-to-regexp 会将第一个参数path 规则中的每一项的关键字抽出来包装在key三种传递给你 |
options | 其他配置项,如是否开启大小写敏感, 是否精确匹配等 |
表格参数功能没看懂没关系, 笔者一开始也不是很懂, 但是你只要看看返回的数据就会秒懂了
我们来看看他的基本使用
import pathToRegexp from 'path-to-regexp';
const path = '/news/:year/:month/:day';
const keys = []; // 这个数组现在是空的, 待会我作为第二个参数丢进去, 他会在函数执行完以后给我一个有东西的数组
const result = pathToRegexp(path, keys, {
sensitive: true, // 是否对大小写敏感
end: true, // 是否精确匹配
});
console.log('keys如下: ', keys); // 会给我们一个数组, 将path参数中的关键字都抽离出来
console.log('根据path和配置项生成的正则如下: ', result); // 输出的就是一套正则表达式
输出结果如下
返回的正则表达式就好像是说把我们的
path
规则和第三个参数配置项通过分析得出一个正则表达式, 而第二个参数keys
只是为了帮助我们更好的进行后续操作准备的, 我们的path
是/news/:year/:month/:day
, 于是keys
中就将year
,month
,day
给我们封装进去了, 后续在使用中这些可能会对我们有帮助, 但是我们也不会用到它, 你可以理解keys
仅仅是一个辅助参数
OK, path-to-exp
的基本了解说到这里, 因为react-router
本身也是直接调用的这个库, 所以我们也因为篇幅问题自己就不写了
2. history是了解和使用
该对象提供了一些方法, 用于控制或监听地址的变化
该对象不是window.history
, 而是一个抽离的对象, 它封装了具体的实现
我们来看看他的基本使用吧
import {
createBrowserHistory } from 'history';
const browserHistory = createBrowserHistory();
console.log('打印出的browserHistory如下', browserHistory);
输出结果如下, 这哥们就是提供了这些方法二次封装了浏览器的history
对象, 提供了更加强大的功能, 这些功能我们随着用随着说, 这里就点到为止, 或许在router
中你会随着使用更加的清晰
3. Router组件的实现
害, 终于进入正题了, 一顿操作猛如虎, 全从Router
开始撸
想要实现Router
, 我们得先知道Router
做了哪些事
- 这哥们本身不做任何的展示, 仅提供路由模式的配置
- 该组件会提供一个上下文, 上下文会提供一些使用的属性和方法, 供其他相关组件使用
- 浏览器中
Router
本身分为以下两种形式- HashRouter: 使用
HashRouter
模式匹配路由 - BrowserRouter: 使用
BrowserRouter
模式展示路由
- HashRouter: 使用
来吧, 来看个实例帮你们整体回忆一下
// App.js
import React from 'react';
import {
BrowserRouter as Router } from 'react-router-dom';
export default function App(props) {
return (
<Router></Router>
)
}
我们来看一下React Devtools
中展示给我们的react
结构
如果图片不够清晰, 或者你看不懂也没关系, 笔者再帮你分析一波
BrowserRouter
当我们引入以后, 其实他内部是还用到了一个核心的Router
组件Router
组件得到一个属性history
为一个对象, 就是我们用history
库构造出来的对象一模一样Router
组件有一个状态location
, 该location
其实就是history
属性中的location
, 只是提出来作为属性而已Router
提供一个上下文, 里面携带一个value
属性, 为一个对象, 对象中内容如下history
: 来自于Router
组件中的history
属性, 保存了当前浏览器历史记录栈的一些方法和信息location
: 来自于Router
组件的location
状态, 保存了当前路由的一些信息match
: 用来判定当前路由跟我们之后要书写的Route
组件的上的path
规则的校验, 它来自于我们自己书写,match
对象携带以下几个属性- isExact:
Boolean
, 是否精确匹配 - params:
- isExact:
Router
的children
会被渲染进页面
这上面的这些基本使用方法我就不再过多的分析了, 别来看
Router
源码了还问我HashRouter
是什么, 说我没写清楚, 那就太尴尬了
那咱一点一点来实现?
src
目录下新建一个react-router
文件夹(当然你自己想建在哪就建在哪), 创建一个Router.js
// Router.js
import React from 'react';
export default class Router extends React.PureComponent {
render() {
{
/*根据我们上面的说法, 这里其实是返回了一个上下文出去*/}
}
}
所以我们先将上下文搞定,
react-router
目录下, 创建一个RouterContext.js
// RouterContext.js
import React from 'react';
const RouterContext = React.createContext();
RouterContext.displayName = 'Router'; // 设置上下文在React Devtools工具中的名称, 这个是一个小细节, 因为我们会发现ReactRouter的上下文在调试工具中显示的是Router.Provider, 就是通过这样改名实现的
export default RouterContext;
回到
Router.js
中
// Router.js
import React from 'react';
import {
default as ctx } from './RouterContext.js'
export default class Router extends React.PureComponent {
render() {
{
/*根据我们上面的说法, 这里其实是返回了一个上下文出去*/}
return (
<ctx.Provider>
</ctx.Provider>
)
}
}
这个时候我们引入我们自己的
Router.js
进App, 并进浏览器看一下结构
结构很明显已经出来了, 只是之前说好的属性都没有, 那我们得给他啊
Router
组件接收一个属性history
, 我们先写着, 等待后续的父组件给他
// Router.js
...
export default class Router extends React.PureComponent {
state = {
// 我们知道location也是从history属性中拿过来的
location: this.props.history.location,
}
render() {
// 我们之前有看到之后提供的上下文里, 有一个value值
// value值里面有history, location, match三个属性
// 要传递给上下文的value对象
const contextValue = {
history: this.props.history,
location: this.state.location,
match: ?
}
return (
{
/*将contextValue传递给Provider*/}
<ctx.Provider value={
contextValue}>
{
this.props.children }
</ctx.Provider>
)
}
}
...
其实我们目前知道
history
和location
最终一定是从父组件来的, 那么match
呢, 这哥们是需要我们自己来构造的, 希望你没有忘记笔者之前说的path-to-regexp
, 如果忘了赶紧回去看看,来吧
在
react-router
目录下新建一个pathMatch.js
// pathMatch.js
import pathToRegExp from 'path-to-regexp';
// 我们知道pathToRegExp就是帮助我们将我们想要设置的浏览器路径规则变成正则表达式, 以方便我们进行比较的
// 写一个方法pathMatch, 他也是最终我们要导出的方法
/**
* 根据调用该方法的人传进来的参数, 用来匹配路径是否符合路径规则, 匹配成功返回一个match对象, 匹配失败返回undefined
* @param {*} path 路径规则
* @param {*} pathname 真实的路径
* @param {*} options 其他配置项: sensitive => 是否大小写敏感, strict => 是否开启严格模式, exact => 是否精确匹配
*/
export default function pathMatch(path, pathname, options) {
const keys = []; // 设置关键字数组, 就跟我们一开始测试pathToRegexp的含义一样
}
我们之前知道, 用户传递进来的的是
sensitive
,strict
,exact
三个属性, 前两个都没有问题, 但是最后一个我们知道path-to-regexp
里精确匹配是为end
, 所以我们必须将用户传递进来的操作转换一下
// pathMatch.js
...
export default function pathMatch(path, pathname, options) {
...
}
/**
* 将传入的react-router的配置转化为path-to-regexp的配置
*/
function getOptions(options) {
const defaultOptions = {
sensitive: false,
strict: false,
exact: false
}
const mergeOptions = {
...defaultOptions, ...options};
return {
end: mergeOptions