深入淺出解析 React Router 源碼

最近組裏有同學做了 React Router 源碼相關的分享,我感覺這是個不錯的選題, React Router 源碼簡練好讀,是個切入前端路由原理的好角度。在分享學習的過程中,自己對前端路由也產生了一些思考和見解,所以寫就本文,和大家分享我對前端路由的理解。

本文會先用原生 JavaScript 實現一個基本的前端路由,再介紹 React Router 的源碼實現,通過比較二者的實現方式,分析 React Router 實現的動機和優點。閱讀完本文,讀者們應該能瞭解:

  1. 前端路由的基本原理

  2. React Router 的實現原理

  3. React Router 的啓發和借鑑

公衆號

一. 我們應該如何實現一個前端路由

一開始,我們先跳出 React Router,思考如何用原生 JavaScript 實現一個的前端路由,所謂前端路由,我們無非要實現兩個功能:監聽記錄路由變化,匹配路由變化並渲染內容。以這兩點需求作爲基本脈絡,我們就能大致勾勒出前端路由的形狀。

路由示例:

1.Hash 實現

我們都知道,前端路由一般提供兩種匹配模式, hash  模式和  history  模式,二者的主要差別在於對 URL 監聽部分的不同,hash 模式監聽 URL 的 hash 部分,也就是 # 號後面部分的變化,對於 hash 的監聽,瀏覽器提供了 onHashChange  事件幫助我們直接監聽 hash 的變化:

<body>
    <a href="#/home">Home</a>
    <a href="#/user">User</a>
    <a href="#/about">About</a>
    <div id="view"></div>
</body>

<script>
    // onHashChange事件回調, 匹配路由的改變並渲染對應內容
    function onHashChange() {
        const view = document.getElementById('view')
        switch (location.hash) {
          case '#/home':
              view.innerHTML = 'Home';
              break;
          case '#/user':
              view.innerHTML = 'User';
              break;
          case '#/about':
              view.innerHTML = 'About';
              break;
        }
    }

    // 綁定hash變化事件,監聽路由變化
    window.addEventListener('hashchange', onHashChange);
</script>

hash 模式的實現比較簡單,我們通過 hashChange 事件就能直接監聽到路由 hash 的變化,並根據匹配到的 hash 的不同來渲染不同的內容。

2.History 實現

相較於 hash 實現的簡單直接,history 模式的實現需要我們稍微多寫幾行代碼,我們先修改一下 a 標籤的跳轉鏈接,畢竟 history 模式相較於 hash 最直接的區別就是跳轉的路由不帶 # 號,所以我們嘗試直接拿掉 #號:

<body>
    <a href="/home">Home</a>
    <a href="/user">User</a>
    <a href="/about">About</a>
    <div id="view"></div>
</body>

點擊 a 標籤,會看到頁面發生跳轉,並提示找不到跳轉頁面,這也是意料之中的行爲,因爲 a 標籤的默認行爲就是跳轉頁面,我們在跳轉的路徑下沒有對應的網頁文件,就會提示錯誤。那麼對於這種非 hash 的路由變化,我們應該怎麼處理呢?大體上,我們可以通過以下三步來實現 history 模式下的路由:

    1. 攔截a標籤 的點擊事件,阻止它的默認跳轉行爲

    2. 使用 H5 的 history API 更新 URL

    3. 監聽和匹配路由改變以更新頁面

在開始寫代碼之前,我們有必要先了解一下 H5 的幾個 history API 的基本用法。其實 window.history  這個全局對象在 HTML4 的時代就已經存在,只不過那時我們只能調用 back()go()等幾個方法來操作瀏覽器的前進後退等基礎行爲,而 H5 新引入的 pushState()和 replaceState()及 popstate事件 ,能夠讓我們在不刷新頁面的前提下,修改 URL,並監聽到 URL 的變化,爲 history 路由的實現提供了基礎能力。

// 幾個 H5 history API 的用法

History.pushState(state, title [, url])
// 往歷史堆棧的頂部添加一個狀態,方法接收三個參數:一個狀態對象, 一個標題, 和一個(可選的)URL
// 簡單來說,pushState能更新當前 url,並且不引起頁面刷新

History.replaceState(stateObj, title[, url]);
// 修改當前歷史記錄實體,方法入參同上
// 用法和 pushState類似,區別在於 pushState 是往頁面棧頂新增一個記錄,而 replaceState 則是修改當前記錄

