背景
業務模塊分開,總體業務流由多個單頁面組成,如:還款途中,須要綁卡,設置交易密碼,或者還須要修改手機號碼,最後一系列操做後回到還款頁面提交還款,整個流程在多個頁面間跳轉,單頁面的數據沒法獲得保存,故須要一套多頁面的數據緩存方案。vue
數據保存
考慮瀏覽器的通常通用性,將數據緩存在sessionStorage中。react
舊方案
每次觸發頁面跳轉時,手動調用window.sessionStorage.setItem保存須要保存的數據,在頁面跳轉回來的時候,讀取sessionStorage中的數據,將其賦給須要覆蓋的變量,而後觸發對應的初始化操做。現有這種方案存在如下一些不足:瀏覽器
- 每一個頁面都須要編寫storeData和restoreData的邏輯,分散的邏輯不易管理;
- 若是引用了組件,組件自身維護了state的話,子組件須要被緩存的數據須要暴露給父組件才能統一在父組件中觸發storeData邏輯,而restoreData的獲得的數據也須要傳遞到子組件才能進行相應的賦值操做,同時要考慮組件渲染的時機,以及數據請求回來的時機之間的前後順序
優化點
- 須要一個統一的數據保存對象
- 須要一個統一的數據管理入口
- 統一的數據保存對象是爲了,跳轉前的直接緩存這個對象,後續跳轉回來,也直接讀取這個對象,而且全局都能訪問到這個對象,即能優化舊方案的第二個點,減小父子組件間的數據傳輸;
- 統一的數據管理入口,是爲了方便管理數據緩存的邏輯,同時避免重複寫代碼;
改進方案
全局對象GlobalStore
- 針對上述的幾點,首先是定義一個globalStore的對象,而後export出來,各個須要緩存的頁面就直接import這個對象。
- 同時爲了不不一樣頁面間的命名污染,須要給每一個頁面賦予一個pageId,並設爲globalstore的key值,故最終GlobalStore格式如:GlobalStorepageId = value。
- 如何標記須要被緩存的數據?考慮通常涉及展現的都是state中的數據,故直接將state保存下來,另外組件的props通常會變化的也是由父組件的state傳遞下來的,因此props不須要額外保存,故最終選取保存的數據是state裏的數據。
數據管理入口
- 因爲業務代碼中會修改state的狀態,在最後頁面跳轉時,如何同步當前的state是須要解決的問題,經過實例this能夠獲取當前的state,但若是當前頁面用到子組件,子組件中的state也須要被緩存,那也須要獲取子組件的實例,從而獲取他的state,這就會使數據管理很困難,總體緩存脈絡也很不清晰,因此思路是是否能在更改state的同時,同步更新全局的GlobalStore;
- 一開始是想要採起相似vue的做法,利用Object.definedproperty,寫一個set的攔截器,但後來發現setstate並非簡單的經過this.state[key] = newValue來修改數據,而是總體替換,因此這個方案行不通;
- 第二個方案就是直接改寫React.component原型上的setState,在其上添加同步更新GlobalStore的代碼,但並非每一個頁面都須要緩存,改寫原型會使其他不須要緩存的頁面頁回去修改GlobalStore,固然這裏也能夠根據當前實例的pageId來判斷是否要寫緩存來規避這個問題,但本着儘可能不改變react邏輯的原則,採用了另外一種方案;
- 使用裝飾器,也就是高階組件,能夠實現反向代理,同時對業務代碼的侵入性更小,只須要在每一個用到的頁面的類上加一個裝飾器並傳入pageid做爲參數就能夠開啓緩存策略;
- 另外利用反向代理,能夠獲取到父類state上的全部屬性,經過super.render()能夠進行渲染,並經過給子類添加setState屬性,覆蓋原型上的setState方法,同時在添加的setState方法上調用super.setState來保證數據能獲得正確更新,以及觸發視圖渲染;
- 而後在添加的setState上獲取到更新的對象,寫入GlobalStore上,可是考慮到setState除了能夠傳入一個對象進行更新,還能夠傳入一個函數進行更新,而函數會接受前一個狀態的state和props爲參數,而若是同時調用了兩次setState,雖然時批量更新,state不會立刻修改值,但後一次setState中的preState是前一個setState修改後的state結果,這種邏輯下,要獲取正確的state對象會比較複雜且容易出錯,因此須要對傳入的數據進行一下包裝,再setState中聲明一個函數,以arguments做爲入參,內部執行一遍傳入的func,入參依舊是arguments,記錄返回值,分析更新了哪一個key,對應修改進GlobalStore,最後返回這個返回值,並把這個內部聲明的函數傳入super.setState中,這樣內部執行修改方法時也會修改到GlobalStore對象;
- 至此利用裝飾器反向代理,咱們實現了在constructor階段,將數據回填覆蓋state,同時用本身實現的setState攔截react.component原型上的setState實現數據同步,最後每次產生外部跳轉的時候,調用一個通用的跳轉方法,在跳轉的同時,將GlobalStore寫入sessionStorage,這樣回來初始化頁面調用構造函數時就能夠回填數據,整個多頁面數據緩存的方案就大致如此。
優化
考慮實際的應用場景,還有如下幾個地方能夠作優化:緩存
公共字段
- 內部頁面之間有可能使用了相同的變量,此時不必針對每一個頁面ID都保存一份副本,而應該把這部分數據提取到一個公共的字段下,例如 [common];
- 那麼就須要有手段去識別出,state上的哪些字段是取自公共字段的,這裏考慮用一個自定義的class來實現這個功能,由於首先class能夠利用instanceOf判斷是否屬於公共字段,其次通常沒有人會在state上設置一個class的實例;
- 如今暫且稱這個class爲CommonData,考慮公共字段要用到的屬性,給他兩個屬性,分別是key和value,其中key爲對應GlobalStore中的字段名(由於state中的字段名不必定要和公共字段名相同),value爲其初始值;
- 同時對外export一個方法,暫稱genCommonData,傳入兩個參數,並在內部實例化CommonData,返回實例;
- 這時,裝飾器就能夠對state中的屬性進行判斷,哪些是公共字段,哪些是頁面獨有的字段,在constructor裏,就能夠進行分別處理,頁面獨有的字段就如以前那樣處理,公共字段要用實例中的value以及GlobalStore中的值進行從新賦值,同時用一個私有變量保存state中公共字段和GlobalStore中公共字段的對應關係;
- 根據這個對應關係,在後續setState中,會判斷其是否爲公共字段,從而決定更新[pageId]裏的屬性,仍是[common]中的屬性
組件數據緩存
- 組件的數據緩存也能夠用上面的一套邏輯,但考慮到一個組件有可能被多個頁面應用,同時還有jsx寫法中難以直接使用裝飾器,在定義組件的文件中,輸出組件的時候,針對會使用緩存的組件,改造時應該額外輸出多一種被裝飾器裝飾的組件做爲緩存組件使用;
- 另外,仍是由於組件可能被多頁面引用,這裏存放在GlobalStore中的數據不能再直接添加一個組件Id作區分,考慮到組件是掛在頁面下的,能夠在GlobalStore[pageId]下保存一個[componentId]的字段去進行保存,而公共字段依然保存在[common]中;
- 因爲渲染的特性,父組件的constructor會在子組件的constructor以前執行,同時一個時間只可能有一個頁面,那麼能夠在GlobalStore中添加一個currentPageId的屬性,去記錄當前的頁面id,等對應子組件加載時,直接利用currentPageId去對應的字段下賦值,從而讓對應頁面的數據記錄在對應頁面下
至此,一個基本能運行的方案就實現了session
有待解決的問題
- 一個頁面下引用了多個相同的組件,怎麼保存數據? - 利用key
- 組件嵌套的狀況下怎麼保存數據? - 只能利用Props指明關係?
- 多個裝飾器時可能有反作用
- 並非全部的state都須要被緩存的 - 參考CommonData的作法?
- 有些須要緩存的數據不是放在state上的,而是直接掛在this上的 - 建議放到state裏,或者一樣時採用CommonData的作法
- hook是否能夠利用?
該方案對Vue的啓發
- vue多頁面也會有一樣的問題,vue中沒法使用裝飾器,但Object.definedPorperty能夠利用,在其勾子函數created和beforeMount之間,能夠對修改其data上的set方法,使得數據能夠同步更新,入口放在mixin;
- 至於公共對象,因爲vue會針對data中的值進行監聽,不能採用CommonData的作法,這種能夠事先在頁面定義一個公共字段的map,傳入data時使用解構,修改監聽的時候針對這個map作篩選哪些是更新到公共字段;
- 但更直接的方案是使用Vuex,直接緩存store對象,進入頁面進行init的時候就對其進行回填便可。