您好,登錄后才能下訂單哦!
對于web開發和移動端開發,兩者在路由上的處理是不同的。對于移動端來說,頁面的路由是相當于棧的結構的。vue-router與keep-alive提供的路由體驗與移動端是有一定差別的,因此常常開發微信公眾號的我想通過一些嘗試來將兩者的體驗拉近一些。
目標
問題
首先一個問題是keep-alive的行為。我們可以通過keep-alive來保存頁面狀態,但這樣的行為對于類似于APP的體驗是有些奇怪的。例如我們的應用有首頁、列表頁、詳情頁3個頁面,當我們從列表頁進入詳情頁再返回,此時列表頁應當是keep-alive的。而當我們從列表頁返回首頁,再次進入列表頁,此時的列表頁應當在退出時銷毀,并在重新進入時再生成才比較符合習慣。
第二個問題是滾動位置。vue-router提供了 scrollBehavior 來幫助維護滾動位置,但這一工具只能將頁面作為滾動載體來處理。但我在實際開發中,喜歡使用flex來布局頁面,滾動列表的載體常常是某個元素而非頁面本身。
使用環境
對于代碼能正確運行的環境,這里嚴格假定為微信(或是APP中內嵌的web頁面),而非通過普通瀏覽器訪問,即:用戶無法通過直接輸入url來跳轉路由。在這樣的前提下,路由的跳轉是代碼可控的,即對應于vue-router的push、replace等方法,而唯一無法干預的是瀏覽器的回退行為。在這樣的前提下,我們可以假定,任何沒有通過vue-router觸發的路由跳轉,是 回退1個記錄 的回退行為。
改造前
這里我列出改造前的代碼,是一個非常簡單的demo,就不詳細說了(這里列表頁有兩個列表,是為了展示改造后的滾動位置維護):
// 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 > <div>列表頁</div> <div > <div v-for="item in list" :key="item.id"> <router-link :to="{ name: 'Detail', params: { id: item.id } }"> {{item.name}} </router-link> </div> </div> <div > <div v-for="item in list" :key="item.id"> <router-link :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');
當我們第一次從首頁進入列表頁時, mounted 和 activated 將被先后觸發,而在此后無論是進入詳情頁再回退,或是回退到首頁再進入列表頁,都只會觸發 deactivated 生命周期。
keep-alive
includes
keep-alive有一個 includes 選項,這個選項可以接受一個數組,并通過這個數組來決定組件的保活狀態:
// 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]) }
這里我注意到,可以動態的修改這個數組,來使得本來處于保活狀態的組件/頁面失活。
afterEach
那我們可以在什么時候去維護/修改includes數組呢?vue-router提供了 afterEach 方法來添加路由改變后的回調:
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 的執行是晚于路由的設置的,但組件的 render 是在 nextTick 中執行的,也就是說,在keep-alive的render方法判斷是否應當從緩存中獲取組件時,組件的保活狀態已經被我們修改了。
劫持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(當然這里需要劫持的方法不止是push,在此僅用push作為示例)和瀏覽器的回退行為用不同的 dir 標記,并根據這個值來維護includes數組。
然后,將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');
維護滾動
接下來,我們將編寫一個 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進行修改,來維護position數組:
const positions = []; router.afterEach((to, from) => { if (dir === 1) { includes.push(to.name); positions.push({}); } ... });
起初我想通過指令來移除事件偵聽(unbind)以及恢復滾動位置,但發現使用unbind并不方便,更重要的是指令的幾個生命周期在路由跳轉到保活的頁面時都不會觸發。
因此這里我還是使用 afterEach 來處理路由維護,這樣在支持回退多步的時候也比較容易去擴展:
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 v-keep-position="'list1'"> <!-- --> </div> <div v-keep-position="'list2'"> <!-- --> </div>
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。