React笔记(六) - 用小白能看懂的方式手写React-Router

Hi, Everyone, 好久不见, 其实想了很久, 到底要不要出一篇react-router的源码级博客, 怕自己的理解不够深, 误导了大家, 但是本着书写学习笔记的习惯, 笔者还是写下来了自己对react路由的理解, 如果有问题之处还请大牛指教, 也希望这篇博客可以帮助到正在学习router原理的你

本博客不会过度的去分析react自身的源码, 因为这些我相信大家从git上可以很轻易的拿到, 笔者是通过从0书写一个自己的react-router来实现跟react同样功能的方式来分享整个路由的思想, 这样也更好的让初入源码学习的同学更好的适应

react-router团队本身在实现router的时候引用了两个比较小的库, 一个叫做path-to-regexp, 一个叫做history, 所以笔者这里也将会直接引用这两个库, 当然势必对帮助大家对这两个库进行一个全面的熟悉和了解, 如果想了解这两个库是怎么写的, 可以移步笔者的另外的博客进行了解

目录结构

    1. path-to-regexp的了解
    1. history的了解
    1. Router的实现
    1. Route的实现
    1. Switch的实现
    1. withRouter的实现
    1. LinkNavLink的实现

在前期的Router编写中, 或许没办法直接演示, 如果小伙伴有看不太明白的地方可以直接提问或者等到Route组件写完然后看笔者的例子再回头看Router可能就醍醐灌顶了, 坚持到最会你就会发现大名鼎鼎的react-router也不过如此


1. path-to-regexp的使用

在书写一个自己的router之前, 笔者必须做一些铺垫, 首当其冲的就是认识path-to-regexp这个库

该库用于将一个字符串正则(路径正则, path regexp), React Router中用到了这个库, 笔者这里不再手写

// 我们书写的Route组件中的path属性, 有时候会写成下面这种形式
<Route path='/news/:year/:month/:day' component={news}/>

// 其中的path属性看着像正则却不是正则, 而path-to-regexp这个库就是帮助我们将
// /news/:year/:month/:day 转化为正儿八经的正则表达式, 然后router才会拿去比对和校验
// 如果不进行转化成真正的正则表达式, js是不认识的

这哥们接受三个参数, 并在执行调用完毕以后返回一个正则表达式, 我们可以用返回的正则表达式

参数 功能
path 要匹配的校验规则
keys path-to-regexp会将第一个参数path规则中的每一项的关键字抽出来包装在key三种传递给你
options 其他配置项,如是否开启大小写敏感, 是否精确匹配等

表格参数功能没看懂没关系, 笔者一开始也不是很懂, 但是你只要看看返回的数据就会秒懂了

我们来看看他的基本使用

import pathToRegexp from 'path-to-regexp';

const path = '/news/:year/:month/:day';
const keys = []; // 这个数组现在是空的, 待会我作为第二个参数丢进去, 他会在函数执行完以后给我一个有东西的数组

const result = pathToRegexp(path, keys, {
    sensitive: true, // 是否对大小写敏感
    end: true, // 是否精确匹配
});

console.log('keys如下: ', keys); // 会给我们一个数组, 将path参数中的关键字都抽离出来
console.log('根据path和配置项生成的正则如下: ', result); // 输出的就是一套正则表达式

输出结果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vOQhT4FZ-1594540331059)(./blogImages/path-to-exp.png)]

返回的正则表达式就好像是说把我们的path规则和第三个参数配置项通过分析得出一个正则表达式, 而第二个参数keys只是为了帮助我们更好的进行后续操作准备的, 我们的path/news/:year/:month/:day, 于是keys中就将year, month, day给我们封装进去了, 后续在使用中这些可能会对我们有帮助, 但是我们也不会用到它, 你可以理解keys仅仅是一个辅助参数

OK, path-to-exp的基本了解说到这里, 因为react-router本身也是直接调用的这个库, 所以我们也因为篇幅问题自己就不写了


2. history是了解和使用

该对象提供了一些方法, 用于控制或监听地址的变化
该对象不是window.history, 而是一个抽离的对象, 它封装了具体的实现

我们来看看他的基本使用吧

