1从VueRouter使用分析
首先,我们在项目中装好VueRouter之后,会自动生成一个router文件夹,然后我们从自带的index.js文件来看。
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
// 1.应用插件
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
// 2.创建实例
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
这里引入了VueRouter之后使用了vue.use()所以我们之后手写的时候需求把我们手写的router变成插件的形式。
在我们进行配置之后进行实例的创建并导出。
**在main.js中使用。**
import router from './krouter'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
这里挂载了router的实例,这里这么做的原因是想要能在全局进行使用,所以我们在插件中使用的时候应该要进行Vue.prototype.$router = router。这样我们在其他组件中才能正常的使用$router。
2 VueRouter工作原理分析
当我们在组件中使用的时候会用到<router-link> 和 router-view,这两个组件是可以直接使用的,所以我们的插件里要有这两个组件的声明和注册。
接着,我们用一幅图来大致描述一下VueRouter的工作原理。
基本思路我们已经有了,现在就可以开始着手实现了。
3 VueRouter实现
我们先看看如何创建的router实例
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
他通过new传入关键字来创建,所以我们创建一个VueRouter类
// crouter/cvue-router.js
let Vue;
class CVueRouter {
constructor(option) {
this.$option = option
}
}
CVueRouter.install = function(_Vue) {
Vue = _Vue
}
export default CVueRouter
这里创建一个全局的Vue是因为vue-router是一个组件,如果我们在这里面也把Vue包含了,这个包就会非常的大。而使用vue.use()时必须提供install方法,会默认调用Install方法并且传入Vue的构造函数,所以我们这里通过`_Vue`就能接受Vue了。这样就可以很好的避免上面的情况了。
我们的VueRouter创建完成之后,我们首先要做的就是让我们的router能在全局使用,我们的rouer配置只在根实例里面才能拿到,要在全局使用我们得拿到根实例里面的配置。于是,我们使用mixin在全局进行混入。这样我们就能拿到每个实例,然后找到根实例,将配置进行全局的挂载。
CVueRouter.install = function(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
console.log('this', this);
if(this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
}
接下来我们就需要来实现一下router-link和router-view这两个全局组件了。
首先因为要创建组件,所以我们用Vue.component来创建组件。
Vue.component('router-link', {
render(h) {
return h()
},
})
router-link的本质其实就是a标签,a标签的使用一般都是<a href="#/about">abc</a>然后我们在使用router-link时都是<router-link to="/about">xxx</router-link>,于是createElement要做什么就很明确了。
h('a', { attrs: { href: '#' + to的参数 } }, 内容)
想要拿到to的参数,我们需要使用props,而内容这里就用上了插槽,我们通过默认插槽就可以直接拿到内容,所以。
Vue.component('router-link', {
props: {
to: {
type: String,
require: true
}
},
render(h) {
return h('a', { attrs: { href: '#' + this.to } }, this.$slots.default)
},
})
接下来我们创建router-view组件——根据当前路径在路由表中获取到组件实例,直接进行渲染。
constructor(option) {
this.$option = option;
this.current = "/";
window.addEventListener("hashchange", () => {
console.log(window.location.hash.slice(1));
this.current = window.location.hash.slice(1);
});
}
Vue.component('router-view', {
render(h) {
let component = null
this.$router.$option.routes.forEach(route => {
if(route.path === this.$router.current) {
component = route.component
}
})
return h(component)
},
})
首页显示
点击about切换页面
现在页面已经能正常显示了。
但是现在又出现了一个问题,我们的render函数现在只会渲染一次。我们当前的current还不是响应式的,不会重复去渲染页面。所以我们就要让current变成响应式的。
这里我们就要借用vue来将current变成响应式数据:`Vue.util.defineReactive(this, 'current', '/')``,因为我们全局设置了Vue变量来存储vue的构造函数,所以我们可以在构造器中直接使用。
但是这里又有个小bug,就是当用户直接刷新的时候,因为我们的current设置了初始值,页面会回到初始值的页面,我们稍稍修改一下。
class CVueRouter {
constructor(option) {
this.$option = option;
Vue.util.defineReactive(this, "current", "/");
window.addEventListener("hashchange", this.onHashChange.bind(this));
// 页面刷新将当前路由传入,更新cuttent触发render渲染对应组件
window.addEventListener("load", this.onHashChange.bind(this));
}
onHashChange() {
this.current = window.location.hash.slice(1);
}
}
这样就完美解决了。
4 优化
这里我们有没有发现,当我们在router-view里面渲染的时候我们每次都是遍历路由表来获取组件,我们为什么不做一个path与commponent的映射表,这样就能快速获取到我们想要的实例了。
constructor(option) {
...
this.routerMap = {};
option.routes.forEach((route) => {
this.routerMap[route.path] = route;
});
}
Vue.component("router-view", {
render(h) {
const { routerMap, current } = this.$router;
const component = routerMap[current].component || null;
return h(component);
},
});
这里我们还可以像vue的源码一样,把router-vew和router-link单独抽离成一个文件,然后导入进行使用。这里只是为了让代码结构更合理,就不做演示了。