react-router原理分析

560 阅读6分钟

在我们使用react这个库搭建前端工程的时候,是我们不可避免需要使用到第三方的路由库来划分各个模块,或者说页面,而主要的路由库就是react-router。为了进一步了解这个库是如何实现前端路由的,需要对其源码有初步的了解。

通常对于路由库功能的理解,就是监听前端路由的改变,动态渲染匹配当前路由的组件。

搭建一个mini的react-router,有助于理解这个库是如何实现前端路由功能的,对其实现的思路有基本了解。以及了解提供的hook之一useHistory的功能和实现方法。至于其他react-router的功能,在此基础上也能很好的理解。

源码:mini-react-router

简单案例

import React from 'react';
import {
  BrowserRouter as Router,
  Route,
} from './mini-react-router/react-router-dom';
import { useHistory } from './mini-react-router/react-router/hooks';
import './App.css';

function App() {
  //简单案例,Route内部比较使用的===比较,因此不会渲染所有符合react-router的路由规则
  return (
    <Router>
      <Route path="/">
        <Home />
      </Route>
      <Route path="/about">
        <About />
      </Route>
      <Route path="/user">
        <User />
      </Route>
    </Router>
  );
}

function Home(props) {
  const history = useHistory();
  return (
    <div>
      <h1>首页</h1>
      <button onClick={() => history.push('/about')}>about</button>
      <button onClick={() => history.push('/user')}>user</button>
    </div>
  );
}
function About() {
  const history = useHistory();
  return (
    <div>
      <h1>关于</h1>
      <button onClick={() => history.push('/')}>home</button>
    </div>
  );
}
function User() {
  const history = useHistory();
  return (
    <div>
      <h1>我的</h1>
      <button onClick={() => history.push('/')}>home</button>
    </div>
  );
}

export default App;

分析react-router源码会发现,在浏览器环境中,我们需要导入react-router-dom中的相关Router组件(这取决于使用的路由匹配模式histroy或者hash),那么我们需要了解下react-router库的组成。

react-router的组成

这是github中的react-router库的项目结构:

react-router项目结构

我们可以看到,react-router仓库分成4个项目:react-router-dom-v5-compat, react-router-dom, react-router-native, react-router

  1. react-router : 核心库,定义了路由的基本组件和逻辑,比如定义了Router、Route组件和相关的hook

  2. react-router-dom : 是在浏览器上环境中使用的库,其依赖react-router

  3. react-router-native : 实在react-native环境中使用的库,其依赖react-router

  4. react-router-dom-v5-compat此软件包通过与v5并行运行,使React Router web App能够增量迁移到v6中的最新API。它是v6的一个副本,带有一对额外的组件以保持两者的同步。

这里默认的宿主环境是浏览器,所以我们主要关注的react-router-domreact-router组件。

另外,react-router-dom依赖独立的history库,该库的主要作用是:

允许您在JavaScript运行的任何地方轻松管理会话历史。历史对象抽象出各种环境中的差异,并提供一个最小的API,允许您管理历史堆栈、导航和会话之间的持久状态

到这里,我们mini-react-router所需要实现的依赖就全部齐活,一共是history, react-router, react-router-dom

react-router-dom

react-router-dom中,定义了很多用于浏览器环境的组件,例如BrowserRouter、HashRouter、Switch或者Link。这其中最主要的是BrowserRouterHashRouter这两个路由器组件。

// BrowserRouter.js
import React, { Component } from 'react';
import { createBrowserHistory as createHistory } from '../history';
import { Router } from './index';

class BrowserRouter extends Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default BrowserRouter;

// index.js
export { Route, Router } from '../react-router';

export { default as BrowserRouter } from './BrowserRouter.js';
export { default as HashRouter } from './HashRouter.js';

通过源码可以看到,BrowserRouter组件中除了创建了Browser History,使用的HTML5定义的history API,创建history的方法来自于独立的history库,只是包装了Router组件,而该组件来自react-router核心库。

那么让我们来看下另外两个依赖。

histroy

history是独立的脚本库,用于在JavaScript运行的任何地方轻松管理会话历史。历史对象抽象出各种环境中的差异,并提供一个最小的API,允许管理历史堆栈、导航和会话之间的持久状态。

function createListenerManager() {
  let listeners = [];
  return {
    //订阅
    subscribe: function (listener) {
      if (typeof listener !== 'function') {
        throw new Error('typeof listener must is function');
      }
      listeners.push(listener);
      //取消订阅函数
      return function unSubscribe() {
        listeners = listeners.filter(function (item) {
          return item !== listener;
        });
      };
    },
    //发布:调用所有监听函数,并传入新的location
    notify: function (location) {
      listeners.forEach((listen) => listen(location));
    },
  };
}
/**
 * browser Histroy,用于支持histroy API的路由
 */
