spa應用中的路由緩存問題與解決方案

單頁面應用中的路由緩存問題

一般咱們在進行頁面先後退時,瀏覽器一般會幫咱們記錄下以前滾動的位置,這使得咱們不會在每次後退的時候都丟失以前的瀏覽器記錄定位。可是在如今愈發流行的SPA(single page application 單頁面應用)中,當咱們從父級頁面打開子級頁面,或者從列表頁面進入詳情頁面,此時若是回退頁面,會發現以前咱們瀏覽的滾動記錄沒有了,頁面被置頂到了最頂部,彷彿是第一次進入這個頁面同樣。
這是由於在spa頁面中的url與路由容器頁面所對應,當頁面路徑與其發生不匹配時,該頁面組件就會被卸載,再次進入頁面時,整個組件的生命週期就會徹底從新走一遍,包括一些數據的請求與渲染,因此以前的滾動位置和渲染的數據內容也都徹底被重置了。javascript

vue中的解決方式

vue.js最貼心的一點就是提供了很是多便捷的API,爲開發者考慮到不少的應用場景。在vue中,若是想緩存路由,咱們能夠直接使用內置的keep-alive組件,當keep-alive包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。html

內置組件keep alive

keep-alive是Vue.js的一個內置組件。它主要用於保留組件狀態或避免從新渲染。vue

使用方法以下:java

<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>
複製代碼

keep-alive組件會去匹配name名稱爲 'a', 'b' 的子組件,在匹配到之後會幫助組件緩存優化該項組件,以達到組件不會被銷燬的目的。node

實現原理

先簡要看下keep-alive組件內部實現代碼,具體代碼能夠見Vue GitHubreact

created () {
  this.cache = Object.create(null)
  this.keys = []
}
複製代碼

created生命週期中會用Object.create方法建立一個cache對象,用來做爲緩存容器,保存vnode節點。
Tip: Object.create(null)建立的對象沒有原型鏈更加純淨git

render () {
  const slot = this.$slots.default
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) {
    // check pattern 檢查匹配是否爲緩存組件,主要根據include傳入的name來對應
    const name: ?string = getComponentName(componentOptions)
    const { include, exclude } = this
    if (
      // not included 該判斷中判斷不被匹配,則直接返回當前的vnode(虛擬dom)
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
    ) {
      return vnode
    }

    const { cache, keys } = this
    const key: ?string = vnode.key == null
      // same constructor may get registered as different local components
      // so cid alone is not enough (#3269)
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
      : vnode.key
    if (cache[key]) {
      //查看cache對象中已經緩存了該組件,則vnode直接使用緩存中的組件實例
      vnode.componentInstance = cache[key].componentInstance
      // make current key freshest 
      remove(keys, key)
      keys.push(key)
    } else {
      //未緩存的則緩存實例
      cache[key] = vnode
      keys.push(key)
      // prune oldest entry
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }

    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
}
複製代碼

上述代碼主要是在render函數中對是不是緩存渲染進行判斷github

vue keep-alive內部實現的基本流程就是:
  1. 首先經過getFirstComponentChild獲取到內部的子組件
  2. 而後拿到該組件的name與keep-alive組件上定義的include與exclude屬性進行匹配,
  3. 若是不匹配就表示不緩存組件,就直接返回該組件的vnode(vnode就是一個虛擬的dom樹結構,因爲原生dom上的屬性很是多,消耗巨大,使用這種模擬方式會減小不少dom操做的開銷)
  4. 若是匹配到,則在cache對象中查看是否已經緩存過該實例,若是有就直接將緩存的vnode的componentInstance(組件實例)覆蓋到目前的vnode上面,不然將vnode存儲在cache中。

React中的解決方案

在react中沒有提供相似於vue的keep-alive的解決方案,這意味這咱們可能須要本身編寫一些代碼或者經過一些第三方的模塊來解決。web

在React項目GitHub的該issue中進行了相關討論,開發維護人員給出了兩種方式來解決:redux

  • 將數據與組件分開緩存。例如,你能夠將state提高到一個不會被卸載的父級組件,或者像redux同樣將其放在一個側面緩存中。咱們也正在爲此開發一類的API支持(context)。
  • 不要去卸載你要「保持活動」的視圖,只需使用style={{display:'none'}}屬性去隱藏它們。