import { createBrowserHistory } from 'history';

const browserHistory = createBrowserHistory();

console.log('打印出的browserHistory如下', browserHistory);

输出结果如下, 这哥们就是提供了这些方法二次封装了浏览器的history对象, 提供了更加强大的功能, 这些功能我们随着用随着说, 这里就点到为止, 或许在router中你会随着使用更加的清晰

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suZpFg74-1594540331061)(./blogImages/history.png)]


3. Router组件的实现

害, 终于进入正题了, 一顿操作猛如虎, 全从Router开始撸

想要实现Router, 我们得先知道Router做了哪些事

  1. 这哥们本身不做任何的展示, 仅提供路由模式的配置
  2. 该组件会提供一个上下文, 上下文会提供一些使用的属性和方法, 供其他相关组件使用
  3. 浏览器中Router本身分为以下两种形式
    • HashRouter: 使用HashRouter模式匹配路由
    • BrowserRouter: 使用BrowserRouter模式展示路由

来吧, 来看个实例帮你们整体回忆一下

// App.js
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';

export default function App(props) {
    return (
         <Router></Router>
    )
}

我们来看一下React Devtools中展示给我们的react结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IyXGxIbv-1594540331062)(./blogImages/router.gif)]

如果图片不够清晰, 或者你看不懂也没关系, 笔者再帮你分析一波

  1. BrowserRouter当我们引入以后, 其实他内部是还用到了一个核心的Router组件
  2. Router组件得到一个属性history为一个对象, 就是我们用history库构造出来的对象一模一样
  3. Router组件有一个状态location, 该location其实就是history属性中的location, 只是提出来作为属性而已
  4. Router提供一个上下文, 里面携带一个value属性, 为一个对象, 对象中内容如下
    • history: 来自于Router组件中的history属性, 保存了当前浏览器历史记录栈的一些方法和信息
    • location: 来自于Router组件的location状态, 保存了当前路由的一些信息
    • match: 用来判定当前路由跟我们之后要书写的Route组件的上的path规则的校验, 它来自于我们自己书写, match对象携带以下几个属性
      • isExact: Boolean, 是否精确匹配
      • params:
  5. Routerchildren会被渲染进页面

这上面的这些基本使用方法我就不再过多的分析了, 别来看Router源码了还问我HashRouter是什么, 说我没写清楚, 那就太尴尬了

那咱一点一点来实现?

src目录下新建一个react-router文件夹(当然你自己想建在哪就建在哪), 创建一个Router.js

// Router.js
import React from 'react';

export default class Router extends React.PureComponent {
    render() {
        {/*根据我们上面的说法, 这里其实是返回了一个上下文出去*/}
    }
}

所以我们先将上下文搞定, react-router目录下, 创建一个RouterContext.js

// RouterContext.js
import React from 'react';

const RouterContext = React.createContext();

RouterContext.displayName = 'Router'; // 设置上下文在React Devtools工具中的名称, 这个是一个小细节, 因为我们会发现ReactRouter的上下文在调试工具中显示的是Router.Provider, 就是通过这样改名实现的

export default RouterContext;

回到Router.js

// Router.js
import React from 'react';
import { default as ctx } from './RouterContext.js'

export default class Router extends React.PureComponent {
    render() {
        {/*根据我们上面的说法, 这里其实是返回了一个上下文出去*/}
        return (
            <ctx.Provider>
            </ctx.Provider>
        )
    }
}

这个时候我们引入我们自己的Router.js进App, 并进浏览器看一下结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PnacSNll-1594540331063)(./blogImages/RouterProvider.png)]

结构很明显已经出来了, 只是之前说好的属性都没有, 那我们得给他啊

Router组件接收一个属性history, 我们先写着, 等待后续的父组件给他

// Router.js
...
export default class Router extends React.PureComponent {

    state = {
        // 我们知道location也是从history属性中拿过来的
        location: this.props.history.location, 
    }

    render() {
        // 我们之前有看到之后提供的上下文里, 有一个value值
        // value值里面有history, location, match三个属性
        // 要传递给上下文的value对象
        const contextValue = {
            history: this.props.history,
            location: this.state.location,
            match: ?
        }

        return (    
            {/*将contextValue传递给Provider*/}
            <ctx.Provider value={contextValue}>
                { this.props.children } 
            </ctx.Provider>
        )
    }
}
...

