前言
本文只是一个demo,有很多功能没有实现,可以之后会出几篇文章将这个vue-router的核心功能补全。
已经实现的核心功能
router-link和router-component$router和$routehash mode和history mode- 监听路由变化切换组件
之后考虑实现的功能
- 嵌套路由
- 动态路由匹配
思考
当你通过vue-cli安装vue全家桶的时候,你翻开src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
// 思考1: 为什么使用Vue.use
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: 为什么要new VueRouter
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
});
export default router;
再点开src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
// 思考3: 为什么要router传进去的是根实例
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
点开src/App.vue中
<template>
<div id="app">
<div id="nav">
<!-- 思考4: 为什么我没有import router-link和router-view但是可以使用 -->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
<script>
export default {
methods: {
xx() {
// 思考5: 为什么我可以在任意组件访问到$router,而且这个$router是同一个东西
this.$router.push("/")
}
},
}
<script>
思考6: 为什么我点击router-link切换视图页面却不会跳转和刷新
我自己的思考
- 使用Vue.use很明显VueRouter是一个插件,我们需要实现一个install方法从而可以安装这个插件
- new VueRouter({}) 说明了VueRouter是一个类,或者说构造函数
- 这个在先卖个关子,在后面进行解答。
- 没有import但是可以直接使用,说明这两个组件是全局组件,那么在那里注册的呢,答案很明显,在Vue.use(VueRouter)的时候执行了全局注册。
- 很明显,任意组件都能访问且是同一个东西,说明这个$router是挂载到Vue.prototype上的。
- router-link本质上是一个a标签,但是默认的跳转事件被拦截了,取而代之的是切换router-view所渲染的组件
正文
在src/router文件夹下创建mRouter.js文件夹编写我们自己的VueRouter类。
1.最初的样子
1)首先定义VueRouter这个类
2)接着实现install方法,接着全局注册两个router-link和router-view这个两个组件
3)router-link和router-view 现在这两个东西是我随便写的,下面会进行完善
src/router/mRouter.js
class VueRouter {
constructor() {}
}
VueRouter.install = function(vue) {
vue.component("router-link", {
render(h) {
return h("a", {}, this.$slots.default);
},
});
vue.component("router-view", {
render(h) {
return h("div", {}, "renderrender");
},
});
};
export default VueRouter;
效果
2.挂载$router
Q: 上面提到,我们想要在Vue.prototype上挂载$router,这时候就产生了一个问题: router是在new Vue({router})中传入的,而如果Vue.use(VueRouter)是在new Vue({})之前执行的,这时候我们在install中无法访问到Vue实例,因此也就挂载不上。
遇到这个问题,我们想一下我们的期待什么。我们希望延迟到将来的某个时刻执行,具体点说就是在new Vue({})这个根实例创建时执行。
实现的方法我们可以看一下源码中如何做到的
A:可以看到,源码的解决方案是全局混入一个beforeCreate的钩子函数,然后在每次beforeCreate的时候判断一下是否是根实例(根实例的options中才有router属性),然后挂载上去。
自己实现
VueRouter.install = function(vue) {
vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
vue.prototype.$router = this.$options.router;
}
},
});
vue.component("router-link", {
props: ["to"],
render(h) {
return h("a", { attrs: { href: this.to } }, this.$slots.default);
},
});
vue.component("router-view", {
render(h) {
// 打印一下
console.log("test-router--", this.$router);
return h("div", {}, "renderrender");
},
});
};
效果
3.url与视图建立响应式
在这里我们需要用一个变量current来记录当前的url,当url发生变化的时候,我们更新current,随着视图也发生变化。
我们的mode有history和hash这两种,我们分别监听popstate或者hashchange这两个事件,当触发的时候更新current。
接下来就出现了一个问题,我们直接更改current,render函数中会知道current更新了吗?我们来试一下。
首先我们把mode切换到hash模式,因为history模式下我们还没做拦截。
class VueRouter {
constructor(options) {
this.$options = options;
this.current = "/";
const strategy = {
hash: () => {
window.addEventListener("hashchange", () => {
// '#/about/id=1123' => 不需要#
this.current = window.location.hash.slice(1);
});
},
history: () => {
window.addEventListener("popstate", () => {
// /abc 就是我们想要的
this.current = window.location.pathname;
});
},
};
strategy[options.mode] && strategy[options.mode]();
}
}
VueRouter.install = function(vue) {
vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
vue.prototype.$router = this.$options.router;
}
},
});
vue.component("router-link", {
props: ["to"],
render(h) {
let prefix = this.$router.$options.mode === "hash" ? "#" : "";
return h("a", { attrs: { href: prefix + this.to } }, this.$slots.default);
},
});
vue.component("router-view", {
render(h) {
// 我们点击不同的url看一下console是否会触发
console.log("render---", this.$router.current);
return h("div", {}, "renderrender");
},
});
};
export default VueRouter;
明显render函数中根本就不知道current发生了变化,所以它只执行了一次。
使用过Vue大概都知道如何处理这种情况,我们只需要让current变成响应式,Vue收集到这个依赖后,就会在current更新时去触发对应的render函数。
在这里又衍生出了一个问题,我们怎么将其变成响应式。直接使用Vue.$set,Object.defineProperty,new Vue({data():{return current:"/"}})或者其他?
在这里直接使用Vue.$set是不行的,至于为什么大家可以想一下$set的应用场景:把一个非响应式的属性添加到一个响应式对象中,使其变成响应式属性。在这里,我们甚至都没有响应式对象,那么如何使用$set呢。
第二种方式可以自己尝试去做,第三种是可行的,但是在这里我们不去用。
大家回头去看 2.挂载$router 里面我截源码那部分的内容,那里出现了一个Vue.util.defineReactive这个函数,这个是Vue里面的工具函数,帮助我们去创建一个响应式属性。
/**
* 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 (process.env.NODE_ENV !== 'production' && 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();
}
});
}
我们来使用这个defineReactive看下是否可行
let Vue = null;
class VueRouter {
constructor(options) {
this.$options = options;
// 这里的Vue是下面的install中弄到的
Vue.util.defineReactive(this, "current", "/");
const strategy = {
hash: () => {
window.addEventListener("hashchange", () => {
// '#/about/id=1123' => 不需要#
this.current = window.location.hash.slice(1);
});
},
history: () => {
window.addEventListener("popstate", () => {
// '#/about/id=1123' => 不需要#
this.current = window.location.pathname;
});
},
};
strategy[options.mode] && strategy[options.mode]();
}
}
VueRouter.install = function(vue) {
// 因为是先执行Vue.use() 再执行 new VueRouter(),所以我们可以在constructor中访问到Vue
Vue = vue;
vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
vue.prototype.$router = this.$options.router;
}
},
});
vue.component("router-link", {
props: ["to"],
render(h) {
let prefix = this.$router.$options.mode === "hash" ? "#" : "";
return h("a", { attrs: { href: prefix + this.to } }, this.$slots.default);
},
});
vue.component("router-view", {
render(h) {
console.log("render---", this.$router.current);
return h("div", {}, "renderrender");
},
});
};
export default VueRouter;
很好,我们就差一步就搞定了,就是根据current去匹配路由表中的path然后渲染对应的component,这里我没考虑嵌套路由和动态路由匹配的情况。
let Vue = null;
class VueRouter {
constructor(options) {
this.$options = options;
Vue.util.defineReactive(this, "current", "/");
// this.current = "/";
const strategy = {
hash: () => {
window.addEventListener("hashchange", () => {
// '#/about/id=1123' => 不需要#
this.current = window.location.hash.slice(1);
});
},
history: () => {
window.addEventListener("popstate", () => {
// '#/about/id=1123' => 不需要#
this.current = window.location.pathname;
});
},
};
strategy[options.mode] && strategy[options.mode]();
}
}
VueRouter.install = function(vue) {
Vue = vue;
vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
vue.prototype.$router = this.$options.router;
}
},
});
vue.component("router-link", {
props: ["to"],
render(h) {
let prefix = this.$router.$options.mode === "hash" ? "#" : "";
return h("a", { attrs: { href: prefix + this.to } }, this.$slots.default);
},
});
vue.component("router-view", {
render(h) {
console.log("render---", this.$router.current);
const component = this.$router.$options.routes.find((item) => {
return item.path === this.$router.current;
})?.component ?? {
render(h) {
return h("div", {}, "404");
},
};
return h(component);
},
});
};
export default VueRouter;
结语
其实这部分的内容很多大佬估计都知道,本菜鸡也是最近才开始看源码,跟着大佬的思考走,然后按照提出来的问题来思考如何一步步解决问题。实在想不出还是得啃源码,毕竟标准答案就在那。