写点什么

react-router 原理分析

作者:正经工程师
  • 2022 年 5 月 21 日
  • 本文字数:5014 字

    阅读完需:约 16 分钟

在我们使用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 仓库分成 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.jsimport 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.jsexport { 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相关问题,起码可以做到**"手上有粮,心中不慌"**。

发布于: 刚刚阅读数: 3
用户头像

软件工程师 2018.12.15 加入

还未添加个人简介

评论

发布
暂无评论
react-router原理分析_React_正经工程师_InfoQ写作社区