问题描述
当我们相对页面组件进行缓存时,需要使用<keep-alive>包裹<router-view>
<keep-alive :include="cacheViews">
<router-view/>
</keep-alive>
如果我们只有一级路由,这样是没毛病的。但是在大多数情况下,一个网站的路由都存在多级嵌套的情况(也就是存在router-view嵌套的情况)。此时由于keep-alive只能作用到下一层<router-view>,导致页面组件缓存不生效。
此时我在网上搜索解决方案,找到了使用操作$route.matched数组来拍平路由嵌套的解决方案:传送门
解决方案
此前在
Menu3组件前还嵌套了两层Layout组件,经过拍平处理后,过滤掉了两层Layout。具体代码如下:
function handleKeepAlive(to) {
if (to.matched && to.matched.length > 2) {
for (let i = 0; i < to.matched.length; i++) {
const element = to.matched[i];
if (element.components.default.name === 'pageView') {
to.matched.splice(i, 1);
handleKeepAlive(to);
}
}
}
}
router.beforeEach((to, from, next) => {
//....
handleKeepAlive(to)
//....
}
遗留问题
这种简单粗暴的方法会不会有问题,在网上又着了一下,果然有问题:传送门
由于暴力的修改了matched会导致面包屑或者用到matched的地方出现问题。解决方案就是备份一份原始的matched给面包屑去用。
matched与router-view的关系
网上大多数都是这种解决方案,但是为什么我们修改了matched就会导致页面路由嵌套的变化呢?我在网上找到了router-view对于matched的使用原理。
router-view是一个 functional 组件,渲染路径匹配到的视图组件。<router-view> 渲染的组件还可以内嵌自己的 <router-view>,根据嵌套路径,渲染嵌套组件
它只有一个名为name的props,这个name还有个默认值,就是default,一般情况下,我们不用传递name,只有在命名视图的情况下,我们需要传递name,命名视图就是在同级展示多个视图,而不是嵌套的展示出来,
router-view组件渲染时是从VueRouter实例._route.matched属性获取需要渲染的组件,也就是我们在vue内部的this.$route.matched上获取的,举个栗子:
<div id="app">
<router-link to="/info/">info页</router-link>
<router-link to="/info/face">page页</router-link>
<hr/>
<router-view></router-view>
</div>
<script>
const info = { template:'<div>info Page<router-view><br/></router-view></div>'} //外层组件
const page = { template:'<div>face Page</div>'} //内层组件
const routes = [
{
path:'/info/',
component:info,
children:[
{path:'face',component:page} //使用了嵌套路由
]
}
]
const app = new Vue({
el:'#app',
router:new VueRouter({routes})
})
</script>
当路由到info页时,控制台打印app.$route.matched
当路由到page页时,在控制台打印app.$route.matched,输出如下:
可以看到matched中保存所有父子组件信息,索引从0开始,依次是顶层组件、然后是一层层下来的子组件。router-view组件内部render实现时就会读取这个matched属性的,如下:
var View = {
name: 'RouterView',
functional: true, //函数式组件
props: {
name: {
type: String,
default: 'default'
}
},
render: function render (_, ref) {
var props = ref.props; //获取props ;例如:{name: "default"}
var children = ref.children; //获取所有子节点
var parent = ref.parent; //父组件的引用
var data = ref.data;
// used by devtools to display a router-view badge
data.routerView = true;
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
var h = parent.$createElement; //获取父组件的$createElement函数引用 这样组件在执行render时可以用命名插槽
var name = props.name;
var route = parent.$route; //当前的路由地址
var cache = parent._routerViewCache || (parent._routerViewCache = {}); //获取父组件的_routerViewCache属性,如果没有则初始化为空对象
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
var depth = 0; //组件嵌套的层次
var inactive = false; //是否在keep-alive组件内
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
if (parent._inactive) { //如果parent._inactive存在
inactive = true; //则设置inactive为true
}
parent = parent.$parent;
}
data.routerViewDepth = depth; //组件嵌套的层次
// render previous view if the tree is inactive and kept-alive
if (inactive) {
return h(cache[name], data, children)
}
var matched = route.matched[depth]; //从matched属性当中获取当前层次的路由对象,这里保存了需要渲染的组件,这就是上面我们通过app.$route.matched获取的对象
// render empty node if no matched route
if (!matched) {
cache[name] = null;
return h()
}
var component = cache[name] = matched.components[name]; //获取需要渲染的组件
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
data.registerRouteInstance = function (vm, val) {
// val could be undefined for unregistration
var current = matched.instances[name];
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val;
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
matched.instances[name] = vnode.componentInstance;
};
// resolve props
var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass);
// pass non-declared props as attrs
var attrs = data.attrs = data.attrs || {};
for (var key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key];
delete propsToPass[key];
}
}
}
return h(component, data, children) //最后渲染该组件
}
}
所以router-view通过判断当前组件的嵌套层次,然后通过这个层次从route.matches数组中获取当前需要渲染的组件,最后调用全局的$createElement来创建对应的VNode完成渲染的。