window.onpopstate
// 當活動歷史記錄條目更改時,將觸發popstate事件
// 需要注意的是,pushState 和 replaceState 對 url 的修改都不會觸發onpopstate,它只會在瀏覽器某些行爲下觸發, 比如點擊後退、前進按鈕、a標籤點擊等

詳細的參數介紹和用法讀者們可以進一步查閱 MDN,這裏只介紹和路由實現相關的要點以及基本用法。瞭解了這幾個 API 以後,我們就能按照我們上面的三步來實現我們的 history 路由:

<body>
    <a href="/home">Home</a>
    <a href="/user">User</a>
    <a href="/about">About</a>
    <div id="view"></div>
</body>

<script>
    // 重寫所有 a 標籤事件
    const elements = document.querySelectorAll('a[href]')
    elements.forEach(el => el.addEventListener('click'(e) ={
      e.preventDefault()    // 阻止默認點擊事件
      const test = el.getAttribute('href')
      history.pushState(null, null, el.getAttribute('href'))     
      // 修改當前url(前兩個參數分別是 state 和 title,這裏暫時不需要用到
      onPopState()          
      // 由於pushState不會觸發onpopstate事件, 所以我們需要手動觸發事件
    }))
    
    // onpopstate事件回調, 匹配路由的改變並渲染對應內容, 和 hash 模式基本相同
    function onPopState() {
        const view = document.querySelector('#view')
        switch (location.pathname) {
          case '/home':
              view.innerHTML = 'Home';
              break;
          case '/user':
              view.innerHTML = 'User';
              break;
          case '/about':
              view.innerHTML = 'About';
              break;
        }
    }

    // 綁定onpopstate事件, 當頁面路由發生更改時(如前進後退),將觸發popstate事件
    window.addEventListener('popstate', onPopState);
</script>

Tips:history 模式的代碼無法通過直接打開 html 文件的形式在本地運行,在切換路由的時候,將會提示:

Uncaught SecurityError: A history state object with URL 'file://xxx.html' cannot be created in a document with origin 'null'.

這是由於 pushState 的 url 必須與當前的 url 同源,而 file:// 形式打開的頁面沒有 origin ,導致報錯。如果想正常運行體驗,可以使用http-server爲文件啓動一個本地服務。

History 模式的實現代碼也比較簡單,我們通過重寫 a 標籤的點擊事件,阻止了默認的頁面跳轉行爲,並通過 history API 無刷新地改變 url,最後渲染對應路由的內容。到這裏,我們基本上了解了hashhistory 兩種前端路由模式的區別和實現原理,總的來說,兩者實現的原理雖然不同,但目標基本一致,都是在不刷新頁面的前提下,監聽匹配路由的變化,並根據路由匹配渲染頁面內容。既然我們能夠如此簡單地實現前端路由,那麼 React Router 的優勢又體現在哪,它的實現能給我們帶來哪些啓發和借鑑呢。

二. React Router 用法回顧

在分析源碼之前,我們先來回顧一下 React Router 的基本用法,從用法中分析一個前端路由庫的基本設計和需求。只有先把握作爲上游的需求和設計,才能清晰和全面地解析作爲下游的源碼。

React Router 的組件通常分爲三種:

import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
// HashRouter 和 BrowserRouter 二者的使用方法幾乎沒有差別,這裏只演示其一

const App = () ={
  return (
    <BrowserRouter>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/user">User</Link>

        <Switch>
            <Route path="/about"><About /></Route>
            <Route path="/user"> <User /></Route>
            <Route path="/"><Home /></Route>
        </Switch>
    </BrowserRouter>
  );
}

const Home = () =(<h2>Home</h2>);
const About = () =(<h2>About</h2>);
const User = () =(<h2>User</h2>);

export default App;

我們使用  React Router 重新實現了一遍開頭原生路由的功能,二者既有對應,也有差別。<Link>  對應 a標籤,實現跳轉路由的功能; <Route>對應 onPopState() 中的渲染邏輯,匹配路由並渲染對應組件;而<BrowserRouter> 對應 addEventListener 對路由變化的監聽。

下面我們就進入 React Router 的源碼,去一探這些組件的實現。

三. React Router 源碼實現

1. 目錄概覽

React Router 的代碼主要存在於 packages 文件夾下,在 v4 版本後,React Router 就分爲了四個包來發布,本文解析的部分主要位於 react-router 和  react-router-dom 文件夾。

