react

React Router原理

history是一個獨立的第三方js庫,可以用來相容在不同瀏覽器、不同環境下對歷史記錄的管理,擁有統一的API...

>

history 是一個獨立的第三方js庫,可以用來相容在不同瀏覽器、不同環境下對歷史記錄的管理,擁有統一的 API 。具體來說裏面的 history 分為三類

  • 老瀏覽器的 history : 主要通過 hash 來實現,對應 createHashHistory
  • 高版本瀏覽器: 通過 html5 裏面的 history ,對應 createBrowserHistory
  • node 環境下: 主要儲存在 memeory 裏面,對應 createMemoryHistory

上面針對不同的環境提供了三個 API ,但是三個 API 有一些共性的操作,將其抽象了一個公共的檔案 createHistory

// 內部的抽象實現
function createHistory(options={}) {
  ...
  return {
    listenBefore, // 內部的hook機制,可以在location發生變化前執行某些行為,AOP的實現
    listen, // location發生改變時觸發回撥
    transitionTo, // 執行location的改變
    push, // 改變location
    replace,
    go,
    goBack,
    goForward,
    createKey, // 建立location的key,用於唯一標示該location,是隨機生成的
    createPath,
    createHref,
    createLocation, // 建立location
  }
}

上述這些方式是 history 內部最基礎的方法, createHashHistorycreateBrowserHistorycreateMemoryHistory 只是覆蓋其中的某些方法而已。其中需要注意的是,此時的 location 跟瀏覽器原生的 location 是不相同的,最大的區別就在於裏面多了 key 欄位, history 內部通過 key 來進行 location 的操作

function createLocation() {
  return {
    pathname, // url的基本路徑
    search, // 查詢欄位
    hash, // url中的hash值
    state, // url對應的state欄位
    action, // 分為 push、replace、pop三種
    key // 生成方法為: Math.random().toString(36).substr(2, length)
  }
}

1.2 內部解析

