这篇文章主要目的是用来梳理一下 Vue-router 的整个实现流程
首先,我们来看一下 Vue-router 源码的目录结构
|——vue-router
|——build // 构建脚本
|——dist // 输出目录
|——docs // 文档
|——examples // 示例
|——flow // 类型声明
|——src // 项目源码
|——components // 组件(view/link)
|——history // Router 处理
|——util // 工具库
|——index.js // Router 入口
|——install.js // Router 安装
|——create-matcher.js // Route 匹配
|——create-route-map.js // Route 映射
当然,我们主要关注的还是 src 目录下的文件。
router 注册
在我们开始使用 Vue-router 之前,要在主函数 main.js 里调用,也就是Vue.use(VueRouter)
,声明这个的目的就是利用了 Vue.js
的插件机制来安装 vue-router
。
当 Vue 通过 use() 来调用插件时,会调用插件的 install 方法,若插件没有 install 方法,则将插件本身作为函数来调用。
通过目录我们可以看出,install 方法是存在的,那我们首先就来看看这个文件。
// src/install.js
// 引入 router-view 和 router-link 组件
import View from './components/view'
import Link from './components/link'
// export 一个私有 Vue 引用
export let _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) => {
// 至少存在一个 VueComponent 时, _parentVnode 属性才存在
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 混入 beforeCreate 钩子函数,在调用该函数时,初始化路由
Vue.mixin({
beforeCreate () {
// 判断组件是否有 router 对象,该对象只在根组件上有
if (isDef(this.$options.router)) {
// 将 router 的根组件指向 Vue 实例
this._routerRoot = this
this._router = this.$options.router
// 初始化 router
this._router.init(this)
// 为 _route 属性实现双向绑定,触发组件渲染
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 用于 router-view 层级判断
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 定义 Vue 原型方法 $router 和 $route 的 getter
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册 router-view 和 router-link 组件
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
}
VueRouter 实例化
在安装过后,就要对 VueRouter 进行实例化操作。
// src/index.js
...
// 实例化时,主要做了两件事: 创建 matcher 对象; 创建 history 实例
export default class VueRouter {
static install: () => void;
static version: string;
app: any;
apps: Array<any>;
ready: boolean;
readyCbs: Array<Function>;
options: RouterOptions;
mode: string;
history: HashHistory | HTML5History | AbstractHistory;
matcher: Matcher;
fallback: boolean;
beforeHooks: Array<?NavigationGuard>;
resolveHooks: Array<?NavigationGuard>;
afterHooks: Array<?AfterNavigationHook>;
constructor (options: RouterOptions = {}) {
// 配置路由对象
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
// 对 mode 做检测 options.fallback 是新增属性,表示是否对不支持 HTML5 history 的浏览器做降级处理
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
// 兼容不支持 history
mode = 'hash'
}
if (!inBrowser) {
// 非浏览器模式
mode = 'abstract'
}
this.mode = 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}`)
}
}
}
// 返回匹配的 route
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
...
}
创建路由匹配对象
// src/create-matcher.js
// 定义 Matcher 类型
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 根据 routes 创建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 添加路由函数
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 如果 name 存在的话,就去 name map 中去找到这条路由记录
if (name) {
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
// 如果没有这条路由记录就去创建一条路由对象
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
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)
}
}
}
// no match
return _createRoute(null, location)
}
...
// 根据不同的条件去创建路由对象;
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
// 返回 matcher 对象
return {
match,
addRoutes
}
}
根据源码我们可以看出,createMatcher 就是根据传入的 routes 生成一个 map 表,并且返回 match 函数以及一个可以增加路由配置项 addRoutes 函数。
我们继续看 route-map 的生成
// src/create-route-map.js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
// 创建映射列表
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历路由配置,为每个配置添加路由记录
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ensure wildcard routes are always at the end 通配符一直保持在最后
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
}
}
// 添加路由记录
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.`
)
}
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// 序列化 path 用 / 替换
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
// 对路径进行正则匹配是否区分大小写
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 创建一个路由记录对象
const record: RouteRecord = {
path: normalizedPath, // 路径
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), //转化为匹配数组
components: route.components || { default: route.component }, // 关联数组
instances: {}, // 实例
name, // 名称
parent, // 父级 router
matchAs,
redirect: route.redirect, // 跳转
beforeEnter: route.beforeEnter, // deforeEnter 钩子函数
meta: route.meta || {}, // 附加参数
props: route.props == null // prop 属性
? {}
: route.components
? route.props
: { default: route.props }
}
// 递归子路由
if (route.children) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
if (process.env.NODE_ENV !== 'production') {
if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 别名
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
})
}
// 按路径存储
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 处理命名路由,按照名字存储
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
从上述代码可以看出, create-route-map.js
的主要功能是根据用户的 routes
配置的 path
、alias
以及 name
来生成对应的路由记录, 方便后续匹配对应。
History 实例化
vueRouter 提供了 HTML5History、HashHistory、AbstractHistory 三种方式,根据不同的 mode 和实际环境去实例化 History
// src/history/base.js
export class History {
router: Router; // router 对象
base: string; // 基准路径
current: Route; // 当前路径
pending: ?Route;
cb: (r: Route) => void;
ready: boolean;
readyCbs: Array<Function>;
readyErrorCbs: Array<Function>;
errorCbs: Array<Function>;
// 子类
// implemented by sub-classes
+go: (n: number) => void;
+push: (loc: RawLocation) => void;
+replace: (loc: RawLocation) => void;
+ensureURL: (push?: boolean) => void;
+getCurrentLocation: () => string;
constructor (router: Router, base: ?string) {
this.router = router
this.base = normalizeBase(base) // 返回基准路径
// start with a route object that stands for "nowhere"
this.current = START // 当前 route
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
...
// 路由化操作
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current) // 找到匹配路由
this.confirmTransition(route, () => { // 确认是否转化
this.updateRoute(route) // 更新 route
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
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
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(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()
}
...
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
在基础的挂载和各种实例都弄完之后,我们就可以从 init 开始入手了
init()
// src/index.js
init (app: any /* Vue component instance */) {
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 从 install 知道 这个 app 是我们实例化的 Vue 实例
this.apps.push(app)
// main app already initialized.
if (this.app) {
return
}
// 将 vueRouter 内部的 app 指向 Vue 实例
this.app = app
const history = this.history
// 对 HTML5History 和 HashHistory 进行特殊处理
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
// 监听路由变化
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 设置路由改变的时候监听
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
我们这里看出,对于 HTML5History 和 HashHistory 进行了不同的处理,因为此时需要根据浏览器地址栏里的 path 或者 hash 来匹配对应的路由。尽管有些不同,但是都调用了 transitionTo 方法,让我们来看一下这个方法。
// src/history/base.js
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// location 为当前路由
// 这里调用了 match 方法来获取匹配的路由对象,this.current 指我们保存的当前状态的对象
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
// 更新当前对象
this.updateRoute(route)
onComplete && onComplete(route)
// 调用子类方法,用来更新 URL
this.ensureURL()
// fire ready cbs once
// 调用成功后的ready的回调函数
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
if (onAbort) {
onAbort(err)
}
// 调用失败的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
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(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
) {
// 调用子类的方法更新url
this.ensureURL()
return abort()
}
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
this.pending = route
// 每一个队列执行的 iterator 函数
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.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.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 执行各种钩子队列
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
})
})
}
其实这里说白了就是各种钩子函数来回秀操作,要注意的就是每个 router 对象都会有一个 matchd 属性,这个属性包含了一个路由记录。
在这里大多数博客都会说一下 src/index.js 的一个小尾巴,我们也不例外
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
这里更新的 _route 的值,这样就可以去通过 render 进行组件重新渲染。
Vue-router 的大致流程就是这些,还有一些 utils 和几种不同的 history 的具体实现还没有讲到,在这里就不一一详解了,还是推荐结合着源码去理解 Vue-router 真正在做什么,这样理解的也能更深入。