1. 程式人生 > >3. react-router-dom原始碼揭祕 - BrowserRouter

3. react-router-dom原始碼揭祕 - BrowserRouter

今天開始,我們開始揭開react-router-dom神祕的頭蓋骨,哦不,面紗。 在此之前,我們需要了解一些預備知識:React的context和react-router-dom的基本使用。需要複習的同學請移步:

下面是我跟小S同學一起閱讀原始碼的過程。 大家可以參照這個思路,進行其他開源專案原始碼的學習。

我: 小S,今天我們來一起學習React-router-dom的原始碼吧

好呀!

我: 首先,react-router的官網上,有基本的使用方法。 這裡

(中文點選這裡) 列出了常用的元件,以及它們的用法

  1. Router (BrowserRouter, HashRouter)
  2. Route
  3. Switch
  4. Link
好的, 繼續

我: 先從這些元件的原始碼入手,那肯定第一個就是BrowserRouter,或者HashRouter

那應該怎麼入手呢?

我: 首先,從github上,得到與文件版本對應的程式碼。
我: 接著看路徑結構。是這樣的:

接下來我一般就是找教程先簡單過一遍,程式碼下下來然後把node__modules複製出來debugger 然後看不懂了就放棄

我: 不,你進入細節之前,要先搞清楚程式碼的結構

恩啊, 不然怎麼找程式碼

我: 你看到這個路徑之後,第一步,應該看一看,這些資料夾都是幹啥的,哪個是你需要的

script是build, website是doc, packges是功能
這個都差不多

我: 對。開啟各個資料夾,會發現,packages裡面的東西,是我們想要的原始碼。

我: 我們肯定先從原始碼看起,因為這次讀原始碼首先要學習的是實現原理,並不是如何構建
我: 那咱們就從react-router-dom開始唄
我: 開啟react-router-dom,奔著modules去

直接從github上下載master的分支麼

我:

為啥看modules
不應該先看package.json和rollup麼

我: 核心程式碼,肯定是在modules裡了。我要先看看整個的結構,有個大致的印象

恩恩

我: 開啟modules就看到了我們剛剛文件中提及的幾個元件了
我: 我們先從BrowserRouter.js入手

嗯哼

我: 那我要開啟這個檔案,開始看程式碼了
我: 我先不關注package.json這些配置檔案

殘暴

我: 因為我這次是要看原理,不是看整個原始碼如何build
我: 配置檔案也是輔助而已

嗯啊。
可是有時候還是很重要的

我: 那就用到了再說

是不是至少看一下都用了什麼和幾個入口

我: 用到了什麼也不需要在package.json中看,因為我關注的那幾個元件,用到啥會import的。所以看原始碼,最重要的是focus on。你要有關注點,因為有的原始碼,是非常龐大的。一不小心就掉進了細節的海洋出不來了。

有道理
比如react

我: 對,你不可能一次就讀懂他裡面的東西,所以你要看很多次
我: 每次的關注點可以不同

恩啊
確實如此

我: 都揉到一起,會覺得非常亂,最後就放棄了
我: 而且,我們學習原始碼,也不一定要把原始碼中的每個特性都在同一個專案中都用到,還是要分開學,分開用

有道理
我就總忍不住亂看

我: 那就先看BrowserRouter.js了。
我: 開啟檔案,看了一下,挺開心,程式碼沒幾行

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

/**
 * The public API for a <Router> that uses HTML5 history.
 */
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

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

if (__DEV__) {
  //此處省略若干行程式碼
}

export default BrowserRouter;
複製程式碼
然後一臉懵逼記不住, 看不懂

我: 哈哈,程式碼這麼少,那肯定是有依賴元件了
我: 先看看依賴了哪些元件
我: 我最感興趣的是history和react-router。如下:

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
複製程式碼
history是庫啊
等等
我有點沒跟上

我: 等待了30秒......
為啥我感興趣這倆呢

你的興趣點對
我以前看過原始碼相關教程,瞭解一點history

我: 嗯。官網說了啊。

Routers

At the core of every React Router application should be a router component. For web projects, react-router-dom provides and routers. Both of these will create a specialized history object for you.

我: 在實現路由的時候,肯定是用到history的
我: 所以,這個可能會作為讀原始碼的預備知識。(如果夥伴們有需求,請在評論中說明,我們可以再加一篇關於history的文章)
我: 但是我先不管他,看看影響react-router的閱讀不
我: 另外,之前說過,這個檔案原始碼行數很少,肯定依賴了其他的元件。看起來,這個react-router擔當了重要職責。
我: 所以現在有兩個Todos: historyreact-router

我: 那一會需要關注的就是react-router這個包了
我: 我暫時先不管剛才的兩個todos,我把這個元件(BrowserRouter)先看看,反正程式碼又不多

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

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

if (__DEV__) {
  //此處省略若干行程式碼
}
複製程式碼

