深入淺出解析 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,最後渲染對應路由的內容。到這裏,我們基本上了解了hash 和history 兩種前端路由模式的區別和實現原理,總的來說,兩者實現的原理雖然不同,但目標基本一致,都是在不刷新頁面的前提下,監聽和匹配路由的變化,並根據路由匹配渲染頁面內容。既然我們能夠如此簡單地實現前端路由,那麼 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 一個歷史即可。不過我們通過第一節對 hash 和 history 路由的原生實現就能明白,不同路由模式之間,操作會話歷史的 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.component、props.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