关于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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理