爲 react-router 寫一個能夠緩存的 Route

原文發佈於個人 GitHub bloghtml

前言

上一篇文章 中介紹了前端路由的實現及 react-router-v4(如下簡稱 rr4) 的源碼分析,目前階段 rr4 已經基本壟斷了 react 生態圈的路由,雖然 v4 版本成功完成了一切皆組件的蛻變,但其實它自己還有諸多問題,好比 keep-alive。前端

keep-alive 的叫法取自 vue-keep-alive,在 vue 中,能夠將某組件暫存於內存,而後跳轉到其餘頁面再從內存中將這個組件拿出來。換算到路由中,咱們能夠想象這樣一個情景 —— 有一個商品列表頁,每一個商品點進去都跳轉到對應的商品詳情頁面,用戶每次瀏覽完一個商品詳情以後回退,列表頁會從新渲染,那麼若是用戶已經往下劃了幾屏以後回退,那麼每次返回後都要先滑到上次瀏覽的位置,這種體驗能夠說是災難性的。vue

如今的瀏覽器很是貼心的實現了 Scroll Restoration(後退時恢復滾動位置),這在非 SPA 頁面有很是好的體驗效果,可是在 SPA 中,會有如下問題:react

  1. 瀏覽器試圖恢復滾動距離時,頁面可能尚未加載完畢。由於回退的頁面須要從新 mount,可能存在異步加載的部分,致使頁面出現跳動。
  2. 點擊連接進入頁面就不會應用滾動恢復這一行爲。只有在點擊瀏覽器按鈕的前進後退按鈕時,纔會觸發 popstate 事件並觸發 scroll restoration,經過點擊連接沒法觸發滾動恢復。
  3. 這是非規範的一個 API(詳見),因此各個瀏覽器的實現並不徹底一致。

其實 iOS 和 Android 端的路由轉換是十分理想的 —— 支持轉場動畫,手勢返回,keep-alive。git

本文中咱們試圖解決爲 rr4 實現一個能夠緩存的 Route 來解決上面例子中的問題,並藉此探索一下 rr4 目前階段的不足之處及能夠增強的地方。說句題外話,rr4 的核心開發者又新搞了一個 reach-router 路由庫,針對 rr4 的缺點進行了針對性的改進,已經欽點了是下一代的路由旗艦管理庫。github

輪子

先放上我造的輪子的倉庫地址 react-live-route 感覺一下本文的最終目的,react-live-route 可使路由在路徑不匹配時隱藏而不被卸載,在匹配路徑時徹底恢復離開頁面時的樣子。歡迎 star 和提 issue。web

PC 端能夠預覽 demoapi

移動端掃碼試玩 (點一下玩一年)數組

qr

思路

咱們先從新將要解決的問題整理一下:瀏覽器

咱們有列表頁面和詳情頁,在列表頁點擊項目進入對應的詳情頁時,儘可能保留列表頁的視圖與數據狀態(包括滾動位置)。在從詳情頁回退到列表頁的時候,但願列表頁能恢復到上次離開時的狀態。

其中咱們要恢復的狀態:

  1. 頁面的滾動位置。
  2. 路由組件的一切狀態,包括路由的組件的全部子元素的狀態。

而且要作到無痛兼容 rr4,侵入性越小越好。咱們的目標是爲 react-router 設計一個加強型的 Route 組件,能夠像 iOS 和 Android 端的路由切換同樣「隱藏」上一個導航的頁面,在這裏有兩種解決問題的思路:

思路1

unmount 時儲存狀態,re-mount 時取回狀態

在列表頁將要 unmount 的時候,將須要保留的數據狀態存在 context(或者 window.sessionStorage 等等)

**優勢:**能夠在 unmount 和 re-mount 時利用生命週期。

缺點:

  1. 須要本身選擇要存儲的信息。
  2. 父組件沒法拿到子組件的狀態進行保存。
  3. 會從新 unmount 和 re-mount,這實際上是不該該發生的,被隱藏的列表頁應該是「潛伏」在詳情頁的下面,等到從新進入列表頁時纔出現,而不是已經被 unmount 了。

思路2

不 unmount,只是根據路由隱藏/顯示對應頁面

在切換到詳情頁的路徑時,不將列表頁 unmount,而是 display: none 掉它,在從詳情頁返回列表頁的時候,再 display: block 將列表頁顯示回來。

優勢: 簡單粗暴,由於沒有卸載組件,因此能夠不用管頁面的數據狀態的保存狀況。只須要管理好恢復顯示、隱藏與正常 re-render,再恢復滾動位置便可。

缺點: 配合轉場動畫可能會有問題。

因爲思路 1 的實現有很大的侷限性,因此按照思路 2 來進行實現。

實現

加強的 Route 組件稱爲 LiveRoute,咱們首先要肯定,這個加強組件在什麼狀況下起做用,以及它有哪幾種狀態,react-router 有一篇關於 Scroll Restoration 的文章 ,是關於 react-router 去除了滾動恢復的功能的緣由,其中有提到緣由:

What got tricky for me was defining an "opt-out" API for when I didn't want the window scroll to be managed.

就是由於實際的應用狀況太多變,他們沒法合適的判斷何時須要進行滾動恢復的管理。

在一開始我是打算使用成對的路由來實現,其中一個 LiveRoute 的存活狀態去控制另外一個須要保留存活的 LiveRoute:

