前言
本文介绍的是,关于讲解关于 React-Router 底层逻辑,模拟实现一下 React-Router 路由,实现过程中,帮助自己理解掌握路由的原理
1. 第一步
构建模拟项目的架构,使用 create-react-app 生成项目模版,这里就不详细叙述
create-react-app react-routet-project
2. 第二步
我们需要模拟实现通过切换路由,跳转到不同的页面(这里我们配置好 3 个页面),下面通过 react-router-dom 这个官方推荐的库实现路由跳转
平常我们大部分项目中也是这样配置处理,遇到路由通常会另外新建一个文件进行配置
export default Routes = [
{
path: '...',
component: xxxx,
},
....
]
在这个基础上我们将 react-router-dom 替换成我们自己模拟实现的 react-router。
import Page1 from './pages/page1'
import Page2 from './pages/page2'
import Page3 from './pages/page3'
import React, { Component } from 'react'
import { Route, Switch, withRouter, BrowserRouter } from 'react-router-dom';
console.log(Route, BrowserRouter)
class Router extends Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={withRouter(Page1)} />
<Route exact path="/page2" component={withRouter(Page2)} />
<Route exact path="/page3" component={withRouter(Page3)} />
</Switch>
</BrowserRouter>
)
}
}
export default Router
这里用到了 withRouter 需要解释一下,使用它的时候,因为外层有 BrowserRouter 包装,通过 withRouter 可以拿到 路由相关的上下文内容
3. 第三步
下面我们需要实现 react-router-dom 中下面的组件
- Route
- Switch
- withRouter
- BrowserRouter
3.1 BrowserRouter
BrowserRouter 的实现 基于 React.createContext() 实现
核心原理
监听popstate事件,当浏览器路由地址history 发生变化的时候,那么在popstate回调中,拿到变化值,进行不同的渲染操作
这里就不具体实现
- basename: string
- getUserConfirmation: func
- forceRefresh: bool
- keyLength: number
- children: node
import React, { Component } from "react";
import Context from './context';
export default class BrowserRouter extends Component {
constructor(props) {
super(props);
this.state = {
location: {
pathname: window.location.pathname || "/",
search: undefined,
},
match: {
}
}
}
componentWillMount() {
// 监听 popstate 事件,如果我们路由发生变化,就会重新修改 state 值
window.addEventListener("popstate", () => {
// 保存当前的 pathname
this.setState({
location: {
pathname: window.location.pathname
}
})
})
}
render() {
const curRoute = {
location: this.state.location,
match: this.state.match,
history: {
// 模拟 页面上需要调用的函数 push, replace 等 浏览器 API
push: (to) => {
// 根据当前 to 去匹配不同的路由 实现路由切换
// to 是对象的情况是 使用了 Link 的情况
// <Link to="/today"/> // renders <a href="/calendar/today">
if (typeof to === 'object') {
let { pathname, query } = to;
// 只是改变当前state的数据, 不触发reRender
this.setState({
location: {
query: to.query,
pathname: to.pathname
}
});
// 实现路由跳转,前进,推入新的路由
window.history.pushState({}, {}, pathname)
} else {
// 如果是字符串
this.setState({
location: {
pathname: to
}
})
// 实现路由跳转
window.history.pushState({}, {}, to)
}
}
}
}
// Context 就是 React.createContext()
return (
<Context.Provider value={curRoute}>{this.props.children}</Context.Provider>
)
}
}
3.2 Route
这里用到了 路由匹配的正则表达式,这里引入第三方组件库 path-to-regexp
原理
通过在上一步 Context.Provider 中注入的数据 curRoute 数据,将拿到的 curRoute 数据 和 组件中传入的 path 参数进行 Diff 匹配,如果匹配成功,那么就渲染传入的 Component 组件,如果没有匹配成功的话,那么渲染空组件
import React, { Component } from "react";
import Context from "./context";
import { pathToRegexp, match } from "path-to-regexp";
export default class Route extends Component {
static contextType = context;
render() {
const currenRoutePath = this.context.location.pathname; // 从上下文context中获取到当前路由
const { path, component: Component, exact = false } = this.props; // 获取Route组件props的路由
const paramsRegexp = match(path, { end: exact }); // 生成获取params的表达式
const matchResult = paramsRegexp(currenRoutePath); // 通过正则获取得到 匹配的路由结果
console.log("路由匹配结果", matchResult);
this.context.match.params = matchResult.params; // 获取到请求参数
// 将上下文全部的内容 赋值到 props
const props = {
...this.context
}
const pathRegexp = pathToRegexp(path, [], { end: exact }); // 生成路径匹配表达式
// 核心关键步骤,如果匹配到浏览器的路由路径和传入路径匹配,那么渲染传入的组件
if (pathRegexp.test(currenRoutePath)) {
return (<Component {...props}></Component>) // 将概念上下文路由信息当作传递给传入的组件
}
return null; // 返回空组件 可以在
}
}
3.3 Switch
Switch 核心是在组件内接收 多个 Route 组件作为 Children, 遍历 Children 然后再做路由的匹配,到底是渲染哪一个组件,这里直接贴源码,实现原理也很简单
var Switch = /*#__PURE__*/function (_React$Component) {
_inheritsLoose(Switch, _React$Component); // 这个可以不管
function Switch() {
// 上下文 this 指向绑定
return _React$Component.apply(this, arguments) || this;
}
var _proto = Switch.prototype;
// 组件函数的 render 方法,写法其实一样
_proto.render = function render() {
var _this = this;
// 只返回一个 React Element
return /*#__PURE__*/React.createElement(context.Consumer, null, function (context) {
!context ? invariant(false, "You should not use <Switch> outside a <Router>") : void 0;
var location = _this.props.location || context.location;
var element, match;
// toArray 会将每个 Child 加 Key
// component at different URLs.
// 遍历 传入的多个 Route 子组件
React.Children.forEach(_this.props.children, function (child) {
if (match == null && /*#__PURE__*/React.isValidElement(child)) {
element = child;
var path = child.props.path || child.props.from;
// 判断是否匹配成功,如果成功就将成功的 path location 等参数传入
match = path ? matchPath(location.pathname, _extends({}, child.props, {
path: path
})) : context.match;
}
});
// 匹配失败 那么就是 null
return match ? /*#__PURE__*/React.cloneElement(element, {
location: location,
computedMatch: match
}) : null;
});
};
return Switch;
}(React.Component);
3.4 withRouter
withRouter 核心其实是用 React.Consumer 包裹一层,将上一层 BrowserRouter 或者 HashRouter 上的 Provider 数值传入到 withRouter 组件中调用继承的作用
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";
import RouterContext from "./RouterContext.js";
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
// 核心逻辑在这里
// context 拿到从父组件获取的上下文
<RouterContext.Consumer>
{context => {
invariant(
context,
`You should not use <${displayName} /> outside a <Router>`
);
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
return hoistStatics(C, Component);
}
export default withRouter;
3.5 Link
import React, { Component } from "react";
import context from "./context";
export default class Link extends Component {
static contextType = context;
render() {
const { to } = this.props;
return (
<a onClick={() => {this.context.history.push(to)}}>
{
this.props.children
}
</a>
)
}
}
小结
致此,这里就基本实现了 React 路由的实现了,简单版本的 React-Router,在参照官方版本的源码的基础上,其实不难理解它的原理,但面试的时候很多情况下都会被问到他们是如何实现的。如果自己亲手实现一遍,基本上就了解它是如何实现并且展开来使用的,这对以后工作帮助也很大。这里 BrowserRoute 实现方式取巧用到的是 React.createContext ,但是我们抛出这个 API,很大概率在面试上会被追问下去,如何实现 createContext的,这里就不详细讲述了
后记
面试的时候,经常会被问到关于,history 路由和 Hash 路由之间的区别。这里也总结一下
- 由于历史的原因,浏览器以前是没有提供 API 去操作触发浏览器重新请求的,所以才有了
hash路由,监听hash值改变去渲染不同的页面,HTML5 以后多了很多 API 例如 onpopstate 去做监听 - SPA 单页应用路由的方式,慢慢由
hash变成history出于美观的话,少了一个# hash路由通过监听onhashchange事件,在回调里面获取hash值的变化history路由就是 通过pushState和replaceState来检测地址的改变,切换不同的页面。