从零手写react-router

2022-10-13 13:18:17 浏览数 (3)

蛮多同学可能会觉得react-router很复杂, 说用都还没用明白, 还从0实现一个react-router, 其实router并不复杂哈, 甚至说你看了这篇博客以后, 你都会觉得router的核心原理也就那么回事

至于react-router帮助我们实现了什么东西我就不过多阐述了, 这个直接移步官方文档, 我们下面直接聊实现

另外: react-router源码有依赖两个库path-to-regexphistory, 所以我这里也就直接引入这两个库了,虽然下面我都会讲到基本使用, 但是同学有时间的话还是可以阅读以下官方文档

还有一个需要注意的点是: 下面我书写的router原理都是使用hooks 函数组件来书写的, 而官方是使用类组件书写的, 所以如果你对hooks还不是很明白的话, 得去补一下这方面的知识, 为什么要选择hooks, 因为现在绝大多数大厂在react上基本都在大力推荐使用hook, 所以我们得跟上时代不是, 而且我着重和大家聊的也是原理, 而不是跟官方一模一样的源码, 如果要1比1的复刻源码不带自己的理解的话, 那你去看官方的源码就行了, 何必看这篇博文了

在本栏博客中, 我们会聊聊以下内容:

  1. 封装自己的生成match对象方法
  2. history库的使用
  3. RouterBrowserRouter的实现
  4. Route组件的实现
  5. SwitchRedirect的实现
  6. withRouter的实现
  7. LinkNavLink实现
  8. 聚合api

封装自己的生成match对象方法

在封装之前, 我想跟大家先分享path-to-regexp这个库

为什么要先聊这个库哈, 主要原因是因为react-router中用到了这个库, 我看了一下其实我们也没必要自己再去实现一个这个库(为什么没必要呢,倒并不是因为react-router没有实现我们就不实现, 而是因为这个库实现的功能非常简单, 但是细节非常繁琐, 有非常多的因素需要去考虑到我觉得没必要), 这个库做的事情非常简单: 将一个字符串变成一个正则表达式

我们知道, react-router的大致原理就是根据路径的不同从而渲染不同的页面, 那么这个过程其实也就是路径A匹配页面B的过程, 所以我们之前会写这样的代码

代码语言:javascript复制
<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就是做这个事情的, 他把我们给他的路径字符串转换为正则表达式, 供我们匹配

代码语言:c#复制
安装: yarn add path-to-regexp -S
代码语言:javascript复制
// 我们可以来随便试试这个库
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中每一步都写上了注释, 应该能够帮助你很好的理解

代码语言:javascript复制
// 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属性我们已经有生成的方法了, 但是locationhistory还得劳烦我们自己写一写

其实location就是history对象身上的一个属性, 我们搞定了location, history自然就搞定了

有个东西我们必须搞清楚哈, history中的方法是用来帮助我们切换路由的, 但是我们知道, 我们的router模式是有hash模式, browser(有时我们也称其为history模式)模式, 甚至在native端有memory模式, 当模式不同的时候, history会帮我们操作不同的地方(比如hash模式下, 操作的就是hash, browser模式下操作的就是浏览器的历史记录), 那么我们也知道, router是根据你引入的是BrowserRouter还是其他Router类型来判定history需要操作哪一块的, 所以我们要做的事就是要搞出这个BrowserRouter, 没问题吧, 由于代码量可能比较多, 但是原理都一致, 我就不写HashRoutermemoryRouter

而在react-router中他也是强依赖了我们上面说到的第三方库: history

我们先来看看history库的使用, 可能下一篇博客我们会直接去书写他的原理, 这个库不像path-to-regexp, 他的原理还是很重要的, 这篇博客因为篇幅问题也就不写history库的源码了

这个库主要实现的功能就是一个: 给你提供创建不同地址栈的history api

说的更简单一点, 就是我们调用这个库具名导出的方法, 再经过一系列包装, 我们就可以直接生成react-router上下文中提供的history对象

我们可以直接来用一用这个库

代码语言:javascript复制
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.locationwindow.history的结合哈, 这个是history自己生成的对象, 他对立面的属性很多都是经过包装的, 别搞混淆了, 后续源码我们会了解的更清晰一点

  • action: action代表的是当前地址栈最后一次操作的类型, 关于action我们需要注意的点如下:
    • 首次通过createBrowserHistory创建的时候action固定为POP
    • 如果调用了historypush方法, action变为PUSH
    • 如果调用了historyreplace方法, 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

