實現全站的頁面緩存,前進刷新,返回走緩存,並且能記住上一頁的滾動位置,參考了很多技術實現,github上的導航組件實現的原理要麼使用的keep-alive,要麼參考了keep-alive的源碼,但是隻用keep-alive沒法實現相同path,不同參數展示不同view,這就有點坑了,所以需要結合自己要實現的功能,適當改造keep-alive,爲了實現每次前進都能刷新,返回走緩存還能自動定位的功能,文章陸續從以下幾個方面展開講:兩套技術方案可選,最後定的技術方案的原因,實現的功能和原理,踩過的坑
方案一:vue的keep-alive組件
具體使用如下:
<keep-alive max="10">
<router-view/>
</keep-alive>
爲什麼這麼使用?
如vue官網(https://cn.vuejs.org/v2/api/#...)介紹:
<keep-alive> 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。和 <transition> 相似,<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在父組件鏈中。
當組件在 <keep-alive> 內被切換,它的 activated 和 deactivated 這兩個生命週期鉤子函數將會被對應執行。主要用於保留組件狀態或避免重新渲染。
因爲緩存的需要通常出現在切換頁面時,所以就需要結合vue-router的router-view來實現
爲什麼keep-alive能實現緩存?
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(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]) {
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])
}
如上keep-alive源碼,其中render函數是這樣實現的,要渲染的試圖組件作爲插槽內容被獲取到,當渲染到路徑匹配到的視圖組件時會根據vnode存儲的內容拿到對應的name,一次將這些組件實例放到變量cache中,這樣根據路由就可以找到緩存的vnode,返回給createComponent方法去執行initComponent,vue組件渲染這塊的代碼如下
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
這裏會有 vnode.elm 緩存了 vnode 創建生成的 DOM 節點。所以對於首次渲染而言,除了在 <keep-alive> 中建立緩存,和普通組件渲染沒什麼區別。從進入到返回的大致執行流程如下
能實現的功能
能夠把要緩存的組件渲染的vnode記到cache裏邊,當返回的時候用緩存裏邊的dom直接渲染,還有keep-alive組件提供的include 和 exclude屬性,可以有條件的緩存想緩存的組件,如果配置了 max 並且緩存的長度超過了這個max的值,還要從緩存中刪除第一個
存在的問題
存在的問題是存儲vnode節點的key是name,也就是定義路由時組件對應的name,這就會導致同樣的path,不同參數的時候打開的是從cache裏邊拿到的vnode,會渲染出同樣的視圖出來,但是很多業務場景都是根據參數來顯示不同內容,而keep-alive底層並沒有對此做擴展,可以看下keep-alive源碼
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
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.key就是路由裏邊定義的name,所以要用這套方案來實現的根據不同參數展示不同視圖的功能就要對這裏的key做改造,但是keep-alive是vue自帶的,沒法改底層,然後就誕生了我的第二套方案
方案二:navigation組件,scrollbehavior
github上找到類似功能的組件vue-navigation,這個vue組件可以實現返回走緩存,底層原理跟keep-alive一樣,實際上是改寫了keep-alive組件,前進刷新時新增了一個參數VNK,這樣在路由發生變化的時候都會用給url帶一個參數,並且cache的key取值依賴這個參數,借鑑這個組件的思路,做了一個類似keep-alive的組件,其中key的值是getKey方法獲取的,改寫以後的render方法如下
render () {
var vnode = this.$slots.default ? this.$slots.default[0] : null
if (vnode) {
vnode.key = vnode.key || (vnode.isComment ? 'comment' : vnode.tag)
const { cache, keys } = this
var key = getKey(this.$route, keyName)
if (vnode.key.indexOf(key) === -1) {
vnode.key = '__navigation-' + key + '-' + vnode.key
}
if (cache[key]) {
if (vnode.key === cache[key].key) {
vnode.componentInstance = cache[key].componentInstance
} else {
cache[key].componentInstance.$destroy()
cache[key] = vnode
}
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
}
getKey方法實現
//url上新增參數vnk的值
export function genKey() {
// const t = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
const t = 'xxxxxxxx'
return t.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0
const v = c === 'x' ? r : (r & 0x3 | 0x8)
return v.toString(16)
})
}
//
export function getKey(route, keyName) {
return `${route.name || route.path}?${route.query[keyName]}`
}
通過新寫一個install方法掛載這個導航組件到vue上就可以實現前進刷新,返回走緩存,並且可以配置最大緩存數,後續開源到github
最後剩下返回上一頁記住上一頁的位置,之所以沒有用開源的這個組件的記位置,是因爲直接套用需要改整體佈局,height:100%;樣式造成$(windows).scrollTop失效,整體考慮改造成本較大,還是使用了vue-router提供的scrollBehavior,在路由配置裏引入
實現如下:
var scrollBehavior = async (to, from, savedPosition) => {
if (savedPosition) {
return savedPosition
} else {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ x: 0, y: to.meta.savedPosition || 0 })
}, 300)
})
}
}
const router = new VueRouter({
mode: 'history',
scrollBehavior,
routes: [{
path: '',
redirect: '/mobile/home.html',
meta: {
needMtaReport: true,
parentsStyle: {
height: '100%',
minHeight: '100%'
}
}
},
{
name: 'scienceCompetition',
path: '/mobile/scienceCompetition.html',
component: scienceCompetition
}]
}
總結:
1.單頁緩存下js加載解析編譯執行的時間縮短了,返回的時候由於走緩存js腳本的佔用時間完全可以忽略,從而整體上縮減了頁面的加載渲染時間
2. 因爲項目以前不是單頁,代碼裏邊定義了很多全局變量或者全局事件綁定,改成單頁後全局變量的值依然存在,就會導致業務邏輯出現bug,所以使用單頁需要注意全局變量或是事件的謹慎使用,具體的踩坑記錄在https://www.cnblogs.com/after...
3.通過push進入下一頁時,head裏邊會累加前面頁面的靜態資源,訪問的頁面越多,最後的頁面掛的靜態的資源越多,返回的時候並不會減少已經加載的靜態資源,單頁緩存是典型的空間換時間的方案,內存的開銷比較大,能否對資源動態增減以及內存佔用的優化一直在探索中,暫時沒有找到很好的解決方法。。。。。