其实我们目前知道historylocation最终一定是从父组件来的, 那么match呢, 这哥们是需要我们自己来构造的, 希望你没有忘记笔者之前说的path-to-regexp, 如果忘了赶紧回去看看,来吧

react-router目录下新建一个pathMatch.js

// pathMatch.js
import pathToRegExp from 'path-to-regexp';

// 我们知道pathToRegExp就是帮助我们将我们想要设置的浏览器路径规则变成正则表达式, 以方便我们进行比较的

// 写一个方法pathMatch, 他也是最终我们要导出的方法
/**
 * 根据调用该方法的人传进来的参数, 用来匹配路径是否符合路径规则, 匹配成功返回一个match对象, 匹配失败返回undefined
 * @param {*} path  路径规则
 * @param {*} pathname 真实的路径
 * @param {*} options  其他配置项: sensitive => 是否大小写敏感, strict => 是否开启严格模式, exact => 是否精确匹配
 */
export default function pathMatch(path, pathname, options) {
    const keys = []; // 设置关键字数组, 就跟我们一开始测试pathToRegexp的含义一样
}

我们之前知道, 用户传递进来的的是sensitive, strict, exact三个属性, 前两个都没有问题, 但是最后一个我们知道path-to-regexp里精确匹配是为end, 所以我们必须将用户传递进来的操作转换一下

// pathMatch.js
...
export default function pathMatch(path, pathname, options) {
    ...
}

/**
 * 将传入的react-router的配置转化为path-to-regexp的配置
 */
function getOptions(options) {
    const defaultOptions = {
        sensitive: false,
        strict: false,
        exact: false
    }

    const mergeOptions = {...defaultOptions, ...options};

    return {
        end: mergeOptions.exact,
        sensitive: mergeOptions.sensitive,
        strict: mergeOptions.strict
    }
}
...

OK, getOptions方法书写完以后, 我们要在pathMatch中进行调用

// pathMatch.js
...
export default function pathMatch(path, pathname, options) {
    const opts = getOptions(options);
    const keys = []; 
    // 调用pathToRegExp方法, 传入对应的参数, 得到一个正则表达式
    const reg = pathToRegExp(path, keys, opts);
    // 使用正则表达式来直接匹配传递进来的pathname真实路径
    const validateResp = reg.excu(pathname);
    // 如果匹配结果为空, 代表当前真实路径不符合path属性规则, 直接抛出null
    if(validateResp == null) return null;
    // 如果匹配结果不是空, 代表有东西, 下面这套流程你玩过正则就懂
    const slicedValidateResp = Array.from(validateResp).slice(1); 
    // 最后我们要去拼params对象, params对象的作用就是
    // 假如你的地址规则是/news/:id, 传进去/news/1
    // 那么params对象应该为{id: 1}

    // 所以我们通过调用getParams方法来获得这个params对象
    const params = getParams(slicedValidateResp, keys);

    // 最后集合成一个match对象返回出去
    return {
        params, 
        isExact: pathname === validateResp[0], // 是否精确匹配一定要真实路径是不是等于匹配结果的第一位(匹配结果第一位是浏览器path路径)
        path,
        url: validateResp[0]
    }
}

/**
 * 通过传递进来的value的数组和key的数组将其包装成一个对象, {key: value}形式
 * @param {*} group 
 * @param {*} keys 
 */
function getParams(group, keys) {
    const resp = {}; // 最后要丢出去的对象
    for(let i = 0, len = group.length; i < len; i++) {
        const val = group[i];
        const name = keys[i].name;
        resp[name] = val;
    }
    return resp;
}

function getOptions(options) {...}
...

我们已经完成了pathMatch.js的编写, 也可以很轻易的通过pathname, path, options获得一个params对象, 那就来吧, 继续回到Router.js

// Router.js
...
import pathMatch from './pathMatch.js';