RouterBrowserRouter的实现

上面说了这么多, 主要都是在跟大家聊path-to-regexphistory库, 这里我们要正式实现Router组件了

在React中, Router组件是用来提供上下文的, 而BrowserRouter创建了一个控制浏览器history apihistory对象以后然后传递给Router

我们在react-router中新建一个文件Router.js, 同时我们新建一个RouterContext.js, 用于存储上下文

代码语言:javascript复制
// react-router/RouterContext.js
import { createContext } from "react";

const routerContext = createContext();

routerContext.displayName = "Router";

export default routerContext;
代码语言:javascript复制
// 我们知道: 这个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.jsBrowserRouter.js

代码语言:javascript复制
// index.js
export { default as BrowserRouter } from "./BrowserRouter.js";
代码语言:javascript复制
// 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文件

代码语言:javascript复制
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组件已经完成

SwitchRedirect的实现

Switch的功能实现其实非常简单, 因为我们需要将Swicth包裹在Route组件外面, 所以我们仔细想想这个逻辑应该很快就出来了, 我们只要在Switch里将children属性挨个遍历然后控制渲染就可以了, 我们从react-router官方的逻辑也可以想到大概是这么回事: 因为你使用了官方Switch以后匹配不上的组件都不会在React组件树里存在

我们在react-router目录下新建一个Switch.js

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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

代码语言:javascript复制
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;
}

LinkNavLink实现

写完这个LinkNavLink我基本也瘫痪了, 不过好在终于要写完了, LinkNavLink本身也不难

如果要说简单一点, 就写个a元素阻止默认事件然后使用history.push跳转就行了, 毕竟人家也就实现了一个无刷新跳转的功能

我们在react-router-dom里新建一个Link.js

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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>
  )
}

至此LinkNavLink我们也写完了, 但是LinkNavLink还有非常多需要完善的地方, 我也只是输出了核心原理, 大家有想法可以自己补充

聚合api

我们知道 , 我们在react-router中引入代码都是直接在react-router-dom中引入各种组件的, 这个也不难我们具名导出一下就好

代码语言:javascript复制
// 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-regexphistory, 所以我这里也就直接引入这两个库了,虽然下面我都会讲到基本使用, 但是同学有时间的话还是可以阅读以下官方文档

还有一个需要注意的点是: 下面我书写的router原理都是使用hooks 函数组件来书写的, 而官方是使用类组件书写的, 所以如果你对hooks还不是很明白的话, 得去补一下这方面的知识, 为什么要选择hooks, 因为现在绝大多数大厂在react上基本都在大力推荐使用hook, 所以我们得跟上时代不是, 而且我着重和大家聊的也是原理, 而不是跟官方一模一样的源码, 如果要1比1的复刻源码不带自己的理解的话, 那你去看官方的源码就行了, 何必看这篇博文了

在本栏博客中, 我们会聊聊以下内容:

  1. 封装自己的生成match对象方法
  2. history库的使用
  3. RouterBrowserRouter的实现
  4. Route组件的实现
  5. SwitchRedirect的实现
  6. withRouter的实现
  7. LinkNavLink实现
  8. 聚合api

封装自己的生成match对象方法

在封装之前, 我想跟大家先分享path-to-regexp这个库

为什么要先聊这个库哈, 主要原因是因为react-router中用到了这个库, 我看了一下其实我们也没必要自己再去实现一个这个库(为什么没必要呢,倒并不是因为react-router没有实现我们就不实现, 而是因为这个库实现的功能非常简单, 但是细节非常繁琐, 有非常多的因素需要去考虑到我觉得没必要), 这个库做的事情非常简单: 将一个字符串变成一个正则表达式

我们知道, react-router的大致原理就是根据路径的不同从而渲染不同的页面, 那么这个过程其实也就是路径A匹配页面B的过程, 所以我们之前会写这样的代码

代码语言:javascript复制
<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就是做这个事情的, 他把我们给他的路径字符串转换为正则表达式, 供我们匹配

代码语言:c#复制
安装: yarn add path-to-regexp -S
代码语言:javascript复制
// 我们可以来随便试试这个库
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中每一步都写上了注释, 应该能够帮助你很好的理解

