当我们使用vue-router插件时会有两步
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(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router;
当执行 Vue.use(VueRouter) 时会自动调用 VueRouter 类的 install 方法,安装 vue-router 插件。所以我们在实现 vue-router 时应该定义 VueRouter 类,并且其上声明 install 方法。
开始撸代码前我们先来捋一捋 vue-router 有哪些特点,首先他有两个内置组件 router-link 和 router-view,其次可以通过 router 拿到 vue-router 实例,最后还可以通过this.$router
拿到 vue-router 实例(包括了路由的跳转方法,钩子函数等),通过this.$route
拿到路由对象(包括path,params,hash,query,fullPath,matched,name等路由信息参数)。所以我们在 install 方法中要做如下工作:
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)
// 每个子组件都获取router属性
Vue.mixin({
// 如果有router 说明你在根实例增加了router
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this;//将当前根实例Vue放到了_routerRoot
this._router = this.$options.router; // 当前VueRouter实例
// 调用初始化函数
this._router.init(this);
// 把路由_route属性变成响应式的
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 将根组件的_routerRoot赋值个子组件的_routerRoot
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
})
// vue原型上添加两个响应式属性
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route;//取current
}
})
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router;
}
})
}
我们再来想一想 vue-router 的核心功能是什么?当然是根据路由匹配对应的记录(记录就是组件加上一些其他信息的对象)啦! 那么就首先需要有路由到记录的映射关系,其次需要有一个方法能返回路由对应记录。
index.js
import { install } from './install';
import createMatcher from './create-matcher'
import BrowsHistory from './browsHistory';
import HashHistory from './hashHistory';
class VueRouter {
constructor(options) {
// 本函数两个功能:匹配路径对应记录功能match, 添加匹配功能addRoutes
this.matcher = createMatcher(options.routes || []);//获取用户的整个路由配置
// 创建历史管理 路由两种模式, 默认是hash
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) {//app指代最外层的Vue
const history = this.history;
// 设置onHashChange,监听hash变化
let setupHashLisener = () => {
history.setupHashLisener();
}
// 跳转路径,会进行匹配操作根据路径获取对应的记录
history.transitionTo(history.getCurrentLocation(), setupHashLisener);
// 当跳转后更新_route,将回调函数传入
history.listen(route => {
app._route = route;//current更新后在更新_route
})
}
}
VueRouter.install = install;
export default VueRouter;
create-matcher.js
addRoutes 是动态添加路由函数,它的参数是路由信息(可以是一个数组),内部会调用 createRouteMap 方法将传入的路由信息格式化成一个路径到记录的映射关系表。
import createRouteMap from './create-router-map';
import { createRoute } from './base';
export default function createMatcher(routes) {
// pathList保存所有的路由路径,同时获得所有路径及其对应记录的映射关系表
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) {
// 将嵌套路由的父路由与当前路由拼接 路由/a的子路由是/b,那么放入path时就是['/a/b']
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);
}
// 将子路由也放到对应的pathmap和pathlist,循环递归处理子路由
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
}
}
vue-router 有三种模式,分别是 history、hash、abstract,它们的特点如下:
hash:使用URL的 hash 来模拟一个完整的URL,当 hash 值发生改变时,页面不会重新加载,其显示的网络路径中会有 # 号,兼容所有的浏览器和服务器。可以使用 JavaScript 来对 loaction.hash 进行赋值,改变URL的 hash 值; 通过 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)。
history:美化后的 hash 模式,路径中会去掉 #。依赖于 html5 的历史记录调用栈,所以要担心IE9及以下的版本,并且还包括back、forward、go三个方法,对应浏览器的前进、后退、跳转操作。
abstract:适用于所有JavaScript环境,例如服务器端使用Node.js。如果没有浏览器API,路由器将自动被强制进入此模式
接下来就来聊聊这个 init 方法,在VueRouter构造函数内部给对象定义了 history 属性,代表的是路由模式,init 函数内部首先拿到了这个属性的值,可以调用该类的路由处理方法。
因为路由的两种模式有很多同样的处理逻辑,所以我们可以定义一个父类,然后再让两种模式继承该父类,这样就提高了代码的复用能力。父类中构造函数先保存 VueRouter 实例,同时匹配根路径对应的所有记录。然后定义路由跳转方法。
还需注意路由匹配逻辑
由于在构建映射表的时候,一个记录里有它的父级路由,例路由 /a/b/c,先将对应记录放入结果数组,那么它的父级路由就是 /a/b,再将对应记录放入结果数组,并且要放在子路由对应记录的前边,再将父路由 /a 对应记录放入结果数组。
base.js
export const createRoute = (record, location) => {//根据路径记录匹配到的所有组件
// 存放匹配到的记录
let matched = [];
// 如果record存在就将其及其所有父组件存入数组中,父组件存放位置在前
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) {
// 路径变化,currnt更新
let current = this.router.match(location);
// 判断本次要跳转的路径是否和当前路径相同
if (this.current.path === location && this.current.matched.length === current.matched.length) return;
// 用最新的匹配结果更新视图
this.current = current;//这个current只是响应式的,它的变化不会更新_route,所以需要在 init方法中手动更新保持数据同步
// 调用回调函数更新_route
this.cb && this.cb(current);
}
// 记录传入的回调函数,该回调函数目的在于更新_route
listen(cb) {
this.cb = cb;
}
}
接下来说说 hash 子类和 history 子类,首先 hash 子类做的是很简单,就是定义了两个方法获取当前最新的 hash 值,监听 hash 变化的函数供外部调用,
hashHistory.js
import History from './base'
// 判断是否有hash值
const ensureSlash = () => {
if (window.location.hash) {
return;
}
window.location.hash = '/';
}
// 继承父类
export default class HashHistory extends History{
constructor(router) {
super(router);
this.router = router;
// 如果使用hash默认如果没有hash应该跳转到首页
ensureSlash();
}
// 获取当前hash值
getCurrentLocation() {
// 得到的hash值带#号所以应该截取一下
return window.location.hash.slice(1);
}
// 定义监听hash变化的函数
setupHashLisener() {
window.addEventListener('hashchange', () => {
// 有变化则跳转路由
this.transitionTo(this.getCurrentLocation())
})
}
}
browsHistory.js
由于 history 模式比较简单。本项目未完善 history 模式子类。
import History from './base'
export default class BrowsHistory extends History {
}
vue-router 的核心功能就是根据路由匹配对应的记录,所以我们就需要在实例化 vue-router 类时调用 createMatcher 方法让他继续处理构建路由到记录的映射表,同时还要返回一个函数用来匹配路由对应路径,最后由于有三种模式可选且所有模式有一些共用的处理逻辑,所以可以声明一个基础类封装一些共用逻辑,再声明三个类继承基础类并且封装各个模式特有的逻辑。到此一个简单的 vue-router 就实现了。