Vue前進刷新後退不刷新,緩存鏈實現

這是什麼?先看實現效果源碼在這html

場景

首先這個交互我也叫不出專業的名字,大體場景是這樣的:vue

  1. 如今有一個小商城,從首頁(Index)能夠進入商品列表頁面(List),這個List是一個無限列表,如今用戶往下翻,看到某一個商品彷佛比較喜歡,因而點擊進入到商品詳情頁面(Info),看完後以爲比較滿意。恩,可是貨比三家嘛,因而用戶返回商品列表頁面,準備繼續往下瀏覽,這時若是你將用戶以前的列表刷新了會怎麼樣?要是我是很是不爽的,這意味着我須要從新操做一波,定位到剛剛中意的商品後,繼續往下貨比三家。
SomePage -> List: List從新加載
List -> Info -> List: List使用緩存
複製代碼
  1. 當用戶肯定好商品後,決定購買了,因而進入訂單頁(Form),訂單頁默認爲用戶輸入了歷史收貨地址,用戶彷佛有想要提醒發貨的商家,因而填寫了備註,而後檢查下訂單有誤否,發現地址能夠再修改一下,因而點擊收貨地址進入到收貨地址列表頁(Address),選好後咱們自動爲用戶返回到訂單頁並更新收貨地址,若是此時備註沒有了會怎麼樣?
SomePage -> Form: Form從新加載
Form -> Address -> Form: Form使用緩存
複製代碼

以上是簡單舉個例子,實際上這相似我在上家公司作移動端(SPA)時碰到的問題,只不過咱們是商家列表,但交互相似,剛開始我是直接用路由方式解決的,相似將Info做爲List的子頁面,將Address做爲Form的子頁面。實現是實現了,但太麻煩了,維護麻煩啊😂,而且對組內其它夥伴不太友好。直到某一次我決定對其進行重構,重構前也沒有什麼好思路,而後Google了一番茅塞頓開:利用keep-alive的include實現,當時知道了思路,我就沒往下看了,本身動手豐衣足食😁。node

統一一下詞彙,下文中說的類列表頁就是List,類詳情頁就是Infowebpack

踩坑記

剛開始我還踩了一個坑,主要是Vue的keep-alive當時不多接觸,都是Copy......git

首先按照以前使用(copy)keep-alive的套路是像這樣的:github

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"/>
複製代碼

而後我將它改形成這樣web

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<keep-alive :include="include">
  <router-view v-if="!$route.meta.keepAlive"/>
</keep-alive>
複製代碼

就是控制這個include,具體的作法是,先在類列表頁的路由選項meta中增長一個cacheTo字段,表示對指定頁面緩存,值爲一個數組,而後:vue-cli

router.beforeEach((to, from, next) => {
  if (isPageLikeList(from)) {
    // 若是from是類列表頁面
    const fromCacheTo = from.meta.cacheTo
    if (fromCacheTo.indexOf(to.name) > -1) {
      // 若是to是類詳情頁面
      // 將from對應的組件設置緩存
    } else {
      // 移除from緩存
    }
  }
  // ...
})
複製代碼

但想的很美,寫完發現Bug比較嚴重,類列表頁緩存老失效,爲何呢?這就要從keep-alive的include機制提及了,因而看了一下keep-alive的源碼,源碼中有這麼一段npm

// ...
  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },
  // ...
複製代碼

而後繼續往下找偵聽include的回調邏輯數組

function pruneCache (keepAliveInstance: any, filter: Function) {
  // 對於include,filter邏輯爲:include包含組件名時返回true,不然返回false
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      // 若組件不在include範圍中
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode ) {
  const cached = cache[key]
  // 銷燬組件
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  // 清除組件的緩存
  cache[key] = null
  remove(keys, key)
}
複製代碼

大體流程是,include變化後(不論是新增了仍是減小了),若是組件不在include中而且當前的cache[組件name]是有緩存的,就執行pruneCacheEntry,銷燬組件並清除組件的緩存。

