一、依賴(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 的一部分。
本文並不旨在介紹兩種依賴庫的具體用法(具體用法請參考官方文件和教程),而主要闡述其實現方式和原理,總結具體的實踐方式和注意事項。在主要內容之前,首先簡要介紹下兩個庫的功能:
- React-RouterReact-Router 做的最重要的事就是將瀏覽器 URL 與程式聯絡起來(藉助 history 庫),它為 React 提供了宣告式的路由系統,通過其提供的導航元件,我們能夠方便地使用 URL 來控制狀態的變化和元件的切換。
- 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 文件和教程)
但在這裡,我們主要討論將路由狀態納入 Redux 架構中的情況。本部分的下文將分為兩部分:
- 手動管理,也就是不使用 React-Router-Redux;
- 藉助 React-Router-Redux 管理,這也是討論的重點。
2.1、手動管理 (Mannually)
在不借助其他庫,一種簡單的做法是手動將路由狀態納入 store 中管理,當 URL 改變時同步修改 store 中的狀態。
如上圖,在手動同步環節,通過一套 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 已經成為其一部分,但官方還是就其是否應該在專案中使用進行了建議:
- 希望在專案中使用完全使用 store 管理路由資料
- 希望使用 dispatch action 的方式進行導航(修改路由)
- 希望除錯時路由支援 time travel
上面是使用 React-Router-Redux 的原則,當然一定程度上也可以是決定將路由納入 store 管理的原則。我覺得還可以增加兩條:
- 專案抽象中,路由資訊應該作為一種全域性的狀態管理
- 有 Redux 強迫症
2.2.1、原理
通過一張圖的方式來了解一下 React-Router-Redux 的實現原理。
上圖實際上也是 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
即將 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 後有多種方式能夠修改路由資訊,如:
- history.method
- context.router.method
- 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 在切換路由元件時,過程是這樣的:
在進行了程式碼分割後,路由元件改為非同步載入,過程變成了這樣:
由於元件 A 將 location 或其相關屬性最為屬性 props 傳入,location 的變化導致了 props 的改變,此時由於元件 B 還未載入成功,導致元件 A 在解除安裝前進行沒有必要的重渲染。
這個問題一般是因為錯誤地使用了變化的路由資訊,如上文中的 state.routing 資訊,由於 state.routing 與 React-Router 路由資訊不同步造成的。解決辦法:參照上文提出的實踐,使用 Route 元件注入的 location 資料進行路由資訊傳遞。
五、參考
- https://github.com/reactjs/react-router-redux
- https://github.com/reactjs/react-router-tutorial
- https://github.com/ReactTraining/history