上一期我们说到了如果想要实现一个路由嵌套,那么就需要判断传递实例化路由时的那个路由信息是否存在children属性,如果有children说明它是二级路由,我们还需要去递归判断,因为它不一定只有一个子路由,接下来实现一下路由嵌套。
1、首先我们在路由文件中配置了路由信息,并且我们嵌套了两个二级路由。
我们可以看到,一级路由是绝对路径,我们二级路由是相对路径,因此找到切入点,我们可以判断是否是子路由。
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/home',
component: () => import('../components/Home/Home.vue')
},
{
path: '/list',
component: () => import('../components/List/List.vue'),
children: [
{
path: 'list1',
component: () => import('../components/List/List1/List1.vue')
},
{
path: 'list2',
component: () => import('../components/List/List2/List2.vue')
}
]
},
]
})
2、自定义router插件定义Router类并且定义一个install方法
和上期一样,我们都需要定义一个router类,并且定义了一个install方法,这都是在第一次实习的基础上做出了小的更改。主要做出了以下几个更改:
(1)、constructor中我们调用了一个方法,我们将监听的方法封装为一个方法,在constructor进行调用(我们当然也可以不封装,封装起来更加直观)
我们可以看到这里跟上一次的逻辑是没有什么大概的变化的,只是把我们监听hash模式以及history的方法封装为一个函数。但是我们封装为函数主要还有一个目的,就是为了我们使用递归匹配所有route的时候方便些。因为我们不管什么模式都需要进行递归我们的路由信息判断它是否存在子级路由信息。
let Vue;
class VueRouter {
constructor(options) {
//保存选项
this.options = options;
// 定义一个current,保存当前的hash值
this.current = location.hash.slice(1,) || '/';// /home. /list/list1
// defineReactive定义响应式的对象
Vue.util.defineReactive(this, 'matched', [])
this.init();
}
init() {
//判断当前的路由模式
if (this.options.mode === 'hash') {
// hashchange事件来监听hash值的变化, 注意this指向问题
addEventListener('hashchange', this.changeHash.bind(this))
} else if (this.options.mode === 'history') {
// history模式监听的事件叫做:popstate, 监听的是浏览器左上角的两个小箭头的变化
addEventListener('popstate', this.changeHistory.bind(this))
}
this.match();
}
//监听hash值变化的函数
changeHash() {
//通过location.hash来获取到hash值
this.current = location.hash.slice(1,)
console.log(this.routesMap, '555');
// console.log(this.current);//当前path路径
// 上一次匹配到的matched清空
this.matched = [];
this.match();
}
// history
changeHistory() {
this.current = location.pathname;
// 上一次匹配到的matched清空
this.matched = [];
this.match();
}
}
(2)、定义match方法为我们匹配路由的函数,作用是为了把我们一级路由一级多级子路由添加到我们定义的数组中,从而我们进行业务逻辑的操作。
这里我们通过递归的方式,将我们的一级路由信息一级子级路由信息全部添加到我们定义的matched数组中,并且我们每次路由发生变化以后会将我们匹配到的路由信息清空,重新再次调用我们的匹配递归方法。这样依次递归,就不会导致我们下一次路由匹配错误。
let Vue;
class VueRouter {
constructor(options) {
//保存选项
this.options = options;
// 定义一个current,保存当前的hash值
this.current = location.hash.slice(1,) || '/';// /home. /list/list1
// defineReactive定义响应式的对象
Vue.util.defineReactive(this, 'matched', [])
this.init();
}
init() {
//判断当前的路由模式
if (this.options.mode === 'hash') {
// hashchange事件来监听hash值的变化, 注意this指向问题
addEventListener('hashchange', this.changeHash.bind(this))
} else if (this.options.mode === 'history') {
// history模式监听的事件叫做:popstate, 监听的是浏览器左上角的两个小箭头的变化
addEventListener('popstate', this.changeHistory.bind(this))
}
this.match();
}
//监听hash值变化的函数
changeHash() {
//通过location.hash来获取到hash值
this.current = location.hash.slice(1,)
console.log(this.routesMap, '555');
// console.log(this.current);//当前path路径
// 上一次匹配到的matched清空
this.matched = [];
this.match();
}
// history
changeHistory() {
this.current = location.pathname;
// 上一次匹配到的matched清空
this.matched = [];
this.match();
}
match(routes) {//递归匹配所有的route
routes = routes || this.options.routes;
//循环routes
routes.forEach(route => {
if (route.path === '/' && this.current.includes(route.path)) {
this.current.push(route)
}
if (route.path !== '/' && this.current.includes(route.path)) {
this.matched.push(route);
if (route.children) {// 递归查找所有route
// this.matched.push(route)
this.match(route.children)
}
}
})
console.log(routes, 'routes');
}
}
(3)、为了逻辑更加直观,将定义router-link全局组件以及router-view全局组件封装起来,以使我们清晰的知道哪个js文件是对哪一块业务逻辑的操作。
import RouterLink from './model/RouterLink'
import RouterView from './model/RouterView'
let Vue;
VueRouter.install = function (_Vue) {
//1、保存Vue
Vue = _Vue
//2、将以下代码延迟到vue实例初始化完毕执行
Vue.mixin({
beforeCreate() {
//判断如果有router属性就执行下方代码
if (this.$options.router) {
Vue.prototype.$router = this.$options.router;
}
}
})
//创建全局组件router-link和router-view
//创建router-link组件
Vue.component('router-link', RouterLink)
//创建router-view组件
Vue.component('router-view', RouterView)
}
以上行为基本上都是对业务逻辑的一个封装,但对于我们路由嵌套还没有根本上的实现。
我们主要是在router-view组件渲染的时候进行业务逻辑的处理,因为是否渲染子组件完全取决于是否存在routerview全局组件。
3、RouterView全局组件的逻辑处理。
由于我们通过每次路由监听都会进行一次路由的匹配,判断是否存在children属性,从而将我们的路由信息传递到我们定义的matched数组当中。
大概思路如下:
(1)、我们给当前的虚拟dom节点标记当前组件时routerView组件(定义布尔值为true)
这样我们每个通过routerView渲染的组件都存在一个routerView的属性,并且值为true
(2)、我们定义当前路由的层级,默认为0。通过获取父节点,循环父节点,判断它的父节点上是否存在我们定义的routerView标记,如果存在,我们将它层级+1这时,我们是二级路由,我们判断父节点也就是我们的一级路由存在routerView标记,那么它就是二级路由,我们层级为二级。
我们会继续循环判断,直到它查找父节点并且没有父节点以后会退出循环。
(3)、这时我们通过router类中定义的matched数组,可以获取到我们当前路由下的一级路由一级子路由,全部添加到这个数组中,我们根据下标可以获取到当前是鸡鸡路由。从而渲染我们的组件。
下面使用图片展示,大家会更加清晰。
match方法通过递归的方式将我们的数据添加到matched数组中,我们可以看到点击一级路由它会获取到一个数组里面是我们一级路由的两个二级路由,当我们点击二级路由时,我们会在数组基础上添加我们当前的二级子路由,如图三所示。这样我们每次存在子路由都可以通过添加到matched数组当中。
这时就与我们定义的当前层级数产生了联系,如果是一级,那么它的层级depath为0,如果是二级路由那么它的depath为1,我们就可以通过下标找到它的组件以及path路径,不理解可以对照图三看看是不是这样。
并且我们通过while循环判断我们当前的routerView的父节点,判断它是否存在生命的routerView标记,如果存在说明我们当前是子路由,依次类推,如果不存在说明我们是一级路由,因此不需要渲染children的子路由。
我们的每一个路由都存在标记的routerView。我们就是通过判断当前路由的父节点是否存在标记routerView来判断是几级路由的。
最后通过下标可以获取到我们所对应的路由组件,通过render()return渲染我们的组件。
代码如下:
export default {
render(h) {
// 标记一下当前的组件是routerView组件
this.$vnode.data.routerView = true;
// 保存当前路由的层级c
let depath = 0;
// 当前routerView组件的父节点
let parent = this.$parent;
console.log(this.$vnode, '$vnode');
while (parent) {
const vnodeData = parent.$vnode && parent.$vnode.data;
// 证明我当前找到的这个节点就是routerView组件
if (vnodeData && vnodeData.routerView) {
// 如果是routerView组件,就让层级+1
depath++;
}
// 不断的向上 查找父节点。直到没有父节点了,退出循环
parent = parent.$parent;
}
// console.log(depath);
const router = this.$router
let component = null;
// 通过depath找到对应的route
let route = router.matched[depath];
if (route) {
component = route.component
}
return h(component)
}
}
4、routerLink全局组件
我们的router-link组件并未做出一些改变,只需要每次history路由模式点击以后,手动调用监听的历史路由模式的方法,这样可以清空我们递归匹配的所有路由信息。
代码如下:
export default {
props: {
to: {
type: String | Object,
required: true
}
},
render(h) {// h 有三个参数,第一个参数创建的元素,第二个参数元素具有的属性, 第三个参数元素的内容
const router = this.$router;
// console.log(router, 'router');
if (router.options.mode === 'hash') {
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
} else if (router.options.mode === 'history') {
return h('a', {
attrs: { href: this.to },
on: {
'click': ev => {
// 1. 阻止a链接的默认跳转行为
ev.preventDefault();
// 2. 调用pushState方法来实现跳转: pushState(obj, title, url)
history.pushState({}, '', this.to)
// 3. 设置current的值
router.current = this.to;
// 4. 点击的时候再次调用changeHistory方法
this.$router.changeHistory();
}
}
}, this.$slots.default)
}
}
};
总结:
总体并未做出很大改变,首先将我们的源码进行了封装,其次由于我们使用到了路由嵌套因此定义了一个递归函数来匹配路由。最后做出极大变化的是router-view组件,我们需要定义标记,并且通过循环判断父节点是否存在我们定义的标记从而判断是第几层级,最后根据层级下标来获取当前子路由的路由信息。