代码语言:javascript复制
// 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属性我们已经有生成的方法了, 但是locationhistory还得劳烦我们自己写一写

其实location就是history对象身上的一个属性, 我们搞定了location, history自然就搞定了

有个东西我们必须搞清楚哈, history中的方法是用来帮助我们切换路由的, 但是我们知道, 我们的router模式是有hash模式, browser(有时我们也称其为history模式)模式, 甚至在native端有memory模式, 当模式不同的时候, history会帮我们操作不同的地方(比如hash模式下, 操作的就是hash, browser模式下操作的就是浏览器的历史记录), 那么我们也知道, router是根据你引入的是BrowserRouter还是其他Router类型来判定history需要操作哪一块的, 所以我们要做的事就是要搞出这个BrowserRouter, 没问题吧, 由于代码量可能比较多, 但是原理都一致, 我就不写HashRoutermemoryRouter

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BJz50Ja2-1665638041531)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17bf5acabb81459d88c0186646c7482c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)

而在react-router中他也是强依赖了我们上面说到的第三方库: history

我们先来看看history库的使用, 可能下一篇博客我们会直接去书写他的原理, 这个库不像path-to-regexp, 他的原理还是很重要的, 这篇博客因为篇幅问题也就不写history库的源码了

这个库主要实现的功能就是一个: 给你提供创建不同地址栈的history api

说的更简单一点, 就是我们调用这个库具名导出的方法, 再经过一系列包装, 我们就可以直接生成react-router上下文中提供的history对象

我们可以直接来用一用这个库

代码语言:javascript复制
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.locationwindow.history的结合哈, 这个是history自己生成的对象, 他对立面的属性很多都是经过包装的, 别搞混淆了, 后续源码我们会了解的更清晰一点

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJFBTXQN-1665638041534)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88c2c00834d54300b032109c449643ed~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)

  • action: action代表的是当前地址栈最后一次操作的类型, 关于action我们需要注意的点如下:
    • 首次通过createBrowserHistory创建的时候action固定为POP
    • 如果调用了historypush方法, action变为PUSH
    • 如果调用了historyreplace方法, 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

RouterBrowserRouter的实现

上面说了这么多, 主要都是在跟大家聊path-to-regexphistory库, 这里我们要正式实现Router组件了

在React中, Router组件是用来提供上下文的, 而BrowserRouter创建了一个控制浏览器history apihistory对象以后然后传递给Router

我们在react-router中新建一个文件Router.js, 同时我们新建一个RouterContext.js, 用于存储上下文

代码语言:javascript复制
// react-router/RouterContext.js
import { createContext } from "react";

const routerContext = createContext();

routerContext.displayName = "Router";

export default routerContext;
代码语言:javascript复制
// 我们知道: 这个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.jsBrowserRouter.js

代码语言:javascript复制
// index.js
export { default as BrowserRouter } from "./BrowserRouter.js";
代码语言:javascript复制
// 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文件

代码语言:javascript复制
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组件已经完成

SwitchRedirect的实现

Switch的功能实现其实非常简单, 因为我们需要将Swicth包裹在Route组件外面, 所以我们仔细想想这个逻辑应该很快就出来了, 我们只要在Switch里将children属性挨个遍历然后控制渲染就可以了, 我们从react-router官方的逻辑也可以想到大概是这么回事: 因为你使用了官方Switch以后匹配不上的组件都不会在React组件树里存在

我们在react-router目录下新建一个Switch.js

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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

代码语言:javascript复制
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;
}

LinkNavLink实现

写完这个LinkNavLink我基本也瘫痪了, 不过好在终于要写完了, LinkNavLink本身也不难

如果要说简单一点, 就写个a元素阻止默认事件然后使用history.push跳转就行了, 毕竟人家也就实现了一个无刷新跳转的功能

我们在react-router-dom里新建一个Link.js

代码语言:javascript复制
// 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

代码语言:javascript复制
// 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>
  )
}

至此LinkNavLink我们也写完了, 但是LinkNavLink还有非常多需要完善的地方, 我也只是输出了核心原理, 大家有想法可以自己补充

聚合api

我们知道 , 我们在react-router中引入代码都是直接在react-router-dom中引入各种组件的, 这个也不难我们具名导出一下就好

代码语言:javascript复制
// 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

0 人点赞