溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何解決vue spa應(yīng)用中的路由緩存問題

發(fā)布時間:2021-07-20 13:58:57 來源:億速云 閱讀:213 作者:小新 欄目:web開發(fā)

這篇文章將為大家詳細(xì)講解有關(guān)如何解決vue spa應(yīng)用中的路由緩存問題,小編覺得挺實(shí)用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

單頁面應(yīng)用中的路由緩存問題

通常我們在進(jìn)行頁面前后退時,瀏覽器通常會幫我們記錄下之前滾動的位置,這使得我們不會在每次后退的時候都丟失之前的瀏覽器記錄定位。但是在現(xiàn)在愈發(fā)流行的SPA(single page application 單頁面應(yīng)用)中,當(dāng)我們從父級頁面打開子級頁面,或者從列表頁面進(jìn)入詳情頁面,此時如果回退頁面,會發(fā)現(xiàn)之前我們?yōu)g覽的滾動記錄沒有了,頁面被置頂?shù)搅俗铐敳?,仿佛是第一次進(jìn)入這個頁面一樣。這是因?yàn)樵趕pa頁面中的url與路由容器頁面所對應(yīng),當(dāng)頁面路徑與其發(fā)生不匹配時,該頁面組件就會被卸載,再次進(jìn)入頁面時,整個組件的生命周期就會完全重新走一遍,包括一些數(shù)據(jù)的請求與渲染,所以之前的滾動位置和渲染的數(shù)據(jù)內(nèi)容也都完全被重置了。

vue中的解決方式

vue.js最貼心的一點(diǎn)就是提供了非常多便捷的API,為開發(fā)者考慮到很多的應(yīng)用場景。在vue中,如果想緩存路由,我們可以直接使用內(nèi)置的keep-alive組件,當(dāng)keep-alive包裹動態(tài)組件時,會緩存不活動的組件實(shí)例,而不是銷毀它們。

內(nèi)置組件keep alive

keep-alive是Vue.js的一個內(nèi)置組件。它主要用于保留組件狀態(tài)或避免重新渲染。

使用方法如下:

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

keep-alive組件會去匹配name名稱為 'a', 'b' 的子組件,在匹配到以后會幫助組件緩存優(yōu)化該項(xiàng)組件,以達(dá)到組件不會被銷毀的目的。

實(shí)現(xiàn)原理

先簡要看下keep-alive組件內(nèi)部實(shí)現(xiàn)代碼,具體代碼可以見Vue GitHub

created () {
 this.cache = Object.create(null)
 this.keys = []
}

在created生命周期中會用Object.create方法創(chuàng)建一個cache對象,用來作為緩存容器,保存vnode節(jié)點(diǎn)。Tip: Object.create(null)創(chuàng)建的對象沒有原型鏈更加純凈