export default class Router extends React.PureComponent {
    ...
    render() {
        const contextValue = {
            ...
            // 在处理根路径的时候, react是直接通过/写死的, 所以我们也写死
            match: pathMatch('/', this.state.location.pathname)
        }
    ...
    }
}
...

到了最后一步了, 虽然我们的location在state里, 但是其实如果用户通过手段跳转了路由, 我们是没办法感知的, 所以我们要在Router.js中加上监听

historyapi提供一个listen方法, 同时在上下文中我们已经有history了, 所以可以直接使用该listen方法

  • listen方法: 该方法接收一个函数作为参数, 作为当路由地址发生改变的时候执行的回调函数, 同时listen方法返回一个一个函数removeListen, 用来解除监听
  • listen方法的参数函数又携带一个对象参数, 里面附带两个属性locationaction, action描述了当前是入历史记录栈还是出历史记录栈操作(你可以不用那么关心), location则是当前路由的相关信息
// Router.js
export default class Router extends React.PureComponent {
    ...

    componentDidMount() {
       // 在挂载完毕以后直接开启监听, 路由一旦修改就会执行回调函数
       this.removeListen = this.props.history.listen(({location, action}) => {
            console.log('路由修改了', location, action);
            // 回调函数执行我们直接强制重新渲染页面
            this.setState({
                location
            })
        })
    }

    componentWillUnmount() {
        // 在组件写在前我们直接卸载监听
        this.removeListen();
    }

    render() {
       ...
    }
}

OK, 到此, 我们的Router已经写完, 可以来加把火写写BrowserRouter, 我们在src目录下新建react-router-dom文件夹, 新建文件BrowserRouter.js

// BrowserRouter.js
// BrowserRouter相当的简单, 我们已经有了History库, 所以直接从该库中导入createBrowserHistory
// 然后调用Router组件并给他相应的history属性即可
import React from 'react';
import Router from '../react-router/Router';
import { createBrowserHistory } from 'history';

export default class BrowserRouter extends React.PureComponent {

   history = createBrowserHistory(this.props);

   render() {
    return (
        <Router history={this.history}>
            { this.props.children }
        </Router>
    )
   }
}

关于HashRouter笔者就不写了, 方案都一样, 引入createHashHistory就OK, 那咱来测试一下?

// App.js
import { BrowserRouter } from './react-router-dom/BrowserRouter';
import React from 'react';

export default App(props) {
    return (
        <BrowserRouter>
            我是children
        </BrowserRouter>
    )
}

结构如下, 已经跟官方的Router结构相差无几了, 该给的参数我们也都给了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZZpkyf4N-1594540331064)(./blogImages/BrowserRouter.png)]


4. Route组件的实现

其实Router组件走完以后, react-router最难的地方已经走完了, 如果你之前的没看太懂, 没关系, 等读完RouterRoute, 你再结合例子自己再揣摩一下也差不多了, OK, 话不多说, 咱开始?

那么Route做了哪些事呢

  1. 根据不同的path属性, 展示不同的组件
  2. 该组件接收几个重要的属性
参数 功能
path 要匹配的路径规则, 默认情况下不区分大小写
component 路径匹配成功以后要渲染的组件
sensitive 开启区分路径大小写
exact 开启路径精确匹配
render 路径匹配成功以后执行的render props
  1. 如果不书写path属性, 则一定会渲染出组件

同样, 整个实例帮你们回忆一下

// App.js
import { BrowserRouter as Router, Route } from 'react-router-dom';

function Page1(props) {
    return (
        <div>
            我是Page1
            <button onClick={() => props.history.push('/page2')}>去Page2</button>
        </div>
    )
}


function Page2(props) {
    return (
        <div>
            我是Page2
            <button onClick={() => props.history.push('/page1')}>去Page1</button>
        </div>
    )
}

export default function App(props) {
    return (
        <Router>
            <Route path='/page1' component={Page1}></Route>
            <Route path='/page2' component={Page2}></Route>
        </Router>
    )
}

整个代码最后的结果如下, React Devtools中的结果也如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1mADTC29-1594540331065)(./blogImages/route.gif)]

