1,需求分析
公司的項目有這樣一個需求: 同一個list組件,根據傳過來的listId渲染成多個頁面,每個頁面都可以下拉。在返回到不同的list頁面時,要保留當時下拉的位置。
說的我自己都有點懵逼了,畫個圖來示范下吧!
這三個頁面都總用的list.vue這個組件。如果三個頁面都渲染后,通過上方的導航,可以跳到對應的list頁面,當然,也要保留當時下拉的位置。由於這幾個list頁面有下拉,都用的mescroll作為下拉組件。
2,代碼分析
看到這個需求,當時第一時間想的就是keep-alive,但是使用keep-alive的話第二次進入list頁面,頁面還是之前的內容。當然,可以監聽$route來進行判斷加載,但這樣的話就保存不了之前的下拉位置。
當然有同學說可以把之前的位置存起來,如果返回到這個頁面,那就直接下拉到這個位置,這種方法我沒試過,但由於list里面的內容是動態加載的,如果我第一個list加載了兩頁, 進入第二個list,再進入第一個list,這時候數據是重新加載的,就算保存了之前的位置,那最多也是下拉到第一頁,第二頁根本拉不出來。
這里參考了github里面關於這方面的一個組件,vue-navigation
這位老哥的思路是,每進一個頁面,就給$route的query里面加一個唯一標識碼,然后將這個標識碼存在sessionStorage里面,將這個頁面的vue實例緩存起來。后面每進一個頁面,就和session里面保存的進行比較,如果之前存過,就把緩存的vue實例直接渲染出來,沒有存過就繼續新建標識碼,緩存vue實例。 退后的時候就將session里面對應的刪除,緩存的vue實例也刪除。
這位老哥做的和我需要的功能已經很接近了,如果大家是做的移動端單頁面,沒有我這種惡心的導航條,就可以直接用了,很方便。
但是還是有點區別,就是他的只能前進和后退,不能隨便進到之前保存的頁面,而我現在的需求是要隨便跳。
那就在這里將老哥的和我改進的說一下吧:
util.js
export function genKey () { // 設置一個8位隨機的字符串, 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]}` } function isRegExp (pattern) { return Object.prototype.toString.call(pattern).substr(8, 6).toLocaleLowerCase() === 'regexp' } export function matches (pattern, name) { if (Array.isArray(pattern)) { return pattern.indexOf(name) > -1 } else if (typeof pattern === 'string') { return pattern.split(',').indexOf(name) > -1 } else if (isRegExp(pattern)) { return pattern.test(name) } return false } export function isEqualObj (obj1, obj2) { // 比較兩個對象是否相等 if (obj1 === obj2) return true const key1 = Object.getOwnPropertyNames(obj1) const key2 = Object.getOwnPropertyNames(obj2) if (key1.length !== key2.length) return false for (const k of key1) { if (obj1[k] !== obj2[k]) { return false } } return true } export function _defineProperty (obj, key, value) { // 如果原對象有key的值,那么就保留,不然就新加一個屬性,值為value if (key in obj) { Object.defineProperty(obj, key, { value: value, configurable: true, enumerable: true, writable: true }) } else { obj[key] = value } return obj }
router.js
let routers = [] if (window.sessionStorage.WX_REPETITION) { routers = JSON.parse(window.sessionStorage.WX_REPETITION) } export default routers
index.js 可以使我們的組件能像vue.use(xxx) 這樣使用
import Routers from './router' import Repetition from './Repetition' import {getKey, genKey, isEqualObj, _defineProperty} from './utils' export default { install: (Vue, {router, store, moduleName = 'repetition' ,keyName = 'WXK'} = {}) => { if (!router || !store) { throw new Error('this component need options router and store') } store.registerModule(moduleName, { state: { visitedView: [] }, mutations: { addViews: (state, view) => { state.visitedView.push(view) }, deleteViews: (state, view) => { for (let k in state.visitedView) { if (state.visitedView[k].route.path === view.route.path) { state.visitedView.splice(k, 1) break } } }, emptyViews: (state, view) => { state.visitedView.splice(0) } } }) router.beforeEach((to, from, next) => { // 在進入之前,檢查sessionStorage里面保存的已經打開的頁面的記錄,如果有記錄,則將之前的標識碼拿過來用,如果沒有,則從新創建一個標識碼,然后放到query里面,next出去 if (!to.query[keyName]) { // let query = {...to.query} let routers = Routers let dif = true for (let route of routers) { if (route[1].path === to.path && isEqualObj(Object.assign({}, to.query, _defineProperty({}, keyName, null)), Object.assign({}, route[1].query, _defineProperty({}, keyName, null)))) { dif = false query[keyName] = route[1].query[keyName] next({name: to.name, params: to.params, query: query}) return } } if (dif) { query[keyName] = genKey() next({name: to.name, params: to.params, query: query}) } } else { next() } }) router.afterEach((to, from) => { // 判斷這次進入的地址在sessionStorage里面是否存在,如果存在就直接出去, 如果不存在就存進sessionStorage let routers = Routers const name = getKey(to, keyName) for (let route of routers) { if (route.indexOf(name) > -1) { return } } routers.push({name: to.name, query: to.query, path: to.path}) window.sessionStorage.WX_REPETITION = JSON.stringify(routers) }) Vue.component('repetition', Repetition(moduleName, keyName)) // 注冊該組件 } }
Repetition.js 組件的定義函數
import {getKey} from './utils'
export default (moduleName, keyName) => {
return {
name: 'repetition',
data: () => {
return {
}
},
created () {
this.cache = new Map() // 緩存每個打開的頁面的實例
},
render () {
const vnode = this.$slots.default ? this.$slots.default[0] :null
if (vnode) {
vnode.key = vnode.key || (vnode.isComment ? 'comment' : vnode.tag)
const key = getKey(this.$route, keyName)
if (vnode.key.indexOf(key) === -1) {
vnode.key = `repetition-${key}-${vnode.key}`
}
if (this.cache.has(key)) { // 如果cache里面已經保存了需要打開的頁面,那么就直接將cache里面保存的實例賦給當前的vnode
vnode.componentInstance = this.cache.get(key).componentInstance
let visiteds = this.$store.state[moduleName].visitedView.map(item => {
return getKey(item, keyName)
})
for (let cacheKey of this.cache.keys()) { // 這里是我自己項目中需要的,因為要用到導航條
if (visiteds.indexOf(cacheKey) === -1) {
this.cache.get(cacheKey).componentInstance.$destroy()
this.cache.delete(cacheKey)
break
}
}
} else { // 如果cache里面沒有需要打開的頁面,將當前vnode存到cache,
this.cache.set(key, vnode)
this.$nextTick(() => {
let route = this.$route
this.$store.commit(`${moduleName}/addViews`, {
name: route.name,
path: route.path,
query: route.query
})
})
}
vnode.data.keepAlive = true
} else {
if (this.$store.state[moduleName].visitedView.length === 0 && this.cache.size) {
let key0 = [...this.cache.keys()]
this.cache.get(key0[0]).componentInstance.$destroy()
this.cache.delete(key0[0])
}
}
return vnode
}
}
}
導航條那里的vue頁面:
<template> <router-link v-for="tag in visitedView" :key="tag.path" :class="{active:isActive(tag)}" :to="{path:tag.route.path,query:tag.route.query,fullPath:tag.route.fullPath}" tag="span" class="tags-item" > {{tag.title}}<span class="el-icon-close" @click.stop="closeSelectTag(tag)"></span></router-link> </template> <script> import Routers from './router' // 這里還是用的上面的router.js export default { name: 'tagsView', data () { return { } }, methods: { closeSelectTag (tag) { // 關閉當前頁面 let routers = Routers console.log(routers) console.log(tag.route.query.WXK) for (let i in routers) { if (routers[i][1].query.WXK === tag.route.query.WXK) { routers.splice(i, 1) } } window.sessionStorage.WX_REPETITION = JSON.stringify(routers) this.$store.commit('tagsView/deleteviews', tag) if (this.visitedView.length === 0) { this.$router.push({name: 'home'}) } else { let visit = this.visitedView[this.visitedView.length - 1] this.$router.push({path: visit.route.path}) } }, isActive (tag) { return tag.route.path === this.$route.path } }, mounted () { }, computed: { visitedView () { return this.$store.state.repetition.visitedView } }, watch: { } } </script>
這樣下來,這個需求就算是差不多完成了,寫的有點亂,因為是根據自己的需求改的,大部分都是借鑒了別人的方法,所以就只是在這里展示一下。