三個 API 的大致的技術實現如下

  • createBrowserHistory : 利用 HTML5 裏面的 history
  • createHashHistory : 通過 hash 來儲存在不同狀態下的 history 資訊
  • createMemoryHistory : 在記憶體中進行歷史記錄的儲存`

1.2.1 執行URL前進

  • createBrowserHistory : pushStatereplaceState
  • createHashHistory : location.hash=*** location.replace()
  • createMemoryHistory : 在記憶體中進行歷史記錄的儲存
// 虛擬碼

// createBrowserHistory(HTML5)中的前進實現
function finishTransition(location) {
  ...
  const historyState = { key };
  ...
  if (location.action === 'PUSH') ) {
    window.history.pushState(historyState, null, path);
  } else {
    window.history.replaceState(historyState, null, path)
  }
}
// createHashHistory的內部實現
function finishTransition(location) {
  ...
  if (location.action === 'PUSH') ) {
    window.location.hash = path;
  } else {
    window.location.replace(
    window.location.pathname + window.location.search + '#' + path
  );
  }
}
// createMemoryHistory的內部實現
entries = [];
function finishTransition(location) {
  ...
  switch (location.action) {
    case 'PUSH':
      entries.push(location);
      break;
    case 'REPLACE':
      entries[current] = location;
      break;
}

1.2.2 檢測URL回退

  • createBrowserHistory : popstate
  • createHashHistory : hashchange
  • createMemoryHistory :因為是在記憶體中操作,跟瀏覽器沒有關係,不涉及 UI 層面的事情,所以可以直接進行歷史資訊的回退
// 虛擬碼

// createBrowserHistory(HTML5)中的後退檢測
function startPopStateListener({ transitionTo }) {
  function popStateListener(event) {
    ...
    transitionTo( getCurrentLocation(event.state) );
  }
  addEventListener(window, 'popstate', popStateListener);
  ...
}
 
// createHashHistory的後退檢測
function startPopStateListener({ transitionTo }) {
  function hashChangeListener(event) {
    ...
    transitionTo( getCurrentLocation(event.state) );
  }
  addEventListener(window, 'hashchange', hashChangeListener);
  ...
}
// createMemoryHistory的內部實現
function go(n) {
  if (n) {
    ...
    current += n;
  const currentLocation = getCurrentLocation();
  // change action to POP
  history.transitionTo({ ...currentLocation, action: POP });
  }
}

1.2.3 state的儲存

爲了維護 state 的狀態,將其儲存在 sessionStorage 裏面:

// createBrowserHistory/createHashHistory中state的儲存
function saveState(key, state) {
  ...
  window.sessionStorage.setItem(createKey(key), JSON.stringify(state));
}
function readState(key) {
  ...
  json = window.sessionStorage.getItem(createKey(key));
  return JSON.parse(json);
}
// createMemoryHistory僅僅在記憶體中,所以操作比較簡單
const storage = createStateStorage(entries); // storage = {entry.key: entry.state}
 
function saveState(key, state) {
  storage[key] = state
}
function readState(key) {
  return storage[key]
}

1.3 小結

路由原理

前端路由實現起來其實很簡單,本質就是監聽 URL 的變化,然後匹配路由規則,顯示相應的頁面,並且無須重新整理。目前單頁面使用的路由就只有兩種實現方式

hash
history

www.test.com/##/ 就是 Hash URL ,當 ## 後面的雜湊值發生變化時,不會向伺服器請求資料,可以通過 hashchange 事件來監聽到 URL 的變化,從而進行跳轉頁面。

img

History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美觀

img

二、react-router的基本原理

實現 URLUI 介面的同步。其中在 react-router 中, URL 對應 Location 物件,而 UI 是由 react components 來決定的,這樣就轉變成 locationcomponents 之間的同步問題

img

2.1 優點

  • React 融為一體,專為 react 量身打造,編碼風格與 react 保持一致,例如路由的配置可以通過 component 來實現
  • 不需要手工維護路由 state ,使程式碼變得簡單
  • 強大的路由管理機制,體現在如下方面
    • 路由配置: 可以通過元件、配置物件來進行路由的配置
    • 路由切換: 可以通過 <Link> Redirect 進行路由的切換
    • 路由載入: 可以同步記載,也可以非同步載入,這樣就可以實現按需載入
  • 使用方式: 不僅可以在瀏覽器端的使用,而且可以在伺服器端的使用

2.2 react-router具體實現

react-routerhistory 庫的基礎上,實現了 URLUI 的同步,分為兩個層次來描述具體的實現。

元件層面描述實現過程

react-router 中最主要的 componentRouter RouterContext Linkhistory 庫起到了中間橋樑的作用

img

browserHistory (一種 history 型別:一個 history 知道如何去監聽瀏覽器位址列的變化, 並解析這個 URL 轉化為 location 物件)為例 :

  • browserHistory 進行路由 state 管理,主要通過 sessionStorage
//儲存 路由state(router state)
function saveState(key, state) {
  ...
  window.sessionStorage.setItem(createKey(key), JSON.stringify(state));
}
//讀取路由state
function readState(key) {
  ...
  json = window.sessionStorage.getItem(createKey(key));
  return JSON.parse(json);
}

其中 saveState 函式傳進來的 state 是個 json 物件,如:

{route: '/about'} ///假設此時的location為'/about'

進行路由匹配,最終渲染對應的元件

const About = React.createClass({/*...*/}) //About 元件
const Inbox = React.createClass({/*...*/}) //Inbox 元件
const Home = React.createClass({/*...*/}) //Home元件
 render() {
    let Child
    switch (this.state.route) {
      case '/about': Child = About; break;
      case '/inbox': Child = Inbox; break;
      default:      Child = Home;
    }

    return (
      <div>
        <h1>App</h1>
        <ul>
          <li><a href="#/about">About</a></li>
          <li><a href="#/inbox">Inbox</a></li>
        </ul>
        <Child/>
      </div>
    )
  }
})

React.render(<App />, document.body)

API層面描述實現過程

爲了簡單說明,只描述使用 browserHistory 的實現, hashHistory 的實現過程是類似的,就不在說明

img

2.3 使用者點選了Link元件後路由系統中到底發生了哪些變化

img

Link 元件最終會渲染為 HTML 標籤 <a> ,它的 toqueryhash 屬性會被組合在一起並渲染為 href 屬性。雖然 Link 被渲染為超鏈接,但在內部實現上使用指令碼攔截了瀏覽器的預設行為,然後呼叫了 history.pushState 方法

  • 系統會將上述 location 物件作為引數傳入到 TransitionTo 方法中,然後呼叫 window.location.hash 或者 window.history.pushState() 修改了應用的 URL ,這取決於你建立 history 物件的方式。同時會觸發 history.listen 中註冊的事件監聽器。
  • 接下來請看路由系統內部是如何修改 UI 的。在得到了新的 location 物件後,系統內部的 matchRoutes 方法會匹配出 Route 元件樹中與當前 location 物件匹配的一個子集,並且得到了 nextState ,具體的匹配演算法不在這裏講解,感興趣的同學可以點選檢視, state的結構如下
nextState = {
  location, // 當前的 location 物件
  routes, // 與 location 物件匹配的 Route 樹的子集,是一個數組
  params, // 傳入的 param,即 URL 中的引數
  components, // routes 中每個元素對應的元件,同樣是陣列
};

Router 元件的 componentWillMount 生命週期方法中呼叫了 history.listen(listener) 方法。 listener 會在上述 matchRoutes 方法執行成功後執行 listener(nextState)nextState 物件每個屬性的具體含義已經在上述程式碼中註釋,接下來執行 this.setState(nextState) 就可以實現重新渲染 Router 元件。舉個簡單的例子,當 URL(準確的說應該是 location.pathname ) 為 /archives/posts 時,應用的匹配結果如下圖所示

img

到這裏,系統已經完成了當用戶點選一個由 Link 元件渲染出的超鏈接到頁面重新整理的全過程

Facebook Profile photo
Written by Nat
This is the author box. A short description about the author of this article. Could be their website link, what they like to read about and so on. Profile