render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  // check pattern 檢查匹配是否為緩存組件,主要根據(jù)include傳入的name來對應(yīng)
  const name: ?string = getComponentName(componentOptions)
  const { include, exclude } = this
  if (
   // not included  該判斷中判斷不被匹配,則直接返回當(dāng)前的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對象中已經(jīng)緩存了該組件,則vnode直接使用緩存中的組件實(shí)例
   vnode.componentInstance = cache[key].componentInstance
   // make current key freshest 
   remove(keys, key)
   keys.push(key)
  } else {
   //未緩存的則緩存實(shí)例
   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函數(shù)中對是否是緩存渲染進(jìn)行判斷

vue keep-alive內(nèi)部實(shí)現(xiàn)的基本流程就是:

  1. 首先通過getFirstComponentChild獲取到內(nèi)部的子組件

  2. 然后拿到該組件的name與keep-alive組件上定義的include與exclude屬性進(jìn)行匹配,

  3. 如果不匹配就表示不緩存組件,就直接返回該組件的vnode(vnode就是一個虛擬的dom樹結(jié)構(gòu),由于原生dom上的屬性非常多,消耗巨大,使用這種模擬方式會減少很多dom操作的開銷)

  4. 如果匹配到,則在cache對象中查看是否已經(jīng)緩存過該實(shí)例,如果有就直接將緩存的vnode的componentInstance(組件實(shí)例)覆蓋到目前的vnode上面,否則將vnode存儲在cache中。

React中的解決方案

在react中沒有提供類似于vue的keep-alive的解決方案,這意味這我們可能需要自己編寫一些代碼或者通過一些第三方的模塊來解決。

在React項(xiàng)目GitHub的該issue中進(jìn)行了相關(guān)討論,開發(fā)維護(hù)人員給出了兩種方式來解決:

  • 將數(shù)據(jù)與組件分開緩存。例如,你可以將state提升到一個不會被卸載的父級組件,或者像redux一樣將其放在一個側(cè)面緩存中。我們也正在為此開發(fā)一類的API支持(context)。

  • 不要去卸載你要“保持活動”的視圖,只需使用style={{display:'none'}}屬性去隱藏它們。

如何解決vue spa應(yīng)用中的路由緩存問題

1. 集中的狀態(tài)管理恢復(fù)快照方式

在React中通過redux或mobx集中的狀態(tài)管理來緩存頁面數(shù)據(jù)以及滾動條等信息,以達(dá)到緩存頁面的效果。

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

handleClik = () => {
 在點(diǎn)擊進(jìn)入下一級頁面前先保存當(dāng)前的滾動距離
 const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
 const {saveScrollTop} = this.props;
 saveScrollTop(scrollTop);
}

首先我們可以在redux中為頁面定義異步的action,將請求回來的數(shù)據(jù)放入集中的store中(redux的該相關(guān)具體用法不在細(xì)述)。在sotre里我們可以保存當(dāng)前頁面的數(shù)據(jù)源、滾動條高度以及其他一些可能要用到的分頁數(shù)據(jù)等來幫助我們恢復(fù)狀態(tài)。

在componentDidMount生命周期里,首先根據(jù)redux里store中的對應(yīng)的字段,判斷是否已經(jīng)加載過數(shù)據(jù)源。如果已經(jīng)緩存過數(shù)據(jù)則不再去請求數(shù)據(jù)源,只去恢復(fù)一下store里的存儲過的一些滾動條位置信息等。如果還未請求過數(shù)據(jù),就使用在redux中定義的異步action去請求數(shù)據(jù),在將數(shù)據(jù)在reducer里將數(shù)據(jù)存到store中。 在render函數(shù)里,我們只需要讀取redux里存儲的數(shù)據(jù)即可。

為了保留要緩存頁面的一些狀態(tài)信息,如滾動條、分頁、操作狀態(tài),我們可以在進(jìn)行對應(yīng)操作時候?qū)⑦@些信息存入redux的store中,這樣當(dāng)我們恢復(fù)頁面時,就可以將這些對應(yīng)狀態(tài)一一讀取并還原。

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

想要display的屬性來切換顯示隱藏路由組件,首先要保證路由組件不會在url變化時候被卸載。在react-router中最使用的Route組件,它可以通過我們定義的path屬性來與頁面路徑來進(jìn)行匹配,并渲染對應(yīng)的組件,從而達(dá)到保持UI與URL同步變化的效果。

首先簡要看下Route組件的實(shí)現(xiàn) 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>
);

上述代碼出現(xiàn)在關(guān)鍵的render方法最后的return中

Route組件會根據(jù)props對象中的match屬性來確定是否要渲染組件,如果match匹配到了就使用Route組件上傳遞的component或者render屬性來渲染對應(yīng)組件,否則就返回null。

然后溯源而上,我們找到了props對象中關(guān)于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方法來根據(jù)當(dāng)前的location.pathname來判斷是否匹配。

然而在react router的Route組件API文檔中我們似乎沒有看到過有關(guān)于computedMatch的介紹,不過在源碼中有一行這樣的注釋

// <Switch> already computed the match for us

該注釋說在<Switch>組件中已經(jīng)為我們計(jì)算了該匹配。

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

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

