React 路由狀態管理總結

一、依賴(Dependencies)

在一般 SPA 開發中,路由的管理十分重要。作為 React 技術體系中的一部分,官方維護的 React-Router 則是首選的路由庫。

在應用 Redux 模式後,React-Router 與 Redux 的配合引發了新的問題,是否需要將路由納入 store 進行管理?如何將路由納入 store 進行管理?這些都是需要考慮的問題。我們將在後文討論第一個問題,而為了解決上述第二個問題,React-Router-Redux 這個輕量級的擴展庫應運而生並得到廣泛應用。

另外需要說明的是,長久以來 React-Router 與 React-Router-Redux 是兩個獨立的庫,但在 React-Router 4.x 版本以後,React-Router-Redux 已經成為了 React-Router 的一部分。

本文並不旨在介紹兩種依賴庫的具體用法(具體用法請參考官方文檔和教程),而主要闡述其實現方式和原理,總結具體的實踐方式和注意事項。在主要內容之前,首先簡要介紹下兩個庫的功能:

  1. React-Router

    React-Router 做的最重要的事就是將瀏覽器 URL 與程序聯繫起來(藉助 history 庫),它為 React 提供了聲明式的路由系統,通過其提供的導航組件,我們能夠方便地使用 URL 來控制狀態的變化和組件的切換。

  2. React-Router-Redux

    按照官方的說法,其實現了「deep integration of react-router and redux」,即 React-Router 與 Redux 的深度集成,它將路由完全納入 store 中進行管理,使 store 成為了 URL(或者說是 history)的數據來源,也使我們能夠通過 dispatch action 的方式來修改 URL。我們將在後文介紹它的實現原理。

二、實踐

路由狀態並非一定要介入 Redux 架構中。在一些簡單的應用場景下,只需要使用 React-Router 提供的聲明式組件(Router, Route, Link 等)即可方便的實現 URL 導航。在一些稍複雜的場景中,只要保證遵循 React 單向數據流動方式,遵照使用方法,也可以完成進行路由信息的讀取和觸發變更,其過程如下圖所示。(使用方法請參照 React-Router 文檔和教程)

React 路由狀態管理總結

但在這裡,我們主要討論將路由狀態納入 Redux 架構中的情況。本部分的下文將分為兩部分:

  1. 手動管理,也就是不使用 React-Router-Redux;

  2. 藉助 React-Router-Redux 管理,這也是討論的重點。

2.1、手動管理 (Mannually)

在不借助其他庫,一種簡單的做法是手動將路由狀態納入 store 中管理,當 URL 改變時同步修改 store 中的狀態。

React 路由狀態管理總結

如上圖,在手動同步環節,通過一套 Redux 機制,實現了路由信息在 store 中的存儲。history 作為數據來源,通過監聽 history,當 URL 狀態改變時 dispatch 相應 action (例如 type = LOCATION_CHANGE),通過添加的 reducer 將 location 信息同步到 store。通過這種方式,組件就可以獲取 store 中的 location 狀態信息,這也是目前 react-redux-starter-kit 採用的方式。

這種相對原始的方式有一定弊端:

  • 沒有將路由完全納入 Redux 管理。

  • 路由不支持 time travel。

  • history 實際也是 react-router 的路由數據來源,這就導致我們 store 中存儲的 location 數據與 react-router 並不一定同步。(例如,這會導致文末討論的重複渲染問題)

2.2、使用 React-Router-Redux

下面我們討論文首提出的問題一:是否需要將路由納入 store 進行管理。雖然在 react-router 4.x 版本後,react-router-redux 已經成為其一部分,但官方還是就其是否應該在項目中使用進行了建議:

  1. 希望在項目中使用完全使用 store 管理路由數據

  2. 希望使用 dispatch action 的方式進行導航(修改路由)

  3. 希望調試時路由支持 time travel

上面是使用 React-Router-Redux 的原則,當然一定程度上也可以是決定將路由納入 store 管理的原則。我覺得還可以增加兩條:

  1. 項目抽象中,路由信息應該作為一種全局的狀態管理

  2. 有 Redux 強迫症

2.2.1、原理

通過一張圖的方式來了解一下 React-Router-Redux 的實現原理。

React 路由狀態管理總結

