在移動端使用vue-router和keep-alive的方法示例
對于web開發(fā)和移動端開發(fā),兩者在路由上的處理是不同的。對于移動端來說,頁面的路由是相當(dāng)于棧的結(jié)構(gòu)的。vue-router與keep-alive提供的路由體驗與移動端是有一定差別的,因此常常開發(fā)微信公眾號的我想通過一些嘗試來將兩者的體驗拉近一些。
目標(biāo)
問題
首先一個問題是keep-alive的行為。我們可以通過keep-alive來保存頁面狀態(tài),但這樣的行為對于類似于APP的體驗是有些奇怪的。例如我們的應(yīng)用有首頁、列表頁、詳情頁3個頁面,當(dāng)我們從列表頁進(jìn)入詳情頁再返回,此時列表頁應(yīng)當(dāng)是keep-alive的。而當(dāng)我們從列表頁返回首頁,再次進(jìn)入列表頁,此時的列表頁應(yīng)當(dāng)在退出時銷毀,并在重新進(jìn)入時再生成才比較符合習(xí)慣。
第二個問題是滾動位置。vue-router提供了 scrollBehavior 來幫助維護(hù)滾動位置,但這一工具只能將頁面作為滾動載體來處理。但我在實(shí)際開發(fā)中,喜歡使用flex來布局頁面,滾動列表的載體常常是某個元素而非頁面本身。
使用環(huán)境
對于代碼能正確運(yùn)行的環(huán)境,這里嚴(yán)格假定為微信(或是APP中內(nèi)嵌的web頁面),而非通過普通瀏覽器訪問,即:用戶無法通過直接輸入url來跳轉(zhuǎn)路由。在這樣的前提下,路由的跳轉(zhuǎn)是代碼可控的,即對應(yīng)于vue-router的push、replace等方法,而唯一無法干預(yù)的是瀏覽器的回退行為。在這樣的前提下,我們可以假定,任何沒有通過vue-router觸發(fā)的路由跳轉(zhuǎn),是 回退1個記錄 的回退行為。
改造前
這里我列出改造前的代碼,是一個非常簡單的demo,就不詳細(xì)說了(這里列表頁有兩個列表,是為了展示改造后的滾動位置維護(hù)):
// css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
#app {
height: 100%;
}
// html <div id="app"> <keep-alive> <router-view></router-view> </keep-alive> </div>
// js
const Index = {
name: 'Index',
template:
`<div>
首頁
<div>
<router-link :to="{ name: 'List' }">Go to List</router-link>
</div>
</div>`,
mounted() {
console.warn('Main', 'mounted');
},
};
const List = {
name: 'List',
template:
`<div style="display: flex;flex-direction: column;height: 100%;">
<div>列表頁</div>
<div style="flex: 1;overflow: scroll;">
<div v-for="item in list" :key="item.id">
<router-link style="line-height: 100px;display:block;" :to="{ name: 'Detail', params: { id: item.id } }">
{{item.name}}
</router-link>
</div>
</div>
<div style="flex: 1;overflow: scroll;">
<div v-for="item in list" :key="item.id">
<router-link style="line-height: 100px;display:block;" :to="{ name: 'Detail', params: { id: item.id } }">
{{item.name}}
</router-link>
</div>
</div>
</div>`,
data() {
return {
list: new Array(10).fill(1).map((_,index) => {
return {id: index + 1, name: `item${index + 1}`};
}),
};
},
mounted() {
console.warn('List', 'mounted');
},
activated() {
console.warn('List', 'activated');
},
deactivated() {
console.warn('List', 'deactivated');
},
};
const Detail = {
name: 'Detail',
template:
`<div>
詳情頁
<div>
{{$route.params.id}}
</div>
</div>`,
mounted() {
console.warn('Detail', 'mounted');
},
};
const routes = [
{ path: '', name: 'Main', component: Index },
{ path: '/list', name: 'List', component: List },
{ path: '/detail/:id', name: 'Detail', component: Detail },
];
const router = new VueRouter({
routes,
});
const app = new Vue({
router,
}).$mount('#app');
當(dāng)我們第一次從首頁進(jìn)入列表頁時, mounted 和 activated 將被先后觸發(fā),而在此后無論是進(jìn)入詳情頁再回退,或是回退到首頁再進(jìn)入列表頁,都只會觸發(fā) deactivated 生命周期。
keep-alive
includes
keep-alive有一個 includes 選項,這個選項可以接受一個數(shù)組,并通過這個數(shù)組來決定組件的?;顮顟B(tài):
// keep-alive
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
return vnode
}
const { cache, keys } = this
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
remove(keys, key)
keys.push(key)
} else {
cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
這里我注意到,可以動態(tài)的修改這個數(shù)組,來使得本來處于?;顮顟B(tài)的組件/頁面失活。
afterEach
那我們可以在什么時候去維護(hù)/修改includes數(shù)組呢?vue-router提供了 afterEach 方法來添加路由改變后的回調(diào):
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
在這里雖然 afterHooks 的執(zhí)行是晚于路由的設(shè)置的,但組件的 render 是在 nextTick 中執(zhí)行的,也就是說,在keep-alive的render方法判斷是否應(yīng)當(dāng)從緩存中獲取組件時,組件的?;顮顟B(tài)已經(jīng)被我們修改了。
劫持router.push
這里我們將劫持router的push方法:
let dir = 1;
const includes = [];
const routerPush = router.push;
router.push = function push(...args) {
dir = 1;
routerPush.apply(router, args);
};
router.afterEach((to, from) => {
if (dir === 1) {
includes.push(to.name);
} else if (dir === -1) {
includes.pop();
}
dir = -1;
});
我們將router.push(當(dāng)然這里需要劫持的方法不止是push,在此僅用push作為示例)和瀏覽器的回退行為用不同的 dir 標(biāo)記,并根據(jù)這個值來維護(hù)includes數(shù)組。
然后,將includes傳遞給keep-alive組件:
// html
<div id="app">
<keep-alive :include="includes">
<router-view></router-view>
</keep-alive>
</div>
// js
const app = new Vue({
router,
data() {
return {
includes,
};
},
}).$mount('#app');
維護(hù)滾動
接下來,我們將編寫一個 keep-position 指令(directive):
Vue.directive('keep-position', {
bind(el, { value }) {
const parent = positions[positions.length - 1];
const obj = {
x: 0,
y: 0,
};
const key = value;
parent[key] = obj;
obj.el = el;
obj.handler = function ({ currentTarget }) {
obj.x = currentTarget.scrollLeft;
obj.y = currentTarget.scrollTop;
};
el.addEventListener('scroll', obj.handler);
},
});
并對router進(jìn)行修改,來維護(hù)position數(shù)組:
const positions = [];
router.afterEach((to, from) => {
if (dir === 1) {
includes.push(to.name);
positions.push({});
}
...
});
起初我想通過指令來移除事件偵聽(unbind)以及恢復(fù)滾動位置,但發(fā)現(xiàn)使用unbind并不方便,更重要的是指令的幾個生命周期在路由跳轉(zhuǎn)到保活的頁面時都不會觸發(fā)。
因此這里我還是使用 afterEach 來處理路由維護(hù),這樣在支持回退多步的時候也比較容易去擴(kuò)展:
router.afterEach((to, from) => {
if (dir === 1) {
includes.push(to.name);
positions.push({});
} else if (dir === -1) {
includes.pop();
unkeepPosition(positions.pop({}));
restorePosition();
}
dir = -1;
});
const restorePosition = function () {
Vue.nextTick(() => {
const parent = positions[positions.length - 1];
for (let key in parent) {
const { el, x, y } = parent[key];
el.scrollLeft = x;
el.scrollTop = y;
}
});
};
const unkeepPosition = function (parent) {
for (let key in parent) {
const obj = parent[key];
obj.el.removeEventListener('scroll', obj.handler);
}
};
最后,我們分別給我們的列表加上我們的指令就可以了:
<div style="flex: 1;overflow: scroll;" v-keep-position="'list1'"> <!-- --> </div> <div style="flex: 1;overflow: scroll;" v-keep-position="'list2'"> <!-- --> </div>
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Vue實(shí)現(xiàn)表格批量審核功能實(shí)例代碼
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)表格批量審核功能實(shí)例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05
詳解從vue-loader源碼分析CSS Scoped的實(shí)現(xiàn)
這篇文章主要介紹了詳解從vue-loader源碼分析CSS Scoped的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Vue3實(shí)現(xiàn)HTML內(nèi)容預(yù)覽功能
這篇文章主要為大家詳細(xì)介紹了如何使用Vue3實(shí)現(xiàn)HTML內(nèi)容預(yù)覽功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03
Vue中的百度地圖定位BMap.GeolocationControl的用法
BMap.GeolocationControl?是百度地圖API中的一個類,用于添加地理定位控件到地圖上,以便用戶可以通過該控件獲取自己的當(dāng)前位置,本文給大家介紹Vue中的百度地圖定位BMap.GeolocationControl的用法,感興趣的朋友跟隨小編一起看看吧2023-10-10
vue中使用pdfjs-dist?+?turnjs實(shí)現(xiàn)頁面的翻書瀏覽功能
這篇文章主要介紹了vue中使用pdfjs-dist?+?turnjs實(shí)現(xiàn)頁面的翻書瀏覽,通過本文學(xué)習(xí)我們知道了pdfjs-dist 的工作原理是把獲取到的 pbf 的文件的數(shù)據(jù)流, 利用 canvas轉(zhuǎn)換成圖片,本文通過實(shí)例代碼詳解講解,需要的朋友可以參考下2022-10-10