<LiveRoute path='/list' liveKey='listToItem' component={List}/>
<LiveRoute path='/item/:id' onLiveKey='listToItem' component={Item}/> 複製代碼

可是路由間須要在 router 上建立 context 來輔助通訊,以下是 react-router 正常更新一次的流程,路由間的通訊會再一次觸發被通知的路由的 setState,這是沒法避免的,可是 Route 做爲整個應用中很是靠上的組件,反作用要儘量的小。

2018-06-22 111552

換個思路,其實緩存頁面的匹配規則就是控制頁面的隱藏/恢復顯示與正常卸載,而 rr4 正常的路由匹配規則就是控制渲染/卸載,經過 path 這個 props 來完成。那麼咱們直接給 LiveRoute 一個額外的來控制隱藏/恢復顯示的 livePath 的路徑便可,其規則就能夠直接套用 path,當路由 livePath 匹配時,則處於隱藏狀態,其餘路徑則按照 rr4 的規則正常渲染/卸載。調用方法:

<LiveRoute path='/list' livePath='/item/:id' component={List}/>
複製代碼

如此一來,LiveRoute 顯示狀態的依賴變爲 context.router,這樣作的好處是依賴變的簡單,全部的路由都會「同時」得到依賴的更新,而且相互之間沒有耦合。

LiveRoute 狀態

LiveRoute 內部有一個狀態機,有三種渲染組件的狀態:

  • HIDE_RENDER:livePath 匹配則須要將 LiveRoute 渲染的組件隱藏掉。進入此狀態時須要備份頁面的滾動位置,而後經過 ReactDOM.findDOMNode 來獲取路由渲染的組件的 DOM,將 dom.style.display = 'none',並備份修改以前的 display 的屬性。

  • NORMAL_RENDER_MATCH:路由正常渲染而且匹配上了。調用原版 Route 的渲染方法便可

    if (component) return match ? React.createElement(component, props) : null;
    if (render) return match ? render(props) : null;
    複製代碼

可是在每次正常匹配渲染的時候都要保存當前的 context.router,做爲以後隱藏渲染時須要保持渲染所需的 router,在 componnetDidUpdate 後查看有沒有備份的滾動位置,若是有就恢復滾動位置並清除備份的滾動位置。

  • NORMAL_RENDER_UNMATCH:正常渲染可是不匹配,即要卸載當前路由的組件。要作的就比較簡單了,清空 LiveRoute 中保存的 DOM 的引用,清除掉保存的滾動位置,而後調用原版的的 Route 的渲染方法(卸載)便可。

實現細節

如何保護路由渲染的組件存活

routerlivePath 匹配 的時候須要將 LiveRoute 置爲隱藏狀態。

可是新的 router 傳入必然會計算出一個新的 match 去 setState,而新的 setState 與當前的 path 並不匹配,因此 LiveRoute 每次隱藏渲染時須要在 componentWillReceiveProps 中計算上次的 prevMatch。 在 render 的部分,須要當前的 router 在計算傳遞給組件的 props,因此須要在最後一次正常渲染的時候保存當前的 router。 最後,將 prevMatch 做爲 setState 的 match,再拿出以前保存的 _prevRoute 完成渲染,一句話說就是將最後一次正常渲染的參數給保留了下來並在須要隱藏的時候拿出來假裝成最後一次正常渲染,再將 DOM 隱藏就完成了核心功能

保存滾動位置

因爲 LiveRoute 攔截了路由的卸載,因此滾動位置不須要再存儲在全局的 sessionStorage 中,LiveRoute 會一直存活,滾動位置直接能夠保存爲 LiveRoute 的屬性。而且,相比 sessionStorage 必須先 JSON.stringify() 保存對象的操做,有了更高的可拓展性。

Switch

有一個問題就是與 Switch 的不兼容性,這個是採用 display:none 這種方法沒法避免的,我也在 文檔 中寫到了。由於 Switch 的目的就是僅渲染第一個匹配的子元素,而 LiveRoute 的目的是強行渲染不匹配的子元素,因此不能在 Switch 中直接嵌套一個 LiveRoute 來使用。解決方法也簡單,就是將 LiveRoute 從 Switch 中拿到外面來,不要讓 LiveRoute 和 Switch 相互干擾,可是要注意此時 LiveRoute 的渲染與否也失去了 Switch 的跳過功能了。

滾動位置的不變性

在一些狀況下 LiveRoute 的 DOM 將會被直接修改,因此在切換路由時滾動位置將不會改變而界面已經發生改變。這並非 react-live-route 帶來的問題,你能夠手動將頁面滾動到頂部,這篇 react-router 提供的 教學文章 中能夠提供一些幫助。另外,若是 LiveRoute 將要恢復滾動位置,因爲 React 的渲染順序,它將發生在 LiveRoute 渲染的組件的滾動操做以後發生(滾動操做發生在 componentDidMount 或 componentDidUpdate 中)。

總結

react-live-route 實現了路由的緩存及復原,可是還有一些其餘的問題須要解決,好比與轉場動畫的兼容性及給 LivePath 傳入一個數組來實現多規則匹配的問題。

最後再放上 react-live-route 的倉庫地址 react-live-route,歡迎 star 和提出 issue。

參考

相關文章
相關標籤/搜索