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>
这样下来,这个需求就算是差不多完成了,写的有点乱,因为是根据自己的需求改的,大部分都是借鉴了别人的方法,所以就只是在这里展示一下。