引言
参考版本:
vue-router@v3.0.1
路由介绍
什么是路由
路由是指确定如何响应不同URL的过程。它是将URL与特定的操作、资源或内容相关联的方法。在Web开发中,路由通常用于确定当用户访问特定URL时应该展示哪个页面或执行哪些操作。
后端路由
路由这个概念,最开始应该是出现在后端的,在前后端没有分离的时代,通过解析不同的 URL 去拼接需要的 Html 或模板。每次路由的改变,都需要向服务器发送请求,得到一个新的HTML页面。
前端路由
在单页面应用中,浏览器中处理URL变化并相应地加载相应视图内容的机制,就是路由。前端路由库会捕捉这些变化,并根据定义的路由规则,动态更新页面的某部分内容,而不是向服务器请求新页面。
用法回顾
基本使用
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-ink> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 --><!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes // (缩写) 相当于 routes: routes
})
// 4. 创建和挂载根实例。
const app = new Vue({
router
}).$mount('#app')
路由模式
vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
hash模式:
history模式:
const router = new VueRouter({mode: 'history',routes: [...]})
router
访问路由器对象,VueRouter实例对象
// 导航守卫
router.beforeEach((to, from, next) => {
next()
})
router.beforeResolve((to, from, next) => {
next()
})
router.afterEach((to, from) => {})
router.push
router.replace
router.go
router.back
router.forward
router.addRoutes
router.addRoute
router.getRoutes
route
访问当前路由对象
{
fullPath: "/application-form/151730645?AUTH_TICKET=2SadvWLh53JlBMT-XiY3ZMjenSCLoDSM-Kx1bz61eFvXNXWwujtEPBPTr-xgKHWdpTelEldR07pq4bT4BnwkCWsRsSDQYLt_I1xnm_IO0eI%3D&LANG=ZH-CN", // 完整路径
hash: "", //hash值
matched: (2) [{…}, {…}], // 当前路由下的嵌套路由路径
meta: {}, // 路由文件中的meta信息
name: "ApplicationForm", // 路由名称
params: {uid: '151730645'}, // 路径参数
path: "/application-form/151730645", // 路由路径
query: {AUTH_TICKET: '2SadvWLh53JlBMT-XiY3ZMjenSCLoDSM-Kx1bz61eFvXNXWwuj…gKHWdpTelEldR07pq4bT4BnwkCWsRsSDQYLt_I1xnm_IO0eI=', LANG: 'ZH-CN'} // URL查询参数,跟在?后
}
路由守卫(导航守卫)
提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。
全局路由守卫
在路由实例上的钩子函数,所有的路由配置都会触发,通常可以用来控制路由的权限
- beforeEach(to,form,next) - 在路由跳转前触发。
- beforeResolve(to,form,next) - 触发时机在全局beforeEach、组件内beforeRouteEnter 之后,afterEach之前调用。
- afterEach(to,form) - 在路由跳转完成后触发
router.beforeEach((to, from, next) => {
if (to.path === '/login') return next();
//获取token
const tokenStr = window.sessionStorage.getItem('token')
if (!tokenStr) return next('/login')
next()
})
守卫参数:
-
to: 要进入的目标路由
-
form: 要离开的路由
-
next: 放行函数
-
next(): 直接进入下一个路由守卫钩子
-
next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
-
next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。参数可以是一个 Location 对象
-
next(error): 如果传入的是一个error,路由跳转终止,并且该错误会传递给router.onError
-
路由独享的守卫
是指在单个路由配置的时候也可以设置的钩子函数,这些守卫与全局守卫beforeEach的方法参数是一样的。
- beforeEnter: (to, from, next): 只会在路由进入时触发,不会在hash改变时触发
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件级路由守卫
- beforeRouteEnter(to, from, next) - 守卫执行前,组件实例还没被创建。因此不能使用this。
- beforeRouteUpdate(to, from, next) - 路由改变时,组件被复用,这个钩子就会被调用,可以访问this。
- beforeRouteLeave(to, from, next) - 导航离开该组件的对应路由时调用。
// beforeRouteEnter 可以通过给next传递一个回调来获取实例
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}
// 这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
源码解析
目录介绍
// vue-router 项目 src下
components
- link.js // router-link 组件
- view.js // router-view 组件
composables // 封装的一些函数
- globals.js
- guards.js
- index.js
- useLink.js
- utils.js
entries // 入口
- cjs.js
- esm.js
history
- abstract.js // abstract模式
- base.js // History 父类
- hash.js // hash 模式
- html5.js // h5模式(history模式)
util // 工具方法
- async.js
- dom.js
- errors.js
- location.js
- misc.js
- params.js
- path.js
- push-state.js
- query.js
- resolve-components.js
- route.js
- scroll.js
- state-key.js
- warn.js
- create-matcher.js // Route 匹配
- create-route-map.js // Route 映射
- index.js // Router 入口
- install.js // Router 插件安装
- router.js // Router 类
- componets是RouterLink和RouterView这两个组件;
- create-matcher.js就是我们创建match的入口文件;
- create-route-map.js用于创建映射表,pathMap,nameMap等;
- history是创建hitory类的逻辑;
- index.js就是我们的入口文件,其中创建了VueRouter这个类;
- install.js是我们挂载vue-router插件的逻辑;
- util定义了很多工具函数;
需要知道的内容
Location
用来描述路由位置的对象,this.route.push(location),当这里是个object,就是Location
declare type Location = {
name?: string; // 路由名称
path?: string; // 路由的路径,可能包含params和query
hash?: string; // Url的hash部分
query?: Dictionary<string>; // url的查询参数,跟在?后面
params?: Dictionary<string>; // 路由参数
append?: boolean;// 当设置为true时,如果path存在,会将路径添加到当前路径之后。例如,如果当前路径是/a,path是/b,那么最终路径将是/a/b。
replace?: boolean;// 设置为true时,在导航时会调用router.replace()而不是router.push(),这意味着导航不会留下历史记录。
}
Route
路由对象,this.route 就是一个路由对象
declare type Route = {
path: string; // 表示路由的路径(不包括查询参数和哈希)
name: ?string; // 路由的名称。
hash: string; // URL中的哈希值,以#开始。
query: Dictionary<string>; // URL中的查询参数
params: Dictionary<string>; // 路由的参数对象。
fullPath: string; // 完整的解析后的URL,包括查询参数和哈希值。
matched: Array<RouteRecord>; // 一个数组,包含了当前路由匹配到的所有路由记录(RouteRecord)。
redirectedFrom?: string; // 如果当前路由是重定向的结果,这个字段会包含重定向前的路径。
meta?: any; // 路由元信息。可以在路由守卫中访问
}
RouteRecord
路由记录(我们写的那个路由配置)
declare type RouteRecord = {
path: string; // 路由的路径,根据 parent 的 path 做计算
alias: Array<string>; // 路由的别名,一个或多个
regex: RouteRegExp; // path解析的正则表达式扩展
components: Dictionary<any>; // 用来存储路由视图对应的组件
instances: Dictionary<any>; // 用来存储路由组件的实例
enteredCbs: Dictionary<Array<Function>>; // 函数数组,这些函数是在路由被确认进入之前需要调用的回调。
name: ?string; // 路由的名称
parent: ?RouteRecord; // 父路由记录
redirect: ?RedirectOption; // 定义路由的重定向规则
matchAs: ?string; // 当路由有别名时,matchAs属性会被设置为别名的路径。
beforeEnter: ?NavigationGuard; // 路由独享守卫
meta: any; // 路由元信息
props: boolean | Object | Function | Dictionary<boolean | Object | Function>; // 用于传递props给组件
}
RouteRegExp 是正则表达式的扩展,它利用了path-to-regexp 这个工具库,把 path 解析成一个正则表达式的扩展
const keys = [];
const regexp = pathToRegexp("/user/:id", keys);
console.log(regexp) // /^/user(?:/([^/#?]+?))[/#?]?$/i
console.log(keys) // [{name: "id", prefix: "/", suffix: "", pattern: "[^/#?]+?", modifier: ""}]
插件安装 —— install.js
vue插件
Vue-router 本质上是官方提供的一个插件,在vue 中用 Vue.use 来注册插件
,使用方式如下:
import Vue from 'vue';
import Router from 'vue-router';
// 在vue中注册组件
Vue.use(Router);
vue.use的源码
export function initUse (Vue: GlobalAPI) {
// plugin 可以使一个函数也可以是一个数组
Vue.use = function (plugin: Function | Object) {
// 组件是否被注册过
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
// this指向的是vue,后面把vue传给了组件,后面在看vue-rputer的install也可以看到
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 最后,将插件添加到installedPlugins中,标注已被注册过
installedPlugins.push(plugin)
return this
}
}
Vue.use接收一个plugin(可以是一个函数也可以是一个Object)的参数,并且维护了一个_installedPlugins,用于缓存注册过的plugin。然后会判断当前的plugin是否被注册过,如果被注册过提前结束并返回vue;如果没有,注册组件,然后把vue传给plugin的注册函数并调用了,这样插件就不需要额外的引入vue了。
install.js
// /src/intall.js
import View from './components/view'
import Link from './components/link'
export let _Vue
// 这里就是就上面传过来的vue实例
export function install (Vue) {
// 防止重复注册
if (install.installed && _Vue === Vue) return
install.installed = true
// 这里把vue给保存起来了
_Vue = Vue
// 是不是根组件
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 这里通过全局mixin,执行一些操作
Vue.mixin({
beforeCreate () {
// 如果是根组件
if (isDef(this.$options.router)) {
this._routerRoot = this
// VueRouter 的实例
this._router = this.$options.router
// vuerouter初始化
this._router.init(this)
// 让_route属性变成了一个响应式对象,当_route发生变化的时候,router-view会重新渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 如果当前组件是子组件,就将我们_root根组件挂载到子组件。
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 为router-view组件关联路由组件
registerInstance(this, this)
},
destroyed () {
// 取消router - view和路由组件的关联
registerInstance(this)
}
})
// 挂载router在组件实例上,可以在vue组件中直接使用this.router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 挂载router在组件实例上,可以在vue组件中直接使用this.router
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 引入RouterView 和 RouterLink 两个全局组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
//设置路由组件守卫的合并策略
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
-
通过installed参数避免重复安装
-
保存install中传入的vue实例
-
全局mixn,给所有的组件注入 beforeCreate 和 destroyed两个生命周期,并初始化路由
- 根组件,让route变成一个响应式属性
- 子组件,把根组件挂载上去
-
将router 和 route 挂载到vue实例上
-
注册全局组件router-view、router-link
-
合并路由守卫策略
VueRouter 实例
// src/router.js
export default class VueRouter {
constructor (options: RouterOptions = {}) {
if (process.env.NODE_ENV !== 'production') {
warn(this instanceof VueRouter, `Router must be called with the new operator.`)
}
this.app = null // 保存挂载实例
this.apps = [] // VueRouter支持多实例
this.options = options //接收的参数
this.beforeHooks = [] // beforeEach hook
this.resolveHooks = [] // beforeResolve hook
this.afterHooks = [] // afterEach hook
this.matcher = createMatcher(options.routes || [], this) // 路由匹配器,传入写好的路由表
let mode = options.mode || 'hash' // 当前路由模式,默认hash
// 当前浏览器不支持history模式,回退到hash
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 非浏览器环境,使用abstract模式
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根据不同的模式,实例化不同的history实例
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
// 根据路由的原始位置和可选的当前路由和重定向来源,返回一个路由对象
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
// 获取当前激活的路由对象
get currentRoute (): ?Route {
return this.history && this.history.current
}
// 初始化路由,接受Vue实例作为参数
init (app: any) {
// ...实现省略...
}
// 添加全局前置守卫
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
// 添加全局解析守卫
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
// 添加全局后置钩子
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
// 当路由完全准备就绪时调用回调函数
onReady (cb: Function, errorCb?: Function) {
// ...实现省略...
}
// 当路由过程中出错时调用
onError (errorCb: Function) {
// ...实现省略...
}
// 导航到一个新URL,向history栈添加一个新纪录
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// ...实现省略...
}
// 导航到一个新URL,替换history栈上的当前纪录
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// ...实现省略...
}
// 在history记录中向前或者后移动特定数量的步骤
go (n: number) {
// ...实现省略...
}
// 后退到历史记录的上一步
back () {
// ...实现省略...
}
// 前进到历史记录的下一步
forward () {
// ...实现省略...
}
// 根据给定的路由获取匹配的组件数组(用于服务端渲染)
getMatchedComponents (to?: RawLocation | Route): Array<any> {
// ...实现省略...
}
// 解析目标路由的路由信息
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {...} {
current = current || this.history.current
const location = normalizeLocation(to, current, append, this)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
normalizedTo: location,
resolved: route,
}
}
// 获取路由匹配器上的路由列表
getRoutes () {
return this.matcher.getRoutes()
}
// 动态添加更多的路由规则
addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
this.matcher.addRoute(parentOrRoute, route)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
// 一次性动态添加多条路由规则
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
new VueRouter 完成了以下内容:
-
接收options 参数,主要是routes(路由表)、mode(路由模式)等
-
保存vue实例
-
全局路由守卫数组初始化
-
创建路由匹配器,传入routes路由表和VueRouter实例
-
确定路由模式
- 没有传路由模式,则默认时hash模式
- 如果mode为history,但当前环境不支持,则回退至hash模式
- 非浏览器环境,强制为abstract模式
-
根据不同的模式创建不同的History实例
matcher
createMatcher
// src/create-matcher.js
// 定义Matcher类型,它包含match、addRoutes、addRoute和getRoutes等方法
export type Matcher = {
// match方法用于匹配给定的原始路由位置raw,并返回一个Route对象
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
// addRoutes方法用于动态添加多个路由配置
addRoutes: (routes: Array<RouteConfig>) => void;
// addRoute方法用于动态添加单个路由配置
addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
// getRoutes方法用于获取当前路由器已注册的所有路由记录
getRoutes: () => Array<RouteRecord>;
};
// createMatcher函数用于创建Matcher对象
export function createMatcher (routes: Array<RouteConfig>, router: VueRouter): Matcher {
// 创建一个路由映射表 const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 添加多个路由配置到路由系统
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 添加单个路由配置到路由系统
function addRoute (parentOrRoute, route) {
// ...
}
// 获取当前已注册的所有路由记录
function getRoutes () {
// ...
}
// 根据给定的原始路由位置raw、当前路由currentRoute和重定向来源redirectedFrom匹配路由
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// ...
}
// 处理重定向路由
function redirect (record: RouteRecord, location: Location): Route {
// ...
}
// 处理别名路由
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {
// ...
}
// 内部函数,用于创建Route对象
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
// ...
}
// 返回Matcher对象,包含上述定义的方法
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
createMatcher 接收 2 个参数,一个是routes,是传进来的路由表配置,另外一个是vue-router实例
// 用户自定义路由配置
const routes = [{ path: '/home', component: Home },{ path: '/login', component: Login }]
从CreateRouterMap()中创建一个路由映射表
- pathList - 保存所有的path
- pathMap - path -> RouteRecord
- pathMap - name -> RouteRecord
返回一个Matcher对象,Matcher对象包含一个用于匹配的match方法、动态添加路由的addRoutes/addRoute方法、获取当前所有路由的getRoutes方法
match
匹配出一个route
declare type RawLocation = string | Location
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 解析一个location对象,规范化
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 如果有命名路由
if (name) {
// 在路由名记录中查找对应的记录
const record = nameMap[name]
// 如果没有找到记录,则创建并返回一个空的路由对象
if (!record) return _createRoute(null, location)
// 提取出路径参数的名称
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
// 如果location中没有params属性,则创建一个空对象
if (typeof location.params !== 'object') {
location.params = {}
}
// 如果有当前路由并且有路由参数
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
// 如果当前路由的参数没有在新location中定义,并且是必需的参数,则从当前路由复制到新location
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
// 使用路由记录中的路径模板和参数生成新的路径
location.path = fillParams(record.path, location.params, `named route "${name}"`)
// 创建并返回新的路由对象
return _createRoute(record, location, redirectedFrom)
}
// 如果没有命名路由,但有路径
else if (location.path) {
// 初始化params为空对象
location.params = {}
// 遍历所有路径
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
// 如果路由记录匹配当前路径
if (matchRoute(record.regex, location.path, location.params)) {
// 创建并返回新的路由对象
return _createRoute(record, location, redirectedFrom)
}
}
}
// 如果没有匹配的命名路由或路径,返回一个空的路由对象
return _createRoute(null, location)
}
-
首先执行了 normalizeLocation,解析得到一个location对象
-
计算出新的 location 后,对 location 的 name 和 path 的两种情况做了处理。
-
name
-
通过nameMap找到路由记录
- 如果没有,就返回一个新创建的空路由,返回一个空路径
-
如果location中没有params属性,则创建一个空对象
-
拿到 record 对应的 paramNames,再对比 currentRoute 中的 params,把交集部分的 params 添加到 location 中
-
通过 fillParams 方法根据 record.path 和 location.path 计算出 location.path
-
调用_createRoute(record, location, redirectedFrom) 去生成一条新路径
-
-
path
-
遍历pathList
-
matchRoute匹配到对应记录
-
调用_createRoute(record, location, redirectedFrom) 去生成一条新路径
-
-
接下来看下_createRoute的实现,不考虑重定向和别名路由,最终都会调用createRoute方法
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
// 重定向相关,忽略
}
if (record && record.matchAs) {
// 别名相关,忽略
}
return createRoute(record, location, redirectedFrom, router)
}
//src/uitl/route.js
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
createRoute方法其实很简单,就是通过record和location 去创建一个route,formatMatch就是一直往上找到根路由为止。
addRoute / addRoutes
// addRoute函数用于添加单个路由规则
function addRoute (parentOrRoute, route) {
// 如果parentOrRoute是一个字符串,则从nameMap中找到对应的父路由记录
const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
// 使用createRouteMap函数添加新的路由记录
createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
}
// addRoutes函数用于添加多个新的路由规则
// 接收一个路由配置数组routes作为参数
function addRoutes (routes) {
// 使用createRouteMap函数添加新的路由记录
createRouteMap(routes, pathList, pathMap, nameMap)
}
addRoute和addRoutes非常简单,就是使用createRouteMap去添加路由
createRouteMap
// src/create-route-map.js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 创建或使用旧的路径列表
const pathList: Array<string> = oldPathList || []
// 创建或使用旧的路径映射表
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// 创建或使用旧的名称映射表
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历添加的routes
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// 确保通配符(*)路径位于列表的末尾
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
createRouteMap的作用是把用户的配置转换为三个部分(pathList、pathMap和nameMap)
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 创建一个新的路由记录对象
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
instances: {},
enteredCbs: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 如果路由配置对象有children属性,递归处理子路由
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 如果pathMap中还没有这个路径的记录,则将其添加到pathList和pathMap中
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if (route.alias !== undefined) {
// 处理别名路由
}
// 如果路由名称存在且nameMap中还没有这个名称的记录
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
- 创建一个新的路由记录
- 递归处理子路由
- 将path添加到pathList、将path -> record 添加到pathMap
- 将name -> record 添加到pathMap
路由切换
路由跳转
下面我们讲一下路由的跳转,路由的push/replace都会调用到这个方法
// src/history/base.js
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
// 获取要跳转的route信息
const route = this.router.match(location, this.current)
// 切换路由的核心方法
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
// 当前路由
const current = this.current
// 待跳转路由
this.pending = route
// 中断函数
const abort = err => {
onAbort && onAbort(err)
}
// 获取当前路由和待处理路由的匹配记录的索引
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
// 如果当前路由和待处理路由是相同的,并且层级也相同,则不需要跳转
if (
isSameRoute(route, current) &&
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
if (route.hash) {
handleScroll(this.router, current, route, false)
}
return abort(createNavigationDuplicatedError(current, route))
}
// 拿到需要更新的所有路由(要跳转路由的父级路由),当前路由,待跳转的路由
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 路由守卫队列
const queue: Array<?NavigationGuard> = [].concat(
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(m => m.beforeEnter),
resolveAsyncComponents(activated)
)
// 迭代器函数,用于迭代执行守卫队列
const iterator = (hook: NavigationGuard, next) => {
// ...
}
// 使用iterator迭代器函数按顺序执行queue队列中的所有导航守卫
runQueue(queue, iterator, () => {
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
// 路由跳转
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
}
- 通过match方法获取要跳转的route信息
- 相同路由则不跳转
- 按顺序执行路由守卫
- 调用不同模式的方法跳转路由
路由模式
Hash
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// 设置监听事件,初始化时调用
setupListeners () {
// ...
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
// hash push
pushHash(route.fullPath)
},
onAbort
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
// hash replace
replaceHash(route.fullPath)
},
onAbort
)
}
go (n: number) {
window.history.go(n)
}
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {
return getHash()
}
}
function pushHash (path) {
// 支持pushState直接使用,直接使用 window .history的api
if (supportsPushState) {
pushState(getUrl(path))
} else {
// 不支持则直接修改hash
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
// 去掉hash的部分
function getUrl (path) {
const href = window.location.href
// 拿到hash值
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
export function pushState (url?: string, replace?: boolean) {
const history = window.history
if (replace) {
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
}
export function replaceState(url?: string) {
pushState(url, true)
}
-
在vueRouter初始化时,会添加监听事件
- 支持pushState的环境,则监听popstate事件
- 不支持pushState的环境,则监听hashchange事件
-
go 调用的是window.history.go 方法·
-
replace
- pushState存在,window.history.pushState
- pushState不存在 ,window.location.replace
-
push
-
pushStatey存在,window.history.replaceState
-
pushState不存在 *, *直接改变hash的值
-
可以看到,在支持pushState存在的环境下,hash模式,会优先使用history的api,并且是监听popstate事件。
html5
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ?string) {
super(router, base)
this._startLocation = getLocation(this.base)
}
// 设置监听事件,初始化时调用
setupListeners () {
// ...
window.addEventListener('popstate', e => {
const current = this.current
const location = getLocation(this.base)
if (this.current === START && location === initLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
}
go (n: number) {
window.history.go(n)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
}, onAbort)
}
ensureURL (push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
}
getCurrentLocation (): string {
return getLocation(this.base)
}
}
export function pushState (url?: string, replace?: boolean) {
const history = window.history
if (replace) {
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
}
export function replaceState(url?: string) {
pushState(url, true)
}
-
在vueRouter初始化时,会添加监听事件
- 监听popstate事件
-
go 调用的是window.history.go 方法·
-
replace调用的是window.history.pushState
-
push调用的是window.history.replaceState
组件
router-link
// src/components/link.js
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
custom: Boolean,
exact: Boolean,
exactPath: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: {
type: String,
default: 'page'
},
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
const router = this.$router
const current = this.$route
//
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
// 样式处理,省略
// ...
// 重定向处理,创建一个新的route
const compareTarget = route.redirectedFrom
? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
: route
// 点击后类名处理
// ...
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
// replace 跳转
router.replace(location, noop)
} else {
// push 跳转
router.push(location, noop)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
// 插槽相关处理
// ...
// 标签处理
// ...
return h(this.tag, data, this.$slots.default)
}
}
-
解析出location, route, href
-
router-link样式处理
-
给router-link的事件都绑定上handler 函数
- 点击router-link实际上就是执行router.push / router.replace 方法
router-view
VueRouter 内置了组件,是用来进行渲染当前路由的组件
// src/components/view.js
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// 标识该组件是通过 route-view 渲染出来的
data.routerView = true
const h = parent.$createElement
const name = props.name
// 获取当前路由对象
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// 计算嵌套的 router-view 深度
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
// 如果是 keep-alive 设置 inactive 为 true
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
// 保存router-view 深度,用于后续的匹配
data.routerViewDepth = depth
// keep-alive
if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
// 渲染keepAlive缓存的组件
if (cachedComponent) {
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
// 如果没有缓存的组件,返回空
return h()
}
}
// 根据深度获取需要渲染的路由
const matched = route.matched[depth]
const component = matched && matched.components[name]
if (!matched || !component) {
cache[name] = null
return h()
}
// 缓存当前组件
cache[name] = { component }
// ...
// 如果路由配置有 props,处理并填充到 data 中的props中
const configProps = matched.props && matched.props[name]
if (configProps) {
// 将route和props也缓存起来
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
}
}
- 计算router-view 的深度
- 通过matched获得具体路由
- 把路由配置的props 注入到 组件的 data.protos 参数中