一谓晌、依賴(lài)(Dependencies)
在一般 SPA 開(kāi)發(fā)中杯拐,路由的管理十分重要互拾。作為 React 技術(shù)體系中的一部分,官方維護(hù)的 React-Router 則是首選的路由庫(kù)萧落。
在應(yīng)用 Redux 模式后践美,React-Router 與 Redux 的配合引發(fā)了新的問(wèn)題,是否需要將路由納入 store 進(jìn)行管理铐尚?如何將路由納入 store 進(jìn)行管理拨脉?這些都是需要考慮的問(wèn)題。我們將在后文討論第一個(gè)問(wèn)題宣增,而為了解決上述第二個(gè)問(wèn)題玫膀,React-Router-Redux 這個(gè)輕量級(jí)的擴(kuò)展庫(kù)應(yīng)運(yùn)而生并得到廣泛應(yīng)用。
另外需要說(shuō)明的是爹脾,長(zhǎng)久以來(lái) React-Router 與 React-Router-Redux 是兩個(gè)獨(dú)立的庫(kù)帖旨,但在 React-Router 4.x 版本以后,React-Router-Redux 已經(jīng)成為了 React-Router 的一部分灵妨。
本文并不旨在介紹兩種依賴(lài)庫(kù)的具體用法(具體用法請(qǐng)參考官方文檔和教程)解阅,而主要闡述其實(shí)現(xiàn)方式和原理,總結(jié)具體的實(shí)踐方式和注意事項(xiàng)泌霍。在主要內(nèi)容之前货抄,首先簡(jiǎn)要介紹下兩個(gè)庫(kù)的功能:
-
React-Router
React-Router 做的最重要的事就是將瀏覽器 URL 與程序聯(lián)系起來(lái)(借助 history 庫(kù)),它為 React 提供了聲明式的路由系統(tǒng)朱转,通過(guò)其提供的導(dǎo)航組件蟹地,我們能夠方便地使用 URL 來(lái)控制狀態(tài)的變化和組件的切換。
-
React-Router-Redux
按照官方的說(shuō)法藤为,其實(shí)現(xiàn)了「deep integration of react-router and redux」怪与,即 React-Router 與 Redux 的深度集成,它將路由完全納入 store 中進(jìn)行管理缅疟,使 store 成為了 URL(或者說(shuō)是 history)的數(shù)據(jù)來(lái)源分别,也使我們能夠通過(guò) dispatch action 的方式來(lái)修改 URL。我們將在后文介紹它的實(shí)現(xiàn)原理存淫。
二耘斩、實(shí)踐
路由狀態(tài)并非一定要介入 Redux 架構(gòu)中。在一些簡(jiǎn)單的應(yīng)用場(chǎng)景下桅咆,只需要使用 React-Router 提供的聲明式組件(Router, Route, Link 等)即可方便的實(shí)現(xiàn) URL 導(dǎo)航煌往。在一些稍復(fù)雜的場(chǎng)景中,只要保證遵循 React 單向數(shù)據(jù)流動(dòng)方式轧邪,遵照使用方法刽脖,也可以完成進(jìn)行路由信息的讀取和觸發(fā)變更,其過(guò)程如下圖所示忌愚。(使用方法請(qǐng)參照 React-Router 文檔和教程)
但在這里曲管,我們主要討論將路由狀態(tài)納入 Redux 架構(gòu)中的情況。本部分的下文將分為兩部分:
手動(dòng)管理硕糊,也就是不使用 React-Router-Redux院水;
借助 React-Router-Redux 管理,這也是討論的重點(diǎn)简十。
2.1檬某、手動(dòng)管理
在不借助其他庫(kù),一種簡(jiǎn)單的做法是手動(dòng)將路由狀態(tài)納入 store 中管理螟蝙,當(dāng) URL 改變時(shí)同步修改 store 中的狀態(tài)恢恼。
如上圖,在手動(dòng)同步環(huán)節(jié)胰默,通過(guò)一套 Redux 機(jī)制场斑,實(shí)現(xiàn)了路由信息在 store 中的存儲(chǔ)。history 作為數(shù)據(jù)來(lái)源牵署,通過(guò)監(jiān)聽(tīng) history漏隐,當(dāng) URL 狀態(tài)改變時(shí) dispatch 相應(yīng) action (例如 type = LOCATION_CHANGE),通過(guò)添加的 reducer 將 location 信息同步到 store奴迅。通過(guò)這種方式青责,組件就可以獲取 store 中的 location 狀態(tài)信息,這也是目前 react-redux-starter-kit 采用的方式取具。
這種相對(duì)原始的方式有一定弊端:
沒(méi)有將路由完全納入 Redux 管理脖隶。
路由不支持 time travel。
history 實(shí)際也是 react-router 的路由數(shù)據(jù)來(lái)源者填,這就導(dǎo)致我們 store 中存儲(chǔ)的 location 數(shù)據(jù)與 react-router 并不一定同步浩村。(例如,這會(huì)導(dǎo)致文末討論的重復(fù)渲染問(wèn)題)
2.2占哟、使用 React-Router-Redux
下面我們討論文首提出的問(wèn)題一:是否需要將路由納入 store 進(jìn)行管理心墅。雖然在 react-router 4.x 版本后,react-router-redux 已經(jīng)成為其一部分榨乎,但官方還是就其是否應(yīng)該在項(xiàng)目中使用進(jìn)行了建議:
希望在項(xiàng)目中使用完全使用 store 管理路由數(shù)據(jù)
希望使用 dispatch action 的方式進(jìn)行導(dǎo)航(修改路由)
希望調(diào)試時(shí)路由支持 time travel
上面是使用 React-Router-Redux 的原則怎燥,當(dāng)然一定程度上也可以是決定將路由納入 store 管理的原則。我覺(jué)得還可以增加兩條:
項(xiàng)目抽象中蜜暑,路由信息應(yīng)該作為一種全局的狀態(tài)管理
有 Redux 強(qiáng)迫癥
2.2.1铐姚、原理
通過(guò)一張圖的方式來(lái)了解一下 React-Router-Redux 的實(shí)現(xiàn)原理。
上圖實(shí)際上也是 React-Router-Redux 如何將 URL 與 state 同步的過(guò)程,在程序中隐绵,主要是通過(guò)如下的幾個(gè)重要的 API 實(shí)現(xiàn)的:
routerMiddleware 與 routerReducer
routerMiddleware 與 routerReducer 的共同作用之众,讓我們能夠處理兩種 action 類(lèi)型:一種類(lèi)型為 LOCATION_CHANGE,與手動(dòng)管理過(guò)程中相同依许,它負(fù)責(zé)修改 store 存儲(chǔ)棺禾;另一種類(lèi)型為 CALL_HISTORY_METHOD,這類(lèi) action 一般會(huì)在組件內(nèi)派發(fā)峭跳,它不負(fù)責(zé) state 的修改膘婶,通過(guò) routerMiddleware 后,會(huì)被轉(zhuǎn)去調(diào)用 history 方法(如 push, replace 等)蛀醉,以修改 URL 狀態(tài)悬襟。**syncHistoryWithStore ** 顧名思義,這個(gè)方法就是處理路由與 store 中信息同步的重要方法拯刁。通過(guò)這個(gè)方法脊岳,我們能獲得一個(gè)新的、增強(qiáng)版的 history 對(duì)象筛璧,這個(gè)對(duì)象重寫(xiě)了 history的listen方法逸绎,原有的 history.listen只負(fù)責(zé) action (LOCATION_CHANGE) 的派發(fā),新的 history.listen則只監(jiān)聽(tīng) store 的變化(使用了 store.subscribe)夭谤,所以當(dāng)我們?cè)诔绦騼?nèi)調(diào)用 history.listen時(shí)棺牧,實(shí)際上是在監(jiān)聽(tīng) store 中的路由信息。
2.2.2朗儒、實(shí)踐:location as a prop
在實(shí)際項(xiàng)目應(yīng)用中颊乘,一種較為合理實(shí)踐方式如下。
即將 location 或子屬性(如 location.pathname 等)作為屬性信息逐層傳遞醉锄,傳遞給關(guān)注路由信息的子組件乏悄,這類(lèi)似于 react-router 原有的使用方法,區(qū)別是恳不,在改變 URL 時(shí)檩小,使用了 dispatch action 的方式。
三烟勋、建議
3.1规求、 謹(jǐn)慎地使用 state.routing
一般地,在使用 React-Router-Redux 時(shí)卵惦,路由信息在 store 中會(huì)以 routing.locationBeforeTransition 的形式體現(xiàn)阻肿。我們?cè)谏衔牡膶?shí)踐中并沒(méi)有直接從 store 中獲取這個(gè)狀態(tài),實(shí)際上官方也不建議這樣做沮尿,從名字來(lái)看丛塌,作者已經(jīng)明確提醒了我們這是一個(gè)變化中的值。
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.
不應(yīng)該直接從 Redux store 中讀取路由狀態(tài)。這是因?yàn)?React-Router 的行為是異步的(例如為了處理組件動(dòng)態(tài)加載等)赴邻,所以你的組件樹(shù)可能不能跟上 Redux 狀態(tài)的變化印衔。應(yīng)該去依賴(lài) React Router 傳遞的屬性,這保證了這些值是在所有異步操作完成后才更新的乍楚。
當(dāng) routing 中的值已經(jīng)改變時(shí)当编,React-Router 可能還沒(méi)有將組件樹(shù)進(jìn)行更新完畢,如果使用這個(gè)值可能引發(fā)一些問(wèn)題徒溪。所以作者依然建議我們采用傳遞 location 屬性的方式讀取路由信息,以確保 React-Router 已經(jīng)處理完畢金顿。
3.2臊泌、只傳遞必要的路由信息
只將必要信息作為 prop 傳遞,例如 location.pathname揍拆、 location.query.page渠概,而不是傳遞整個(gè) location。這能夠盡量避免可能的重復(fù)渲染嫂拴。
3.3播揪、 只使用 dispatch action 的方式修改路由
實(shí)際上,除了使用 Link 組件筒狠,使用 React-Router-Redux 后有多種方式能夠修改路由信息猪狈,如:
history.method
context.router.method
dispatch ROUTER-ACTION
筆者仍然建議只使用 dispatch action 方式修改路由,這種方式更為遵循 Redux 流程辩恼,同時(shí)方便組件的解耦雇庙。在實(shí)際應(yīng)用中,應(yīng)該使用統(tǒng)一的 Action Creator 來(lái)創(chuàng)建修改路由的 action灶伊。
3.4疆前、 謹(jǐn)慎地使用 withRouter高階組件(裝飾器)
React-Router 提供了withRouter高階組件以便組件訪(fǎng)問(wèn)路由狀態(tài)信息(match, location, history),但同時(shí)一旦引用的路由屬性發(fā)生變化就會(huì)觸發(fā)重渲染流程聘萨,如果使用不當(dāng)竹椒,則可能導(dǎo)致組件進(jìn)行多余的重復(fù)渲染。
四米辐、常見(jiàn)問(wèn)題
4.1胸完、re-render(重復(fù)渲染)問(wèn)題
在使用 React-Router 和路由組件異步加載后,一個(gè)常見(jiàn)的問(wèn)題是組件切換時(shí)發(fā)生意外的重復(fù)渲染儡循。 一般情況下(未進(jìn)行代碼分割時(shí))舶吗,React-Router 在切換路由組件時(shí),過(guò)程是這樣的:
在進(jìn)行了代碼分割后择膝,路由組件改為異步加載誓琼,過(guò)程變成了這樣:
由于組件 A 將 location 或其相關(guān)屬性最為屬性 props 傳入,location 的變化導(dǎo)致了 props 的改變,此時(shí)由于組件 B 還未加載成功腹侣,導(dǎo)致組件 A 在卸載前進(jìn)行沒(méi)有必要的重渲染叔收。
這個(gè)問(wèn)題一般是因?yàn)殄e(cuò)誤地使用了變化的路由信息,如上文中的 state.routing 信息傲隶,由于 state.routing 與 React-Router 路由信息不同步造成的饺律。解決辦法:參照上文提出的實(shí)踐,使用 Route 組件注入的 location 數(shù)據(jù)進(jìn)行路由信息傳遞跺株。