如果图片不够清晰, 或者你看不懂也没关系, 笔者再帮你分析一波

  1. 我们是可以看到首先Route组件成为了路由上下文的消费者, 之前我们知道Router是上下文的提供者
  2. 作为路由的消费者, Route组件得到了上下文中的value属性毋庸置疑(history, location, match)
  3. 我们又发现, Route组件又提供了一个Provider, 他为什么要又提供一个上下文呢, 原因还是比较简单, 因为我们知道historylocation都没有问题, 但是match使我们在根组件写死的吧, 因为那个时候我们根本没办法得到路径规则, 路径规则是Route上的path属性, 所以在这里我们要重新提供一次上下文, 并修改match属性
  4. 如果Route组件出现children属性, 则不管path匹不匹配都会渲染相应的children
  5. 如果Route路径匹配, 则优先渲染render属性, 最后再component属性

来吧, 根据上面的条数, 开始撸, 在react-router目录下新建一个Route.js

// Route.js
import React from 'react';
import { default as ctx } from './RouterContext.js';

export default class Route extends React.PureComponent {

    // 封装一个新的getMatch方法
    getMatch = location => {
        const { pathname } = location;
        const { path='/', exact = false, sensitive = false, strict = false } = this.props;
        return pathMatch(path, pathname, {
                exact,
                strict,
            sensitive
        })
    }

    // 上下文处理函数
    consumerHandler = value => {
        // 在这里, 我们要处理一下match规则
        this.ctxValue = {
            history: value.history,
            location: value.location,
            match: this.getMatch(value.location), // 获取新的match对象
        }
        return <RouterContext.Provider value={ this.ctxValue }>
                { this.renderChildren(this.ctxValue) }
        </RouterContext.Provider>
    }

     // 根据不同的规则渲染不同的属性
     renderChildren = ctx => {
        if(this.props.children !== undefined && this.props.children !== null) {
            if(typeof this.props.children === 'function')  return this.props.children();
            else return this.props.children; 
        }else if(!ctx.match) {
            return null;
        }else if(typeof this.props.render === 'function') {
            return this.props.render(ctx);
        }else if(this.props.component) {
            const Component = this.props.component;
            return <Component {...ctx} />
        }else {
            return null;
        }
    }

    render() {
        {/*作为上下文的消费者, Route组件一定由Consumer包装*/}
        <ctx.Consumer>
            { this.consumerHandler }
        </ctx.Consumer>
    }
}

因为Router比较简单, 笔者直接就一气呵成了, 而且代码也都比较容易读懂, 那么我们来测试一下?

// App.js
import React from 'react';
// 引入自己写的BrowserRouter, 和自己写的Route
import { BrowserRouter } from './react-router-dom/index';
import { Route } from './react-router/index'


function Page1(props) {
  return (
    <div>
      我是Page1
      <button onClick={() => {
        props.history.push('/page2')
      }}>去Page2</button>
    </div>
  );
}

function Page2(props) {
  return <div>
            我是Page2
            <button onClick={() => {
              props.history.push('/page1')
            }}>去Page1</button>
        </div>;
}

function Page3(props) {
  return <div>我是无论如何都要进行渲染的</div>
}

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Route path='/page1' component={Page1} />
        <Route path='/page2' component={Page2} />
        <Route component={Page3} />
      </BrowserRouter>
    </div>
  );
}

export default App;

实现效果和React Devtools如下, 至此我们已经实现了RouterRoute组件, 并且之后的组件会更加的easy

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SrE7KnRp-1594540331066)(./blogImages/手写Route.gif)]


5. Switch组件的实现

这哥们太简单了, 简单到令人发指, 他的功能也很简单

  1. 匹配到第一个符合path规则的哥们就停

不多废话, 这么简单的组件我相信你不需要演示吧? 直接一气呵成

import React from 'react';
import RouterContext from './RouterContext';
import pathMatch from './pathMatch';

export default class Switch extends React.PureComponent {

    getRenderChild = ({ location }) => {
        let lastChildren = [];

        if (this.props.children instanceof Array) {
            lastChildren = this.props.children;
        } else if (typeof this.props.children === 'object') {
            lastChildren = [this.props.children];
        }
        for (let child of lastChildren) {
            const { path = '/', exact = false, strict = false, sensitive = false } = child.props;

            // 将Route的path属性规则和真实pathname进行比较, 如果有返回params对象代表匹配成功
            const validateResp = pathMatch(path, location.pathname, { exact, strict, sensitive });
            if (validateResp != null) {
                return child;
            }
        }

        return null;
    }