我們翻開Switch組件的實(shí)現(xiàn)源碼:

let element, match; // 定義最后返回的組件元素,和match匹配變量
 
 React.Children.forEach(this.props.children, child => {
  if (match == null && React.isValidElement(child)) { // 如果match沒有內(nèi)容則進(jìn)入該判斷
   element = child;
 
   const path = child.props.path || child.props.from;
 
   match = path // 該三元表達(dá)式只有在匹配到后會給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也是根據(jù)matchPath來判斷是否匹配的,matchPath是react router中的一個API,該方法會根據(jù)你傳入的第一個參數(shù)pathname與第二個要匹配的props屬性參數(shù)來判斷是否匹配。如果匹配就返一個對象類型并包含相關(guān)的屬性,否則返回null。

在React.Children.forEach循環(huán)子元素的方法中,matchPath方法判斷當(dāng)前pathname是否匹配,如果匹配就給定義的match變量進(jìn)行賦值,所以當(dāng)match被賦值以后,后續(xù)的循環(huán)就也不會再進(jìn)行匹配賦值,因?yàn)镾witch組件只會渲染第一次與之匹配的組件。

3. 實(shí)現(xiàn)一個路由緩存組件

我們知道Switch組件只會渲染第一項(xiàng)匹配的子組件,如果可以將匹配到的組件都渲染出來,然后只用display的block和none來切換是否顯示,這也就實(shí)現(xiàn)了第二種解決方案。

參照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)) { // 驗(yàn)證是否為是react element
    const {path} = child.props;
    const match = matchPath(location.pathname, {...child.props, path});

    if (match && (include === true || include.includes(path))) {
     //如果匹配,則將對應(yīng)path的computedMatch屬性加入cache對象里
     //當(dāng)include為true時,緩存全部組件,當(dāng)include為數(shù)組時緩存對應(yīng)組件
     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組件會根據(jù)它的this.props.computedMatch來判斷是否要渲染該組件。

我們在組件內(nèi)部創(chuàng)建一個cache對象,將已經(jīng)匹配到的組件的computedMatch屬性寫入該緩存對象中。這樣即使當(dāng)url不再匹配時,也能通過讀取cache對象中該路徑的值,并使用React .cloneElement方法將computedMatch屬性賦值給組件的props。這樣已緩存過的路由組件就會被一直渲染出來,組件就不會被卸載掉。

因?yàn)榻M件內(nèi)部可能會包裹多個路由組件,所以使用React.Children.map方法將內(nèi)部包含的子組件都循環(huán)返回。

為了UI與路由對應(yīng)顯示正確,我們通過當(dāng)前的計(jì)算得出的match屬性,來隱藏掉不匹配的組件,只為我們展示匹配的組件即可。如果你不想在組件外再套一層div,也可以在組件內(nèi)部通過this.props.match中的display屬性來切換顯示組件。

仿照vue keep alive的形式,設(shè)置一個 include 參數(shù)API。當(dāng)參數(shù)為true時緩存內(nèi)部的所有子組件,當(dāng)參數(shù)為數(shù)組時則緩存對應(yīng)的path路徑組件。

使用效果

如何解決vue spa應(yīng)用中的路由緩存問題

在最初時,從未被url匹配過的組件不會被渲染,里面的dom結(jié)構(gòu)是空的。

如何解決vue spa應(yīng)用中的路由緩存問題

當(dāng)切換到對應(yīng)組件時,當(dāng)前的組件被渲染,而之前已匹配的組件不會被卸載,只是被隱藏

如何解決vue spa應(yīng)用中的路由緩存問題

在輸出日志中可以看到,當(dāng)我們不停的來回切換時,componentDidMount生命周期也只執(zhí)行一次,在props.match中我們可以獲取到當(dāng)前的display值。

4. 另外的也可以采用一些第三方組件模塊來實(shí)習(xí)緩存機(jī)制:

react-keeper
react-router-cache-route
react-live-route

關(guān)于“如何解決vue spa應(yīng)用中的路由緩存問題”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學(xué)到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI