这篇文章主要目的是用来梳理一下 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 _Vueexport function install (Vue) {// 判断是否重复安装插件if (install.installed && _Vue === Vue) returninstall.installed = true// 将 Vue 实例赋值给全局变量_Vue = Vueconst isDef = v => v !== undefinedconst registerInstance = (vm, callVal) => {// 至少存在一个 VueComponent 时, _parentVnode 属性才存在let i = vm.$options._parentVnodeif (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 = thisthis._router = this.$options.router// 初始化 routerthis._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 的 getterObject.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 hooksstrats.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 = nullthis.apps = []this.options = optionsthis.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 !== falseif (this.fallback) {// 兼容不支持 historymode = 'hash'}if (!inBrowser) {// 非浏览器模式mode = 'abstract'}this.mode = mode// 根据 mode 创建 history 实例switch (mode) {case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback)breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}}}// 返回匹配的 routematch (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 matchreturn _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.jsexport 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 priorityconst pathList: Array<string> = oldPathList || []// $flow-disable-lineconst pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)// $flow-disable-lineconst 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 } = routeif (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, // 父级 routermatchAs,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}`): undefinedaddRouteRecord(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.jsexport 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 = routerthis.base = normalizeBase(base) // 返回基准路径// start with a route object that stands for "nowhere"this.current = START // 当前 routethis.pending = nullthis.ready = falsethis.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) // 更新 routeonComplete && onComplete(route)this.ensureURL()// fire ready cbs onceif (!this.ready) {this.ready = truethis.readyCbs.forEach(cb => { cb(route) })}}, err => {if (onAbort) {onAbort(err)}if (err && !this.ready) {this.ready = truethis.readyErrorCbs.forEach(cb => { cb(err) })}})}// 确认是否转化路由confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {const current = this.currentconst 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 toroute.matched.length === current.matched.length) {this.ensureURL()return abort()}...updateRoute (route: Route) {const prev = this.currentthis.current = routethis.cb && this.cb(route)this.router.afterHooks.forEach(hook => {hook && hook(route, prev)})}}
在基础的挂载和各种实例都弄完之后,我们就可以从 init 开始入手了
init()
// src/index.jsinit (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 = appconst 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.jstransitionTo (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)// 调用子类方法,用来更新 URLthis.ensureURL()// fire ready cbs once// 调用成功后的ready的回调函数if (!this.ready) {this.ready = truethis.readyCbs.forEach(cb => { cb(route) })}}, err => {if (onAbort) {onAbort(err)}// 调用失败的err回调函数;if (err && !this.ready) {this.ready = truethis.readyErrorCbs.forEach(cb => { cb(err) })}})}// 确认跳转confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {const current = this.currentconst 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 toroute.matched.length === current.matched.length) {// 调用子类的方法更新urlthis.ensureURL()return abort()}const {updated,deactivated,activated} = resolveQueue(this.current.matched, route.matched)const queue: Array<?NavigationGuard> = [].concat(// in-component leave guardsextractLeaveGuards(deactivated),// global before hooksthis.router.beforeHooks,// in-component update hooksextractUpdateHooks(updated),// in-config enter guardsactivated.map(m => m.beforeEnter),// async componentsresolveAsyncComponents(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 URLthis.ensureURL(true)abort(to)} else if (typeof to === 'string' ||(typeof to === 'object' && (typeof to.path === 'string' ||typeof to.name === 'string'))) {// next('/') or next({ path: '/' }) -> redirectabort()if (typeof to === 'object' && to.replace) {this.replace(to)} else {this.push(to)}} else {// confirm transition and pass on the valuenext(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 guardsconst enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)const queue = enterGuards.concat(this.router.resolveHooks)runQueue(queue, iterator, () => {if (this.pending !== route) {return abort()}this.pending = nullonComplete(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 真正在做什么,这样理解的也能更深入。