上圖實際上也是 React-Router-Redux 如何將 URL 與 state 同步的過程,在程序中,主要是通過如下的幾個重要的 API 實現的:

  • routerMiddleware 與 routerReducer

    routerMiddleware 與 routerReducer 的共同作用,讓我們能夠處理兩種 action 類型:一種類型為 LOCATION_CHANGE,與手動管理過程中相同,它負責修改 store 存儲;另一種類型為 CALL_HISTORY_METHOD,這類 action 一般會在組件內派發,它不負責 state 的修改,通過 routerMiddleware 後,會被轉去調用 history 方法(如 push, replace 等),以修改 URL 狀態。

  • syncHistoryWithStore

    顧名思義,這個方法就是處理路由與 store 中信息同步的重要方法。通過這個方法,我們能獲得一個新的、增強版的 history 對象,這個對象重寫了 history.listen 方法,原有的 history.listen 只負責 action (LOCATION_CHANGE) 的派發,新的 history.listen 則只監聽 store 的變化(使用了 store.subscribe),所以當我們在程序內調用 history.listen 時,實際上是在監聽 store 中的路由信息。

2.2.2、實踐:location as a prop

在實際項目應用中,一種較為合理實踐方式如下。

React 路由狀態管理總結

即將 location 或子屬性(如 location.pathname 等)作為屬性信息逐層傳遞,傳遞給關注路由信息的子組件,這類似於 react-router 原有的使用方法,區別是,在改變 URL 時,使用了 dispatch action 的方式。

三、建議

3.1、 謹慎地使用 state.routing

一般地,在使用 React-Router-Redux 時,路由信息在 store 中會以 routing.locationBeforeTransition 的形式體現。我們在上文的實踐中並沒有直接從 store 中獲取這個狀態,實際上官方也不建議這樣做,從名字來看,作者已經明確提醒了我們這是一個變化中的值。

You should not read the location state directly from the Redux store. This is because React Router operates asynchronously (to handle things such as dynamically-loaded components) and your component tree may not yet be updated in sync with your Redux state. You should rely on the props passed by React Router, as they are only updated after it has processed all asynchronous code.

不應該直接從 Redux store 中讀取路由狀態。這是因為 React-Router 的行為是異步的(例如為了處理組件動態加載等),所以你的組件樹可能不能跟上 Redux 狀態的變化。應該去依賴 React Router 傳遞的屬性,這保證了這些值是在所有異步操作完成後才更新的。

當 routing 中的值已經改變時,React-Router 可能還沒有將組件樹進行更新完畢,如果使用這個值可能引發一些問題。所以作者依然建議我們採用傳遞 location 屬性的方式讀取路由信息,以確保 React-Router 已經處理完畢。

3.2、只傳遞必要的路由信息

只將必要信息作為 prop 傳遞,例如 location.pathname、 location.query.page,而不是傳遞整個 location。這能夠儘量避免可能的重複渲染。

3.3、 只使用 dispatch action 的方式修改路由

實際上,除了使用 Link 組件,使用 React-Router-Redux 後有多種方式能夠修改路由信息,如:

  1. history.method

  2. context.router.method

  3. dispatch ROUTER-ACTION

筆者仍然建議只使用 dispatch action 方式修改路由,這種方式更為遵循 Redux 流程,同時方便組件的解耦。在實際應用中,應該使用統一的 Action Creator 來創建修改路由的 action。

3.4、 謹慎地使用 withRouter 高階組件(裝飾器)

React-Router 提供了 withRouter 高階組件以便組件訪問路由狀態信息(match, location, history),但同時一旦引用的路由屬性發生變化就會觸發重渲染流程,如果使用不當,則可能導致組件進行多餘的重複渲染。

四、常見問題

4.1、re-render(重複渲染)問題

在使用 React-Router 和路由組件異步加載後,一個常見的問題是組件切換時發生意外的重複渲染。 一般情況下(未進行代碼分割時),React-Router 在切換路由組件時,過程是這樣的:

React 路由狀態管理總結

在進行了代碼分割後,路由組件改為異步加載,過程變成了這樣:

React 路由狀態管理總結

由於組件 A 將 location 或其相關屬性最為屬性 props 傳入,location 的變化導致了 props 的改變,此時由於組件 B 還未加載成功,導致組件 A 在卸載前進行沒有必要的重渲染。

這個問題一般是因為錯誤地使用了變化的路由信息,如上文中的 state.routing 信息,由於 state.routing 與 React-Router 路由信息不同步造成的。解決辦法:參照上文提出的實踐,使用 Route 組件注入的 location 數據進行路由信息傳遞。

五、參考

  1. https://github.com/reactjs/react-router-redux

  2. https://github.com/reactjs/react-router-tutorial

  3. https://github.com/ReactTraining/history


分享到:


相關文章: