日常开发中,如果是Vue生态,Vue Router 的使用,可以说几乎是不可或缺的。以下是 Vue Router插件核心逻辑的拆解及简单实现。
在深入源码内部探究之前,有一个很重要的前置知识是Vue.use。
Vue.use到底做了什么:
- 目的是安装Vue插件
- 会将Vue作为参数传入插件的install方法(如果插件是个函数,那么他就是install方法本身)
- Vue.use的核心方法在
src/core/global-api/use.js下- 总计不到20行,核心逻辑如下
- 获取已安装的插件数组,如果存在则直接返回this
- 不存在则执行插件的install方法
官方文档中所说的插件
Vue.use( plugin )
参数:
{Object | Function} plugin
用法:
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
该方法需要在调用 new Vue() 之前被调用。
当 install 方法被同一个插件多次调用,插件将只会被安装一次。
// Vue.js 的插件应该暴露一个 install 方法。
// 这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
Vue-Router需求分析
单页面应用程序中,url发生变化时候,不能刷新,显示对应视图内容
- SPA页面不能刷新
- hash (#/about
- history (api /about
- 根据url显示对应的内容
- router-view
- 数据响应式:current变量持有url地址,一旦变化,重新动态渲染对应组件
思路
实现一个插件
- 实现VueRouter类
- 处理路由选项
- 监控url变化,hashchange
- 响应这个变化
- 实现install方法
- $router注册
- 两个全局组件
代码
let Vue;
// 声明插件VueRouter
class VueRouter {
constructor(options) {
// 1.保存路由选项
this.$options = options;
// 路由变动视图更新的核心
// 用Vue.set不行的原因,set对参数要求其本身已经是响应式对象
// Vue.util应该是Vue内部的方法
// 同时为current设置初始值
Vue.util.defineReactive(
this,
"current",
window.location.hash.slice(1) || "/"
);
// 2.监控hash变化
window.addEventListener("hashchange", () => {
// hash: #/about
this.current = window.location.hash.slice(1);
});
}
}
// 文档有说,调用install方法时,会传入Vue构造函数
VueRouter.install = function(_Vue) {
Vue = _Vue;
// 最简单的拍脑袋的做法就是,把router的实例放到Vue的原型上
// Vue.prototype = router
// 但问题在于install方法在执行的时候,router还未实例化
// 所以无法在install方法执行时将router实例挂载到Vue原型上
// 即,要将挂载到原型上的操作延迟执行,延迟到router和vue都实例化完毕之后
// 1.注册$router,让所有组件实例都可以访问它
// 混入:Vue.mixin({})
// 选用Vue.mixin配合beforeCreate实现router注册的逻辑
// install方法有要求,需要在new Vue之前调用
// 那么这就意味着,从时机上来说,use的时候,你无法直接拿到VueRouter的实例,你甚至也拿不到自己写的路由表
// 我们为什么要拿到VueRouter的实例,按照现在官方实现,不管在哪一个组件实例中,你都可以直接使用 this.$router.push 这些方法
// 那么,这就要求我们必须得把VueRouter 的实例挂载到Vue的构造函数中,这样无论是哪一个组件实例都可以直接用上VueRouter实例中的方法和属性
Vue.mixin({
beforeCreate() {
// 延迟执行:延迟到router实例和vue实例都创建完毕
if (this.$options.router) {
// 如果存在说明是根实例,在根实例还是构造函数?放一份,
// 就可以通过原型拿到了
Vue.prototype.$router = this.$options.router;
}
},
});
// 注册两个全局组件,使得我们可以直接在template里使用 router-link router-view
// <router-link to="/home">home</router-link>
// - // 注册组件,传入一个选项对象 (自动调用 Vue.extend)
// - Vue.component('my-component', { /* ... */ }) // 常用
// router-link功能的本质就是路由跳转,这里实现的hash 模式的
Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
// <a href="#/home">xxx</a>
// h是render函数调用时,框架传入的createElement
// 等同于react中createElement,返回vdom
// return <a href={'#'+this.to}>{this.$slots.default}</a>
return h(
"a",
{
attrs: {
href: "#" + this.to,
},
},
this.$slots.default//存放不具名插槽的数据
);
},
});
// router-view功能的本质就是渲染路由对应的组件
Vue.component("router-view", {
render(h) {
let component = null;
// 1.获取当前url的hash部分
// 2.根据hash部分从路由表中获取对应的组件
// 下一行的this是啥,是router-view组件的实例
const route = this.$router.$options.routes.find(
(route) => route.path === this.$router.current
);
if (route) {
component = route.component;
}
return h(component);
},
})
};
export default VueRouter;
当然,上面的代码实现很简陋,只对hash模式下的部分功能进行了实现。
代码实现中,我觉得两个全局组件的实现会相对好理解些,其本质无非是组件的渲染和路由的跳转。
而相对陌生的是Vue.util.defineReative这一API的使用,可以说这也是数据响应式概念的体现,数据的变动会让使用到数据的组件得到通知从而重新渲染。