vue-router源码解析及手撕代码
项目环境
本项目有vue-cli创建,package.json内容如下
{
"name": "myvuerouter1",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^2.6.14",
"vue-router": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"vue-template-compiler": "^2.6.14"
}
}
当我们使用vue-router插件时会有两步
- Vue.use(VueRouter) 注册组件
- new VueRouter() 实例一个VueRouter类,同时把路由信息传给实例
import Vue from 'vue'
import VueRouter from '../vuerouter'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import( '../views/AboutView.vue')
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router;
当执行 Vue.use(VueRouter) 时会自动调用 VueRouter 类的 install 方法。所以我们应该定义 VueRouter 类,并且其上有 install 方法。
大方向好了,接下来我们来思考一下 install 方法和 VueRouter 类具体做了什么事。
- install方法 :
- 会注册一些全局组件
- 调用 Vue.mixin() 方法使每个组件都可以获取 router 属性,这里是重点难点,具体看代码注释。
- 在Vue对象原型上定义 响应式的$router 和 $route 属性,便于全局进行路由操作。
install.js
import routerLink from "./components/router-link";
import routerView from "./components/router-view";
export let Vue = null;
export const install = function (_Vue) {
Vue = _Vue;
Vue.component('router-link', routerLink)
Vue.component('router-view', routerView)
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
})
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route;
}
})
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router;
}
})
}
- VueRouter类:
- 在构造函数中定义matcher属性,它是一个函数,此函数有两个功能,匹配路径对应的记录,添加新的路由,在该函数首先会先调用 createRouteMap 方法创建路径与记录的映射关系。创建历史管理,选择路由的模式hash还是history。
- 定义 match 方法,返回路径对应的记录
- 定义 init 方法,初始化监听 hash 变化的方法,跳转方法,注意这里是难点要点
index.js
import { install } from './install';
import createMatcher from './create-matcher'
import BrowsHistory from './browsHistory';
import HashHistory from './hashHistory';
class VueRouter {
constructor(options) {
this.matcher = createMatcher(options.routes || []);
this.mode = options.mode || 'hash';
switch (this.mode) {
case 'history':
this.history = new BrowsHistory(this);
break;
case 'hash':
this.history = new HashHistory(this);
break;
}
}
match(location) {
return this.matcher.match(location);
}
init(app) {
const history = this.history;
let setupHashLisener = () => {
history.setupHashLisener();
}
history.transitionTo(history.getCurrentLocation(), setupHashLisener);
history.listen(route => {
app._route = route;
})
}
}
VueRouter.install = install;
export default VueRouter;
VueRouter的构造函数第一步就是获取两个方法:匹配路径对应记录功能 match, 添加匹配功能 addRoutes,同时调用 createRouteMap 方法将传入的路由信息格式化成一个路径记录的的映射关系。
create-matcher.js
import createRouteMap from './create-router-map';
import { createRoute } from './base';
export default function createMatcher(routes) {
let { pathList, pathMap } = createRouteMap(routes);
function match(location) {
let record = pathMap[location];
return createRoute(record, {
path:location
})
}
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap);
}
return {
match,
addRoutes
}
}
注意这里可能会出现路由嵌套,一旦监测到有路由嵌套,就循环调用递归处理子路由
create-router-map.js
function addRouteRecord(route, pathList, pathMap, parentRecord) {
let path = parentRecord ? `${parentRecord.path}/${route.path}`: route.path;
let record = {
path,
component: route.component,
parent: parentRecord
}
if (!pathMap[path]) {
pathMap[path] = record;
pathList.push(path);
}
if (route.children) {
route.foreach(child => {
addRouteRecord(child, pathList, pathMap, record);
})
}
}
export default function createRouteMap(routes, oldPathList, oldPathMap) {
let pathList = oldPathList || [];
let pathMap = oldPathMap || {};
routes.forEach(route => {
addRouteRecord(route, pathList, pathMap);
});
return {
pathList,
pathMap
}
}
接下来就来聊聊这个 init 方法,在VueRouter构造函数内部给对象定义了 history 属性,代表的是路由模式,init 函数内部首先拿到了这个属性的值,可以调用该类的方法。因为路由的两种模式又很多同样的API,所以我们可以定义一个父类,然后再让两种模式继承该父类。
父类中构造函数先保存VueRouter实例,同时匹配根路径对应的所有记录。然后定义路由跳转方法。
base.js
export const createRoute = (record, location) => {
let matched = [];
if (record) {
while (record) {
matched.unshift(record);
record = record.parent;
}
}
return {
...location,
matched
}
}
export default class Base{
constructor(router) {
this.router = router;
this.current = createRoute(null, {
path: '/'
})
}
transitionTo(location, complete) {
let current = this.router.match(location);
if (this.current.path === location && this.current.matched.length === current.matched.length) return;
this.current = current;
this.cb && this.cb(current);
}
listen(cb) {
this.cb = cb;
}
}
接下来说说 hash 子类和 history 子类,首先 hash 子类做的是很简单,就是定义了两个方法获取当前最新的 hash 值,监听 hash 变化的函数供外部调用,本项目未完善 history 模式子类
hashHistory.js
import History from './base'
const ensureSlash = () => {
if (window.location.hash) {
return;
}
window.location.hash = '/';
}
export default class HashHistory extends History{
constructor(router) {
super(router);
this.router = router;
ensureSlash();
}
getCurrentLocation() {
return window.location.hash.slice(1);
}
setupHashLisener() {
window.addEventListener('hashchange', () => {
this.transitionTo(this.getCurrentLocation())
})
}
}
browsHistory.js
import History from './base'
export default class BrowsHistory extends History {
}
总结
Vue-Router 的实现逻辑还是有点难的,但是只要多看看,再写一遍基本就能理清了。难点就是构建路径和记录的映射关系,因为会出现嵌套路由所以需要递归处理,要点就是 hash 模式和 history 模式以及他们的公共类 base,其中逻辑要多看才能理解。
|