1. 集中的狀態管理恢復快照方式

在React中經過redux或mobx集中的狀態管理來緩存頁面數據以及滾動條等信息,以達到緩存頁面的效果。

componentDidMount() {
  const {app: {dataSoruce = [], scrollTop}, loadData} = this.props;
  if (dataSoruce.length) { //判斷redux中是否已經有數據源
    // 有數據則再也不加載收據,只恢復滾動狀態
    window.scrollTo(0, scrollTop);
  } else { //沒有數據就去請求數據源
    this.props.loadData(); // 在redux中定義的數據請求的action
  }
}

handleClik = () => {
  在點擊進入下一級頁面前先保存當前的滾動距離
  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
  const {saveScrollTop} = this.props;
  saveScrollTop(scrollTop);
}
複製代碼

首先咱們能夠在redux中爲頁面定義異步的action,將請求回來的數據放入集中的store中(redux的該相關具體用法不在細述)。在sotre裏咱們能夠保存當前頁面的數據源、滾動條高度以及其餘一些可能要用到的分頁數據等來幫助咱們恢復狀態。

componentDidMount生命週期裏,首先根據redux裏store中的對應的字段,判斷是否已經加載過數據源。若是已經緩存過數據則再也不去請求數據源,只去恢復一下store裏的存儲過的一些滾動條位置信息等。若是還未請求過數據,就使用在redux中定義的異步action去請求數據,在將數據在reducer裏將數據存到store中。 在render函數裏,咱們只須要讀取redux裏存儲的數據便可。

爲了保留要緩存頁面的一些狀態信息,如滾動條、分頁、操做狀態,咱們能夠在進行對應操做時候將這些信息存入redux的store中,這樣當咱們恢復頁面時,就能夠將這些對應狀態一一讀取並還原。

2. 使用display的屬性來切換顯示隱藏路由組件

想要display的屬性來切換顯示隱藏路由組件,首先要保證路由組件不會在url變化時候被卸載。
在react-router中最使用的Route組件,它能夠經過咱們定義的path屬性來與頁面路徑來進行匹配,並渲染對應的組件,從而達到保持UI與URL同步變化的效果。

首先簡要看下Route組件的實現 GitHub Route.js