    render() {
        return (
            <RouterContext.Consumer>
                { this.getRenderChild }
            </RouterContext.Consumer>
        )
    }

}

这太简单了, 我们来自己试验一下

// App.js
import React from 'react';
// import './react-router/pathMatch';
// import './react-router/browserHistory';
// import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { BrowserRouter } from './react-router-dom/index';
import { Route, Switch } from './react-router/index'


function Page1(props) {
  return (
    <div>我是Page1
      <button onClick={() => {
        props.history.push('/page2')
      }}>去Page2</button>
    </div>
  );
}

function Page2(props) {
  return <div>我是Page2</div>;
}

function Page3(props) {
  return <div>我是无论如何都要进行渲染的</div>
}

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Switch>
            <Route path='/page1' component={Page1} />
            <Route path='/page2' component={Page2} />
            <Route component={Page3} />
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

页面渲染结果如下, 如果没有Switch组件, 我们知道Page3无论如何都需要渲染的, 加上Switch以后就不一样了, 所以这下你也明白了为什么react-router说在写Route的时候顺序不要乱写, 完全因为他的这种匹配机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S3mxjaX9-1594540331066)(./blogImages/Switch.png)]


6. withRouter的实现

withRouter高阶组件

这哥们的功能也非常的简单, 经过他包装以后的组件会直接享有路由上下文

不用演示了吧? 这种这么简单的功能还要演示的话完全是在侮辱我啊, 主要是懒

import RouterContext from './RouterContext';

export default function withRouter(Comp) {

   function RouterWrapper (props) {
        return (
            <RouterContext.Consumer>
                {value => <Comp {...value} {...props} />}
            </RouterContext.Consumer>
        )
    }  

    RouterWrapper.displayName = `withRouter(${Comp.displayName || Comp.name})`; // 修改显示的名称

    return RouterWrapper;
}

我们来看看效果

// App.js
import React from 'react';
import { BrowserRouter } from './react-router-dom/index';
import withRouter from './react-router/withRouter';

function Page1(props) {
  return (
    <div>我是Page1
      路径: { props.history.location.pathname }
    </div>
  );
}

// HOC包装一层
const RouterPage1 = withRouter(Page1);

function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <RouterPage1 />
      </BrowserRouter>
    </div>
  );
}

export default App;

我们知道如果不进行高阶组件包装, 那么Page1由于没有上下文联系, 他是生死获取不到location的, 他一定会报错, 但是经过我们这么一包装, 你就可以看下面的结果了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QXzfrdvm-1594540331067)(./blogImages/withRouter.png)]


7. Link和NavLink的实现

Link

Link的核心功能如下: 无刷新跳转页面, 都不想说了我们直接肝吧, 这个小家伙边写边说吧

react-router-dom目录下新建Link.js

// Link.js
import React from 'react';

export default function Link(props) {
    return (
        <a href={to}>
            { props.children }
        </a>
    )
}

OK, 写完上面这块我们测试一下

// App.js
import React from 'react';
import { BrowserRouter } from './react-router-dom/index';
import { Route, Switch } from './react-router/index'
import Link from './react-router-dom/Link';

function Page1(props) {
  return (
    <div>我是Page1
      路径:{ props.history.location.pathname}
    </div>
  );
}

function Page2(props) {
  return <div>我是Page2</div>;
}

function Page3(props) {
  return <div>
    <Link to='/page1'>去Page1</Link>
    <Link to='/page2'>去Page2</Link>
  </div>
}



function App() {
  return (
    <div className="App">
      <BrowserRouter>
        <Switch>
          <Route path='/page1' component={Page1}></Route>
          <Route path='/page2' component={Page2}></Route>
          <Route path='/' component={Page3}></Route>
        </Switch>
      </BrowserRouter>
    </div>
  );
}

export default App;

