乐趣区

关于javascript:从零手写reactrouter

蛮多同学可能会感觉 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 的过程, 所以咱们之前会写这样的代码

<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 属性咱们曾经有生成的办法了, 然而 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 对象

咱们能够间接来用一用这个库

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, 用于存储上下文

// 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.jsBrowserRouter.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 组件曾经实现

SwitchRedirect 的实现

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;
}

LinkNavLink 实现

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

如果要说简略一点, 就写个 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>
  )
}

至此 LinkNavLink咱们也写完了, 然而 LinkNavLink还有十分多须要欠缺的中央, 我也只是输入了外围原理, 大家有想法能够本人补充

聚合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-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 的过程, 所以咱们之前会写这样的代码

<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 属性咱们曾经有生成的办法了, 然而 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…)]

而在 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.locationwindow.history的联合哈, 这个是 history 本人生成的对象, 他对立面的属性很多都是通过包装的, 别搞混同了, 后续源码咱们会理解的更清晰一点

[外链图片转存失败, 源站可能有防盗链机制, 倡议将图片保留下来间接上传(img-HJFBTXQN-1665638041534)(https://p3-juejin.byteimg.com…)]

  • 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, 用于存储上下文

// 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.jsBrowserRouter.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 组件曾经实现

SwitchRedirect 的实现

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;
}

LinkNavLink 实现

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

如果要说简略一点, 就写个 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>
  )
}

至此 LinkNavLink咱们也写完了, 然而 LinkNavLink还有十分多须要欠缺的中央, 我也只是输入了外围原理, 大家有想法能够本人补充

聚合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

退出移动版