本文将讲解,如何手动实现一个mini版的vue-router,(本文中我们称为KRouter)
我们的KRouter要实现的功能:
- 正常的路由跳转
需求分析
我们需要考虑以下俩点:
-
spa页面应用不能刷新,所以我们想到了俩种模式。(本文使用hash,后续在写vuex的时候会用History)
- hash模式 (eg:
#/about、event:onchange) - History api模式 (eg:
/about、event:pushState)
因为是单页面应用,所以我们只能通过
hash、history这俩种方式实现我们的需求,并且这俩种方式都有对应的事件。 - hash模式 (eg:
-
根据url显示对应的内容
- router-view
- 数据响应式:current变量持有url地址,一旦变化,动态重新执行渲染
router-view其实我们可以理解为组件容器,定义current变量代表当前的url,根据current从路由表里面取出对应的component,也就实现了router-view展示的功能。还需要考虑的一点是,如果current改变,那我们对应的router-view中展示的component也要相应改变。
代码实现
我们的写代码的目标:
- 实现KRouter类
- 实现install方法
1、环境搭建
-
创建项目
vue create myapp npm run serve -
新建
src/KRouter/index.js -
修改
src/router/index.js,引用我们自己的KRouter;import VueRouter from "vue-router"; // 修改为 import VueRouter from "../KRouter/index";
此时我们浏览器看一下我们的项目运行报错了。这一点不奇怪,因为我们KRouter/index.js文件是空的,无任何导出。接下来我们看看如何去实现!
2、实现KRouter的外壳儿
-
在
src/KRouter/index.js文件中class KRouter {} KRouter.install = function () {}; export default KRouter; -
此时看一下浏览器,是不是里程碑式的效果。此时的报错想要解决还不简单吗?
-
修改
src/KRouter/index.js文件因为我们编写的是一个插件,在
Vue.use(Plugin)时,会给install方法传入Vue的构造函数,(这也是编写插件的格式,不会的同学请自行百度),所以我们只要利用Vue.component这个全局api来注册router-link、router-view这俩个全局组件就可以。class KRouter {} KRouter.install = function (_Vue) { _Vue.component("router-link", { render(h) { return h("a", "超链接"); }, }); _Vue.component("router-view", { render(h) { return h("div", "组件容器"); }, }); }; export default KRouter; -
此时再看浏览器,是不是很完美,很简单,一点错误都没有。
3、实现KRouter类
之前的第二步的时候先简单的实现了install方法,只是为了不报错,并不完善。接下来我们顺着思维先来实现KRouter类。
首先,我们先思考平时我们使用vue-router的方式,一般都是this.$router.xxx,然后再配置一个路由表,实例化之后挂载到Vue实例上。如下图:
从图中我们可以看出,会将routes传到构造函数里面,其次我们需要一个变量(我们定义为current)来存储我们当前的url。我们顺着这个思维一步一步实现。所以代码变成下面这个样子:
class KRouter {
constructor(options) {
this.$options = options;
this.current = "/";
}
}
KRouter.install = function (_Vue) {
_Vue.component("router-link", {
render(h) {
return h("a", "超链接");
},
});
_Vue.component("router-view", {
render(h) {
return h("div", "组件容器");
},
});
};
export default KRouter;
4、将KRouter挂载到Vue的原型上
Vue.mixin这个全局api,如果有不理解的同学可以去看一下,我们通过this.$options这个常用实例api可以拿到给组件实例传入的参数,可以自己输出看一下。
官网传送门:Vue.mixin 和 this.$options
class KRouter {
...
}
KRouter.install = function (_Vue) {
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
...
};
export default KRouter;
现在this.$options.router其实就是根组件,因为我们只在根组件里面传入了router,如下图:
什么,你还不相信?
那我们改代码测试一下
class KRouter {
...
}
KRouter.install = function (_Vue) {
_Vue.mixin({
beforeCreate() {
console.log(this.$options.router);
},
});
...
};
export default KRouter;
看一下浏览器的输出,是不是只有一个组件有this.$options.router,没错那就是我们的根组件。学废了没
5、完善router-view组件
class KRouter {
...
}
KRouter.install = function (_Vue) {
...
_Vue.component("router-view", {
render(h) {
const current = this.$router.current;
const component = this.$router.$options.routes.find(
(v) => v.path == current
).component;
return h(component);
},
});
};
export default KRouter;
在上一步_Vue.mixin的时候,已经在Vue.prototype上挂载了KRouter实例,也就是$router。
所以我们直接用this.$router就可以拿到当前的url,也就是我们的变量current,根据这个变量从routes里面查找,找到符合path === current这个条件的某一项,拿到component。
最后通过h函数进行渲染,这也就是router的核心原理。
6、最重要的一步,响应式处理。
现在我们的KRouter已经可以渲染组件了,但是既然是路由,不能跳转,那是不是太low了点。所以接下来我们要实现的就是路由跳转。
修改router-link组件
正常使用router-link组件如下:
<router-link to="/home">首页</router-link>
所以我们修改我们router-link的代码
KRouter.install = function (_Vue) {
_Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
return h("a", { attrs: { href: `#${this.to}` } }, this.$slots.default);
},
});
};
export default KRouter;
KRouter中监听hashchange事件
class KRouter {
constructor(options) {
this.$options = options;
this.current = "/";
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1);
});
}
}
KRouter.install = function(){
...
}
export default KRouter;
响应式处理current
此时你发现,该做的都做了,当你点击路由的时候router-view还是不能渲染出对应的组件。没错,那是因为你current不是响应式,接下来我们要做的的这个处理就是让current变成响应式。
Vue中给我们提供了一个方法defineReactive。调用方式:Vue.util.defineReactive(obj,key,value)
class KRouter {
constructor(options) {
this.$options = options;
Vue.util.defineReactive(this, "current", "/");//给实例添加一个current属性,默认值是"/"
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1);
});
}
}
export default KRouter;
有人会问,诶,你的Vue从哪里来的,可以直接import Vue from "vue"吗?
答案是:当然可以
但是你想一下,你开发一个插件你还要npm install vue 吗?显然会造成资源浪费。
所以调整一下思路,然后再贴一份完整代码,如下:
let Vue;
class KRouter {
constructor(options) {
this.$options = options;
Vue.util.defineReactive(this, "current", "/");
window.addEventListener("hashchange", () => {
this.current = window.location.hash.slice(1);
});
}
}
KRouter.install = function (_Vue) {
Vue = _Vue;
_Vue.mixin({
beforeCreate() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router;
}
},
});
_Vue.component("router-link", {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
return h("a", { attrs: { href: `#${this.to}` } }, this.$slots.default);
},
});
_Vue.component("router-view", {
render(h) {
const current = this.$router.current;
const component = this.$router.$options.routes.find(
(v) => v.path == current
).component;
return h(component);
},
});
};
export default KRouter;
大家可以发散思维,想想this.$router.push这种类似的api,要怎么实现?是不是已经有想法了呢!
总结
来掘金写文章,本意是想做一下笔记,防止后面自己会忘。做程序员的都知道,过一段时间不触碰,就会生疏。索性就写的详细一点,跟大家互相交流。愿我们在程序的道路上,互相扶持,不忘初心。
排版有点low,大家不要介意,哈哈哈。