实现效果如下, 其实这个时候已经可以实现跳转了, 只不过是有刷新的跳转

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nIIc4rip-1594540331068)(./blogImages/Link1.gif)]

所以说我们做的还远远不够, 我们来考虑另一些问题, 对象形式的to属性和无刷新跳转

import React from 'react';
import { default as ctx } from '../react-router/RouterContext';
import { parsePath } from 'history'

export default function Link(props) {

    // 如果props.to是对象的话, 我们要从history库中拿出一个方法来将他变成一个路径

    // 所以我们必须要拿到上下文

    const {to, ...rest} = props;

    return (
        <ctx.Consumer>
            { value => {
                const curLocation = to;
                let href;
                if(typeof curLocation === 'object') {
                    href = value.history.createHref(curLocation);
                   
                }else {
                    // 这里为什么要这样转一层啊, 就是通过createHref构造的href会有basename
                    // 而我们自己写的是没有basename的 
                    href = value.history.createHref(parsePath(to))
                }
                return <a {...rest} onClick={
                    e => {
                        {/** 直接组织默认事件就无刷新跳转了 */}
                        e.preventDefault();
                        value.history.push(href);
                    }
                } href={href}>{ props.children }</a>
            } }
        </ctx.Consumer>
    )
}

实现效果如下, 我们已经做到了无刷新跳转和对象形式的props.to, 那么这个Link组件也基本OK了, 至于那些replace之类的细节我们真的没必要探究, 因为他太简单了, 你都只需要判断一下replace属性有没有传true, true的话就用history.replace跳转

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BrDhuCpW-1594540331068)(./blogImages/Link2.gif)]

NavLink

NavLink无非就是添加了一个类名, 特别是我们在写好了Link以后, NavLink就好写多了

react-router-dom目录下新建一个NavLink.js

// NavLink.js
import React from 'react';
import Link from './Link';
import { default as ctx } from '../react-router/RouterContext'; 
import { parsePath } from 'history';
import pathMatch from '../react-router/pathMatch';

export default function NavLink(props) {

    // 我们是怎么判断当前是激活路径的呢
    // 也很简单, 我们通过props.to中的路径和当前浏览器中的路径进行比较
    // 如果一致当前就是激活路径了
    const { activeClass = 'active', sensitive = false,
            exact = false, strict = false, ...rest } = props;

    return (
        <ctx.Consumer>
            { ({location}) => {
                let loc = props.to;
                if(typeof props.to == 'string') {
                    // 如果当前的props.to是字符串, 我们是要将他们变成对象的
                    loc = parsePath(props.to);
                } 
                // 拿去校验, 我们之前写好的方法, 校验成功是会给我们返回params对象的
                const validateResp = pathMatch(loc.pathname, location.pathname, {sensitive, exact, strict});;
                if(validateResp) {
                    console.log('adasdasdasdas');
                    return <Link {...rest} className={activeClass}></Link>
                }else {
                    return <Link {...rest}></Link>
                }

            } }
        </ctx.Consumer>
    )
}

我们来测试一下

import React from 'react';
import { BrowserRouter } from './react-router-dom/index';
import { Route, Switch } from './react-router/index'
import NavLink from './react-router-dom/NavLink';

function Page1(props) {
  return (
    <div>我是Page1
      路径:{ props.history.location.pathname}
    </div>
  );
}

function Page2(props) {
  return <div>我是Page2</div>;
}

function Page3(props) {
  return <div>
    <NavLink to={{
     pathname: '/page1',
     search: '?a=1&b=2'
    }}>去Page1</NavLink>
    <NavLink to='/page2'>去Page2</NavLink>
  </div>
}



function App() {
  return (
    <div className="App">
      <BrowserRouter>
          <Route path='/page1' component={Page1}></Route>
          <Route path='/page2' component={Page2}></Route>
          <Route path='/' component={Page3}></Route>
      </BrowserRouter>
    </div>
  );
}

export default App;

实现效果如下, 我们已经可以根据不同的路由地址切换不同的active

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dx5UQA40-1594540331069)(./blogImages/Nav.gif)]

OK, 至此我们LinkNavLink也全部写完了


终于7个大个都给我整完了, 感谢观看, 如有问题, 希望赐教

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/107300559