├── packages
    ├── react-router    // 核心、公用代碼
    ├── react-router-config   // 路由配置
    ├── react-router-dom   // 瀏覽器環境路由
    └── react-router-native   // React Native 路由

2.BrowserRouter 和 HashRouter

<BrowserRouter><HashRouter> 都是路由容器組件,所有的路由組件都必須被包裹在這兩個組件中才能使用:

const App = () ={
  return (
    <BrowserRouter>
        <Route path="/" component={Home}></Route>
    </BrowserRouter>
  );
}

爲什麼會有這樣的用法,其實我們在看過這兩者的實現後就會理解:

// <BrowserRouter> 源碼

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

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

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

export default BrowserRouter;
// <HashRouter> 源碼

import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";

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

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

export default HashRouter;

我們會發現這二者就是一個殼,兩者的代碼量很少,代碼也幾乎一致,都是創建了一個 history對象,然後將其和子組件一起透傳給了**<Router>**,二者區別只在於引入的 createHistory() 不同。因此對於這二者的解析,其實是對 **<Router>** 和 history 庫的解析。

history 庫

history 源碼倉庫: https://github.com/ReactTraining/history

先來看 history 庫,這裏的 history 並非 H5 的 history 對象,而是一個有着 7k+ star 的會話歷史管理庫,是 React Router 的核心依賴。本小節我們來看 history 庫的用法,以及瞭解爲什麼 React Router 要選擇 history 來管理會話歷史。

在看具體用法之前,我們先思考一下我們的 "會話歷史管理" 的需求。所謂會話歷史管理,我們很容易想到維護一個頁面訪問歷史棧,跳轉頁面的時候 push 一個歷史,後退 pop 一個歷史即可。不過我們通過第一節對 hashhistory 路由的原生實現就能明白,不同路由模式之間,操作會話歷史的 API 不同、監聽會話歷史的方式也不同,而且前端路由並不只有這兩種模式,React Router 還提供了 memory 模式 static 模式,分別用於 RN 開發和 SSR。

所以我們希望在中間加一層抽象,來屏蔽幾種模式之間操作會話歷史的差別,而不是將這些差別和判斷帶進 React Router 的代碼中。

history 使您可以在任何運行 JavaScript 的地方輕鬆管理會話歷史記錄。一個 history 對象可以抽象出各種環境中的差異,並提供一個最小的 API,使您可以管理歷史記錄堆棧,導航和在會話之間保持狀態。

這是 history 文檔的第一句,很好地概括了 history 的作用、優勢和使用範圍,直接來看 API:

import { createBrowserHistory } from 'history';
// 創建history實例
const history = createBrowserHistory();
// 獲取當前 location 對象,類似 window.location
const location = history.location;
// 設置監聽事件回調,回調接收兩個參數 location 和 action
const unlisten = history.listen((location, action) ={   
  console.log(location.pathname, location.state);
});

// 可以使用 push 、replace、go 等方法來修改會話歷史
history.push('/home'{ some: 'state' });                 
// 如果要停止監聽,調用listen()返回的函數.
unlisten();

API 簡潔好懂,就不再贅述了。出於篇幅的考慮,本小節只介紹 history 庫部分用法,其實現原理放到末尾番外篇,好讓讀者先專注瞭解 React Router 的實現。

Router 的實現

我們已經知道,<BrowserRouter><HashRouter> 本質上都是 <Router>,只是二者引入的 createHistory() 方法不同。<Router> 的代碼在 react-router 這個包裏,是一個相對公共的組件,其他包的 <Router> 都引自這裏:

// 這個 RouterContext 並不是原生的 React Context, 由於React16和15的Context互不兼容, 所以React Router使用了一個第三方的 context 以同時兼容 React 16 和 15
// 這個 context 基於 mini-create-react-context 實現, 這個庫也是React context的Polyfil, 所以可以直接認爲二者用法相同
import RouterContext from "./RouterContext";
import React from 'react';

