本文将浅析vue-router的具体实现,并且实现一个简易的vue-router。
一.前置知识
如果你很了解vue的插件应该怎么写,可以跳过此部分。
1.Vue.use( plugin) 安装 Vue.js 插件。
Vue.js 的插件应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
2.Vue.mixin( mixin )
全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
二.流程
1.使用一个current变量储存当前url
2.监听浏览器url变化,改变current
3.监听current,获取应该渲染的新组件
4.渲染新组件
下面细讲:
1.使用一个current变量储存当前url
class HistoryRoute{
constructor(){
this.current=null;
}
}
class vueRouter{
constructor(options){
this.mode=options.mode||'hash';
this.routes=options.routes;
this.history=new HistoryRoute;
}
}
export default vueRouter;
在上述代码中,我们写了两个类,current用于保存当前路径,vueRouter则是我们要export出去的对象,所以他可以接受到一个options,这个options是从main.js(如果是vue-cli3则是router/index.js)中,new vueRouter时传进来的。(如下图,红框部分)
我们在构造函数中,取出mode,如果没有则默认是hash模式。把路由表保存到routes中。
2.监听浏览器url变化,改变current
window.addEventListener('load',()=>{
this.history.current=location.hash.slice(1);
})
window.addEventListener('hashchange',()=>{
this.history.current=location.hash.slice(1);
})
通过对load的监听,初始化history中current的值,通过对hashchange的监听,改变current的值。
3.监听current,获取应该渲染的新组件
current如何监听?vue提供了一个defineReactive的公共方法:
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
//warn: warn,
//extend: extend,
//mergeOptions: mergeOptions,
defineReactive: defineReactive$$1
};
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
有了这个方法,我们就可以把监听current了
vueRouter.install=function(vue){
vue.mixin({
beforeCreate(){
if(this.$options&&this.$options.router){
this._root=this;
this._router=this.$options.router;
vue.util.defineReactive(this,'current',this._router.history);
}else{
this._root=this.$parent._root;
}
}
})
}
4.渲染新组件
我们能拿到当前hash了,也有一个router的路由可以对上了,如果我们把router改造成hash为对象的key的数组,这样取对应的组件的时候就容易多了
createMap(routes){
return routes.reduce((memo,current)=>{
memo[current.path]=current.component;
return memo
},{})
}
这样我们就可以通过memo[hash] 很快的取到对应的组件。
vue.component('router-view',{
render(h){
let current=this._self._root._router.history.current;
let routesMap=this._self._root._router.routesMap;
return h(routesMap[current])
}
})
这样,我们就基本完工啦~
测试:
新建一个vue项目,在项目中新建一个my-router的文件夹,在文件夹中新建index.js,把VueRouter的引用指向新的文件
import VueRouter from '../myrouter'
在浏览器地址栏更改地址,成功跳转!
留个小问题:
请在myrouter/index.js中实现一个简单的<router-link>
答案:
vue.component('router-link',{
props:{
to: String,
},
render(h){
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
var headingId = getChildrenTextContent(this.$slots.default);
return h(
"div",
[
h('a', {
attrs: {
name: headingId,
href: '#' + this.to
}
},this.$slots.default)
]
)
}
})
完整代码已经上传到文章附件中