vue-router:VueRouter模拟实现与源码解读
1、Vue.use( )
源码
源码位置:vue/src/core/global-api/use.js
export function initUse (Vue: GlobalAPI) {
//use方法的参数接收的是一个插件,该插件的类型可以是一个函数,也可以是一个对象
Vue.use = function (plugin: Function | Object) {
//_installedPlugins数组中存放已经安装的插件。
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
//判断一下传递过来的插件是否在installedPlugins中存在,如果存在,则直接返回
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
//将arguments转换成数组,并且将数组中的第一项去除。
const args = toArray(arguments, 1)
//把this(也就是Vue,这里是通过Vue.use来调用的)插入到数组中的第一个元素的位置。
args.unshift(this)
//这时plugin是一个对象,看一下是否有install这个函数。
if (typeof plugin.install === 'function') {
//如果有install这个函数,直接调用
//这里通过apply将args数组中的每一项展开传递给install这个函数。
// plugin.install(args[0],args[1])
//而args[0],就是上面我们所插入的Vue.这一点与我们前面在模拟install方法的时候是一样的。
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
//如果plugin是一个函数,则直接通过apply去调用
plugin.apply(null, args)
}
//将插件存储到installedPlugins数组中。
installedPlugins.push(plugin)
return this
}
}
2、install方法分析
我们先来看一下vue-router
的目录结构
我们先来核心的文件。
components
目录下面,有两个文件。分别为link.js
和view.js
文件。
link.js
文件创建RouterLink
组件
view.js
文件创建RouterView
组件。
history
目录下的文件是记录路由的历史记录(hash.js
文件是关于hash
模式,html5.js
关于html5
的方式,base.js
公共的内容,abstract.js
是在服务端渲染中实现的路由历史记录)。
index.js
文件是用来创建VueRouter
install.js
文件是关于install
方法
我们自己模拟的VueRouter
也实现上面的目录结构。
下面先来在index.js
文件中实现基本代码。
export default class VueRouter {
//在创建VueRouter对象的时候,会传递选项
constructor(options) {
//获取routes选项,该选项中定义路由规则
this._options = options.routes || [];
}
// 注册路由变化的事件。该方法的参数是一个Vue实例,后期完善
init(Vue) {}
}
下面实现install.js
基本代码(通过查看源代码来实现)
export let _Vue = null; //将其导出,在其它文件中也可以使用Vue实例,而不需要单独的引入Vue的js文件
export default function install(Vue) {
//获取Vue构造函数
_Vue = Vue;
_Vue.mixin({
//通过混入以后,所有的Vue实例中都会有beforeCreate方法
beforeCreate() {
//判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项)
if (this.$options.router) {
//通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上
this._routerRoot = this;
this._router = this.$options.router;
//调用index.js文件中定义的init方法
this._router.init(this);
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
}
3、组件创建测试
下面需要将install
方法挂载到VueRouter
上。
import install from "./install";
export default class VueRouter {
//在创建VueRouter对象的时候,会传递选项
constructor(options) {
//获取routes选项,该选项中定义路由规则
this._routes = options.routes || [];
}
// 注册路由变化的事件。
init(Vue) {}
}
//将install方法挂载到VueRouter上
VueRouter.install = install;
下面,我们可以简单实现一下Router-link
组件与Router-view
组件,来做一个简单的测试。(接下来讲解如下内容)
components
目录下的view.js
文件。
export default {
render(h) {
return h("div", "router-view");
},
};
以上是Router-View
组件的基本功能,后面在继续完善。
link.js
文件的实现如下:
export default {
props: {
to: {
type: String,
required: true,
},
},
render(h) {
//通过插槽获取`a`标签内的文本。
return h("a", { domProps: { href: "#" + this.to } }, [this.$slots.default]);
},
};
在install.js
文件中,导入上面的组件进行测试。
import View from "./components/view";
import Link from "./components/link";
export let _Vue = null; //将其导出,在其它文件中也可以使用Vue实例,而不需要单独的引入Vue的js文件
export default function install(Vue) {
//获取Vue构造函数
_Vue = Vue;
_Vue.mixin({
//通过混入以后,所有的Vue实例中都会有beforeCreate方法
beforeCreate() {
//判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项)
if (this.$options.router) {
//通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上
this._routerRoot = this;
this._router = this.$options.router;
//调用index.js文件中定义的init方法
this._router.init(this);
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
});
//完成组件的注册
Vue.component("RouterView", View);
Vue.component("RouterLink", Link);
}
在上面的代码中,导入组件,并且完成组件的注册。
下面,我们测试一下。
在src
目录下,在router.js
文件中导入自己定义的VueRouter
.
import Router from "./my-vue-router";
4、解析路由规则
下面,我们要做的就是对所有的路由规则进行解析,将其解析到一个数组中。方便根据地址找到对应的组件。
在源码的index.js
文件中,创建了VueRouter
类,对应的构造方法中,有如下代码:
this.matcher = createMatcher(options.routes || [], this)
createMatcher
方法是在create-matcher.js
文件中创建的。
该方法返回的matcher
就是一个匹配器,其中有两个成员,match
,另外一个是addRoutes
match
:根据路由地址匹配相应的路由规则对象。
addRoutes
动态添加路由
首先在我们自己的index.js
文件中添加如下的代码:
import install from "./install";
import createMatcher from "./create-matcher";
export default class VueRouter {
//在创建VueRouter对象的时候,会传递选项
constructor(options) {
//获取routes选项,该选项中定义路由规则
this._routes = options.routes || [];
this.matcher = createMatcher(this._routes);
}
// 注册路由变化的事件。
init() {}
//init(Vue){}
}
//将install方法挂载到VueRouter上
VueRouter.install = install;
在上面的代码中,导入了createMatcher
方法。
并且在调用该方法的时候传递了路由规则。
create-matcher.js
文件的代码如下:
import createRouteMap from "./create-route-map";
export default function createMatcher(routes) {
const { pathList, pathMap } = createRouteMap(routes);
function match() {}
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap);
}
return {
match,
addRoutes,
};
}
下面,我们需要在create-route-map.js
文件中实现createRouteMap
这个方法。
export default function createRouteMap(routes, oldPathList, oldPathMap) {
const pathList = oldPathList || [];
const pathMap = oldPathMap || {};
//遍历所有的路由规则,进行解析。同时还要考虑children的形式,
//所以这里需要使用递归的方式。
routes.forEach((route) => {
addRouteRecord(route, pathList, pathMap);
});
return {
pathList,
pathMap,
};
}
function addRouteRecord(route, pathList, pathMap, parentRecord) {
//从路由规则中获取path。
const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path;
//构建记录
const record = {
path,
component: route.component,
parent: parentRecord, //如果是子路由的话,记录子路由对应的父record对象(该对象中有path,component),相当于记录了父子关系
};
//如果已经有了path,相同的path直接跳过
if (!pathMap[path]) {
pathList.push(path);
pathMap[path] = record;
}
//判断route中是否有子路由
if (route.children) {
//遍历子路由,把子路由添加到pathList与pathMap中。
route.children.forEach((childRoute) => {
addRouteRecord(childRoute, pathList, pathMap, record);
});
}
}
下面测试一下上面的代码
import createRouteMap from "./create-route-map";
export default function createMatcher(routes) {
const { pathList, pathMap } = createRouteMap(routes);
console.log("pathList==", pathList);
console.log("pathMap==", pathMap);
function match() {}
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap);
}
return {
match,
addRoutes,
};
}
在上面的代码中,我们打印了pathList
与pathMap
.
当然,现在在我们所定义的路由规则中,还没有添加children
,构建相应的子路由。下面重新修改一下。
在项目根目录下的router.js
文件中,添加对应的子路由规则。
import Vue from "vue";
// import Router from "vue-router";
// import Router from "./vuerouter";
import Router from "./my-vue-router";
import Login from "./components/Login.vue";
import Home from "./components/Home.vue";
import About from "./components/About.vue";
import Users from "./components/Users";
Vue.use(Router);
export default new Router({
// model: "history",
routes: [
{ path: "/", component: Home },
{ path: "/login", component: Login },
{
path: "/about",
component: About,
children: [{ path: "users", component: Users }],
},
],
});
这时候可以查看对应的输出结果。
5、match
函数实现
在create-matcher.js
文件中,我们实现了createRouteMap
方法,同时还需要实现match
方法。
match
方法的作用就是根据路由地址,匹配一个路由对象。其实就是从pathMap
中根据路由地址,找出对应的路由记录。路由记录中记录了组件信息,找到以后就可以完成组件的创建,渲染了。
function match(path) {
const record = pathMap[path];
if (record) {
//根据路由地址,创建route路由规则对象
return createRoute(record, path);
}
return createRoute(null, path);
}
在上面的代码中,我们调用match
方法的时候,会传递过来一个路径,我们根据这个路径可以从pathMap
中找到对应的路由记录信息(这块在上一小节已经创建完毕),如果找到了,我们还需要做进一步的处理,为什么呢?因为,我们传递过来的路径有可能是子路径,这时不光要获取到对应的子路由信息,我们还需要去查找对应的父路由的信息。所以这里需要进一步的处理,关于这块的处理封装到了createRoute
这个方法中,而该方法在其它位置还需要,所以我们定义到util
这个目录下import createRoute from "./util/route";
。
create-matcher.js
文件完整代码如下:
import createRouteMap from "./create-route-map";
import createRoute from "./util/route";
export default function createMatcher(routes) {
const { pathList, pathMap } = createRouteMap(routes);
console.log("pathList==", pathList);
console.log("pathMap==", pathMap);
//实现match方法
function match(path) {
const record = pathMap[path];
if (record) {
//根据路由地址,创建route路由规则对象
return createRoute(record, path);
}
return createRoute(null, path);
}
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap);
}
return {
match,
addRoutes,
};
}
下面我们需要在my-vue-router
目录下面在创建一个util
目录,在该目录下面创建route.js
文件,该文件实现的代码如下:
export default function createRoute(record, path) {
const matched = [];
while (record) {
matched.unshift(record);
record = record.parent;
}
return {
path,
matched,
};
}
总结:match
这个方法的作用就是根据路径,创建出路由规则对象,而所谓的路由规则对象其实就是包含了路径以及对应的路由记录的信息(这里有可能包含了父路由以及子路由记录,这块内容存储到一个数组中)。
以后,我们就可以根据路径直接获取到包含了整个路由记录的这个数组,从而可以将对应的组件全部创建出来。
6、历史记录处理
关于路由有三种模式:hash
模式,html5
模式,abstract
模式(该模式与服务端渲染有关)
在这里我们实现hash
模式的历史记录管理,不管是哪种模式,都有相同的内容,这里我们相同的内容定义到
父类中。
在该父类中主要有如下内容:
router
属性:路由对象(ViewRouter
)
current
属性,记录当前路径对应的路由规则对象{path:'/',matched:[]}
,关于该对象,我们在前面已经处理完了。也就是在createRoute
方法中返回的内容。
transitionTo()
跳转到指定的路径,根据当前路径获取匹配的路由规则对象route
,然后更新视图。
在my-vue-router
目录下的,history
目录下的base.js
文件,编写如下的代码:
import createRoute from "../util/route";
export default class History {
// router路由对象ViewRouter
constructor(router) {
this.router = router;
this.current = createRoute(null, "/");
}
transitionTo(path, onComplete) {
this.current = this.router.matcher.match(path);
//该回调函数在调用transitionTo方法的时候,会传递过来。
onComplete && onComplete();
}
}
父类已经实现了,下面实现对应的子类。也就是HashHistory
HashHistory
继承History
, 同时要确保首次访问的地址为#/
.
在History
中还需要定义两个方法,第一个方法为:getCurrentLocation( )
获取当前的路由地址(#
后面的部分)
setUpListener( )
方法监听路由地址改变的事件(hashchange
)。
在history
目录下的hash.js
文件中的代码实现如下:
import History from "./base";
export default class HashHistory extends History {
constructor(router) {
//将路由对象传递给父类的构造函数
super(router);
//确保 首次 访问地址加上 #/ (//由于没有添加this,为普通方法)
ensureSlash();
}
// 获取当前的路由地址 (# 后面的部分)所以这里需要去除#
getCurrentLocation() {
return window.location.hash.slice(1);
}
// 监听hashchange事件
//也就是监听路由地址的变化
setUpListener() {
window.addEventListener("hashchange", () => {
//当路由地址发生变化后,跳转到新的路由地址。
this.transitionTo(this.getCurrentLocation());
});
}
}
function ensureSlash() {
//判断当前是否有hash
// 如果单击的是链接,肯定会有hash
if (window.location.hash) {
return;
}
window.location.hash = "/";
}
7、Init
方法实现
我们知道当创建VueRouter
的时候,需要可以传递mode
,来指定路由的形式,例如是hash
模式还是html5
模式等。
所以这里需要根据指定的mode
的模式,来选择history
目录中中不同js
来处理。
所以在my-vue-router
目录中的index.js
文件中,做如下的修改:
import install from "./install";
import createMatcher from "./create-matcher";
import HashHistory from "./history/hash";
import HTML5History from "./history/html5";
export default class VueRouter {
//在创建VueRouter对象的时候,会传递选项
constructor(options) {
//获取routes选项,该选项中定义路由规则
this._routes = options.routes || [];
this.matcher = createMatcher(this._routes);
//获取传递过来的选项中的mode,mode中决定了用户设置的路由的形式。
//这里给VueRouter添加了mode属性
const mode = (this.mode = options.mode || "hash");
switch (mode) {
case "hash":
this.history = new HashHistory(this);
break;
case "history":
this.history = new HTML5History(this);
break;
default:
throw new Error("mode error");
}
}
// 注册路由变化的事件。
init() {}
//init(Vue){}
}
//将install方法挂载到VueRouter上
VueRouter.install = install;
首先导入HashHistory
与HTML5History
.
import HashHistory from "./history/hash";
import HTML5History from "./history/html5";
下面获取选项中的mode
,如果在创建VueRouter
对象的时候,没有指定mode
,那么默认的值为hash
.
下面就对获取到的mode
进行判断,根据mode
的不同的值,创建不同的history
的实例。
//获取传递过来的选项中的mode,mode中决定了用户设置的路由的形式。
//这里给VueRouter添加了mode属性
const mode = (this.mode = options.mode || "hash");
switch (mode) {
case "hash":
this.history = new HashHistory(this);
break;
case "history":
this.history = new HTML5History(this);
break;
default:
throw new Error("mode error");
}
同时html5.js
文件,添加了基本的代码
import History from "./base";
export default class HTML5History extends History {}
关于Html5
的形式这里不在实现了。
下面完善一下init
方法
// 注册路由变化的事件。
init() {}
具体的实现代码如下:
// 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。
init() {
const history = this.history;
const setUpListener = () => {
history.setUpListener();
};
history.transitionTo(
history.getCurrentLocation(),
//如果直接history.setUpListener
// 这样的话setUpListener里面的this会有问题。
setUpListener
);
}
在这里,调用了transitionTo
方法的原因是,在hash.js
文件中的ensureSlash
方法中,完成了一次地址的修改,所以这里需要跳转一次。
同时完成了hashchange
事件的绑定(路由变化的事件)。
下面可以进行测试一下,在base.js
文件中的transitionTo
方法中,打印出current
属性的值。
transitionTo(path, onComplete) {
this.current = this.router.matcher.match(path);
console.log("current===", this.current);
//该回调函数在调用transitionTo方法的时候,会传递过来。
onComplete && onComplete();
}
下面,在浏览器的地址栏中输入了不同的URL
地址后,在控制台上呈现出了不同的路由规则对象,也就是路由记录信息。
http://localhost:8080/#/about/users
输入以上地址,该地址为子路由的地址,最终也输出了对应的父路由的记录信息。
后期就可以获取具体的组件来进行渲染。
8、设置响应式的_route
下面我们要做的就是渲染组件。
这里我们先创建一个与路由有关的响应式属性,当路由地址发生变化了,对应的该属性也要发生变化,从而完成页面的重新渲染。
在install.js
文件中添加如下的代码:
Vue.util.defineReactive(this, "_route", this._router.history.current);
以上完成了响应式属性的创建,但是要注意的是defineReactive
方法为Vue
的内部方法,不建议平时通过该方法来创建响应式对象。
beforeCreate() {
//判断是否为Vue的实例,如果条件成立为Vue的实例,否则为其它对应的组件(因为在创建Vue实例的时候会传递选项)
if (this.$options.router) {
//通过查看源码发现,Vue的实例会挂在到当前的私有属性_routerRoot属性上
this._routerRoot = this;
this._router = this.$options.router;
//调用index.js文件中定义的init方法
this._router.init(this);
//在Vue的实例上创建一个响应式的属性`_route`.
Vue.util.defineReactive(this, "_route", this._router.history.current);
}
下面要考虑的就是当路由地址发生了变化后,需要修改_route
属性的值。
在哪完成_route
属性值的修改呢?
在base.js
文件中,因为在该文件中定义了transitionTo
方法,而该方法就是用来完成地址的跳转,同时完成组件的渲染。
base.js
文件修改后的代码如下:
import createRoute from "../util/route";
export default class History {
// router路由对象ViewRouter
constructor(router) {
this.router = router;
this.current = createRoute(null, "/");
//这个回调函数是在hashhistory中赋值,作用是更改vue实例上的_route,_route的值发生变化,视图会进行刷新操作
this.cb = null;
}
//给cb赋值
listen(cb) {
this.cb = cb;
}
transitionTo(path, onComplete) {
this.current = this.router.matcher.match(path);
// 调用cb
this.cb && this.cb(this.current);
// console.log("current===", this.current);
//该回调函数在调用transitionTo方法的时候,会传递过来。
onComplete && onComplete();
}
}
在History
中的构造方法中初始化cb
函数
this.cb = null;
定义listen
方法给cb
函数赋值。
//给cb赋值
listen(cb) {
this.cb = cb;
}
在transitionTo
方法中调用cb
函数,同时传递获取到的当前的路由规则对象也就是路由记录信息。
this.cb && this.cb(this.current);
在什么地方调用listen
方法呢?
在index.js
文件中的init
方法中完成listen
方法的调用。
// 注册路由变化的事件(初始化事件监听器,监听路由地址的变化)。
init(app) {
const history = this.history;
const setUpListener = () => {
history.setUpListener();
};
history.transitionTo(
history.getCurrentLocation(),
//如果直接history.setUpListener
// 这样的话setUpListener里面的this会有问题。
setUpListener
);
//调用父类的中的listen方法
history.listen((route) => {
app._route = route;
});
}
在上面的代码中调用了父类中的listen
方法,然后将箭头函数传递到了listen
中。
这时候,在transitionTo
方法中调用cb
,也就是调用箭头函数,这时传递过来的参数route
,为当前更改后的路由规则信息,交给了app
中的_route
属性。
app
这个参数其实就是Vue
的实例,因为在install.js
文件中调用了init
方法,并且传递的就是Vue
的实例。
这样就完成了对Vue
实例上的响应式属性_route
值的修改,从而会更新组件。
9、$route/$router
创建
创建$route
与$router
的目的是能够在所有的Vue
实例(组件)中,可以获取到。
$route
是路由规则对象,包含了path
,component
等内容
$router
为路由对象(ViewRouter
对象)。
通过查看源码(install.js
)可以发现,其实就是将$router
与$route
挂载到了Vue
的原型上。
所以可以直接将源码内容复制过来就可以了。
Object.defineProperty(Vue.prototype, "$router", {
get() {
return this._routerRoot._router;
},
});
Object.defineProperty(Vue.prototype, "$route", {
get() {
return this._routerRoot._route;
},
});
通过上面的代码,可以看到$route
与$router
都是只读的,因为对应的值,在前面已经设置完毕,这里只是获取。
$router
是通过_routerRoot
来获取。
$route
是通过_routerRoot._route
来获取。
Vue.util.defineReactive(this, "_route", this._router.history.current);
在Vue
对象上创建了_route
属性,该属性的值为路由规则内容
10、Router-View
创建
router-view
就是一个占位符,会用具体的组件来替换该占位符。
router-view
的创建过程如下:
- 获取当前组件的
$route
路由规则对象 - 找到路由规则对象里面的
matched
匹配的record
(里面有component) - 如果是
/about
,matched
匹配到一个record
,直接渲染对应的组件 - 如果是
/about/users
,matched
匹配到两个record
(第一个是父组件,第二个是子组件)
my-vue-router/components
目录下的view.js
代码如下:
export default {
render(h) {
//获取当前匹配的路由规则对象
const route = this.$route;
//获取路由记录对象.只有一个内容,所以获取的是`matched`中的第一项。
const record = route.matched[0];
if (!record) {
return h();
}
//获取记录中对应的组件
const component = record.component;
return h(component);
},
};
以上的代码处理的是没有子路由的情况。
下面,看一下子路由情况的处理。
当然在编写子路由的处理代码之前,我们先把案例中的路由完善一下。
在src
目录下的App.vue
这个组件中,添加一个“关于”的链接。
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link>
<router-link to="/login">Login</router-link>
<router-link to="/about">About</router-link>
</div>
<router-view></router-view>
</div>
</template>
<script>
export default {};
</script>
对应在About
这个组件中,完成子路由应用
<template>
<div>
关于组件
<router-link to="/about/users">用户</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {};
</script>
<style>
</style>
下面完善一下对子路由的处理。
export default {
render(h) {
//获取当前匹配的路由规则对象
const route = this.$route;
let depth = 0;
//记录当前组件为RouterView
this.routerView = true;
let parent = this.$parent;
while (parent) {
if (parent.routerView) {
depth++;
}
parent = parent.$parent;
}
//获取路由记录对象.
// 如果是子路由,例如:子路由/about/users
//子路由是有两部分内容,matched[0]:是父组件内容,matched[1]是子组件内容
const record = route.matched[depth];
if (!record) {
return h();
}
//获取记录中对应的组件
const component = record.component;
return h(component);
},
};
假如,现在我们在浏览器的地址栏中输入了:http://localhost:8080/#/about
地址,
是没有父组件,那么depth
属性的值为0,这时候获取的第一个组件然后进行渲染。
如果地址栏的内容为:http://localhost:8080/#/about/users
这时候有子组件。对应的获取对应的父组件内容,开始进行循环。
在循环的时候,做了一个判断,判断的条件就是当前的父组件必须为:RouterView
组件(子组件中router-view
与父组件中的router-view
构成了父子关系),才让depth
加1,然后取出子组件进行渲染。