那麼這對我上述的邏輯有什麼影響呢?按照我以前的流程,動態控制include,這樣作刪除組件緩存是沒有問題,可是新增就會有問題了,由於新增操做發生在從類列表頁離開進入到類詳情頁以前,此時類列表頁已經存在了,而後源碼中偵聽include的流程中也只有清除緩存的操做。

知道了問題,如何解決就很清晰了

實現

分析

咱們能夠將對include的操做放到類列表頁進入前(router.beforeEach),這樣就能有效緩存類列表頁了,其實這東西就是這麼簡單,先無論三七二十一,只要是類列表頁,進入以前都將其緩存,離開時判斷新路由是不是該類列表頁指定的類詳情頁,若是不是就清除緩存。那麼在實現以前,我們先將場景再細化一下,以前簡直比玩具還玩具......

SomePage -> List: List從新加載

// 第一種
List -> Info -> List: List使用緩存

// 第二種
List -> Info -> SomePage -> List: List從新加載

// 第三種,不限層級
List -> Info -> OtherList -> OtherInfo -> ... -> List: List、Info、OtherList...使用緩存

其中第三種,從OtherInfo返回OtherList時,OtherInfo應該是要被清除緩存的,依次類推。也就是說,返回時不保留以前的緩存
複製代碼

如今,咱們根據例舉的場景將大體邏輯肯定一下。這裏有一個點是很明確的,那就是: 咱們只須要關心路由切換時的tofrom,從這2個路由對象着手去解決。那麼這裏能夠例舉出4中狀況:

  1. tofrom都不是類列表頁
  2. tofrom都是類列表頁
  3. to是類列表頁
  4. from是類列表頁

如今根據這4種狀況細化一下

  1. 第一種狀況,tofrom都不是類列表頁
    • 不須要緩存,而且能夠將以前的緩存所有清除
  2. 第二種狀況,tofrom都是類列表頁
    • to不在from的配置中,清空緩存,同時要新增to緩存;
    • 不然,保留from的緩存,新增to緩存
  3. 第三種狀況,to是類列表頁
    • from不在to的配置中,清空緩存,新增to緩存
    • 不然,無需任何處理
  4. 第四種
    • to不在from的配置中,清空緩存

如何判斷是不是類列表頁?

[
  {
    path: '/list',
    name: 'list',
    meta: {
      cacheTo: ['info']
    }
    // ...
  },
  {
    path: '/info',
    name: 'info',
    // ...
  }
]
複製代碼

如上,在路由中維護一個字段如cacheTo,若是配置了組件名,就認爲是類列表頁面

具體實現

邏輯理清楚咯,接下來具體實現,咱們將它作得通用一點(可拔插),而且儘可能保證對原項目有較小的侵入性,我將它命名爲VKeepAliveChain(緩存鏈)

首先,若是像踩坑記那樣維護include,不太具有可拔插的特性,我還得去搞一個store,那麼這個include可使用Vue.observable處理

// VKeepAliveChain.js
const state = Vue.observable({
  caches: []
})
const clearCache = () => {
  if (state.caches.length > 0) {
    state.caches = []
  }
}
const addCache = name => state.caches.push(name)
複製代碼

爲了不像踩坑記那樣直接使用<keep-alive :include="include">,咱們實現一個函數式組件來解決

// VKeepAliveChain.js
export const VKeepAliveChain = {
  install (Vue, options = { key: 'cacheTo' }) {
    const { key } = options

    // 支持一下自定義key
    if (key) {
      cacheKey = key
    }
    
    // 直接透傳children,因此會像keep-alive同樣,只拿第一個組件節點
    const component = {
      name: 'VKeepAliveChain',
      functional: true,
      render (h, { children }) {
        return h(
          'keep-alive',
          { props: { include: state.caches } },
          children
        )
      }
    }

    Vue.component('VKeepAliveChain', component)
  }
}
複製代碼

