蛮多同学可能会觉得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
的过程, 所以我们之前会写这样的代码
// 如果路径匹配上了/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 (
{ props.children }
)
}
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 (
{ props.children }
)
}
至此我们的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 (
{ 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 (
{ getRenderChildren(props.children, props.render, props.component, ctxValue) }
) } }
)
}
/** * 根据一定的匹配逻辑来渲染该渲染的元素 * 这就是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都没有
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 (
{ 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 :
} } // 如果循环了一轮都没有匹配到 return null; } }
)
}
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 (
{ ({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; } }
)
}
至此, redirect组件也完成了
withRouter
的实现
这个是一个hoc, 他的作用非常简单, 就是将路由上下文作为属性注入到组件中
我们在
react-router
目录下新建一个withRouter.js
import React from "react";
import routerContext from "./RouterContext";
export default function(Comp) {
// 他接受一个Comp作为参数, 返回一个新的组件
function newComp(props) {
return (
{ values => ( ) }
)
}
// 设置显示的名字这个没什么好说的吧
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 (
{value => { return ( { e.preventDefault(); // 这里我就简单写了, 其实我们知道还要考虑to为对象一些情况 // 而且还有to需要传参的一些情况, 这个时候就是你要写一些函数来帮助你解析字符串或者解析对象 // 其实有些时候还要考虑basename的情况, 所以最好用history.createHref来生成地址比较好 // 还有就是根据一个参数是否是replace还是push // 不过核心原理就是这个, 其他的细节我就不考虑啦 // 有想法的同学可以自己完善一下 value.history.push(props.to); }}> { props.children }
) }}
)
}
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 (
{ value => { const match = pathMatch(to, value.location.pathname, ) console.log("match result", match); return ( { props.children }
) } }
)
}
至此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
的过程, 所以我们之前会写这样的代码
// 如果路径匹配上了/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 (
{ props.children }
)
}
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 (
{ props.children }
)
}
至此我们的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 (
{ 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 (
{ getRenderChildren(props.children, props.render, props.component, ctxValue) }
) } }
)
}
/** * 根据一定的匹配逻辑来渲染该渲染的元素 * 这就是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都没有
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 (
{ 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 :
} } // 如果循环了一轮都没有匹配到 return null; } }
)
}
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 (
{ ({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; } }
)
}
至此, redirect组件也完成了
withRouter
的实现
这个是一个hoc, 他的作用非常简单, 就是将路由上下文作为属性注入到组件中
我们在
react-router
目录下新建一个withRouter.js
import React from "react";
import routerContext from "./RouterContext";
export default function(Comp) {
// 他接受一个Comp作为参数, 返回一个新的组件
function newComp(props) {
return (
{ values => ( ) }
)
}
// 设置显示的名字这个没什么好说的吧
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 (
{value => { return ( { e.preventDefault(); // 这里我就简单写了, 其实我们知道还要考虑to为对象一些情况 // 而且还有to需要传参的一些情况, 这个时候就是你要写一些函数来帮助你解析字符串或者解析对象 // 其实有些时候还要考虑basename的情况, 所以最好用history.createHref来生成地址比较好 // 还有就是根据一个参数是否是replace还是push // 不过核心原理就是这个, 其他的细节我就不考虑啦 // 有想法的同学可以自己完善一下 value.history.push(props.to); }}> { props.children }
) }}
)
}
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 (
{ value => { const match = pathMatch(to, value.location.pathname, ) console.log("match result", match); return ( { props.children }
) } }
)
}
至此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