export function createBrowserHistory() {
  const location = {
    pathname: '/',
  };
  const lietenerManager = createListenerManager();
  window.addEventListener('popstate', handlePop);
  //监听事件
  function listen(listener) {
    return lietenerManager.subscribe(listener);
  }
  /**
   * onpopstate事件响应函数
   * 处理history变更,主要是监听浏览器的前进和回退,histroy.go、histroy.go、histroy.go也会触发onpopstate
   * history.pushState和history.replaceState API调用并不会触发该事件
   * @param {*} e
   */
  function handlePop(e) {
    lietenerManager.notify({ pathname: window.location.pathname });
  }

  function push(path) {
    //虽然pushState不会触发popstate事件,但是不可省略
    //这样可以保持state状态在当前路由记录中得到正确更改,保证state信息一致
    window.history.pushState(null, '', path);
    lietenerManager.notify({ pathname: path });
  }
  return {
    listen,
    location,
    push,
  };
}

可以看到,在BrowserRouter组件中使用的createBrowserHistory方法返回的对象中有三个基本属性:

  1. listen : 新增一个监听函数,用于外部订阅路由的变化
  2. location : 路由信息对象
  3. push : 给路由栈中新增一条记录

其中值得注意的是:

  1. HTML5 history API虽然规定了路由变化事件onpopstate,但是受其触发的条件限制,在push过程中需要调用history.pushState来保证浏览器的路由栈一致。

  2. window.addEventListener('popstate', handlePop),监听浏览器popstate事件,保证路由变化是正确响应。

  3. createListenerManager实现了订阅-发布模式,统一管理路由的变化

createHashHistory返回的history接口和createBrowserHistory一致,其内部是对hash的处理。

react-router

react-router核心库,负责处理核心逻辑,适用于浏览器环境和react-native,其中定义的Router,Route组件是其中关键组件。

Router

import React from 'react';

import HistoryContext from './HistoryContext.js';
import RouterContext from './RouterContext.js';

/**
 * Router组件的作用主要:
 * 1. 监听history的最新location,并当作props传递给子组件
 * 2. 渲染children
 */
class Router extends React.Component {
  // 静态方法,计算当前pathname是否匹配根路径
  static computeRootMatch(pathname) {
    return { path: '/', url: '/', params: {}, isExact: pathname === '/' };
  }

  constructor(props) {
    console.log('router constructor');
    super(props);

    this.state = {
      location: props.history.location, //挂载history的location属性
    };
  }
  componentDidMount() {
    this.unlisten = this.props.history.listen((location) => {
      this.setState({ location });
    });
  }
  componentWillUnmount() {
    if (this.unlisten) {
      this.unlisten();
    }
  }

  render() {
    //传递两个context给子组件
    //一个是路由相关属性,包括history、location、match(是否匹配根路由)
    //一个是history信息,同时将子组件渲染出来
    console.log('render');
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history,
          location: this.state.location,
          match: Router.computeRootMatch(this.state.location.pathname),
        }}
      >
        <HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>
    );
  }
}
export default Router;

Router路由器组件,用于监听histroy的变更,并通过两个Context向子孙组件提供相关对象。通过Context的使用,当路由变更的时候可以触发React的更新机制,来重新渲染匹配的路由。

Route

import React, { Component } from 'react';
import RouterContext from './RouterContext.js';

const matchPath = (pathName, path) => {
  return pathName === path;
};
/**
 * 消费RouterContext,判断是否匹配路由,匹配则根据children、component、render
 * 这三个prop情况渲染路由组件
 * 优先级:children=>component=>render
 */
class Route extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {(context) => {
          const location = this.props.location || context.location;
          const match = matchPath(location.pathname, this.props.path);
          const props = { ...context, location, match };
          let { children, component, render } = this.props;
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }
          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === 'function'
                    ? children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === 'function'
                ? children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

export default Route;

Route路由组件,主要用于消费Router提供RouterContext,比较当前的location和path pros来是否需要渲染相关组件。渲染优先级:children=>component=>render

hook:useHistory

import React from 'react';
import HistoryContext from './HistoryContext';

export function useHistory() {
  return React.useContext(HistoryContext);
}

useHistory的实现很简单,通过使用React.useContext直接读取先前定义的HistoryContext。就能获取确定的history对象。

总结

以上的相关代码只是选择了react-router库相关源码,实现其基本的路由功能。

其基本思路是:

  1. 定义路由操作接口,和订阅发布方式
  2. 路由器组件订阅路由更新
  3. 路由变更通知订阅对象,更新相关状态
  4. 触发重新渲染,匹配相关路由

通过mini-react-router,我们能够知道react-router 的组成和实现的基本思路。再遇到react-router相关问题,起码可以做到**"手上有粮,心中不慌"**。