class Router extends React.Component {
  // 該方法用於生成根路徑的 match 對象
  // 第一次看這個 match 對象可能有點懵逼, 其實後面看到 <Route> 實現的時候就能理解 match 對象的用處, 這個對象是提供給<Route>判斷當前匹配頁面的
  static computeRootMatch(pathname) {
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    // 從傳入的 history 實例中取了 location 對象存到了state裏, 後面會通過setState更改location來觸發重新渲染
    // location 對象包含 hash/pathname/search/state 等屬性, 其實就是當前的路由信息
    this.state = {
      location: props.history.location
    };
    // isMounted 和 pendingLocation 這兩個私有變量可能讓讀者有點迷惑, 源碼其實也在這進行了一整段註釋說明, 解釋爲什麼在 constructor 而不是 componentDidMount 中去監聽路由變化
    // 簡單來說, 由於子組件會比父組件更早完成掛載, 如果在 componentDidMount 進行監聽, 則有可能在監聽事件註冊之前 history.location 已經發生改變, 因此我們需要在 constructor 中就註冊監聽事件, 並把改變的 location 記錄下來, 等到組件掛載完了以後, 再更新到 state 上去
    // 其實如果去掉這部分的hack, 這裏只是簡單地設置了路由監聽, 並在路由改變的時候更新 state 中的路由信息
    // 判斷組件是否已經掛載, componentDidMount階段會賦值爲true
    this._isMounted = false;
    // 儲存在構造函數執行階段發生改變的location
    this._pendingLocation = null;

    // 判斷是否處於服務端渲染 (staticContext 是 staticRouter 傳入<Router>的屬性, 用於服務端渲染)
    if (!props.staticContext) {
      // 使用 history.listen() 添加路由監聽事件
      this.unlisten = props.history.listen(location ={
        if (this._isMounted) {
          // 如果組件已經掛載, 直接更新 state 的 location
          this.setState({ location });
        } else {
          // 如果組件未掛載, 就先把 location 存起來, 等到 didmount 階段再 setState
          this._pendingLocation = location;
        }
      });
    }
  }

  // 對應構造函數里的處理, 將 _isMounted 置爲 true, 並使用 setState 更新 location
  componentDidMount() {
    this._isMounted = true;

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

  // 組件被卸載時, 同步解綁路由的監聽
  componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  }

  render() {
    return (
      // Provider將value向下傳遞給組件樹上的組件
      <RouterContext.Provider
        // 透傳子組件
        children={this.props.children || null}
        value={{
          // context 傳遞的值, <Router>下的組件樹就能通過 this.context.xxx 這樣的方式取得這裏的值
          // 透傳 history 對象
          history: this.props.history,
          // 當前路由信息
          location: this.state.location,
          // 是否爲根路徑
          match: Router.computeRootMatch(this.state.location.pathname),
          // 服務端渲染用到的 staticContext
          staticContext: this.props.staticContext
        }}
      />
    );
  }
}

export default Router

代碼看起來不少,但如果刨除當中各種判斷場景的代碼,其實 <Router> 只做了兩件事,一是給子組件包了一層context,讓路由信息( history 和 location 對象)能傳遞給其下所有子孫組件;二是綁定了路由監聽事件,使每次路由的改變都觸發setState

其實看到這我們就能明白,爲什麼 <Route> 等路由組件要求被包裹在 <BrowerRouter> 等路由器容器組件內才能使用,因爲路由信息都由外層的容器組件通過 context 的方式,傳遞給所有子孫組件,子孫組件在拿到當前路由信息後,才能匹配並渲染出對應內容。此外在路由發生改變的時候,容器組件<Router> 會通過 setState() 的方式,觸發子組件重新渲染。

本章小結

在看完了 <Router> 的實現後,我們來和原生實現做一個比較,我們之前提到,前端路由主要的兩個點是監聽和匹配路由的變化,而<Router> 就是幫我們完成了監聽這一步。在原生實現中,我們分別實現了 hash 模式和 history 模式的監聽,又是綁定事件,又是劫持 a 標籤的點擊,而在 React Router 中,這一步由 history 庫來完成,代碼內調用了history.listen 就完成了對幾種模式路由的監聽。

Route 的實現

我們前面提到,前端路由的核心在於監聽和匹配,上面我們使用 <Router> 實現了監聽,那麼本小節就來分析 <Route> 是如何做匹配的,同樣地我們先回顧 <Route> 的用法:

匹配模式:

// 精確匹配
// 嚴格匹配
// 大小寫敏感
<Route path="/user" exact component={User} />
<Route path="/user" strict component={User} />
<Route path="/user" sensitive component={User} />

路徑 path 寫法:

// 字符串形式
// 命名參數
// 數組形式
<Route path="/user" component={User} />
<Route path="/user/:userId" component={User} />
<Route path={["/users""/profile"]} component={User} />

