共计 10693 个字符,预计需要花费 27 分钟才能阅读完成。
VueRouter 安装和初始化导航
想在 Vue 应用中使用 VueRouter, 我们首先需要安装 VueRouter。方法如下:
安装 VueRouter
Vue.use(VueRouter)
const router = new VueRouter(options)
new Vue({
el: '#app',
data: state,
router, // 将路由器对象 router 作为一个选项添加到 Vue 根实例中
render: h => h(AppLayout),
})
安装 VueRouter,分为以下两步:
- 使用 Vue.use 通过调用 VueRouter 插件中的 install 方法把 VueRouter 安装到 Vue
- 为了每个 Vue 实例都能访问 VueRouter 实例,创建 Vue 根实例时,把路由器对象 router 注入到 Vue 实例
使用 Vue.use 把 VueRouter 安装到 Vue
Vue.use 通过调用 VueRouter 插件中的 install 方法把 VueRouter 安装到 Vue。我们从 VueRouter 的 install 方法入手看一下安装流程
install 方法
做了以下两件事:
- 判断 VueRouter 是否安装
如果已经安装,退出函数
- 更新 Vue 对象
2.1 通过 Vue.mixin 更新 Vue 对象的 beforeCreate 和destroyed 钩子
2.2 通过 Object.defineProperty 给 Vue 对象添加 $router 和 $route 属性。
2.3 在 Vue 对象上注册 RouterView 和 RouterLink 组件
2.4 给 Vue 对象添加路由钩子函数 beforeRouteEnter,beforeRouteLeave 和 beforeRouteUpdate。
具体实现如下:
var _Vue;
function install(Vue) {if (install.installed && _Vue === Vue) {return} // 已安装过,退出函数
install.installed = true; // installed 标记已安装
// 保存 Vue,同时用于检测是否重复安装
_Vue = Vue;
//
var isDef = function (v) {return v !== undefined;};
var registerInstance = function (vm, callVal) {
var i = vm.$options._parentVnode;
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal);
}
};
// 更新 Vue 对象中的 beforeCreate 和 destroyed 钩子
Vue.mixin({beforeCreate: function beforeCreate() {if (isDef(this.$options.router)) { // 调用 Vue 构造函数创建实例时传入了 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) || this;
}
registerInstance(this, this); // 把 RouterView 添加到虚拟 Node
},
destroyed: function destroyed() {registerInstance(this);// 从虚拟 node 中移除 RouterView
}
});
// 给 Vue 对象添加 $router 属性
Object.defineProperty(Vue.prototype, '$router', {get: function get() {return this._routerRoot._router}
});
// 给 Vue 对象添加 $route 属性
Object.defineProperty(Vue.prototype, '$route', {get: function get() {return this._routerRoot._route}
});
Vue.component('RouterView', View); // 注册 RouterView 组件,对应模板中的 <router-view />
Vue.component('RouterLink', Link);// 注册 RouterLink 组件,对应模板中的 <router-link />
// 给 Vue 对象添加路由钩子函数 beforeRouteEnter,beforeRouteLeave,beforeRouteUpdate
var strats = Vue.config.optionMergeStrategies;
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}
创建 Vue 根实例时,把路由器对象 router 注入到 Vue 实例
new Vue({
el: '#app',
data: state,
router, // 将路由器对象 router 作为一个选项添加到 Vue 根实例中
render: h => h(AppLayout),
})
由于 调用 install 方法时,更新了 Vue 对象构造器,那么调用 new Vue(options) 构建 Vue 根实例时,
每个 Vue 实例的初始化阶段都会在 beforeCreate 钩子中更新路由相关的操作,销毁阶段在destroyed 钩子中销毁路由相关的虚拟节点
每个 Vue 实例中都有 $router 和 $route 属性。每个 Vue 实例都可以通过 this.$router 访问全局的 VueRouter 实例,通过 this.$route 访问当前路由记录。
每个 Vue 实例都能使用 RouterView 和 RouterLink 组件
每个 Vue 实例包含路由钩子函数,可用于 组件内 路由导航守卫。
Vue 实例的初始化阶段在 beforeCreate 钩子做了哪些路由相关的操作?
- 给 Vue 实例添加_routerRoot 属性,用于 $router 属性 和 $route 属性从中读取值
如果当前 Vue 实例是根实例
那么给当前 Vue 实例添加_routerRoot 属性,_router 属性 和 _route 属性
由于_routerRoot 指向当前 Vue 实例,那么 _routerRoot 中就包含了 _router 属性 和 _route 属性。
如果不是根实例,
那么给当前 Vue 实例添加_routerRoot 属性,_routerRoot 是从父实例中获取的。
这样一来所有 Vue 实例就都有了_routerRoot 属性。所有 Vue 实例 _routerRoot 属性都是从相同的地方获取值。
- 把 RouterView 组件需要展示的视图对应的虚拟节点添加到虚拟节点树
Vue 实例的销毁阶段在destroyed 钩子做了哪些路由相关的操作?
- 把 RouterView 组件需要展示的视图对应的虚拟节点从虚拟节点树移除
Vue.mixin({beforeCreate: function beforeCreate() { // 混淆进 Vue 的 beforeCreacte 钩子中
if (isDef(this.$options.router)) { // 调用 Vue 构造函数创建实例时传入了 router 选项(根组件)this._routerRoot = this;// this._routerRoot 指向当前 Vue 实例
this._router = this.$options.router;
this._router.init(this); // 调用 VueRouter 实例的 init 方法进行初始化导航
// 使 Vue 根实例中的_route 响应式
//history.current 记录当前路由对象信息
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this); // 把 RouterView 组件需要展示的视图对应的虚拟节点添加到虚拟节点树
},
destroyed: function destroyed() { // 混淆进 Vue 的 destroyed 钩子中
registerInstance(this); // 把 RouterView 组件需要展示的视图对应的虚拟节点从虚拟节点树移除
}
});
Vue 实例的 $router 属性 和 $route 属性都从 Vue 实例的_routerRoot 属性中获取值。
// 给 Vue 对象添加 $router 属性
Object.defineProperty(Vue.prototype, '$router', {get: function get() {return this._routerRoot._router}
});
// 给 Vue 对象添加 $route 属性
Object.defineProperty(Vue.prototype, '$route', {get: function get() {return this._routerRoot._route}
});
下面我们看一下如何调用 VueRouter 实例的 init 方法进行初始化导航。
VueRouter 的 init 方法
做了以下几件事:
- 检查 VueRouter 安装
- 更新 VueRouter 实例 apps 属性 和 app 属性
在 VueRouter 实例的 apps 属性中记录调用当前 VueRouter 实例的所有 Vue 实例。
在 VueRouter 实例的 app 属性中记录调用当前 VueRouter 实例的 Vue 实例。
- 根据路由的当前路径进行导航
history.getCurrentLocation()获取当前路径 eg : ‘/login’
- 监听 VueRouter 实例中记录会话历史的 history 对象。如果发生变化,更新 apps 数组中每个 Vue 实例的_route 属性。
VueRouter.prototype.init = function init(app /* Vue component instance */) {
var this$1 = this;
assert(
install.installed,
"not installed. Make sure to call `Vue.use(VueRouter)`" +
"before creating root instance."
); //1. 检查 VueRouter 安装
this.apps.push(app);// 2. 把当前 Vue 实例添加到 VueRouter 实例的 apps 属性中
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', function () {
// clean out app from this.apps array once destroyed
// 如果当前 Vue 实例调用了 destroyed 钩子函数。那么从 VueRouter 实例的 apps 属性中删除当前 Vue 实例
var index = this$1.apps.indexOf(app);
if (index > -1) {this$1.apps.splice(index, 1); }
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this$1.app === app) {this$1.app = this$1.apps[0] || null; }
});
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {return}
this.app = app; // 更新 VueRouter 实例中的 app 属性指向当前 Vue 实例
var history = this.history; // 根据 router 的 mode 不同,HTML5History 对象,HashHistory 对象,AbstractHistory 对象
if (history instanceof HTML5History) { // history 是 HTML5History 对象, mode == 'history'
//history.getCurrentLocation() 获取当前路径 '/'
history.transitionTo(history.getCurrentLocation()); // 导航到当前路径对应的页面
} else if (history instanceof HashHistory) { // history 是 HashHistory 对象, mode == 'hash'
var setupHashListener = function () {history.setupListeners();
};
history.transitionTo(history.getCurrentLocation(),
setupHashListener,
setupHashListener
);
}
// 监听记录会话历史的 history 对象。如果路由发生变化,更新 apps 数组中每个 Vue 实例的_route 属性
history.listen(function (route) {this$1.apps.forEach(function (app) {app._route = route;});
});
};
通过代码可以看到 VueRouter 中实现导航的使用 transitionTo 方法。
transitionTo
作用:
导航到期望的路由
参数:
location: 是一个字符串路径
onComplete:一个回调函数,在确认导航后执行
onAbort:一个回调函数,在终止导航解析 (导航到相同的路由,或在当前导航完成之前导航到另一个不同的路由) 时执行
工作流程:
- 根据参数 location 获取目的路由对象 route
- 调用confirmTransition 确认导航
2.1 如果确认要导航到目的路由,执行以下操作
a) 调用 updateRoute 更新路由
b) 如果存在 onComplete,调用 onComplete 回调函数
c) 调用 ensureURL 进行导航
d) 如果还没有执行需要在 完成初始化导航 后需要调用的回调函数,那么就执行回调,这些回调函数通过 onReady 注册,保存在 readyCbs 中
2.2 如果终止导航解析,执行以下操作
a) 如果存在 onAbort,调用 onAbort 回调函数
b) 如果还没有执行 初始化路由解析运行出错 时需要调用的回调函数,那么就执行回调。这些回调函数通过 onReady 定义,保存在 readyErrorCbs 中
具体实现如下:
History.prototype.transitionTo = function transitionTo(
location,
onComplete,
onAbort
) {
var this$1 = this;
// 获取目的路由对象
var route = this.router.match(location, this.current);
this.confirmTransition( // 确认导航
route,
function () { // 进行导航
this$1.updateRoute(route); // 更新路由
onComplete && onComplete(route);
this$1.ensureURL();// 进行导航
// fire ready cbs once
if (!this$1.ready) {// 没有执行需要在完成初始化导航后需要调用的回调函数
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {cb(route);
});
}
},
function (err) { // 终止导航
if (onAbort) { // 如果调用 transitionTo 传入的参数中有回调函数
onAbort(err);
}
if (err && !this$1.ready) {// 没有执行初始化路由解析运行出错时需要调用的回调函数
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) {cb(err);
});
}
}
);
};
调用 confirmTransition 确认导航
confirmTransition
作用:
确认导航
参数:
route: 要确认的路由对象
onComplete:一个回调函数,在确认导航后执行
onAbort:一个回调函数,在终止导航解析 (导航到相同的路由,或在当前导航完成之前导航到另一个不同的路由) 时执行
工作原理:
要解析的目的路由对象和当前路由对象是否相同,
- 如果相同
调用 ensureURL 直接导航到当前路由
调用 abort 来停止导航解析流程
- 如果不同,按照以下顺序解析导航
- 在失活的组件里调用离开路由守卫beforeRouteLeave
- 调用全局前置守卫beforeEach
- 在重用的组件里调用更新路由守卫 beforeRouteUpdate
- 在路由配置里调用beforeEnter
- 解析 异步路由组件
- 在被激活的组件里调用beforeRouteEnter
- 调用全局解析守卫beforeResolve
- 导航被确认
- 调用 updateRoute 更新路由
调用全局后置守卫afterEach
- 调用 ensureURL 进行导航
- 触发 DOM 更新
- 在 DOM 更新之后调用回调函数,这些回调函数是通过 beforeRouteEnter 守卫传给 next 的。
具体实现如下:
History.prototype.confirmTransition = function confirmTransition(route, onComplete, onAbort) {
var this$1 = this;
var current = this.current;
var abort = function (err) {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {if (this$1.errorCbs.length) { // this$1.errorCbs 是通过 onError 注册的回调
this$1.errorCbs.forEach(function (cb) {cb(err);
});
} else {warn(false, 'uncaught error during route navigation:');
console.error(err);
}
}
onAbort && onAbort(err);
};
// 如果目的路由对象和当前路由对象相同 -- 导航重复
if (isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
// 那么直接导航到当前路由
this.ensureURL();
return abort(new NavigationDuplicated(route))// 停止导航解析流程
}
var ref = resolveQueue(
this.current.matched,
route.matched
);
var updated = ref.updated;
var deactivated = ref.deactivated;
var activated = ref.activated;
// 把守卫钩子存在 queue 中,按顺序执行
var queue = [].concat(
// in-component leave guards -- 在失活的组件里调用离开路由守卫 beforeRouteLeave
extractLeaveGuards(deactivated),
// global before hooks -- 调用全局前置守卫 beforeEach
this.router.beforeHooks,
// in-component update hooks -- 在重用的组件里调用更新路由守卫 beforeRouteUpdate
extractUpdateHooks(updated),
// in-config enter guards -- 在路由配置里调用 beforeEnter
activated.map(function (m) {return m.beforeEnter;}),
// async components -- 解析异步组件
resolveAsyncComponents(activated)
);
this.pending = route;
var iterator = function (hook, next) {if (this$1.pending !== route) {return abort()
}
try {hook(route, current, function (to) {if (to === false || isError(to)) {// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {// next('/') or next({path: '/'}) -> redirect
abort();
if (typeof to === 'object' && to.replace) {this$1.replace(to);
} else {this$1.push(to);
}
} else {
// confirm transition and pass on the value
next(to);
}
});
} catch (e) {abort(e);
}
};
runQueue(queue, iterator, function () {// 执行完 queue 中的钩子函数后
var postEnterCbs = [];
var isValid = function () { return this$1.current === route;};
// wait until async components are resolved before
// extracting in-component enter guards -- 在被激活的组件里调用 beforeRouteEnter
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
//this$1.router.resolveHooks 调用全局解析守卫 beforeResolve
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {if (this$1.pending !== route) {return abort() // 停止导航解析流程
}
this$1.pending = null;
onComplete(route); // 导航被确认,调用在 transitionTo 中定义的确认后的回调函数
if (this$1.router.app) {this$1.router.app.$nextTick(function () {
// 在 DOM 更新之后调用回调函数,这些回调函数是通过 beforeRouteEnter 守卫传给 next 的
postEnterCbs.forEach(function (cb) {cb();
});
});
}
});
});
};
调用 updateRoute 更新路由
更新路由,并调用全局后置守卫 afterEach
History.prototype.updateRoute = function updateRoute(route) {
var prev = this.current;
// 更新保存当前路由记录的变量 this.current
this.current = route;
this.cb && this.cb(route);
// 调用全局后置守卫 afterEach
this.router.afterHooks.forEach(function (hook) {hook && hook(route, prev);
});
};
完整导航解析流程
- 导航被触发,进入transitionTo 函数
- 获取目的路由对象 route
- 调用confirmTransition 确认导航目的路由对象 route
- 在失活的组件里调用离开路由守卫beforeRouteLeave
- 调用全局前置守卫beforeEach
- 在重用的组件里调用更新路由守卫 beforeRouteUpdate
- 在路由配置里调用beforeEnter
- 解析 异步路由组件
- 在被激活的组件里调用beforeRouteEnter
- 调用全局解析守卫beforeResolve
- 确认要导航到目的路由
- 调用 updateRoute 更新路由
调用全局后置守卫afterEach
- 调用 ensureURL 进行导航
- 触发 DOM 更新
- 在 DOM 更新之后调用回调函数,这些回调函数是通过 beforeRouteEnter 守卫传给 next 的。