在移動(dòng)端使用vue-router和keep-alive的方法示例
對(duì)于web開(kāi)發(fā)和移動(dòng)端開(kāi)發(fā),兩者在路由上的處理是不同的。對(duì)于移動(dòng)端來(lái)說(shuō),頁(yè)面的路由是相當(dāng)于棧的結(jié)構(gòu)的。vue-router與keep-alive提供的路由體驗(yàn)與移動(dòng)端是有一定差別的,因此常常開(kāi)發(fā)微信公眾號(hào)的我想通過(guò)一些嘗試來(lái)將兩者的體驗(yàn)拉近一些。
目標(biāo)
問(wèn)題
首先一個(gè)問(wèn)題是keep-alive的行為。我們可以通過(guò)keep-alive來(lái)保存頁(yè)面狀態(tài),但這樣的行為對(duì)于類(lèi)似于APP的體驗(yàn)是有些奇怪的。例如我們的應(yīng)用有首頁(yè)、列表頁(yè)、詳情頁(yè)3個(gè)頁(yè)面,當(dāng)我們從列表頁(yè)進(jìn)入詳情頁(yè)再返回,此時(shí)列表頁(yè)應(yīng)當(dāng)是keep-alive的。而當(dāng)我們從列表頁(yè)返回首頁(yè),再次進(jìn)入列表頁(yè),此時(shí)的列表頁(yè)應(yīng)當(dāng)在退出時(shí)銷(xiāo)毀,并在重新進(jìn)入時(shí)再生成才比較符合習(xí)慣。
第二個(gè)問(wèn)題是滾動(dòng)位置。vue-router提供了 scrollBehavior 來(lái)幫助維護(hù)滾動(dòng)位置,但這一工具只能將頁(yè)面作為滾動(dòng)載體來(lái)處理。但我在實(shí)際開(kāi)發(fā)中,喜歡使用flex來(lái)布局頁(yè)面,滾動(dòng)列表的載體常常是某個(gè)元素而非頁(yè)面本身。
使用環(huán)境
對(duì)于代碼能正確運(yùn)行的環(huán)境,這里嚴(yán)格假定為微信(或是APP中內(nèi)嵌的web頁(yè)面),而非通過(guò)普通瀏覽器訪問(wèn),即:用戶無(wú)法通過(guò)直接輸入url來(lái)跳轉(zhuǎn)路由。在這樣的前提下,路由的跳轉(zhuǎn)是代碼可控的,即對(duì)應(yīng)于vue-router的push、replace等方法,而唯一無(wú)法干預(yù)的是瀏覽器的回退行為。在這樣的前提下,我們可以假定,任何沒(méi)有通過(guò)vue-router觸發(fā)的路由跳轉(zhuǎn),是 回退1個(gè)記錄 的回退行為。
改造前
這里我列出改造前的代碼,是一個(gè)非常簡(jiǎn)單的demo,就不詳細(xì)說(shuō)了(這里列表頁(yè)有兩個(gè)列表,是為了展示改造后的滾動(dòng)位置維護(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> 首頁(yè) <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>列表頁(yè)</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> 詳情頁(yè) <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)我們第一次從首頁(yè)進(jìn)入列表頁(yè)時(shí), mounted 和 activated 將被先后觸發(fā),而在此后無(wú)論是進(jìn)入詳情頁(yè)再回退,或是回退到首頁(yè)再進(jìn)入列表頁(yè),都只會(huì)觸發(fā) deactivated 生命周期。
keep-alive
includes
keep-alive有一個(gè) includes 選項(xiàng),這個(gè)選項(xiàng)可以接受一個(gè)數(shù)組,并通過(guò)這個(gè)數(shù)組來(lái)決定組件的?;顮顟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]) }
這里我注意到,可以動(dòng)態(tài)的修改這個(gè)數(shù)組,來(lái)使得本來(lái)處于?;顮顟B(tài)的組件/頁(yè)面失活。
afterEach
那我們可以在什么時(shí)候去維護(hù)/修改includes數(shù)組呢?vue-router提供了 afterEach 方法來(lái)添加路由改變后的回調(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í)行的,也就是說(shuō),在keep-alive的render方法判斷是否應(yīng)當(dāng)從緩存中獲取組件時(shí),組件的?;顮顟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ù)這個(gè)值來(lái)維護(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ù)滾動(dòng)
接下來(lái),我們將編寫(xiě)一個(gè) 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); }, });
并對(duì)router進(jìn)行修改,來(lái)維護(hù)position數(shù)組:
const positions = []; router.afterEach((to, from) => { if (dir === 1) { includes.push(to.name); positions.push({}); } ... });
起初我想通過(guò)指令來(lái)移除事件偵聽(tīng)(unbind)以及恢復(fù)滾動(dòng)位置,但發(fā)現(xiàn)使用unbind并不方便,更重要的是指令的幾個(gè)生命周期在路由跳轉(zhuǎn)到?;畹捻?yè)面時(shí)都不會(huì)觸發(fā)。
因此這里我還是使用 afterEach 來(lái)處理路由維護(hù),這樣在支持回退多步的時(shí)候也比較容易去擴(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>
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
vue2.0使用Sortable.js實(shí)現(xiàn)的拖拽功能示例
本篇文章主要介紹了vue2.0使用Sortable.js實(shí)現(xiàn)的拖拽功能示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02element中el-cascader級(jí)聯(lián)選擇器只有最后一級(jí)可以多選
本文主要介紹了element中el-cascader級(jí)聯(lián)選擇器只有最后一級(jí)可以多選,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01Vue.js中使用${}實(shí)現(xiàn)變量和字符串的拼接方式
這篇文章主要介紹了Vue.js中使用${}實(shí)現(xiàn)變量和字符串的拼接方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07vue日常開(kāi)發(fā)基礎(chǔ)Axios網(wǎng)絡(luò)庫(kù)封裝
這篇文章主要為大家介紹了vue日常開(kāi)發(fā)基礎(chǔ)Axios網(wǎng)絡(luò)庫(kù)封裝示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08element中一個(gè)單選框radio時(shí)的選中和取消代碼詳解
這篇文章主要給大家介紹了關(guān)于element中一個(gè)單選框radio時(shí)的選中和取消的相關(guān)資料,文中通過(guò)圖文以及代碼示例介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考價(jià)值,需要的朋友可以參考下2023-09-09