我: 我要把if(__DEV__)的分支略過,因為我現在要看的是最最核心的東西
我: 切記過早的進入__DEV__,那個是方便開發用的,通常與核心的概念關係不大
我: 那就只剩倆東西了

//......
class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}
//......
複製程式碼

我: 所以現在BrowserRouter的任務,就是建立一個history物件,傳給react-router的<Router>元件

這個時候

我: 嗯,你說

你會選擇看react-router還是history

我: 哈哈,這個時候,我其實想看一眼HashRouter

我也是

我: 因為import的那句話

import { createBrowserHistory as createHistory } from "history";
複製程式碼

所以我有理由懷疑,HashRouter的程式碼類似,只是從history包中匯入了不同的函式

HashRouter.js

import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";
複製程式碼

還真是

我: 那我就把關注點,放在react-router上了
我: 因為

  1. history我猜出他是幹啥了,跟瀏覽器路徑有關
  2. router裡面如果用到history了,我等到在讀碼時,遇到了阻礙,再去看history,這樣行不行
恩啊

我: 回到這個路徑

我: 去看react-router

為什麼是他

我: 因為它匯入包時,沒加相對路徑啊
我: 說明這是一個已經發布的node包,匯入時需要在node_modules路徑下找

import { Router } from "react-router";
複製程式碼

我: 我就往上翻一翻唄,當然,估計在配置檔案中,應該會有相關配置

恩恩

我: 進這個路徑,檔案真tmd多,mmp的

我: 匯入的是一個包,包下有index.js,我是不是應該先看這個js

我是這個習慣,先看index是不是隻做了import

我: 但是其實我們在使用recat-router-dom的時候,網上會有一些與react-router的比較的討論,

沒太注意
稀裡糊塗

我: 所以,react-router是一個已經發布的node包。但是,我並不確定他的程式碼在哪,如果找不到,我可能會從github上其他的位置找,或者從npm的官網找連結了

恩啊

我: 進index.js吧

"use strict";

if (process.env.NODE_ENV === "production") {
  module.exports = require("./cjs/react-router.min.js");
} else {
  module.exports = require("./cjs/react-router.js");
}
複製程式碼

我: 程式碼不多,分成production和else倆分支
我: 我會選擇else分支
我: 但是發現一個問題啊,我艹
我: 當前路徑下,沒有cjs資料夾
我: 因為BrowserRouter匯入的是一個包
我: 所以這個包,得是build之後的

這個時候就要看packge的script了

我: 嗯,可以的
我: 不過我感覺略微跑偏了
我: 我要回到router本身上

好好
繼續
怎麼回到router本身

我: /react-router下,有一個router.js檔案
我: 開啟看,只有那兩行程式碼,不是我要的東西啊
我: 它匯出的,還是index.js編譯之後的

看modules

我: 對,看modules
我: 開啟modules下的Router.js

要是我的話, 這個時候就跑偏了
直接去看rollup了
然後最後找到router
router.js

我: 我也可能會跑偏
我: 我之前就跑到history上去了
我: 但是後來想想,這樣不太好
我: 從看原始碼角度說,直接找到modules下的Router.js很容易
我: 因為其他檔案,一看就不是原始碼實現

嗯啊

我: 現在開啟它,一看,挺像啊,那先看看有多少行
我: 百十來行,有信心了,哈哈

import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import RouterContext from "./RouterContext";
import warnAboutGettingProperty from "./utils/warnAboutGettingProperty";

function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}

/**
 * The public API for putting history on context.
 */
class Router extends React.Component {
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };

    // This is a bit of a hack. We have to start listening for location
    // changes here in the constructor in case there are any <Redirect>s
    // on the initial render. If there are, they will replace/push when
    // they mount and since cDM fires in children before parents, we may
    // get a new location before the <Router> is mounted.
    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

  componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  }

  render() {
    const context = getContext(this.props, this.state);

    return (
      <RouterContext.Provider
        children={this.props.children || null}
        value={context}
      />
    );
  }
}

// TODO: Remove this in v5
if (!React.createContext) {
  Router.childContextTypes = {
    router: PropTypes.object.isRequired
  };

  Router.prototype.getChildContext = function() {
    const context = getContext(this.props, this.state);

    if (__DEV__) {
      const contextWithoutWarnings = { ...context };

      Object.keys(context).forEach(key => {
        warnAboutGettingProperty(
          context,
          key,
          `You should not be using this.context.router.${key} directly. It is private API ` +
            "for internal use only and is subject to change at any time. Instead, use " +
            "a <Route> or withRouter() to access the current location, match, etc."
        );
      });

      context._withoutWarnings = contextWithoutWarnings;
    }

    return {
      router: context
    };
  };
}