渲染方式:

// 通過子組件渲染
// 通過 props.component 渲染
// 通過 props.render 渲染
<Route path='/home'><Home /></Route>
<Route path='/home' component={Home}></Route>
<Route path='/home' render={() => <p>home</p>}></Route>

// 例子: 這裏最終的渲染結果是User, 優先級是子組件 > component > render
<Route path='/home' component={Home} render={() => <p>About</p>}>
  <User />
</Route>

<Route> 所做的事情也很簡單,匹配到傳入的 path,渲染對應的組件。此外 <Route> 還提供了幾種不同的匹配模式、path 寫法以及渲染方式,<Route> 的源碼實現,和這些配置項有着緊密的聯繫:

import React from "react";
import RouterContext from "./RouterContext";
import matchPath from "../utils/matchPath.js";

function isEmptyChildren(children) {
  return React.Children.count(children) === 0;
}

class Route extends React.Component {
  render() {
    return (
      {/* Consumer 接收了 <Router> 傳下來的 context, 包含了history對象, location(當前路由信息), match(匹配對象)等信息 */}
      <RouterContext.Consumer>
      {/* 拿到路由信息
        拿到 match 對象(來源優先級:Switch → props.path → context)
        props.computedMatch 是 <Switch> 傳下來的, 是已經計算好的match, 優先級最高
        <Route> 組件上的 path 屬性, 優先級第二
        計算 match 對象, 下一小節會詳解這個 matchPath
        context 上的 match 對象
        把當前的 location 和 match 拼成新的 props,這個 props 會通過 Provider 繼續向下傳
        <Route>組件提供的三種渲染方式, 優先級 children > component > render
        這裏對children爲空的情況做了一個兼容, 統一賦爲null, 這是因爲 Preact 默認使用空數組來表示沒有children的情況 (Preact是一個3kb的React替代庫, 挺有趣的, 讀者們可以看看)
      */}
        {context ={
          const location = this.props.location || context.location;
        
          const match = this.props.computedMatch
            ? this.props.computedMatch  
            : this.props.path           
            ? matchPath(location.pathname, this.props)  
            : context.match;

          
          const props = { ...context, location, match };
          let { children, component, render } = this.props;         
          if (Array.isArray(children) && isEmptyChildren(children)) { 
            children = null;
          }

          // 把拼好的新的props通過context繼續往下傳
          // 第一層判斷: 如果有 match 對象, 就渲染子組件 children 或 Component
          // 第二層判斷: 如果有子組件 children, 就渲染 children, 沒有就渲染 component
          // 第三層判斷: 如果子組件 children 是函數, 那就先執行函數, 並將路由信息 props 作爲回調參數
          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 的實現相對簡單,代碼分爲兩部分:獲取 match 對象和渲染組件。我們在代碼中會看到多次 match 對象,這個 match 對象其實是由根組件的 computedMatch()matchPath() 生成,包含了當前匹配信息。對於這個 match 對象的生成過程,我們放到下一小節,這裏我們只需要知道,如果當前 Route 匹配了路由,那麼會生成對應 match 對象,如果沒有匹配,match 對象爲 null

// match 對象實例
{
  isExact: true,
  params: {},
  path: "/",
  url: "/"
}

第二部分是 <Route> 組件的渲染邏輯,這部分代碼還是得從 <Route> 的行爲去理解,Route 提供了三種渲染方式:子組件、props.componentprops.render,三者之間又存在優先級,因此就形成了我們看到了多層三元表達式渲染的結構。

這部分渲染邏輯不用細看,參照下邊的樹狀圖理解即可,代碼用了四層三元表達式的嵌套,來實現 子組件> component屬性傳入的組件 > children是函數 這樣的優先級渲染。

紅色節點是最終渲染結果:

matchPath

如果讓我們去實現路由匹配,我們會怎麼去做呢?全等比較?正則判斷?反正看起來應該是很簡單的一個實現,但如果我們打開matchPath()的代碼,卻會發現它用了 60 行代碼、引了一個第三方庫來做這件事情:

import pathToRegexp from "path-to-regexp";

// 這裏建議先直接看 matchPath() 的代碼, 調用到的時候再看 compilePath
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

// compilePath 的作用是根據路由路徑path 和匹配參數options等參數拼出正則regexp,和路徑參數keys 是路徑參數
function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];                                  
  // keys是個空數組, pathToRegexp會將在path中解析到的參數追加到keys裏
  const regexp = pathToRegexp(path, keys, options); 
  // 由pathToRegexp拼出正則, pathToRegexp是個將字符串路徑轉換爲正則表達式的工具, 用法比較簡單, 讀者可以自己查查用法
  const result = { regexp, keys };                  
  // 返回結果: 正則regexp, 路徑裏解析到的參數keys

  console.log('cacheCount', cacheCount);
  console.log('cacheLimit', cacheLimit);
  console.log('pathCache', pathCache);;
  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }

  return result;
}

function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {      
  // 正常情況下, options 是 <Route> 的 props, 是個對象; 這裏判斷, 是爲了兼容 `react-router-redux`庫中某個調用傳入的 options 只有 path
    options = { path: options };
  }

  const { path, exact = false, strict = false, sensitive = false } = options;   
  // 取出路由路徑 path 和匹配參數 exact 等, 並賦初值

  const paths = [].concat(path);                    
  // 統一 path 類型 (path可能是數組形式['/''/user']或字符串形式"/user")

  return paths.reduce((matched, path) ={          
  // 這裏用 reduce, 其實是爲了在遍歷路徑集合 paths 的同時, 只輸出一個結果, 如果用 map之類的 api 做循環, 會得到一個數組
    if (!path && path !== "") return null;          
    // 沒有 path, 返回 null
    if (matched) return matched;                    
    // 已經匹配到了, 就返回上次匹配結果

    const { regexp, keys } = compilePath(path, {    
    // 根據路由路徑 path 和匹配參數 exact 等參數拼出正則 regexp, keys 是路徑參數(比如/user:id的id)
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);            
    // 調用正則原型方法exec, 返回一個結果數組或null

    if (!match) return null;                        
    // 沒匹配到, 返回 null

    const [url, ...values] = match;                 
    // 從結果數組裏
    const isExact = pathname === url;               
    // 是否準確匹配

    if (exact && !isExact) return null;             
    // 要求準確匹配卻沒有全等匹配到, 返回 null

    //  這裏給幾個例子, 幫助大家直觀理解這個調用過程
    //  傳入的path:    /user
    //  regexp:       /^\/user\/?(?=\/|$)/i
    //  url:          /user
    //  返回結果:       {"path":"/user","url":"/user","isExact":true,"params":{}}

    //  例子2
    //  傳入的path:    /user/:id
    //  regexp:       /^\/user\/(?:([^\/]+?))\/?(?=\/|$)/i
    //  url:          /user/1
    //  返回結果:       {"path":"/user/:id","url":"/user/1","isExact":true,"params":{"id":"1"}}

    return {
      path,                                           
      // 用於匹配的 path
      url: path === "/" && url === "" ? "/" : url,    
      // url 匹配的部分
      isExact,                                        
      // 是否準確匹配
      params: keys.reduce((memo, key, index) ={     
      // 把 path-to-regexp 直接返回的路由參數 keys 做一次格式轉換
        memo[key.name] = values[index];
        return memo;
      }{})
    };
  }, null);
}

export default matchPath;

小結

本小節我們通過對 <Route> 和 mathPath 源碼的解析,講解 React Router 實現匹配和渲染的過程,匹配路由這部分的工作由 mathPath 通過 path-to-regexp進行,<Route> 其實相當於一個高階組件,以不同的優先級和匹配模式渲染匹配到的子組件。

尾聲

到這裏,我們基本完成了對 React Router 的主要組件源碼解析,最後回顧一下整體的實現:

整體而言,React Router 的源碼相對簡單清晰,源碼中所體現的前端路由的設計實現,也相信會對讀者們有所啓發借鑑。雖然本文對 React Router 源碼的解析就到此爲止, 但有關前端路由以及 React Router 的探索不會停止,怎樣從源碼到落地,怎樣爲項目做路由選型,怎樣設計一個合理的前端路由系統... 對於前端路由, 我們需要挖掘的東西還有很多, 源碼解析只是在這條道路路上邁出了一小步。在當下這波前端技術的滔滔浪潮中,前端路由,也還會在前端 er 的不斷迭代中, 繼續摸索和前進, 在更廣闊的場景上, 去發揮它的價值。

由於時間緊張, 本文成文比較匆忙,潦草之處,敬請諒解,以下有些坑還沒來得及填, 算是留給讀者們的思考題了~

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AJeqXJsKeYjlFz4E8PCpSA