return (
  <RouterContext.Provider value={props}> {children && !isEmptyChildren(children) ? children : props.match // props.match 屬性來肯定是否要渲染組件 ? component ? React.createElement(component, props) : render ? render(props) : null : null} </RouterContext.Provider> ); 複製代碼

上述代碼出如今關鍵的render方法最後的return中

Route組件會根據props對象中的match屬性來肯定是否要渲染組件,若是match匹配到了就使用Route組件上傳遞的component或者render屬性來渲染對應組件,不然就返回null。

而後溯源而上,咱們找到了props對象中關於match的定義:

const location = this.props.location || context.location;
const match = this.props.computedMatch
  ? this.props.computedMatch // <Switch> already computed the match for us
  : this.props.path
    ? matchPath(location.pathname, this.props)
    : context.match;

const props = { ...context, location, match };
複製代碼

上述代碼顯示,match首先會從組件的this.props中的computedMatch屬性來判斷:
若是this.props中存在computedMatch則直接使用定義好的computedMatch屬性賦值給match,
不然若是this.props.path存在,就會使用matchPath方法來根據當前的location.pathname來判斷是否匹配。

然而在react router的Route組件API文檔中咱們彷佛沒有看到過有關於computedMatch的介紹,不過在源碼中有一行這樣的註釋

// <Switch> already computed the match for us
複製代碼

該註釋說在<Switch>組件中已經爲咱們計算了該匹配。

接下來咱們再去了解一下Switch組件

Switch組件只會渲染第一個被location匹配到的而且做爲子元素的<Route>或者<Redirect>

咱們翻開Switch組件的實現源碼

let element, match; // 定義最後返回的組件元素,和match匹配變量
  
  React.Children.forEach(this.props.children, child => {
    if (match == null && React.isValidElement(child)) { // 若是match沒有內容則進入該判斷
      element = child;
  
      const path = child.props.path || child.props.from;
  
      match = path  // 該三元表達式只有在匹配到後會給match賦值一個對象,不然match一直爲null
        ? matchPath(location.pathname, { ...child.props, path })
        : context.match;
    }
  });
  
  return match
    ? React.cloneElement(element, { location, computedMatch: match })
    : null;
複製代碼

首先咱們找到computedMatch屬性是在React.cloneElement方法中,cloneElement方法會將追加定義的屬性合併到該clone組件元素上,並返回clone後的React組件,等於就是將新的props屬性傳入組件並返回新組件。

在上文中找到computedMatch的值match也是根據matchPath來判斷是否匹配的,matchPathreact router中的一個API,該方法會根據你傳入的第一個參數pathname與第二個要匹配的props屬性參數來判斷是否匹配。若是匹配就返一個對象類型幷包含相關的屬性,不然返回null。

React.Children.forEach循環子元素的方法中,matchPath方法判斷當前pathname是否匹配,若是匹配就給定義的match變量進行賦值,因此當match被賦值之後,後續的循環就也不會再進行匹配賦值,由於Switch組件只會渲染第一次與之匹配的組件。

3. 實現一個路由緩存組件

咱們知道Switch組件只會渲染第一項匹配的子組件,若是能夠將匹配到的組件都渲染出來,而後只用display的block和none來切換是否顯示,這也就實現了第二種解決方案。

參照Switch組件來封裝一個RouteCache組件:

import React from 'react';
import PropTypes from 'prop-types';
import {matchPath} from 'react-router';
import {Route} from 'react-router-dom';

class RouteCache extends React.Component {

  static propTypes = {
    include: PropTypes.oneOfType([
      PropTypes.bool,
      PropTypes.array
    ])
  };

  cache = {}; //緩存已加載過的組件

  render() {
    const {children, include = []} = this.props;

    return React.Children.map(children, child => {
      if (React.isValidElement(child)) { // 驗證是否爲是react element
        const {path} = child.props;
        const match = matchPath(location.pathname, {...child.props, path});

        if (match && (include === true || include.includes(path))) {
          //若是匹配,則將對應path的computedMatch屬性加入cache對象裏
          //當include爲true時,緩存所有組件,當include爲數組時緩存對應組件
          this.cache[path] = {computedMatch: match};
        }

        //能夠在computedMatch裏追加入一個display屬性,能夠在路由組件的props.match拿到
        const cloneProps = this.cache[path] && Object.assign(this.cache[path].computedMatch, {display: match ? 'block' : 'none'});

        return <div style={{display: match ? 'block' : 'none'}}>{React.cloneElement(child, {computedMatch: cloneProps})}</div>;
      }

      return null;
    });
  }
}

// 使用
<RouteCache include={['/login', '/home']}>
  <Route path="/login" component={Login} /> <Route path="/home" component={App} /> </RouteCache> 複製代碼

在閱讀了源碼後,咱們知道Route組件會根據它的this.props.computedMatch來判斷是否要渲染該組件。

咱們在組件內部建立一個cache對象,將已經匹配到的組件的computedMatch屬性寫入該緩存對象中。這樣即便當url再也不匹配時,也能經過讀取cache對象中該路徑的值,並使用React .cloneElement方法將computedMatch屬性賦值給組件的props。這樣已緩存過的路由組件就會被一直渲染出來,組件就不會被卸載掉。

由於組件內部可能會包裹多個路由組件,因此使用React.Children.map方法將內部包含的子組件都循環返回。

爲了UI與路由對應顯示正確,咱們經過當前的計算得出的match屬性,來隱藏掉不匹配的組件,只爲咱們展現匹配的組件便可。若是你不想在組件外再套一層div,也能夠在組件內部經過this.props.match中的display屬性來切換顯示組件。

仿照vue keep alive的形式,設置一個 include 參數API。當參數爲true時緩存內部的全部子組件,當參數爲數組時則緩存對應的path路徑組件。

使用效果


在最初時,從未被url匹配過的組件不會被渲染,裏面的dom結構是空的。


當切換到對應組件時,當前的組件被渲染,而以前已匹配的組件不會被卸載,只是被隱藏


在輸出日誌中能夠看到,當咱們不停的來回切換時, componentDidMount生命週期也只執行一次,在props.match中咱們能夠獲取到當前的 display值。

4. 另外的也能夠採用一些第三方組件模塊來實習緩存機制:

  1. react-keeper
  2. react-router-cache-route
  3. react-live-route
相關文章
相關標籤/搜索