if (__DEV__) {
  Router.propTypes = {
    children: PropTypes.node,
    history: PropTypes.object.isRequired,
    staticContext: PropTypes.object
  };

  Router.prototype.componentDidUpdate = function(prevProps) {
    warning(
      prevProps.history === this.props.history,
      "You cannot change <Router history>"
    );
  };
}

export default Router;
複製程式碼
然後這麼少的程式碼
第一反應看一下引入

我:
我: 但是你看,一共五個

import React from "react";
import PropTypes from "prop-types";
import warning from "tiny-warning";

import RouterContext from "./RouterContext";
import warnAboutGettingProperty from "./utils/warnAboutGettingProperty";
複製程式碼
前三個忽略,一看就沒用

我: 是的
我: 我現在其實有點關注第五個了

我會看render

我: 先不著急
我: 因為如果第五個的名字叫做warnXXXX
我: 是警告的意思

恩恩
搜一下

我: 警告通常都是開發版本的東西,如果能排除,那就剩第四個依賴了

可能沒用
再一看,是在__DEV__裡面的

我: 對,當前檔案搜尋了一下,在__DEV__分支下,不看了,哈哈
我: 那就剩一個context.js了唄

過分

我: 我覺得我現在想掃一眼這個檔案,如果內容不多,我就先搞他,如果多的話,那就先放那

恩恩

我: 那我去看一看吧,哈哈
我: 進RouterContext.js這個檔案了

// TODO: Replace with React.createContext once we can assume React 16+
import createContext from "create-react-context";

const context = createContext();

context.Provider.displayName = "Router.Provider";
context.Consumer.displayName = "Router.Consumer";

export default context;
複製程式碼

我: 我次奧了

我: 十行不到,我把他搞定,我就可以專注Router.js那個檔案了。那個檔案裡面的內容,就是全部Router的核心了
我: 這裡是標準context用法,店長推薦的,參見這個
我: 返回Router.js了哈

然後呢
看createContext麼

我: createContex就是最新的context用法,參見這個
我: 所以,需要有準備知識,哈哈
我: 簡單點說,就是一個提供者(Provider),一個是消費者(Consumer)
我: 我這次看的是react-router
我: 別跑偏了
我: 回到router.js去了
我: 這個時候,可以稍微進入細節一些了
我: 從第一個函式定義開始

function getContext(props, state) {
  return {
    history: props.history,
    location: state.location,
    match: Router.computeRootMatch(state.location.pathname),
    staticContext: props.staticContext
  };
}
複製程式碼

我: 從名字看,是獲取context的,每次呼叫返回一個新建立的物件,多餘的不知道,先放著,往後看

我: 我先大概掃一眼元件都有哪些方法。另外發現,除了元件,還有其他程式碼
我: 除了元件內容,元件下面有一個判斷,看起來應該是處理老版本react的相容問題的。那我就先不看了

// TODO: Remove this in v5
if (!React.createContext) {
  Router.childContextTypes = {
    router: PropTypes.object.isRequired
  };

  Router.prototype.getChildContext = function() {
    const context = getContext(this.props, this.state);

    if (__DEV__) {
      const contextWithoutWarnings = { ...context };

      Object.keys(context).forEach(key => {
        warnAboutGettingProperty(
          context,
          key,
          `You should not be using this.context.router.${key} directly. It is private API ` +
            "for internal use only and is subject to change at any time. Instead, use " +
            "a <Route> or withRouter() to access the current location, match, etc."
        );
      });

      context._withoutWarnings = contextWithoutWarnings;
    }

    return {
      router: context
    };
  };
}
複製程式碼

我: 所以,重點就是在這個元件裡面了。元件裡面就是一些生命週期函式
我: constructor、componentDidMount
我: 這倆,是初始化的地方

嗯嗯

我: 一個一個看
我: 重點是那個判斷

if (!props.staticContext) {
  this.unlisten = props.history.listen(location => {
    if (this._isMounted) {
      this.setState({ location });
    } else {
      this._pendingLocation = location;
    }
  });
}
複製程式碼

我: if (!props.staticContext) {}的作用,是保證Router裡面再巢狀Router時,使用的是相同的history
我: 裡面是一個監聽,監聽history中的location的改變,也就是說,當通過這個history改變路徑時,會統一監聽,統一處理

嗯嗯

我: 那裡面就呼叫了setState了唄,接著render就執行了

我: render非常簡單,就是把context的value值,修改了一下

嗯啊

我: 我們知道,只要context的value一變化,對應的consumer的函式,就會被呼叫,是吧

嗯嗯

我: 那現在Router就結束了
我: 接下來,我們好奇的是,哪些元件使用了Consumer

找route

我: 對。根據React-router的使用,估計就是每個<Route>,都會監聽這個context,然後進行路徑匹配,決定是否要渲染自己的component屬性所指定的內容
我: 接下來,我們就可以繼續看這個元件了。先吃飯去吧,<Route>解讀,且聽下回分解。

嗯,好的。拜拜。