单页面框架一个常见的问题就是地址回退的页面缓存,即从某个页面回退到上个页面不必从新加载,并且保留上次来到时的状态。
<keep-alive>简介
<keep-alive>
是vue
的内置组件,并且是一个形象组件,它承受3个属性值:
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多能够缓存多少组件实例,超过此下限时,缓存组中最久没被拜访的组件会销毁。
用<keep-alive>
包裹组件时,会缓存不流动的组件实例,而不是销毁它们,这不包含函数式组件,因为它们没有实例。vue-router
中配合<keep-alive>
缓存页面的个别写法如下
<keep-alive :include="['routeA','routeB', ...]" :max="{{10}}"> <router-view></router-view></keep-alive>
缓存的组件在激活、失活时会触发activated
和deactivated
钩子。
history与location接口
前端更新浏览器地址次要有location
和history
2个接口,除去会从新加载页面的api:
- location.hash=[string] - 不会从新加载页面,触发
hashchange
事件 - history.pushState(...)、history.replaceState(...) - 页面更新与替换,不触发任何事件
- history.back()、history.forward() - 后退后退,触发
hashchange/popstate
事件,浏览器自身的按钮性能与这相似 - history.go([number]) - 当参数是0相当于
reload
,从新加载页面;不为0时与下面的back
、forward
类似
另一边vue-router
提供了hash
和state
2种模式, 默认应用state
, 在不反对html5的环境会降级成hash
。他们与api对应的关系以及会触发的事件查看下表
api或者操作 | vue-router模式 | 触发的事件 |
---|---|---|
location.hash = ... | hash* | hashchange |
history.pushState(...), history.replaceState(...) | state | none |
history.back(), history.forward(), history.go(...) | all | hashchange/popstate |
点击浏览器后退/后退 | all | hashchange/popstate |
*阐明:vue-router
若设置为hash
模式,也并不一定调用location.hash
办法, 查源码可知底层仍旧是优先调用pushState
办法, 不反对的环境才会降级成location.hash
。
//vue-router 源码function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)); } else { window.location.hash = path; }}function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)); } else { window.location.replace(getUrl(path)); }}
vue-route缓存历史的难点
直观的看,<keep-alive>
只须要页面组件名字,实现不会很难。实际上,include
的值在路由后退后退时必须是变动的,否则会产生很多凌乱。
思考这种状况:routeA
和routeB
都须要缓存,从routeA
进入到routeB
再回退到routeA
后,此时routeB
是缓存未激活状态,如果此时再进入routeB
看到的就是缓存的页面,而不会刷新,这显然会呈现bug。正确的做法是从routeB
回退后,include
就须要去掉routeB
的了。
所以随着路由后退后退批改include
,保障只有history
里的路由被缓存就十分必要。
个别的做法是利用全局钩子,然而钩子不能判断是后退还是后退,这里论述下我的办法。
vuex存储路由数据
先不思考如何怎么实现代码,首先设计数据结构贮存历史路由数据。显然数组最直观,路由变动操作对应于增删数组末位项。
此时数据结构如下图:
然而有一种状况比拟非凡,浏览器后退再后退时,此时只触发了2次pop,而pop事件不带url地址,无奈取得必要信息。看上去浏览器的路由历史齐全没有变动,然而数组最初一项却是空了。
所以后退的时候删除数组末位项行不通,一个方法是把路由都保留下来,而后用索引标识以后路由的地位。同时设置一个参数direction标识路由是后退还是后退。
更改模型后的数据结构如下图:
数据用vuex
保留。
//store数据结构state = { records: [], //历史路由数组 index: 0, //以后路由索引 direction: '', //history变动方向, forward/backward}
路由变动时对应的数据变动
- push新路由, 数组增加新数据,
direction
是forward - replace路由, 数组末位项替换数据,
direction
是forward - pop后退/后退, 更改索引
index
,direction
须要判断
路由记录独自写成一个module
:
//history.js//假设route的meta里蕴含keepAlive和componentName属性const formRecord = (vueRoute) => { return { name: vueRoute.name, path: vueRoute.fullPath, keepAlive: vueRoute.meta && vueRoute.meta.keepAlive, componentName: r.meta && r.meta.componentName ? r.meta.componentName : '' }}export default { namespaced: true, state: { records: [], //历史路由数组 index: 0, //以后路由索引 direction: '', //history变动方向, forward/backward }, getters: { routes: state => { const { records, index } = state if(records.length > 0 && index < records.length) { return records.slice(0, index + 1) } return [] } }, mutations: { //记录 router.push PUSH_ROUTE(state, vueRoute) { const record = formRecord(vueRoute) const { records, index } = state if (index + 1 < records.length) { records = records.slice(0, index + 1) } records.push(record) state.records = records state.index = records.length - 1 state.direction = 'forward' }, //记录 router.replace REPLACE_ROUTE(state, vueRoute) { const record = formRecord(vueRoute) const { records, index } = state if (index + 1 < records.length) { records = records.slice(0, index + 1) } records.pop() records.push(record) state.records = records state.index = index state.direction = 'forward' }, //记录 router.pop 后退/后退 //count是跳跃的历史记录数, >0是后退, <0是回退,path是以后的location.href POP_ROUTE(state, { count, path }) { let { records, index, direction } = state if (count) { direction = count > 0 ? 'forward' : 'backward' index += count index = Math.min(records.length, index) index = Math.max(0, index) } else { //后退 if (index > 0) { let prevPath = records[index - 1].path if (prevPath === path) { direction = 'backward' index -= 1 } } //后退 if (index < records.length - 1) { let nextPath = records[index + 1].path if (nextPath === path) { direction = 'forward' index += 1 } } } state.records = records state.index = index state.direction = direction } }}
在vux
中应用
//store.jsimport history from './history'Vue.use(Vuex)export default new Vuex.Store({ modules: { history }})
记录路由
数据结构设计好了,该把数据放进去了,然而路由什么时候变动,怎么获取路由信息是个难点。
大体的思路分2种:1.事件监听;2.全局钩子
从源代码上看vue-router
不论什么模式底层优先调用pushState
、replaceState
,这2个办法不触发任何事件,事件监听的想法显然走不通。
另一方面,vue-router
提供了导航的全局钩子,这如同代替了事件监听
router.beforeEach((to, from, next) => { /* ... */ })
然而尝试过之后发现beforeEach
只给了路由信息,没有给出引起路由变动的办法,到底是replace
还是push
,不晓得办法路由数组就不精确。全局钩子的想法也只能放弃。
从以上两点来看路由的push
和replace
是无奈精确监听的,这也要求咱们换个思路,不是去监听路由变动,而是想方法挖掘引起变动的办法。
能够看到vue-router
输入的是对象,对象中蕴含了push
和replace
办法,咱们能够继承对象重写办法,在调用的时候就记录下路由(当然也能够自定事件)。
继承Vue Router对象
class myRouter extends VueRouter { push() { ... super.push() } replace() { ... super.replace() }}
这样push
和replace
记录好了,还有pop
事件怎么解决。pop
其实分2种状况,一种是router.go()
,另一种是用户操作浏览器后退/后退。对于前一种能够重写router,后一种须要用钩子事件,并且判断不是router
办法导致的。
残缺的代码如下
let routerTrigger = falseclass myRouter extends VueRouter { push(location, onComplete, onAbort) { routerTrigger = true store.commit('history/PUSH_ROUTE', super.resolve(location).resolved) super.push(location, onComplete, onAbort) } replace(location, onComplete, onAbort) { routerTrigger = true store.commit('history/REPLACE_ROUTE', super.resolve(location).resolved) super.replace(location, onComplete, onAbort) } go(n) { if (n !== 0) { routerTrigger = true store.commit('history/POP_ROUTE', { count: n }) super.go(n) } else { window.location.reload() } }}const router = new MyRouter(...)router.afterEach((to, from) => { if (to.matched.length > 0 && store.state.history.records.length === 0) { store.commit('history/PUSH_ROUTE', to) } else if (!routerTrigger && to.fullPath) { store.commit('history/POP_ROUTE', { path: to.fullPath }) } routerTrigger = false})
app.vue
里能够计算出须要缓存的组件数组。
<keep-alive :include="keepAliveComponents"> <router-view></router-view> </keep-alive> computed: { ...mapGetters('history', ['routes']), keepAliveComponents() { let array = [] if (this.routes) { array = this.routes.filter(r => !!r.keepAlive).map(h => h.componentName) } return array } }
因为历史路由全副被记录在vuex里,所以是能够更加细粒度的管制缓存数组的。比方在store
减少一个人为的数组,每次获取历史数组时调整路由的keep-alive
值
//store ... state: { manualRecords: [], }, getters: { routes: state => { const routes = [] const { records, index } = state if (records.length > 0 && index < records.length) { routes = records.slice(0, index + 1) } routes.map((item) => { const m = this.manualRecords.find((i) => i.name === item.name) if (m) item.keepAlive = m.keepAlive return item }) return routes } }, mutations: { EDIT_KEEPALIVE(state, routeName, keepAlive) { let { manualRecords } = state manualRecords = manualRecords.filter((item) => item.name !== routeName) manualRecords.push({name: routeName, keepAlive}) state.manualRecords = manualRecords } }