如今咱們來實現緩存控制的主要邏輯,因爲要利用router.beforeEach,約定了儘可能小的侵入性,這裏能夠merge一下

// VKeepAliveChain.js
const defaultHook = (to, from, next) => next()
export const mergeBeforeEachHook = (hook = defaultHook) => {
  return (to, from, next) => {
    // 緩存控制邏輯
    // 1. 都不是類列表頁
    // 清空緩存
    // 2. 都是類列表頁
    // 若`to`不在`from`的配置中,清空緩存,同時要新增`to`緩存
    // 保留`from`的緩存,新增`to`緩存
    // 3. 新路由是類列表頁
    // 若`from`不在`to`的配置中,清空緩存,新增`to`緩存
    // 不然,無需處理
    // 4. 舊路由是類列表頁
    // 若`to`不在`from`的配置中,清空緩存

    const toName = to.name
    const toCacheTo = (to.meta || {})[cacheKey]
    const isToPageLikeList = toCacheTo && toCacheTo.length > 0
    const fromName = from.name
    const fromCacheTo = (from.meta || {})[cacheKey]
    const isFromPageLikeList = fromCacheTo && fromCacheTo.length > 0

    if (!isToPageLikeList && !isFromPageLikeList) {
      clearCache()
    } else if (isToPageLikeList && isFromPageLikeList) {
      if (fromCacheTo.indexOf(toName) === -1) {
        clearCache()
      }
      addCache(toName)
    } else if (isToPageLikeList) {
      if (toCacheTo.indexOf(fromName) === -1) {
        clearCache()
        addCache(toName)
      }
    } else if (isFromPageLikeList) {
      if (fromCacheTo.indexOf(toName) === -1) {
        clearCache()
      }
    }
    return hook(to, from, next)
  }
}
複製代碼

那麼整個緩存鏈的功能就實現了,同時我將它發不到了npm v-keep-alive-chain上。

食用方式

首先引入並註冊它

// main.js
import { mergeBeforeEachHook, VKeepAliveChain } from 'v-keep-alive-chain'

Vue.use(VKeepAliveChain, {
  key: 'cacheTo' // 可選的 默認爲cacheTo
})

// 若是你沒有註冊過beforeEach
router.beforeEach(mergeBeforeEachHook())

// 若是有註冊beforeEach
router.beforeEach(mergeBeforeEachHook((to, from, next) => {
  next()
}))
複製代碼

而後在App.vue(視你的狀況而定)中

<keep-alive>
  <router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<VKeepAliveChain>
  <router-view v-if="!$route.meta.keepAlive"/>
</VKeepAliveChain>
複製代碼

接着在router中配置你的需求

[
  {
    path: '/list',
    name: 'list',
    meta: {
      cacheTo: ['info']
    }
    // ...
  },
  {
    path: '/info',
    name: 'info',
    // ...
  }
]
複製代碼

而後就能愉快的玩耍了

注意事項

  1. 路由配置不能少了name屬性,而且這個name須要和組件name同樣
  2. cacheTo優先級小於keepAlive,因此,處理這種需求的頁面不要設置keepAlive
  3. 能夠設置2個頁面以前僅在相互切換時緩存,不過我還沒發現可用的場景
  4. webpack、vue-cli通常我是拿來即用的,打包的腳手架是vue-cli4.0,不多深刻研究這些東西,而後我看了下發布到npm包的源碼,發現有不少無用的polyfill被打進去了,致使Gzip的包都有4Kb多,暫時尚未找到解決方法,知曉的朋友麻煩告知一下啊😂

文章寫的比較快,若有什麼錯誤,能夠下方留言咯

朋友,看到這裏,但願文章對你有啓發,本人很是歡迎技術交流,若是你以爲文章對你有用,還請給老弟一個👍,平時我是不在意這些個的,但因爲我立刻要投遞簡歷了,須要點東西撐門面,沒得辦法,履歷太差了,謝謝你,願生活帶給你美好!!!

相關文章
相關標籤/搜索