蛮多同学可能会感觉
react-router
很简单, 说用都还没用明确, 还从 0 实现一个react-router
, 其实router
并不简单哈, 甚至说你看了这篇博客当前, 你都会感觉router
的外围原理也就那么回事
至于 react-router
帮忙咱们实现了什么货色我就不过多论述了, 这个间接移步官网文档, 咱们上面间接聊实现
另外: react-router
源码有依赖两个库 path-to-regexp
和history
, 所以我这里也就间接引入这两个库了, 尽管上面我都会讲到根本应用, 然而同学有工夫的话还是能够浏览以下官网文档
还有一个须要留神的点是: 上面我书写的 router
原理都是应用 hooks
+ 函数组件来书写的, 而官网是应用类组件书写的, 所以如果你对hooks
还不是很明确的话, 得去补一下这方面的常识, 为什么要抉择 hooks
, 因为当初绝大多数大厂在react
上根本都在鼎力举荐应用hook
, 所以咱们得跟上时代不是, 而且我着重和大家聊的也是原理, 而不是跟官网截然不同的源码, 如果要 1 比 1 的复刻源码不带本人的了解的话, 那你去看官网的源码就行了, 何必看这篇博文了
在本栏博客中, 咱们会聊聊以下内容:
- 封装本人的生成
match
对象办法 history
库的应用Router
和BrowserRouter
的实现Route
组件的实现Switch
和Redirect
的实现withRouter
的实现Link
和NavLink
实现- 聚合
api
封装本人的生成 match
对象办法
在封装之前, 我想跟大家先分享 path-to-regexp
这个库
为什么要先聊这个库哈, 次要起因是因为
react-router
中用到了这个库, 我看了一下其实咱们也没必要本人再去实现一个这个库(为什么没必要呢, 倒并不是因为react-router
没有实现咱们就不实现, 而是因为这个库实现的性能非常简单, 然而细节十分繁琐, 有十分多的因素须要去思考到我感觉没必要), 这个库做的事件非常简单: 将一个字符串变成一个正则表达式
咱们晓得, react-router
的大抵原理就是依据门路的不同从而渲染不同的页面, 那么这个过程其实也就是 门路 A
匹配 页面 B
的过程, 所以咱们之前会写这样的代码
<Route path="/news/:id" component={News} /> // 如果门路匹配上了 /news/:id 这样的门路, 则渲染 News 组件
那么 react-router
他是怎么去判断浏览器地址栏的门路和这个 Route
组件中的 path
属性匹配上的?
path 填写的如果是 /news/:id
这样的门路, 那么 /news/123
/news/321
这种都可能被 react-router
匹配上
咱们可能想到的办法是不是大略能够如下:
将所有的
path
属性全副转换为 正则表达式 (比方/news/:id
转换为/^\/news(?:\/([^\/#\?]+?))[\/#\?]?$/i
), 而后将地址栏的path
值取出来跟该正则表达式进行匹配, 匹配上了就要渲染相应的路由, 匹配不上就渲染其余的逻辑
path-to-regexp
就是做这个事件的, 他把咱们给他的门路字符串转换为正则表达式, 供咱们匹配
装置: yarn add path-to-regexp -S
// 咱们能够来轻易试试这个库
import {pathToRegexp} from "path-to-regexp";
const keys = [];
// pathToRegexp(path, keys?, options?)
// path: 就是咱们要匹配的门路规定
// keys: 如果你传递了, 当他匹配上当前, 会把绝对应的参数 key 传递到 keys 数组中
// options: 给 path 门路规定的一些附加规定, 比方 sensitive 大小写敏感之类的
const result = pathToRegexp("/news/:id", keys);
console.log("result", result);
console.log(result.exec("/news/123")); // 输入 ["/news/123", "123", index: 0, input: "/news/123", groups: undefined]
console.log(result.exec("/news/details/123")); // 输入 null
console.log(keys); // 输入一个数组, 数组的有一个对象{modifier: "name:"id", pattern:"[^\/#\?]+?", prefix:"/", suffix:""}
当然, 这个库还有很多玩法, 他也不是专门为 react-router
实现的, 只是刚好被 react-router
拿过去用了, 对这个库有趣味的同学能够去看看他的文档
咱们应用这个库, 次要是为了封装一个公共办法, 为后续咱们写 router
源码的时候提供一些基石, 因为咱们晓得, react-router
一旦门路匹配上了, 是会向组件里注入 history
, location
等属性的, 这些货色咱们要提前准备好, 所以咱们此刻的指标很简略
如果一个
path
值跟指定的path
正则匹配上了, 那么咱们要生成一个蕴含了location
,history
等属性的对象, 供后续应用, 说的更直白一点就是要失去react-router
中那个的match
对象
咱们会发现这个性能其实是独立的, 这样拆分进去他能够用在任何中央, 只有匹配我就生成一个对象, 我也不论你拿这个对象去干嘛不关我屁事, 这也是软件开发中的一种较好的开发方式, 大家能够停下来在这里认真思考一下这样的益处
所以接下来我要做的事件非常简单, 就是封装一个跟解决门路相干的办法, 为后续咱们开发其余 router
性能的时候提供基层反对
咱们在 react
工程中本人建设一个 react-router
目录, 在其中新建一个文件pathMatch.js
这也意味着咱们将不再从
npm
上拉react-router
, 而是间接在本人的工程里援用本人的react-router
pathMatch.js
中每一步都写上了正文, 应该可能帮忙你很好的了解
// src/react-router/pathMatch.js
import {pathToRegexp} from "path-to-regexp";
/** * * @param {String} path 传递进来的 path 规定 * @param {String} url 须要校验 path 规定的 url * @param {Object} options 一些配置: 如是否准确匹配, 是否大小写敏感等 * * 这个函数要做的事件非常简单, 当我调用这个函数并且传递了相应 * 参数当前, 这个函数须要返回给我一个对象, 对象成员如下 * {* params: { 门路匹配胜利当前的参数值, 匹配不上就是 null * key: value *}, * path: path 规定 * url: 跟 path 规定匹配的那一段 url, 如果匹配不上就是 null * isExact: 是否准确匹配 * } * */
function pathMatch(path = "", url ="", options = {}) {
// 所以在这个函数外部, 咱们要做的事件如下:
// 1. 调用 path-to-regex 库且依据配置来帮忙咱们进行匹配参数值
// 2. 将匹配后果返回进来
// 首先, 如果你读了这个 path-to-regex 的文档的话, 你会发现一个问题
// 咱们在 react-router 中传递 exact 为准确匹配, 而在该库中则是应用 end
// 所以咱们第一步先将用户传递的配置对象变成 path-to-regex 想要的配置对象
const matchOptions = getOptions(options);
const matchKeys = []; // 这个 matchKeys 其实就是咱们用来装匹配胜利后参数 key 的数组
// 而后在 path-to-regexp 中失去绝对应的正则表达式
const pathRegexp = pathToRegexp(path, matchKeys, matchOptions);
// 这里咱们要应用对应的正则表达式来匹配用户传递的 url
const matchResult = pathRegexp.exec(url);
console.log("matchResult", matchResult);
// 如果没有匹配上, 那间接返回 null 了
if(!matchResult) return null;
// 如果匹配上了, 咱们晓得他返回的是一个类数组, 咱们须要将 matchKeys 和类数组进行遍历
// 生成最终的 match 对象里的 params 对象
const paramsObj = paramsCreator(matchResult, matchKeys);
return {
params: paramsObj,
path,
url: matchResult[0], // matchResult 作为类数组的第 0 项就是匹配门路规定的局部
isExact: matchResult[0] === url
}
}
/** * * @param {Object} options 配置对象 * 这个办法次要就是将用户传递的配置对象, 转换为 path-to-regex 须要的配置对象 */
function getOptions({sensitive = false, strict = false, exact = false}) {
const defaultOptions = {
sensitive: false,
strict: false,
end: false
}
return {
...defaultOptions,
sensitive,
strict,
end: exact
}
}
/** * * @param {*} matchResult * @param {*} matchKeys * 这个办法次要是将 matchResult 和 matchKeys 相组合最终生成一个新的 params 对象 */
function paramsCreator(matchResult = [], matchKeys = []) {
// 首先这个 matchResult 是一个类数组, 咱们须要将它转换为实在数组
// 你能够应用 Array.from, 也能够应用[].slice.call 等办法都能够
// 而且咱们晓得 matchResult 的第一项是门路, 咱们是不须要的, 所以间接是 slice.call 更不便
const matchVals = [].slice.call(matchResult, 1);
const paramsObj = {};
matchKeys.forEach((k, i) => {
// 别忘记, 这个 k 是一个对象, 而咱们只须要他的 name 属性
paramsObj[k.name] = matchVals[i];
})
return paramsObj; // 最初将这个参数对象丢进来
}
export default pathMatch;
至此, 咱们的
pathMacth
模块就生成了, 每次调用pathMatch
办法, 都会依据参数返回给咱们一个react-router
中的 match 对象,参考 前端手写面试题具体解答
history
库的应用
咱们晓得, 当路由匹配组件当前, react-router
会向组件外部注入一些属性, 其中的 match
属性咱们曾经有生成的办法了, 然而 location
和history
还得劳烦咱们本人写一写
其实 location
就是 history
对象身上的一个属性, 咱们搞定了 location
, history
天然就搞定了
有个货色咱们必须搞清楚哈,
history
中的办法是用来帮忙咱们切换路由的, 然而咱们晓得, 咱们的router
模式是有hash
模式,browser
(有时咱们也称其为history
模式)模式, 甚至在 native 端有memory
模式, 当模式不同的时候,history
会帮咱们操作不同的中央 ( 比方hash
模式下, 操作的就是hash
,browser
模式下操作的就是浏览器的历史记录 ), 那么咱们也晓得,router
是依据你引入的是BrowserRouter
还是其余 Router 类型来断定history
须要操作哪一块的, 所以咱们要做的事就是要搞出这个BrowserRouter
, 没问题吧, 因为代码量可能比拟多, 然而原理都统一, 我就不写HashRouter
和memoryRouter
了
而在 react-router
中他也是强依赖了咱们下面说到的第三方库: history
咱们先来看看
history
库的应用, 可能下一篇博客咱们会间接去书写他的原理 , 这个库不像path-to-regexp
, 他的原理还是很重要的, 这篇博客因为篇幅问题也就不写history
库的源码了
这个库次要实现的性能就是一个: 给你提供创立不同地址栈的 history api
说的更简略一点, 就是咱们调用这个库具名导出的办法, 再通过一系列包装, 咱们就能够间接生成
react-router
上下文中提供的history
对象
咱们能够间接来用一用这个库
import {createBrowserHistory} from "history"; // 导入一个创立操作浏览器 history api 的函数
// 这个函数还能够接管一个配置对象, 你也能够不传
// createBrowserHistory(config?);
const history = createBrowserHistory({
// basename 配置用于设置基门路, 大部分状况下, 咱们网站的根门路是 /
// 所以咱们少数状况下不思考 basename, 假如你须要思考的话, 就在这填就好了
// 填写这个的结果就是: 比方你填写 basename 为 /news, 当前你拜访 /news/details
// 的时候你的 pathname 就会被解析成 /details
basename: "/",
forceRefresh: false, // 示意是否强制刷新页面, history api 是不会刷新页面的, 而如果设置该属性为 true 当前,
// 则你调用 push 等办法的时候会间接数显页面
keyLength: 6, // location 对象应用的 key 值长度(key 值用来确定唯一性, 比方你同时拜访了同一个 path, 如果没有 key 值的话就出问题了)
getUserConfirmation: (msg, cb) => cb(window.confirm(msg)), // 用来确定用户是否真的须要跳转(然而必须设置 history 的 block 函数并且页面真正进行跳转才会触发)
});
console.log("history");
输入后果如下, 咱们会发现, 他其实曾经和咱们在 react
中应用 BrowserRouter
提供的上下文对象中的 history
对象差不多了, 然而还有轻微的区别, 咱们先来看看这个 history
对象中成员的逻辑断定计划, 这对咱们后续写他的源码有用途
须要留神的中央就是: 同学不要感觉这个是 window.location
和window.history
的联合哈, 这个是 history
本人生成的对象, 他对立面的属性很多都是通过包装的, 别搞混同了, 后续源码咱们会理解的更清晰一点
-
action: action 代表的是以后地址栈最初一次操作的类型, 对于 action 咱们须要留神的点如下:
- 首次通过
createBrowserHistory
创立的时候action
固定为POP
- 如果调用了
history
的push
办法,action
变为PUSH
- 如果调用了
history
的replace
办法,action
变为REPLACE
- 首次通过
- push: 向以后地址栈指针地位入栈一个地址
- go: 管制以后地址栈指针偏移, 如果是 0 则地址不变(咱们晓得浏览器的
history.go(0)
会刷新页面), 负数后退, 正数退后 - goBack: 相当于
go(-1)
- goForwar: 相当于
go(1)
- replace: 替换指针所在的地址
- listen: 这是
react-router
实现从新渲染页面的要害, 这个函数用于监听地址栈指针的变动, 该函数接管一个函数作为参数, 示意地址发生变化当前的回调, 回调函数又接管两个参数(location 对象, action), 他返回一个函数用于解除监听, 后续咱们用到的时候我置信你就懂了 - location 对象: 表白以后地址栏中的信息
- createHref: 传递一个 location 对象进去, 他依据 location 的内容给你生成一个地址
- block: 设置一个阻塞, 当用户跳转页面的时候会触发该阻塞, 同时该阻塞的信息参数会被传递到
getUserConirmation
中
Router
和 BrowserRouter
的实现
下面说了这么多, 次要都是在跟大家聊 path-to-regexp
和history
库, 这里咱们要正式实现 Router
组件了
在 React 中, Router
组件是用来提供上下文的, 而 BrowserRouter
创立了一个管制浏览器 history api
的history
对象当前而后传递给Router
咱们在
react-router
中新建一个文件Router.js
, 同时咱们新建一个RouterContext.js
, 用于存储上下文
// react-router/RouterContext.js
import {createContext} from "react";
const routerContext = createContext();
routerContext.displayName = "Router";
export default routerContext;
// 咱们晓得: 这个 Router 组件是肯定须要一个 history 对象的, 他不论 history 对象是怎么来的, 然而必须通过属性传递给他
import React, {useState, useEffect} from "react";
import pathMatch from "./pathMatch";
import routerContext from "./RouterContext";
/** * Router 组件要做的事件就只有一个: 他要提供一个上下文 * 上下文中的内容有 history, match, location * * 咱们晓得创立 history 的时候, 有 createBrowserHistory, createHashHistory 等 * 所以咱们在 Router 里怎么都不能写死, 咱们把 history 作为属性传递过去 * 而在内部咱们在依据不同的组件来创立不同的 history 传递给 Router 组件, * React 也是这么做的 * @param {*} props */
export default function Router(props) {
// 咱们在 Router 中写的逻辑如下:
// 1. 将 match 对象, location 对象和 history 对象都拿到而后进行拼凑
// 2. 如果一旦页面地址发生变化, Router 要从新渲染以响应变动, 怎么响应, 就是通过 listen 办法
// 为什么要将 location 变成状态, 次要是因为当咱们的页面地址产生变动的时候, 咱们须要做的事件有几个
// - 将 history 里 action 的状态进行变更, 比方 go 要变成 POP, push 要变成 PUSH, 如果咱们没有本人的状态
// 那么咱们没有中央能够批改这个 location 了
// - 当页面地址发生变化的时候, 咱们须要从新渲染组件, 咱们能够应用 listen 来监听, 然而从新渲染组件咱们
// 能够应用本人封装一个 forceUpdateHook 来解决, 然而如果有了 location 状态, 能够一石二鸟不是更好
const [locationState, setLocationState] = useState(props.history.location);
const [action, setAction] = useState(props.history.action);
useEffect(() => {const removeListen = props.history.listen(({location, action}) => {
// 当每次页面地址发生变化的时候, 我这边都心愿可能监听到, 监听到了当前我要从新刷新组件
setLocationState(location)
setAction(action);
})
return removeListen;
}, [])
const match = pathMatch("/", props.history.location.pathname);
return (
<routerContext.Provider value={{match, location: locationState, history: { ...props.history, action} }}>
{props.children} </routerContext.Provider>
)
}
Router
组件实现了还不够, 咱们须要去编写BrowserRouter.js
组件
在src
下新建一个react-router-dom
文件目录, 新建文件index.js
和BrowserRouter.js
// index.js
export {default as BrowserRouter} from "./BrowserRouter.js";
// BrowserRouter.js
// BrowserRouter 要做的事件非常简单, 创立一个能够管制 history api 的 history 对象
// 作为属性传递给 Router 组件
import React from "react";
import Router from "../react-router/Router.js";
import {createBrowserHistory} from "history";
export default function BrowserRouter(props) {const history = createBrowserHistory(props);
return (<Router history={history}>
{props.children} </Router>
)
}
至此咱们的
BrowserRouter
组件也写完了
Route
组件的实现
Route
组件次要是用来依据不同的门路匹配不同的组件的, 其实他没那么简单, 就是 通过不同的门路来渲染不同的组件 , 如果你写的粗率一点, 齐全能够应用if else
来始终进行判断也能够写好Route
组件, 那咱们话不多说, 来看看Route
组件的实现过程吧咱们在
react-router
中建设Route.js
文件
import React from "react";
import pathMatch from "./pathMatch";
import routerContext from "./RouterContext";
// 首先咱们必须要搞清楚一些流程上的货色:
// 1. Route 组件上会有一些属性如下:// - path
// - children
// - component
// - render
// - sensitive
// - strict
// - exact
// 在 chilren, component, render 中又有一些逻辑规定如下:
// children: 只有你给了 children 属性值, 那么无论该路由是否匹配胜利 chilren 都会显示
// render: 一旦匹配胜利, 执行的渲染函数
// component: 一旦匹配胜利, 会渲染的 component
// 三个的优先级: children > render > component
// 当然你能够应用 propTypes 来束缚一些 props, 也能够应用 ts 来束缚
// 我就不束缚了, 懒一点哈哈
export default function Route(props) {
// 作为 Route 组件, 他身上也有 history, location 和 match 对象
// 你能够本人从新来组装这些对象, 然而我认为没必要, 咱们间接
// 应用上下文里的数据就好, 只不过 match 对象咱们倒是的确要从新
// 匹配一下
return (
<routerContext.Consumer>
{value => { const { location, history} = value; // 间接从上下文里解构出 location, history const {sensitive = false, exact = false, strict = false} = props; const match = pathMatch(props.path, location.pathname, { sensitive, exact, strict}) const ctxValue = {location, history, match} // 这个时候咱们要讲新的数据持续共享上来, 间接在提供一次 Provider 不就好了 return (<routerContext.Provider value={ctxValue}>
{getRenderChildren(props.children, props.render, props.component, ctxValue) } </routerContext.Provider>
) } } </routerContext.Consumer>
)
}
/** * 依据肯定的匹配逻辑来渲染该渲染的元素 * 这就是 Route 组件的外围性能 */
function getRenderChildren(children, render, component, ctxValue) {
// 依据咱们之前的逻辑, 咱们晓得一旦 children 属性有值, 那不用说间接疏忽其余值
if(children != null) {
// chilren 咱们晓得是能够写函数的, 写成函数的话能够获取上下文的值
return typeof children === "function" ? children(ctxValue) : children;
}
// 如果 children 没有值, 就要看是否匹配了, 如果没有匹配间接
if(ctxValue.match == null) return null;
// 这个时候代表匹配上了, 匹配上了如果有 render 就间接运行 render
if(typeof render === "function") return render(ctxValue);
// 最初渲染 component
if(component) {
let Component = component;
// 咱们晓得: 在被匹配的组件中也是有 location, history, match 等属性的
return <Component {...ctxValue}/>
}
// 最初代表他 component 都没有
return null; // 仍旧给他来 null 就好了
}
其实咱们这里咱们跟 react-router
还有一点区别, 当他的 Route
组件 path 没有的时候, 他也会间接渲染所匹配的组件, 我这里没有写, 为什么呢, 因为我感觉他这样不合逻辑, 你 path
都没给我我凭什么帮你渲染, 我为什么要提这一点哈, 因为我认为咱们去学习一个框架或者一个货色的时候, 要带着本人的思维逻辑去学(比方他为什么要这样做, 如果是你你会怎么做), 他不肯定是对的, 你也不肯定是错的, 你晓得了他的逻辑, 如果你感觉不合理, 你肯定要保留本人的逻辑, 这样能力防止做学习机器, 而且能够锤炼咱们的思维能力
至此
Route
组件曾经实现
Switch
和 Redirect
的实现
Switch
的性能实现其实非常简单, 因为咱们须要将Swicth
包裹在Route
组件里面, 所以咱们认真想想这个逻辑应该很快就进去了, 咱们只有在Switch
里将children
属性挨个遍历而后管制渲染就能够了, 咱们从react-router
官网的逻辑也能够想到大略是这么回事: 因为你应用了官网Switch
当前匹配不上的组件都不会在React
组件树里存在咱们在
react-router
目录下新建一个Switch.js
// react-router/Swicth.js
import React from "react";
import routerContext from "./RouterContext";
import pathMatch from "./pathMatch";
export default function Swicth(props) {
// 咱们要做的事件就是: 将 props 中的 children 挨个拿出来看, 而后如果哪一个的 path 门路和以后门路相匹配了
// 就渲染, 而且一旦渲染了一个, 前面的都不会再渲染了
// 那么咱们怎么晓得以后门路呢, 是不是又要用到上下文
return (
<routerContext.Consumer>
{value => { const { location} = value; const {children = {}} = props; // 这个时候咱们把 children 拿进去遍历, 然而遍历之前咱们要晓得, children 可能会是多个状况 // 1. 是数组: 证实传了多个 react 元素进来, 咱们不论 // 2. 是对象: 证实只传了一个进来, 咱们要将他变成数组 // 当然还有一些细节解决, 然而因为咱们不是做产品级, 没必要搞的那么巨细无遗尴尬本人 let resultChildren = []; if( children instanceof Array) resultChildren = children; else if(children instanceof Object) resultChildren = [children]; for(const item of resultChildren) {const { path = "", exact = false, sensitive = false, strict = false, component: Component = null} = item.props; // 咱们晓得 location.pathname 是正儿八经的浏览器地址, 而咱们书写在 Route 组件上的是 path 规定 // 所以咱们要匹配只能应用咱们之前封装好的 pathMatch 函数 const match = pathMatch(path, location.pathname, { exact, sensitive, strict}) // 只有不等于 null 就是匹配到了 if(match != null) {console.warn("i am warning"); return Component == null ? Component : <Component />
} } // 如果循环了一轮都没有匹配到 return null; } } </routerContext.Consumer>
)
}
Swicth
组件就实现了, 其实这些组件并不是很难, 你只有顺着他的逻辑去捋一捋, 肯定是能够实现的当初咱们要做的就是去实现咱们的
Redirect
组件, 在react-router
目录下新建一个Redirect.js
// react-router/Redirect.js
// Redirect 组件其实就是用来做重定向的, 其实逻辑也能够非常简单, 当你遇到了 Redirect 组件, 你通过 location 上
// 的 replace 办法将他去渲染指定的门路就行了
import React from "react";
import routerContext from "./RouterContext";
import pathMatch from "./pathMatch";
export default function Redirect(props) {console.log("我匹配上了")
// 咱们晓得 Redirect 会承受以下的属性
// 1. from: 代表匹配到的门路
// 2. to: 代表匹配到门路当前要去的门路, 如果 to 为一个对象的话, 外面是能够带参数的
// - pathname: 匹配到当前要去的门路
// - search: 就是一般的 search
// - state: 就是你要附加的一些状态
// pathname 是对象的模式我就懒得写了, 其实你也是去解析他的 pathname 顺便把参数作为属性丢过来就行了
// 3. push: 代表是否应用 history.push 来解决(因为他默认会应用 replace)
// 其余的就是 Route 该有的属性: exact, sensitive, strict
const {from = "", to ="", push = false, exact = false, sensitive = false, strict = false} = props;
// 这个时候咱们要拿 from 来和以后的 location 进行比拟所以又要用到上下文
return (
<routerContext.Consumer>
{({location, history}) => {console.log("props", props); const match = pathMatch(from, location.pathname, { strict, sensitive, exact}) if(match != null) {// 代表匹配上了, 匹配上了咱们要做的事件就是将他推去相应的组件 console.log("to", to); // 因为 history.push 如果你不放入异步队列的话, 这个时候 listen 事件 // 可能还没有初始化结束, 而后他就监听不到了, 我的了解是这样 // 如果有其余了解的话欢送沟通 setTimeout(() => { history.push(to) }, 0) } // 如果没有匹配上, 那就啥都不干呗 return null; } } </routerContext.Consumer>
)
}
至此, redirect 组件也实现了
withRouter
的实现
这个是一个 hoc, 他的作用非常简单, 就是将路由上下文作为属性注入到组件中
咱们在
react-router
目录下新建一个withRouter.js
import React from "react";
import routerContext from "./RouterContext";
export default function(Comp) {
// 他承受一个 Comp 作为参数, 返回一个新的组件
function newComp(props) {
return (
<routerContext.Consumer>
{values => (<Comp {...props} {...values}/>) } </routerContext.Consumer>
)
}
// 设置显示的名字这个没什么好说的吧
newComp.displayName = `withRouter(${Comp.displayName || Comp.name})`;
return newComp;
}
Link
和 NavLink
实现
写完这个 Link
和NavLink
我根本也瘫痪了, 不过好在终于要写完了, Link
和 NavLink
自身也不难
如果要说简略一点, 就写个
a
元素阻止默认事件而后应用history.push
跳转就行了, 毕竟人家也就实现了一个无刷新跳转的性能咱们在
react-router-dom
里新建一个Link.js
// react-router-dom/Link.js
import React from "react";
import routerContext from "../react-router/RouterContext";
export default function Link(props) {const {to, ...rest} = props;
return (
<routerContext.Consumer>
{value => { return ( <a href={to} {...rest} onClick={e => { e.preventDefault(); // 这里我就简略写了, 其实咱们晓得还要思考 to 为对象一些状况 // 而且还有 to 须要传参的一些状况, 这个时候就是你要写一些函数来帮忙你解析字符串或者解析对象 // 其实有些时候还要思考 basename 的状况, 所以最好用 history.createHref 来生成地址比拟好 // 还有就是依据一个参数是否是 replace 还是 push // 不过外围原理就是这个, 其余的细节我就不思考啦 // 有想法的同学能够本人欠缺一下 value.history.push(props.to); }}> {props.children} </a>
) }} </routerContext.Consumer>
)
}
NavLink
这个组件不用说了吧, 其实就是只有 location
匹配上了, 他就给你加个类名就完事了
咱们在
react-router-dom
下新建一个NavLink.js
// react-router-dom/NavLink.js
import React from "react";
import Link from "./Link";
import routerContext from "../react-router/RouterContext"
import pathMatch from "../react-router/pathMatch";
export default function(props) {const {activeClass = "active", to = "", ...rest} = props;
return (
<routerContext.Consumer>
{value => { const match = pathMatch(to, value.location.pathname,) console.log("match result", match); return (<Link to={to} className={match ? activeClass : ""} {...rest}>{props.children}</Link>
) } } </routerContext.Consumer>
)
}
至此
Link
和NavLink
咱们也写完了, 然而Link
和NavLink
还有十分多须要欠缺的中央, 我也只是输入了外围原理, 大家有想法能够本人补充
聚合api
咱们晓得 , 咱们在 react-router
中引入代码都是间接在 react-router-dom
中引入各种组件的, 这个也不难咱们具名导出一下就好
// react-router-dom/index.js
export {default as Redirect} from "../react-router/Redirect";
export {default as Route} from "../react-router/Route";
export {default as Router} from "../react-router/Router";
export {default as Switch} from "../react-router/Switch";
export {default as withRouter} from "../react-router/withRouter";
export {default as Link} from "./Link";
export {default as NavLink} from "./NavLink";
这样就没故障了
至此, 完结, 心愿可能有大手子点拨指教 0.0
至于 react-router
帮忙咱们实现了什么货色我就不过多论述了, 这个间接移步官网文档, 咱们上面间接聊实现
另外: react-router
源码有依赖两个库 path-to-regexp
和history
, 所以我这里也就间接引入这两个库了, 尽管上面我都会讲到根本应用, 然而同学有工夫的话还是能够浏览以下官网文档
还有一个须要留神的点是: 上面我书写的 router
原理都是应用 hooks
+ 函数组件来书写的, 而官网是应用类组件书写的, 所以如果你对hooks
还不是很明确的话, 得去补一下这方面的常识, 为什么要抉择 hooks
, 因为当初绝大多数大厂在react
上根本都在鼎力举荐应用hook
, 所以咱们得跟上时代不是, 而且我着重和大家聊的也是原理, 而不是跟官网截然不同的源码, 如果要 1 比 1 的复刻源码不带本人的了解的话, 那你去看官网的源码就行了, 何必看这篇博文了
在本栏博客中, 咱们会聊聊以下内容:
- 封装本人的生成
match
对象办法 history
库的应用Router
和BrowserRouter
的实现Route
组件的实现Switch
和Redirect
的实现withRouter
的实现Link
和NavLink
实现- 聚合
api
封装本人的生成 match
对象办法
在封装之前, 我想跟大家先分享 path-to-regexp
这个库
为什么要先聊这个库哈, 次要起因是因为
react-router
中用到了这个库, 我看了一下其实咱们也没必要本人再去实现一个这个库(为什么没必要呢, 倒并不是因为react-router
没有实现咱们就不实现, 而是因为这个库实现的性能非常简单, 然而细节十分繁琐, 有十分多的因素须要去思考到我感觉没必要), 这个库做的事件非常简单: 将一个字符串变成一个正则表达式
咱们晓得, react-router
的大抵原理就是依据门路的不同从而渲染不同的页面, 那么这个过程其实也就是 门路 A
匹配 页面 B
的过程, 所以咱们之前会写这样的代码
<Route path="/news/:id" component={News} /> // 如果门路匹配上了 /news/:id 这样的门路, 则渲染 News 组件
那么 react-router
他是怎么去判断浏览器地址栏的门路和这个 Route
组件中的 path
属性匹配上的?
path 填写的如果是 /news/:id
这样的门路, 那么 /news/123
/news/321
这种都可能被 react-router
匹配上
咱们可能想到的办法是不是大略能够如下:
将所有的
path
属性全副转换为 正则表达式 (比方/news/:id
转换为/^\/news(?:\/([^\/#\?]+?))[\/#\?]?$/i
), 而后将地址栏的path
值取出来跟该正则表达式进行匹配, 匹配上了就要渲染相应的路由, 匹配不上就渲染其余的逻辑
path-to-regexp
就是做这个事件的, 他把咱们给他的门路字符串转换为正则表达式, 供咱们匹配
装置: yarn add path-to-regexp -S
// 咱们能够来轻易试试这个库
import {pathToRegexp} from "path-to-regexp";
const keys = [];
// pathToRegexp(path, keys?, options?)
// path: 就是咱们要匹配的门路规定
// keys: 如果你传递了, 当他匹配上当前, 会把绝对应的参数 key 传递到 keys 数组中
// options: 给 path 门路规定的一些附加规定, 比方 sensitive 大小写敏感之类的
const result = pathToRegexp("/news/:id", keys);
console.log("result", result);
console.log(result.exec("/news/123")); // 输入 ["/news/123", "123", index: 0, input: "/news/123", groups: undefined]
console.log(result.exec("/news/details/123")); // 输入 null
console.log(keys); // 输入一个数组, 数组的有一个对象{modifier: "name:"id", pattern:"[^\/#\?]+?", prefix:"/", suffix:""}
当然, 这个库还有很多玩法, 他也不是专门为 react-router
实现的, 只是刚好被 react-router
拿过去用了, 对这个库有趣味的同学能够去看看他的文档
咱们应用这个库, 次要是为了封装一个公共办法, 为后续咱们写 router
源码的时候提供一些基石, 因为咱们晓得, react-router
一旦门路匹配上了, 是会向组件里注入 history
, location
等属性的, 这些货色咱们要提前准备好, 所以咱们此刻的指标很简略
如果一个
path
值跟指定的path
正则匹配上了, 那么咱们要生成一个蕴含了location
,history
等属性的对象, 供后续应用, 说的更直白一点就是要失去react-router
中那个的match
对象
咱们会发现这个性能其实是独立的, 这样拆分进去他能够用在任何中央, 只有匹配我就生成一个对象, 我也不论你拿这个对象去干嘛不关我屁事, 这也是软件开发中的一种较好的开发方式, 大家能够停下来在这里认真思考一下这样的益处
所以接下来我要做的事件非常简单, 就是封装一个跟解决门路相干的办法, 为后续咱们开发其余 router
性能的时候提供基层反对
咱们在 react
工程中本人建设一个 react-router
目录, 在其中新建一个文件pathMatch.js
这也意味着咱们将不再从
npm
上拉react-router
, 而是间接在本人的工程里援用本人的react-router
pathMatch.js
中每一步都写上了正文, 应该可能帮忙你很好的了解
// src/react-router/pathMatch.js
import {pathToRegexp} from "path-to-regexp";
/** * * @param {String} path 传递进来的 path 规定 * @param {String} url 须要校验 path 规定的 url * @param {Object} options 一些配置: 如是否准确匹配, 是否大小写敏感等 * * 这个函数要做的事件非常简单, 当我调用这个函数并且传递了相应 * 参数当前, 这个函数须要返回给我一个对象, 对象成员如下 * {* params: { 门路匹配胜利当前的参数值, 匹配不上就是 null * key: value *}, * path: path 规定 * url: 跟 path 规定匹配的那一段 url, 如果匹配不上就是 null * isExact: 是否准确匹配 * } * */
function pathMatch(path = "", url ="", options = {}) {
// 所以在这个函数外部, 咱们要做的事件如下:
// 1. 调用 path-to-regex 库且依据配置来帮忙咱们进行匹配参数值
// 2. 将匹配后果返回进来
// 首先, 如果你读了这个 path-to-regex 的文档的话, 你会发现一个问题
// 咱们在 react-router 中传递 exact 为准确匹配, 而在该库中则是应用 end
// 所以咱们第一步先将用户传递的配置对象变成 path-to-regex 想要的配置对象
const matchOptions = getOptions(options);
const matchKeys = []; // 这个 matchKeys 其实就是咱们用来装匹配胜利后参数 key 的数组
// 而后在 path-to-regexp 中失去绝对应的正则表达式
const pathRegexp = pathToRegexp(path, matchKeys, matchOptions);
// 这里咱们要应用对应的正则表达式来匹配用户传递的 url
const matchResult = pathRegexp.exec(url);
console.log("matchResult", matchResult);
// 如果没有匹配上, 那间接返回 null 了
if(!matchResult) return null;
// 如果匹配上了, 咱们晓得他返回的是一个类数组, 咱们须要将 matchKeys 和类数组进行遍历
// 生成最终的 match 对象里的 params 对象
const paramsObj = paramsCreator(matchResult, matchKeys);
return {
params: paramsObj,
path,
url: matchResult[0], // matchResult 作为类数组的第 0 项就是匹配门路规定的局部
isExact: matchResult[0] === url
}
}
/** * * @param {Object} options 配置对象 * 这个办法次要就是将用户传递的配置对象, 转换为 path-to-regex 须要的配置对象 */
function getOptions({sensitive = false, strict = false, exact = false}) {
const defaultOptions = {
sensitive: false,
strict: false,
end: false
}
return {
...defaultOptions,
sensitive,
strict,
end: exact
}
}
/** * * @param {*} matchResult * @param {*} matchKeys * 这个办法次要是将 matchResult 和 matchKeys 相组合最终生成一个新的 params 对象 */
function paramsCreator(matchResult = [], matchKeys = []) {
// 首先这个 matchResult 是一个类数组, 咱们须要将它转换为实在数组
// 你能够应用 Array.from, 也能够应用[].slice.call 等办法都能够
// 而且咱们晓得 matchResult 的第一项是门路, 咱们是不须要的, 所以间接是 slice.call 更不便
const matchVals = [].slice.call(matchResult, 1);
const paramsObj = {};
matchKeys.forEach((k, i) => {
// 别忘记, 这个 k 是一个对象, 而咱们只须要他的 name 属性
paramsObj[k.name] = matchVals[i];
})
return paramsObj; // 最初将这个参数对象丢进来
}
export default pathMatch;
至此, 咱们的
pathMacth
模块就生成了, 每次调用pathMatch
办法, 都会依据参数返回给咱们一个react-router
中的 match 对象,参考 前端手写面试题具体解答
history
库的应用
咱们晓得, 当路由匹配组件当前, react-router
会向组件外部注入一些属性, 其中的 match
属性咱们曾经有生成的办法了, 然而 location
和history
还得劳烦咱们本人写一写
其实 location
就是 history
对象身上的一个属性, 咱们搞定了 location
, history
天然就搞定了
有个货色咱们必须搞清楚哈,
history
中的办法是用来帮忙咱们切换路由的, 然而咱们晓得, 咱们的router
模式是有hash
模式,browser
(有时咱们也称其为history
模式)模式, 甚至在 native 端有memory
模式, 当模式不同的时候,history
会帮咱们操作不同的中央 ( 比方hash
模式下, 操作的就是hash
,browser
模式下操作的就是浏览器的历史记录 ), 那么咱们也晓得,router
是依据你引入的是BrowserRouter
还是其余 Router 类型来断定history
须要操作哪一块的, 所以咱们要做的事就是要搞出这个BrowserRouter
, 没问题吧, 因为代码量可能比拟多, 然而原理都统一, 我就不写HashRouter
和memoryRouter
了
[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-BJz50Ja2-1665638041531)(https://p9-juejin.byteimg.com…)]
而在 react-router
中他也是强依赖了咱们下面说到的第三方库: history
咱们先来看看
history
库的应用, 可能下一篇博客咱们会间接去书写他的原理 , 这个库不像path-to-regexp
, 他的原理还是很重要的, 这篇博客因为篇幅问题也就不写history
库的源码了
这个库次要实现的性能就是一个: 给你提供创立不同地址栈的 history api
说的更简略一点, 就是咱们调用这个库具名导出的办法, 再通过一系列包装, 咱们就能够间接生成
react-router
上下文中提供的history
对象
咱们能够间接来用一用这个库
import {createBrowserHistory} from "history"; // 导入一个创立操作浏览器 history api 的函数
// 这个函数还能够接管一个配置对象, 你也能够不传
// createBrowserHistory(config?);
const history = createBrowserHistory({
// basename 配置用于设置基门路, 大部分状况下, 咱们网站的根门路是 /
// 所以咱们少数状况下不思考 basename, 假如你须要思考的话, 就在这填就好了
// 填写这个的结果就是: 比方你填写 basename 为 /news, 当前你拜访 /news/details
// 的时候你的 pathname 就会被解析成 /details
basename: "/",
forceRefresh: false, // 示意是否强制刷新页面, history api 是不会刷新页面的, 而如果设置该属性为 true 当前,
// 则你调用 push 等办法的时候会间接数显页面
keyLength: 6, // location 对象应用的 key 值长度(key 值用来确定唯一性, 比方你同时拜访了同一个 path, 如果没有 key 值的话就出问题了)
getUserConfirmation: (msg, cb) => cb(window.confirm(msg)), // 用来确定用户是否真的须要跳转(然而必须设置 history 的 block 函数并且页面真正进行跳转才会触发)
});
console.log("history");
输入后果如下, 咱们会发现, 他其实曾经和咱们在 react
中应用 BrowserRouter
提供的上下文对象中的 history
对象差不多了, 然而还有轻微的区别, 咱们先来看看这个 history
对象中成员的逻辑断定计划, 这对咱们后续写他的源码有用途
须要留神的中央就是: 同学不要感觉这个是 window.location
和window.history
的联合哈, 这个是 history
本人生成的对象, 他对立面的属性很多都是通过包装的, 别搞混同了, 后续源码咱们会理解的更清晰一点
[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-HJFBTXQN-1665638041534)(https://p3-juejin.byteimg.com…)]
-
action: action 代表的是以后地址栈最初一次操作的类型, 对于 action 咱们须要留神的点如下:
- 首次通过
createBrowserHistory
创立的时候action
固定为POP
- 如果调用了
history
的push
办法,action
变为PUSH
- 如果调用了
history
的replace
办法,action
变为REPLACE
- 首次通过
- push: 向以后地址栈指针地位入栈一个地址
- go: 管制以后地址栈指针偏移, 如果是 0 则地址不变(咱们晓得浏览器的
history.go(0)
会刷新页面), 负数后退, 正数退后 - goBack: 相当于
go(-1)
- goForwar: 相当于
go(1)
- replace: 替换指针所在的地址
- listen: 这是
react-router
实现从新渲染页面的要害, 这个函数用于监听地址栈指针的变动, 该函数接管一个函数作为参数, 示意地址发生变化当前的回调, 回调函数又接管两个参数(location 对象, action), 他返回一个函数用于解除监听, 后续咱们用到的时候我置信你就懂了 - location 对象: 表白以后地址栏中的信息
- createHref: 传递一个 location 对象进去, 他依据 location 的内容给你生成一个地址
- block: 设置一个阻塞, 当用户跳转页面的时候会触发该阻塞, 同时该阻塞的信息参数会被传递到
getUserConirmation
中
Router
和 BrowserRouter
的实现
下面说了这么多, 次要都是在跟大家聊 path-to-regexp
和history
库, 这里咱们要正式实现 Router
组件了
在 React 中, Router
组件是用来提供上下文的, 而 BrowserRouter
创立了一个管制浏览器 history api
的history
对象当前而后传递给Router
咱们在
react-router
中新建一个文件Router.js
, 同时咱们新建一个RouterContext.js
, 用于存储上下文
// react-router/RouterContext.js
import {createContext} from "react";
const routerContext = createContext();
routerContext.displayName = "Router";
export default routerContext;
// 咱们晓得: 这个 Router 组件是肯定须要一个 history 对象的, 他不论 history 对象是怎么来的, 然而必须通过属性传递给他
import React, {useState, useEffect} from "react";
import pathMatch from "./pathMatch";
import routerContext from "./RouterContext";
/** * Router 组件要做的事件就只有一个: 他要提供一个上下文 * 上下文中的内容有 history, match, location * * 咱们晓得创立 history 的时候, 有 createBrowserHistory, createHashHistory 等 * 所以咱们在 Router 里怎么都不能写死, 咱们把 history 作为属性传递过去 * 而在内部咱们在依据不同的组件来创立不同的 history 传递给 Router 组件, * React 也是这么做的 * @param {*} props */
export default function Router(props) {
// 咱们在 Router 中写的逻辑如下:
// 1. 将 match 对象, location 对象和 history 对象都拿到而后进行拼凑
// 2. 如果一旦页面地址发生变化, Router 要从新渲染以响应变动, 怎么响应, 就是通过 listen 办法
// 为什么要将 location 变成状态, 次要是因为当咱们的页面地址产生变动的时候, 咱们须要做的事件有几个
// - 将 history 里 action 的状态进行变更, 比方 go 要变成 POP, push 要变成 PUSH, 如果咱们没有本人的状态
// 那么咱们没有中央能够批改这个 location 了
// - 当页面地址发生变化的时候, 咱们须要从新渲染组件, 咱们能够应用 listen 来监听, 然而从新渲染组件咱们
// 能够应用本人封装一个 forceUpdateHook 来解决, 然而如果有了 location 状态, 能够一石二鸟不是更好
const [locationState, setLocationState] = useState(props.history.location);
const [action, setAction] = useState(props.history.action);
useEffect(() => {const removeListen = props.history.listen(({location, action}) => {
// 当每次页面地址发生变化的时候, 我这边都心愿可能监听到, 监听到了当前我要从新刷新组件
setLocationState(location)
setAction(action);
})
return removeListen;
}, [])
const match = pathMatch("/", props.history.location.pathname);
return (
<routerContext.Provider value={{match, location: locationState, history: { ...props.history, action} }}>
{props.children} </routerContext.Provider>
)
}
Router
组件实现了还不够, 咱们须要去编写BrowserRouter.js
组件
在src
下新建一个react-router-dom
文件目录, 新建文件index.js
和BrowserRouter.js
// index.js
export {default as BrowserRouter} from "./BrowserRouter.js";
// BrowserRouter.js
// BrowserRouter 要做的事件非常简单, 创立一个能够管制 history api 的 history 对象
// 作为属性传递给 Router 组件
import React from "react";
import Router from "../react-router/Router.js";
import {createBrowserHistory} from "history";
export default function BrowserRouter(props) {const history = createBrowserHistory(props);
return (<Router history={history}>
{props.children} </Router>
)
}
至此咱们的
BrowserRouter
组件也写完了
Route
组件的实现
Route
组件次要是用来依据不同的门路匹配不同的组件的, 其实他没那么简单, 就是 通过不同的门路来渲染不同的组件 , 如果你写的粗率一点, 齐全能够应用if else
来始终进行判断也能够写好Route
组件, 那咱们话不多说, 来看看Route
组件的实现过程吧咱们在
react-router
中建设Route.js
文件
import React from "react";
import pathMatch from "./pathMatch";
import routerContext from "./RouterContext";
// 首先咱们必须要搞清楚一些流程上的货色:
// 1. Route 组件上会有一些属性如下:// - path
// - children
// - component
// - render
// - sensitive
// - strict
// - exact
// 在 chilren, component, render 中又有一些逻辑规定如下:
// children: 只有你给了 children 属性值, 那么无论该路由是否匹配胜利 chilren 都会显示
// render: 一旦匹配胜利, 执行的渲染函数
// component: 一旦匹配胜利, 会渲染的 component
// 三个的优先级: children > render > component
// 当然你能够应用 propTypes 来束缚一些 props, 也能够应用 ts 来束缚
// 我就不束缚了, 懒一点哈哈
export default function Route(props) {
// 作为 Route 组件, 他身上也有 history, location 和 match 对象
// 你能够本人从新来组装这些对象, 然而我认为没必要, 咱们间接
// 应用上下文里的数据就好, 只不过 match 对象咱们倒是的确要从新
// 匹配一下
return (
<routerContext.Consumer>
{value => { const { location, history} = value; // 间接从上下文里解构出 location, history const {sensitive = false, exact = false, strict = false} = props; const match = pathMatch(props.path, location.pathname, { sensitive, exact, strict}) const ctxValue = {location, history, match} // 这个时候咱们要讲新的数据持续共享上来, 间接在提供一次 Provider 不就好了 return (<routerContext.Provider value={ctxValue}>
{getRenderChildren(props.children, props.render, props.component, ctxValue) } </routerContext.Provider>
) } } </routerContext.Consumer>
)
}
/** * 依据肯定的匹配逻辑来渲染该渲染的元素 * 这就是 Route 组件的外围性能 */
function getRenderChildren(children, render, component, ctxValue) {
// 依据咱们之前的逻辑, 咱们晓得一旦 children 属性有值, 那不用说间接疏忽其余值
if(children != null) {
// chilren 咱们晓得是能够写函数的, 写成函数的话能够获取上下文的值
return typeof children === "function" ? children(ctxValue) : children;
}
// 如果 children 没有值, 就要看是否匹配了, 如果没有匹配间接
if(ctxValue.match == null) return null;
// 这个时候代表匹配上了, 匹配上了如果有 render 就间接运行 render
if(typeof render === "function") return render(ctxValue);
// 最初渲染 component
if(component) {
let Component = component;
// 咱们晓得: 在被匹配的组件中也是有 location, history, match 等属性的
return <Component {...ctxValue}/>
}
// 最初代表他 component 都没有
return null; // 仍旧给他来 null 就好了
}
其实咱们这里咱们跟 react-router
还有一点区别, 当他的 Route
组件 path 没有的时候, 他也会间接渲染所匹配的组件, 我这里没有写, 为什么呢, 因为我感觉他这样不合逻辑, 你 path
都没给我我凭什么帮你渲染, 我为什么要提这一点哈, 因为我认为咱们去学习一个框架或者一个货色的时候, 要带着本人的思维逻辑去学(比方他为什么要这样做, 如果是你你会怎么做), 他不肯定是对的, 你也不肯定是错的, 你晓得了他的逻辑, 如果你感觉不合理, 你肯定要保留本人的逻辑, 这样能力防止做学习机器, 而且能够锤炼咱们的思维能力
至此
Route
组件曾经实现
Switch
和 Redirect
的实现
Switch
的性能实现其实非常简单, 因为咱们须要将Swicth
包裹在Route
组件里面, 所以咱们认真想想这个逻辑应该很快就进去了, 咱们只有在Switch
里将children
属性挨个遍历而后管制渲染就能够了, 咱们从react-router
官网的逻辑也能够想到大略是这么回事: 因为你应用了官网Switch
当前匹配不上的组件都不会在React
组件树里存在咱们在
react-router
目录下新建一个Switch.js
// react-router/Swicth.js
import React from "react";
import routerContext from "./RouterContext";
import pathMatch from "./pathMatch";
export default function Swicth(props) {
// 咱们要做的事件就是: 将 props 中的 children 挨个拿出来看, 而后如果哪一个的 path 门路和以后门路相匹配了
// 就渲染, 而且一旦渲染了一个, 前面的都不会再渲染了
// 那么咱们怎么晓得以后门路呢, 是不是又要用到上下文
return (
<routerContext.Consumer>
{value => { const { location} = value; const {children = {}} = props; // 这个时候咱们把 children 拿进去遍历, 然而遍历之前咱们要晓得, children 可能会是多个状况 // 1. 是数组: 证实传了多个 react 元素进来, 咱们不论 // 2. 是对象: 证实只传了一个进来, 咱们要将他变成数组 // 当然还有一些细节解决, 然而因为咱们不是做产品级, 没必要搞的那么巨细无遗尴尬本人 let resultChildren = []; if( children instanceof Array) resultChildren = children; else if(children instanceof Object) resultChildren = [children]; for(const item of resultChildren) {const { path = "", exact = false, sensitive = false, strict = false, component: Component = null} = item.props; // 咱们晓得 location.pathname 是正儿八经的浏览器地址, 而咱们书写在 Route 组件上的是 path 规定 // 所以咱们要匹配只能应用咱们之前封装好的 pathMatch 函数 const match = pathMatch(path, location.pathname, { exact, sensitive, strict}) // 只有不等于 null 就是匹配到了 if(match != null) {console.warn("i am warning"); return Component == null ? Component : <Component />
} } // 如果循环了一轮都没有匹配到 return null; } } </routerContext.Consumer>
)
}
Swicth
组件就实现了, 其实这些组件并不是很难, 你只有顺着他的逻辑去捋一捋, 肯定是能够实现的当初咱们要做的就是去实现咱们的
Redirect
组件, 在react-router
目录下新建一个Redirect.js
// react-router/Redirect.js
// Redirect 组件其实就是用来做重定向的, 其实逻辑也能够非常简单, 当你遇到了 Redirect 组件, 你通过 location 上
// 的 replace 办法将他去渲染指定的门路就行了
import React from "react";
import routerContext from "./RouterContext";
import pathMatch from "./pathMatch";
export default function Redirect(props) {console.log("我匹配上了")
// 咱们晓得 Redirect 会承受以下的属性
// 1. from: 代表匹配到的门路
// 2. to: 代表匹配到门路当前要去的门路, 如果 to 为一个对象的话, 外面是能够带参数的
// - pathname: 匹配到当前要去的门路
// - search: 就是一般的 search
// - state: 就是你要附加的一些状态
// pathname 是对象的模式我就懒得写了, 其实你也是去解析他的 pathname 顺便把参数作为属性丢过来就行了
// 3. push: 代表是否应用 history.push 来解决(因为他默认会应用 replace)
// 其余的就是 Route 该有的属性: exact, sensitive, strict
const {from = "", to ="", push = false, exact = false, sensitive = false, strict = false} = props;
// 这个时候咱们要拿 from 来和以后的 location 进行比拟所以又要用到上下文
return (
<routerContext.Consumer>
{({location, history}) => {console.log("props", props); const match = pathMatch(from, location.pathname, { strict, sensitive, exact}) if(match != null) {// 代表匹配上了, 匹配上了咱们要做的事件就是将他推去相应的组件 console.log("to", to); // 因为 history.push 如果你不放入异步队列的话, 这个时候 listen 事件 // 可能还没有初始化结束, 而后他就监听不到了, 我的了解是这样 // 如果有其余了解的话欢送沟通 setTimeout(() => { history.push(to) }, 0) } // 如果没有匹配上, 那就啥都不干呗 return null; } } </routerContext.Consumer>
)
}
至此, redirect 组件也实现了
withRouter
的实现
这个是一个 hoc, 他的作用非常简单, 就是将路由上下文作为属性注入到组件中
咱们在
react-router
目录下新建一个withRouter.js
import React from "react";
import routerContext from "./RouterContext";
export default function(Comp) {
// 他承受一个 Comp 作为参数, 返回一个新的组件
function newComp(props) {
return (
<routerContext.Consumer>
{values => (<Comp {...props} {...values}/>) } </routerContext.Consumer>
)
}
// 设置显示的名字这个没什么好说的吧
newComp.displayName = `withRouter(${Comp.displayName || Comp.name})`;
return newComp;
}
Link
和 NavLink
实现
写完这个 Link
和NavLink
我根本也瘫痪了, 不过好在终于要写完了, Link
和 NavLink
自身也不难
如果要说简略一点, 就写个
a
元素阻止默认事件而后应用history.push
跳转就行了, 毕竟人家也就实现了一个无刷新跳转的性能咱们在
react-router-dom
里新建一个Link.js
// react-router-dom/Link.js
import React from "react";
import routerContext from "../react-router/RouterContext";
export default function Link(props) {const {to, ...rest} = props;
return (
<routerContext.Consumer>
{value => { return ( <a href={to} {...rest} onClick={e => { e.preventDefault(); // 这里我就简略写了, 其实咱们晓得还要思考 to 为对象一些状况 // 而且还有 to 须要传参的一些状况, 这个时候就是你要写一些函数来帮忙你解析字符串或者解析对象 // 其实有些时候还要思考 basename 的状况, 所以最好用 history.createHref 来生成地址比拟好 // 还有就是依据一个参数是否是 replace 还是 push // 不过外围原理就是这个, 其余的细节我就不思考啦 // 有想法的同学能够本人欠缺一下 value.history.push(props.to); }}> {props.children} </a>
) }} </routerContext.Consumer>
)
}
NavLink
这个组件不用说了吧, 其实就是只有 location
匹配上了, 他就给你加个类名就完事了
咱们在
react-router-dom
下新建一个NavLink.js
// react-router-dom/NavLink.js
import React from "react";
import Link from "./Link";
import routerContext from "../react-router/RouterContext"
import pathMatch from "../react-router/pathMatch";
export default function(props) {const {activeClass = "active", to = "", ...rest} = props;
return (
<routerContext.Consumer>
{value => { const match = pathMatch(to, value.location.pathname,) console.log("match result", match); return (<Link to={to} className={match ? activeClass : ""} {...rest}>{props.children}</Link>
) } } </routerContext.Consumer>
)
}
至此
Link
和NavLink
咱们也写完了, 然而Link
和NavLink
还有十分多须要欠缺的中央, 我也只是输入了外围原理, 大家有想法能够本人补充
聚合api
咱们晓得 , 咱们在 react-router
中引入代码都是间接在 react-router-dom
中引入各种组件的, 这个也不难咱们具名导出一下就好
// react-router-dom/index.js
export {default as Redirect} from "../react-router/Redirect";
export {default as Route} from "../react-router/Route";
export {default as Router} from "../react-router/Router";
export {default as Switch} from "../react-router/Switch";
export {default as withRouter} from "../react-router/withRouter";
export {default as Link} from "./Link";
export {default as NavLink} from "./NavLink";
这样就没故障了
至此, 完结, 心愿可能有大手子点拨指教 0.0