上一篇文章咱们讲了React-Router
的根本用法,并实现了常见的前端路由鉴权。本文会持续深刻React-Router
讲讲他的源码,套路还是一样的,咱们先用官网的API实现一个简略的例子,而后本人手写这些API来替换官网的并且放弃性能不变。
本文全副代码曾经上传GitHub,大家能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-router-code
简略示例
本文用的例子是上篇文章开始那个不带鉴权的简略路由跳转例子,跑起来是这样子的:
咱们再来回顾下代码,在app.js
外面咱们用Route
组件渲染了几个路由:
import React from 'react';import { BrowserRouter as Router, Switch, Route,} from "react-router-dom";import Home from './pages/Home';import Login from './pages/Login';import Backend from './pages/Backend';import Admin from './pages/Admin';function App() { return ( <Router> <Switch> <Route path="/login" component={Login}/> <Route path="/backend" component={Backend}/> <Route path="/admin" component={Admin}/> <Route path="/" component={Home}/> </Switch> </Router> );}export default App;
每个页面的代码都很简略,只有一个题目和回首页的链接,比方登录页长这样,其余几个页面相似:
import React from 'react';import { Link } from 'react-router-dom';function Login() { return ( <> <h1>登录页</h1> <Link to="/">回首页</Link> </> );}export default Login;
这样咱们就实现了一个最简略的React-Router
的利用示例,咱们来剖析下咱们用到了他的哪些API,这些API就是咱们明天要手写的指标,认真一看,咱们如同只用到了几个组件,这几个组件都是从react-router-dom
导出来的:
BrowserRouter: 被咱们重命名为了Router
,他包裹了整个React-Router
利用,感觉跟以前写过的react-redux
的Provider
相似,我猜是用来注入context
之类的。Route: 这个组件是用来定义具体的路由的,接管路由地址
path
和对应渲染的组件作为参数。Switch:这个组件是用来设置匹配模式的,不加这个的话,如果浏览器地址匹配到了多个路由,这几个路由都会渲染进去,加了这个只会渲染匹配的第一个路由组件。
Link:这个是用来增加跳转链接的,性能相似于原生的
a
标签,我猜他外面也是封装了一个a
标签。
BrowserRouter源码
咱们代码外面最外层的就是BrowserRouter
,咱们先去看看他的源码干了啥,地址传送门:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/BrowserRouter.js
看了他的源码,咱们发现BrowserRouter
代码很简略,只是一个壳:
import React from "react";import { Router } from "react-router";import { createBrowserHistory as createHistory } from "history";class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />; }}
在这个壳外面还援用了两个库react-router
和history
,BrowserRouter
仅仅是调用history
的createHistory
失去一个history
对象,而后用这个对象渲染了react-router
的Router
组件。看起来咱们要搞懂react-router-dom
的源码还必须得去看react-router
和history
的源码,当初咱们手上有好几个须要搞懂的库了,为了看懂他们的源码,咱们得先理分明他们的构造关系。
React-Router的我的项目构造
React-Router
的构造是一个典型的monorepo
,monorepo
这两年开始风行了,是一种比拟新的多项目管理形式,与之绝对的是传统的multi-repo
。比方React-Router
的我的项目构造是这样的:
留神这里的packages
文件夹上面有四个文件夹,这四个文件夹每个都能够作为一个独自的我的项目公布。之所以把他们放在一起,是因为他们之前有很强的依赖关系:
react-router:是React-Router
的外围库,解决一些共用的逻辑react-router-config:是
React-Router
的配置解决,咱们个别不须要应用react-router-dom:浏览器上应用的库,会援用
react-router
外围库react-router-native:反对
React-Native
的路由库,也会援用react-router
外围库
像这样多个仓库,公布多个包的状况,传统模式是给每个库都建一个git repo
,这种形式被称为multi-repo
。像React-Router
这样将多个库放在同一个git repo
外面的就是monorepo
。这样做的益处是如果出了一个BUG或者加一个新性能,须要同时改react-router
和react-router-dom
,monorepo
只须要一个commit
一次性就改好了,公布也能够一起公布。如果是multi-repo
则须要批改两个repo
,而后别离公布两个repo
,公布的时候还要协调两个repo
之间的依赖关系。所以当初很多开源库都应用monorepo
来将依赖很强的模块放在一个repo
外面,比方React源码也是一个典型的monorepo
。
yarn
有一个workspaces
能够反对monorepo
,应用这个性能须要在package.json
外面配置workspaces
,比方这样:
"workspaces": { "packages": [ "packages/*" ] }
扯远了,monorepo
能够前面独自开一篇文章来讲,这里讲这个次要是为了阐明React-Router
分拆成了多个包,这些包之间是有比拟强的依赖的。
后面咱们还用了一个库是history
,这个库没在React-Router
的monorepo
外面,而是独自的一个库,因为官网把他写的性能很独立了,不肯定非要联合React-Router
应用,在其余中央也能够应用。
React-Router架构思路
我之前另一篇文章讲Vue-Router的原理提到过,前端路由实现无非这几个关键点:
- 监听URL的扭转
- 扭转vue-router外面的current变量
- 监督current变量
- 获取对应的组件
- render新组件
其实React-Router
的思路也是相似的,只是React-Router
将这些性能拆分得更散,监听URL变动独立成了history库,vue-router外面的current
变量在React外面是用Context API
实现的,而且放到了外围库react-router
外面,一些跟平台相干的组件则放到了对应的平台库react-router-dom
或者react-router-native
外面。依照这个思路,咱们本人写的React-Router
文件夹上面也建几个对应的文件夹:
手写本人的React-Router
而后咱们顺着这个思路一步一步的将咱们代码外面用到的API替换成本人的。
BrowserRouter组件
BrowserRouter
这个代码后面看过,间接抄过来就行:
import React from "react";import { Router } from "react-router";import { createBrowserHistory as createHistory } from "history";class BrowserRouter extends React.Component { history = createHistory(this.props); render() { return <Router history={this.history} children={this.props.children} />; }}export default BrowserRouter;
react-router的Router组件
下面的BrowserRouter
用到了react-router
的Router
组件,这个组件在浏览器和React-Native
端都有应用,次要获取以后路由并通过Context API
将它传递上来:
import React from "react";import HistoryContext from "./HistoryContext.js";import RouterContext from "./RouterContext.js";/** * The public API for putting history on context. */class Router extends React.Component { // 静态方法,检测以后路由是否匹配 static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; } constructor(props) { super(props); this.state = { location: props.history.location // 将history的location挂载到state上 }; // 上面两个变量是防御性代码,避免根组件还没渲染location就变了 // 如果location变动时,以后根组件还没渲染进去,就先记下他,等以后组件mount了再设置到state上 this._isMounted = false; this._pendingLocation = null; // 通过history监听路由变动,变动的时候,扭转state上的location this.unlisten = props.history.listen(location => { if (this._isMounted) { this.setState({ location }); } else { this._pendingLocation = location; } }); } componentDidMount() { this._isMounted = true; if (this._pendingLocation) { this.setState({ location: this._pendingLocation }); } } componentWillUnmount() { if (this.unlisten) { this.unlisten(); this._isMounted = false; this._pendingLocation = null; } } render() { // render的内容很简略,就是两个context // 一个是路由的相干属性,包含history和location等 // 一个只蕴含history信息,同时将子组件通过children渲染进去 return ( <RouterContext.Provider value={{ history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), }} > <HistoryContext.Provider children={this.props.children || null} value={this.props.history} /> </RouterContext.Provider> ); }}export default Router;
上述代码是我精简过的代码,原版代码能够看这里。这段代码次要是创立了两个context
,将路由信息和history
信息放到了这两个context
上,其余也没干啥了。对于React的Context API
我在另外一篇文章具体讲过,这里不再赘述了。
history
后面咱们其实用到了history的三个API:
createBrowserHistory: 这个是用在BrowserRouter外面的,用来创立一个history对象,前面的listen和unlisten都是挂载在这个API的返回对象下面的。history.listen:这个是用在Router组件外面的,用来监听路由变动。
history.unlisten:这个也是在Router组件外面用的,是
listen
办法的返回值,用来在清理的时候勾销监听的。
上面咱们来实现这个history:
// 创立和治理listeners的办法function createEvents() { let handlers = []; return { push(fn) { handlers.push(fn); return function () { handlers = handlers.filter(handler => handler !== fn); }; }, call(arg) { handlers.forEach(fn => fn && fn(arg)); } }}function createBrowserHistory() { const listeners = createEvents(); let location = { pathname: '/', }; // 路由变动时的回调 const handlePop = function () { const currentLocation = { pathname: window.location.pathname } listeners.call(currentLocation); // 路由变动时执行回调 } // 监听popstate事件 // 留神pushState和replaceState并不会触发popstate // 然而浏览器的后退后退会触发popstate // 咱们这里监听这个事件是为了解决浏览器的后退后退 window.addEventListener('popstate', handlePop); // 返回的history上有个listen办法 const history = { listen(listener) { return listeners.push(listener); }, location } return history;}export default createBrowserHistory;
上述history
代码是超级精简版的代码,官网源码很多,还反对其余性能,咱们这里只拎进去外围性能,对官网源码感兴趣的看这里:https://github.com/ReactTraining/history/blob/28c89f4091ae9e1b0001341ea60c629674e83627/packages/history/index.ts#L397
Route组件
咱们后面的利用外面还有个很重要的组件是Route
组件,这个组件是用来匹配路由和具体的组件的。这个组件看似是从react-router-dom
外面导出来的,其实他只是相当于做了一个转发,一成不变的返回了react-router
的Route
组件:
这个组件其实只有一个作用,就是将参数上的path
拿来跟以后的location
做比照,如果匹配上了就渲染参数上的component
就行。为了匹配path
和location
,还须要一个辅助办法matchPath
,我间接从源码抄这个办法了。大抵思路是将咱们传入的参数path
转成一个正则,而后用这个正则去匹配以后的pathname
:
import pathToRegexp from "path-to-regexp";const cache = {};const cacheLimit = 10000;let cacheCount = 0;function compilePath(path, options) { const cacheKey = `${options.end}${options.strict}${options.sensitive}`; const pathCache = cache[cacheKey] || (cache[cacheKey] = {}); if (pathCache[path]) return pathCache[path]; const keys = []; const regexp = pathToRegexp(path, keys, options); const result = { regexp, keys }; if (cacheCount < cacheLimit) { pathCache[path] = result; cacheCount++; } return result;}/** * Public API for matching a URL pathname to a path. */function matchPath(pathname, options = {}) { if (typeof options === "string" || Array.isArray(options)) { options = { path: options }; } const { path, exact = false, strict = false, sensitive = false } = options; const paths = [].concat(path); return paths.reduce((matched, path) => { if (!path && path !== "") return null; if (matched) return matched; const { regexp, keys } = compilePath(path, { end: exact, strict, sensitive }); const match = regexp.exec(pathname); if (!match) return null; const [url, ...values] = match; const isExact = pathname === url; if (exact && !isExact) return null; return { path, // the path used to match url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL isExact, // whether or not we matched exactly params: keys.reduce((memo, key, index) => { memo[key.name] = values[index]; return memo; }, {}) }; }, null);}export default matchPath;
而后是Route
组件,调用下matchPath
来看下以后路由是否匹配就行了,以后路由记得从RouterContext
外面拿:
import React from "react";import RouterContext from "./RouterContext.js";import matchPath from "./matchPath.js";/** * The public API for matching a single path and rendering. */class Route extends React.Component { render() { return ( <RouterContext.Consumer> {context => { // 从RouterContext获取location const location = context.location; const match = matchPath(location.pathname, this.props); // 调用matchPath检测以后路由是否匹配 const props = { ...context, location, match }; let { component } = this.props; // render对应的component之前先用最新的参数match更新下RouterContext // 这样上层嵌套的Route能够拿到对的值 return ( <RouterContext.Provider value={props}> {props.match ? React.createElement(component, props) : null} </RouterContext.Provider> ); }} </RouterContext.Consumer> ); }}export default Route;
上述代码也是精简过的,官网源码还反对函数组件和render
办法等,具体代码能够看这里:https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Route.js
其实到这里,React-Router
的外围性能曾经实现了,然而咱们开始的例子中还用到了Switch
和Link
组件,咱们也一起来把它实现了吧。
Switch组件
咱们下面的Route
组件的性能是只有path
匹配上以后路由就渲染组件,也就意味着如果多个Route
的path
都匹配上了以后路由,这几个组件都会渲染。所以Switch
组件的性能只有一个,就是即便多个Route
的path
都匹配上了以后路由,也只渲染第一个匹配上的组件。要实现这个性能其实也不难,把Switch
的children
拿进去循环,找出第一个匹配的child
,给它增加一个标记属性computedMatch
,顺便把其余的child
全副干掉,而后批改下Route
的渲染逻辑,先检测computedMatch
,如果没有这个再应用matchPath
本人去匹配:
import React from "react";import RouterContext from "./RouterContext.js";import matchPath from "./matchPath.js";class Switch extends React.Component { render() { return ( <RouterContext.Consumer> {context => { const location = context.location; // 从RouterContext获取location let element, match; // 两个变量记录第一次匹配上的子元素和match属性 // 应用React.Children.forEach来遍历子元素,而不能应用React.Children.toArray().find() // 因为toArray会给每个子元素增加一个key,这会导致两个有同样component,然而不同URL的<Route>反复渲染 React.Children.forEach(this.props.children, child => { // 先检测下match是否曾经匹配到了 // 如果曾经匹配过了,间接跳过 if (!match && React.isValidElement(child)) { element = child; const path = child.props.path; match = matchPath(location.pathname, { ...child.props, path }); } }); // 最终<Switch>组件的返回值只是匹配子元素的一个拷贝,其余子元素被忽略了 // match属性会被塞给拷贝元素的computedMatch // 如果一个都没匹配上,返回null return match ? React.cloneElement(element, { location, computedMatch: match }) : null; }} </RouterContext.Consumer> ); }}export default Switch;
而后批改下Route
组件,让他先查看computedMatch
:
// ... 省略其余代码 ...const match = this.props.computedMatch ? this.props.computedMatch : matchPath(location.pathname, this.props); // 调用matchPath检测以后路由是否匹配
Switch
组件其实也是在react-router
外面,源码跟咱们下面写的差不多:https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/Switch.js
Link组件
Link
组件性能也很简略,就是一个跳转,浏览器上要实现一个跳转,能够用a
标签,然而如果间接应用a
标签可能会导致页面刷新,所以不能间接应用它,而应该应用history API
,history API
具体文档能够看这里。咱们这里要跳转URL能够间接应用history.pushState
。应用history.pushState
须要留神一下几点:
history.pushState
只会扭转history
状态,不会刷新页面。换句话说就是你用了这个API,你会看到浏览器地址栏的地址变动了,然而页面并没有变动。- 当你应用
history.pushState
或者history.replaceState
扭转history
状态的时候,popstate
事件并不会触发,所以history
外面的回调不会主动调用,当用户应用history.push
的时候咱们须要手动调用回调函数。history.pushState(state, title[, url])
接管三个参数,第一个参数state
是往新路由传递的信息,能够为空,官网React-Router
会往里面加一个随机的key
和其余信息,咱们这里间接为空吧,第二个参数title
目前大多数浏览器都不反对,能够间接给个空字符串,第三个参数url
是可选的,是咱们这里的要害,这个参数是要跳往的指标地址。- 因为
history
曾经成为了一个独立的库,所以咱们应该将history.pushState
相干解决加到history
库外面。
咱们先在history
外面新加一个APIpush
,这个API会调用history.pushState
并手动执行回调:
// ... 省略其余代码 ...push(url) { const history = window.history; // 这里pushState并不会触发popstate // 然而咱们依然要这样做,是为了放弃state栈的一致性 history.pushState(null, '', url); // 因为push并不触发popstate,咱们须要手动调用回调函数 location = { pathname: url }; listeners.call(location);}
下面说了咱们间接应用a
标签会导致页面刷新,然而如果不应用a
标签,Link
组件应该渲染个什么标签在页面上呢?能够轻易渲染个span
,div
什么的都行,然而可能会跟大家平时的习惯不一样,还可能导致一些款式生效,所以官网还是抉择了渲染一个a
标签在这里,只是应用event.preventDefault
禁止了默认行为,而后用history api
本人实现了跳转,当然你能够本人传component
参数进去扭转默认的a
标签。因为是a
标签,不能兼容native
,所以Link
组件其实是在react-router-dom
这个包外面:
import React from "react";import RouterContext from "../react-router/RouterContext";// LinkAnchor只是渲染了一个没有默认行为的a标签// 跳转行为由传进来的navigate实现function LinkAnchor({navigate, ...rest}) { let props = { ...rest, onClick: event => { event.preventDefault(); navigate(); } } return <a {...props} />;}function Link({ component = LinkAnchor, // component默认是LinkAnchor to, ...rest}) { return ( <RouterContext.Consumer> {context => { const { history } = context; // 从RouterContext获取history对象 const props = { ...rest, href: to, navigate() { history.push(to); } }; return React.createElement(component, props); }} </RouterContext.Consumer> );}export default Link;
上述代码是精简版的Link
,根本逻辑跟官网源码一样:https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js
到这里结尾示例用到的全副API都换成了咱们本人的,其实也实现了React-Router
的外围性能。然而咱们只实现了H5 history
模式,hash
模式并没有实现,其实有了这个架子,增加hash
模式也比较简单了,根本架子不变,在react-router-dom
外面增加一个HashRouter
,他的根本构造跟BrowserRouter
是一样的,只是他会调用history
的createHashHistory
,createHashHistory
外面不仅仅会去监听popstate
,某些浏览器在hash
变动的时候不会触发popstate
,所以还须要监听hashchange
事件。对应的源码如下,大家能够自行浏览:
HashRouter: https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/HashRouter.js
createHashHistory: https://github.com/ReactTraining/history/blob/28c89f4091ae9e1b0001341ea60c629674e83627/packages/history/index.ts#L616
总结
React-Router
的外围源码咱们曾经读完了,上面咱们来总结下:
React-Router
因为有跨平台的需要,所以分拆了好几个包,这几个包采纳monorepo
的形式治理:react-router
是外围包,蕴含了大部分逻辑和组件,解决context
和路由匹配都在这里。react-router-dom
是浏览器应用的包,像Link
这样须要渲染具体的a
标签的组件就在这里。react-router-native
是react-native
应用的包,外面蕴含了android
和ios
具体的我的项目。
- 浏览器事件监听也独自独立成了一个包
history
,跟history
相干的解决都放在了这里,比方push
,replace
什么的。 React-Router
实现时外围逻辑如下:- 应用不刷新的路由API,比方
history
或者hash
- 提供一个事件处理机制,让
React
组件能够监听路由变动。 - 提供操作路由的接口,当路由变动时,通过事件回调告诉
React
。 - 当路由事件触发时,将变动的路由写入到
React
的响应式数据上,也就是将这个值写到根router
的state
上,而后通过context
传给子组件。 - 具体渲染时将路由配置的
path
和以后浏览器地址做一个比照,匹配上就渲染对应的组件。
- 应用不刷新的路由API,比方
在应用
popstate
时须要留神:- 原生
history.pushState
和history.replaceState
并不会触发popstate
,要告诉React
须要咱们手动调用回调函数。 - 浏览器的后退后退按钮会触发
popstate
事件,所以咱们还是要监听popstate
,目标是兼容后退后退按钮。
- 原生
本文全副代码曾经上传GitHub,大家能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/react-router-code
参考资料
官网文档:https://reactrouter.com/web/guides/quick-start
GitHub源码地址:https://github.com/ReactTraining/react-router/tree/master/packages
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。
作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges