vue-router是一个插件,包括在使用Vue-cli创建一个Vue项目的过程中,都可以选择当前项目使用或者不使用Vue-router,但是在任何一个使用l路由管理的Vue项目中,在main.js都可以看到如下代码。
new Vue({
router,
render: h => h(App)
}).$mount('#app')
答案自然是很简单,在此处将router加入到vue的选项对象后,新建出来的Vue实例的所有子实例都可以使用Vue-router,也就是在全局都可以使用了。那么问题又来了
让我们带着这两个问题来深入Vue-router的源码吧。
vue-router的主要代码如下所示
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
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')
},
{
path: '/3dview',
name: 'topo-3d',
// 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/topo-3D.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
可以看到,此时第二行引用的是Vue官方提供的router,我们将此行注释掉,换成我们自己实现的Xrouter
我们将第二行的
import VueRouter from 'vue-router'
换成
import VueRouter from './x-router'
然后在同级目录下新建x-router.js
对于vue-router的index.js文件的第六行
Vue.use(VueRouter)
前文中已经提到,router是一个插件,我们需要了解的是Vue对于插件的定义是必须要暴露一个install方法,这样才可以在Vue.use中正常使用,不然会报错。现在正式开始从实现一个vue-router来实现一个vue插件。
首先搭一个基本的骨架
// 这里是自己实现的Vue-router
let vue = null;
class myVueRouter{
constructor(options){
}
}
myVueRouter.install = function(_vue){
// vue在执行install的时候,会将Vue的构造函数传递进去
// 这里我们将_vue保存到变量中,这样我们就可以在上面定义的myVueRouter中去使Vue构造函数了
vue = _vue;
}
很简单的几行代码,vue在执行install方法时会将vue构造函数传递进去,然后我们将它保存到变量vue中,这样就可以在myVueRouter类中去使用了。
这里可能会有人提出疑问(包括我自己)
既然要使用vue构造函数,为什么一开始直接在第一行中import一个Vue呢?
这样做自然也是可以的,但是插件之所以是插件,自然还是要做到尽可能的轻量化,如果引入vue,那么webpack在打包的时候就必定会额外引入一大堆Vue相关的包,而这完全可以避免的。
好,现在进行下一步。
所有插件的install方法中都会将插件挂载到全局中。
也就是执行
vue.prototype.$router = router;
问题是,我们如何拿到
new Vue({
router,
render: h => h(App)
}).$mount('#app')
中的router项呢。
思路如下,我们知道,每一个vue组件都是通过new Vue()的构造函数构造出来的,但是只有根组件的构造函数的选项对象options中会传入router配置项。那么,我们只要在每一个组件的构造函数中判断选项对象中有没有router即可。
这里要用到一个日常开发中比较少见的一个api ----- 全局混入传送门
vue官方文档给出的警告正是我们想要的效果。
思路清晰之后便可以直接写代码了。
myVueRouter.install = function(_vue){
// vue在执行install的时候,会将Vue的构造函数传递进去
// 这里我们将_vue保存到变量中,这样我们就可以在上面定义的myVueRouter中去使Vue构造函数了
vue = _vue;
vue.mixin({
beforeCreate () {
if(this.$options.router){
// 如果选项对象中含有router选项,那么将$router挂载到全局中
// 至于为什么只在根组件的时候才执行这条语句呢?看上文中第二个问题便可获得答案
vue.prototype.$router = this.$options.router;
}
}
})
}
这样配置完我们自定义的vuerouter之后便可以在全局使用了,但是此时打开项目依然会报错如下。
没有关系,这是因为我们没有在自定义的路由插件中定义router-link和router-view的缘故。这正是我们下一步需要实现的功能。
如何实现一个名为router-link,或是router-view的元素呢,很自然的是使用组件的形式。
myVueRouter.install = function (_vue) {
vue = _vue;
vue.mixin({
beforeCreate() {
if (this.$options.router) {
vue.prototype.$router = this.$options.router;
}
}
})
vue.component('router-link',{
})
vue.component('router-view',{
})
}
此时,需要思考一个问题。在定义这些组件时,能不能使用template呢?
vue.component('router-link',{
template:`这是一个超链接`
})
这就是vue-router的陷阱了,注意在这个地方是不能使用template语法的。具体原因是在运行程序时主要有两个环境,一个是携带编译器的环境,也就是在浏览器中,它可以实时的将template转换成真实dom,一个是预编译环境,也就是在webpack打包过程中,是无法 识别template语法的。
所以我们只能用渲染函数来编写虚拟dom。
由于渲染函数个人感觉写起来比较麻烦,所以我在这里用JSX来偷个懒。
myVueRouter.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,
}
},
render(){
return <a style='color:#42b983' href={
'#'+this.to}>{
this.$slots.default}</a>
}
})
vue.component('router-view',{
})
}
此时再次打开首页,可以看到的是,router-link的功能已经正常了。点击页面链接首页url也可以正常变化。
那么很自然的就知道下一步需要实现的是监听url的变化并渲染对应的组件。
那么在router的构造函数中可以实现监听url变化的逻辑。
class myVueRouter {
constructor(options) {
this.$options = options;
eventBus = new vue();
this.currentUrl = '/';
window.addEventListener('hashchange',function(){
this.currentUrl = window.location.hash.slice(1);
})
}
}
可以看到在这里我们将变化后的hash值保存到了this.currentUrl中,所以可以在此构造函数实例化后的对象通过this.currentUrl拿到当前的url值,而在之前我们已经将router挂在到了全局,所以在Vue中可以通过this.$router.currentUrl拿到此值。
有此思路之后便可以直接编写router-view了。
vue.component('router-view',{
render(h){
let currentComp = null;
this.$router.$options.routes.forEach(route => {
if(route.path === this.$router.currentUrl){
currentComp = route.component
}
})
return h(currentComp);
}
})
可以看到,我们将当前url值和配置项中的比较,然后拿到应该渲染的组件直接挂载。此时页面也应该可以成功展示了。
最后一步,渲染是没有问题的,但是我们并没有做响应式的逻辑。因此,此时点击router-link是无法跳转的。所以最后一步是在当前url更新时通知到router-view。这里可以各展神通,博主用的是一个自定义的发布订阅模式。具体代码如下
let vue = null;
class EventBusClass{
constructor(){
this.eventStore=[]
}
emit(type,params=null){
this.eventStore[type](params);
}
on(type,callBack){
this.eventStore[type] = callBack;
}
}
const eventBus = new EventBusClass();
class myVueRouter {
constructor(options) {
this.$options = options;
this.currentUrl = '/';
vue.util.defineReactive(this,'currentUrl','/');
window.addEventListener('hashchange',function(){
this.currentUrl = window.location.hash.slice(1);
eventBus.emit('bashUpdate',this.currentUrl);
})
}
}
myVueRouter.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,
}
},
render(){
return <a style='color:#42b983' href={
'#'+this.to}>{
this.$slots.default}</a>
}
})
vue.component('router-view',{
mounted(){
eventBus.on('bashUpdate',(currentUrl)=>{
this.$router.currentUrl = currentUrl;
this.$forceUpdate();
})
},
render(h){
let currentComp = null;
this.$router.$options.routes.forEach(route => {
if(route.path === this.$router.currentUrl){
currentComp = route.component
}
})
return h(currentComp);
}
})
}
export default myVueRouter
在这里我杀鸡用牛刀的用了一个发布订阅模式,甚至还用了forceUpdate,但是总归是实现了一个基本的vue-router,其实vue自己提供了将变量变成响应式的方法,但是我使用之后发现视图并没有更新于是就自己实现了一个。其实真正的vue-router提供的方法以及功能肯定是要复杂的多,这里我们仅仅是从router的最基本的功能来阅读源码甚至入手来自己实现一个view-router啦。
到现在为止代码是可以正常运行且实现了router的基本功能的,但是可以看到的是,在router-view的渲染函数中,每次url更新之后都要在route里面遍历一次才能找到对应的component。而router配置项又是不变的,所以路由跳转都遍历一次显然是不合理的。所以我们可以将组件和路由之间的对应关系映射到一张表中。改写如下
let vue = null;
// 中央事件总线
class EventBusClass{
constructor(){
this.eventStore=[]
}
emit(type,params=null){
this.eventStore[type](params);
}
on(type,callBack){
this.eventStore[type] = callBack;
}
}
const eventBus = new EventBusClass();
class myVueRouter {
constructor(options) {
this.$options = options;
this.currentUrl = '/';
window.addEventListener('hashchange',function(){
this.currentUrl = window.location.hash.slice(1);
eventBus.emit('bashUpdate',this.currentUrl);
})
this.componentMap = new Map();
options.routes.forEach(routeItem => {
this.componentMap.set(routeItem.path,routeItem.component);
})
}
}
myVueRouter.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,
}
},
render(){
return <a style='color:#42b983' href={
'#'+this.to}>{
this.$slots.default}</a>
}
})
vue.component('router-view',{
mounted(){
eventBus.on('bashUpdate',(currentUrl)=>{
this.$router.currentUrl = currentUrl;
this.$forceUpdate();
})
},
render(h){
let currentComp = this.$router.componentMap.get(this.$router.currentUrl) || null;
return h(currentComp);
}
})
}
export default myVueRouter
finished!
入乡随俗,既然都用了Vue,为啥不直接用它的响应式呢。duck不必自己实现响应式。优优化代码如下。待童鞋消化理解
// 这里是自己实现的Vue-router
let Vue = null;
class myVueRouter{
constructor(options){
this.$options = options;
this.componentMap = new Map()
options.routes.forEach(routeItem =>{
this.componentMap.set(routeItem.path,routeItem.component);
})
this.activeParams = new Vue({
data () {
return {
currentUrl:location.hash ? location.hash.slice(1) : '/'
};
}
});
window.addEventListener('hashchange',() => {
this.activeParams.currentUrl = location.hash.slice(1);
})
}
}
myVueRouter.install = function(_Vue){
// vue在执行install的时候,会将Vue的构造函数传递进去
// 这里我们将_vue保存到变量中,这样我们就可以在上面定义的myVueRouter中去使Vue构造函数了
Vue = _Vue;
Vue.mixin({
beforeCreate () {
if(this.$options.router){
Vue.prototype.$router = this.$options.router;
}
}
})
Vue.component('router-link',{
props: {
to:{
type:String
}
},
render() {
return <a href={
`#${
this.to}`}>{
this.$slots.default}</a>
}
})
Vue.component('router-view',{
render(h) {
const currentComponent = this.$router.componentMap.get(this.$router.activeParams.currentUrl);
return h(currentComponent);
}
})
}
export default myVueRouter