Open dwqs opened 7 years ago
在前端框架 React、Vue.js 和 Angular 三足鼎立的年代, Vue.js 因其易用、易学、学习成本低等特点已经成为了广大前端er的新宠, 而其对应的路由 vue-router 也是简单好用, 功能强大. 本文将结合 Vue.js 来分析 vue-router 的整体流程.
vue-router
本文分析的 vue-router 的版本为 2.6.0, vue 的版本为 2.3.3.
vue-router@2.6.0 的整体目录结构如下:
|——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 映射
主要关注点就是 components、history 目录以及 create-matcher.js、create-route-map.js、index.js、install.js 等文件. 下面以一个小 demo 来分析vue-router 的整体流程.
components
history
create-matcher.js
create-route-map.js
index.js
install.js
首先看 demo 入口的代码部分:
// 1.包引入 import Vue from 'vue'; import VueRouter from "vue-router"; // 2.作为插件使用: Vue.use(VueRouter); // 3.引入各组件 const App = r => require.ensure([], () => r(require('./app')), 'app'); const Hello = r => require.ensure([], () => r(require('./hello), 'hello'); import Info from './info' const Wrap = {template: '<router-view></router-view>'}; // 4.创建 VueRouter 实例 const router = new VueRouter({ mode: 'history', base: __dirname, routes: [ { path: '/', component: Wrap, children: [ { path: 'index', component: App, alias: '', name: 'index' }, { path: 'hello', name: 'hello', alias: ['hello/index'], components: { default: Hello, info: Info } } ] } ] }); // 5.创建 Vue 实例, 启动应用 const app = new Vue({ router, ...Wrap }).$mount('#app');
(2和4并无特定的顺序关系)
在上述代码的第2步中, 利用了 Vue.js 的插件机制来安装 vue-router, 这有三个作用:
Vue.js
$router
$route
<router-view>
<router-link>
Vue.js 通过 use(plugin) 来安装插件时, 会调用 plugin 的 install 方法, 如果没有该方法, 则将 plugin 本身作为函数来调用. 其实现如下:
use(plugin)
install
# src/core/global-api/use.js Vue.use = function (plugin: Function | Object) { // ... if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } // ... }
VueRouter 是在 src/index.js 中导出的, 提供了静态的 install 方法:
src/index.js
// 引入 install import {install} from './install' // ... import {inBrowser} from './util/dom' // ... export default class VueRouter { // 静态属性 static install: () => void; static version: string; // ... } // 静态属性赋值 VueRouter.install = install VueRouter.version = '__VERSION__' // 自动使用插件 if (inBrowser && window.Vue) { window.Vue.use(VueRouter) }
这是 Vue.js 插件的常规开发方式, 给 plugin 对象增加 install 方法, 然后在 install 中实现具体逻辑. 此外, 并作浏览器环境检测, 如果是在浏览器环境并且存在 window.Vue 就自动使用 plugin.
window.Vue
浏览器环境的检测很简单:
// src/util/dom.js export const inBrowser = typeof window !== 'undefined'
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) 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)) { // https://github.com/dwqs/blog/issues/54#View 组件 i(vm, callVal) } } 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 { // 2.6.0 新增: 确保 this._routerRoot 有值 // 用于查找 router-view 组件的层次判断 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // 注册 VueComponent,进行 observer 处理 registerInstance(this, this) }, destroyed () { // 取消 VueComponent 的注册 registerInstance(this) } }) // 定义 $router 和 $route 的 getter Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 注册组件 Vue.component('router-view', View) Vue.component('router-link', Link) // 钩子的合并策略 const strats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.created }
这里导出一个私有的 Vue 引用的目的是: 插件不必将 Vue.js 作为一个依赖打包, 但插件的其它模块有可能要依赖 Vue 实例的一些方法, 其它模块可以从这里获取到 Vue 实例引用.
在 beforeCreate mixin 中, 在创建 Vue 实例时, 如果判断传入了 router(不传入 router, 在渲染 router-view 组件时会因获取不到 matched 属性而出错), 就将 router 赋值给私有属性 _router, 便于后续的初始化和 getter 定义.
beforeCreate mixin
router-view
matched
_router
在 Vue.js 应用中, 所有组件都是 Vue 实例的扩展, 也就意味着所有的组件都可以访问到这个实例原型上定义的属性. 所以, VueRouter 将 $route 和 $router 属性定义在了 Vue 实例的原型上.
在应用入口文件中, 对 VueRouter 进行了实例化, 并将其作为参数传给 Vue 实例的 options. VueRouter 类的入口在 src/index.js:
options
import {install} from './install' //... import {HashHistory} from './history/hash' import {HTML5History} from './history/html5' import {AbstractHistory} from './history/abstract' import type {Matcher} from './create-matcher' export default class VueRouter{ // ... constructor(options: RouterOptions = {}){ // ... this.options = options // 钩子 this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] // 创建路由匹配对象 this.matcher = createMatcher(options.routes || [], this) // 对 mode 作检测 // options.fallback 是2.6.0 新增, 表示是否对不支持 HTML5 history 的浏览器采用降级处理 // https://github.com/vuejs/vue-router/releases/tag/v2.6.0 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) } }
在实例化时, 主要作了两件事:
matcher
matcher 对象是由 src/create-matcher.js 中的 createMatcher 创建的:
src/create-matcher.js
createMatcher
// 定义 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 创建路由 map const {pathList, pathMap, nameMap} = createRouteMap(routes) // 添加路由函数 function addRoutes(routes) { createRouteMap(routes, pathList, pathMap, nameMap) } // 路由匹配 function match(raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route { // ... } // ... // 返回 matcher 对象 return { match, addRoutes } }
createMatcher 根据传入的 routes 配置生成对应的路由 map, 然后直接返回一个 matcher 对象.
routes
继续来看 src/create-route-map.js 中的 createRouteMap 函数:
src/create-route-map.js
createRouteMap
import Regexp from 'path-to-regexp' import {cleanPath} from './util/path' import {assert, warn} from './util/warn' export function createRouteMap(routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord>): { pathList: Array<string>; pathMap: Dictionary<RouteRecord>; nameMap: Dictionary<RouteRecord>; } { // path 列表 const pathList: Array<string> = oldPathList || [] // path map 映射 const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) // name map 映射 const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) // 遍历路由配置对象 增加路由记录 routes.forEach(route => { addRouteRecord(pathList, pathMap, nameMap, route) }) // 保证通配符在最后 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) { // 获取 path/name const {path, name} = route // ... // 序列化 path, 作 / 替换 const normalizedPath = normalizePath(path, parent) // path-to-regexp 选项: 2.6.0 新增 const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} // 对路径进行正则匹配是否区分大小写, 该属性是 2.6.0 新增 if (typeof route.caseSensitive === 'boolean') { pathToRegexpOptions.sensitive = route.caseSensitive } // 创建一个路由记录对象 const record: RouteRecord = { path: normalizedPath, // 将 path 和 regex 作解析映射 regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || {default: route.component}, instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : {default: route.props} } // 递归子路由 if (route.children) { // ... route.children.forEach(child => { const childMatchAs = matchAs ? cleanPath(`${matchAs}/${child.path}`) : undefined addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) }) } // 增加 alias 对应的 route 记录 if (route.alias !== undefined) { // alias 作数组处理 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 ) }) } // 更新 map 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}" }` ) } } } function normalizePath(path: string, parent?: RouteRecord): string { path = path.replace(/\/$/, '') if (path[0] === '/') return path if (parent == null) return path return cleanPath(`${parent.path}/${path}`) }
cleanPath 的逻辑比较简单, 只是对双 / 作正则替换
cleanPath
/
// src/util/path.js export function cleanPath(path: string): string { return path.replace(/\/\//g, '/') }
从上述代码可以看出, create-route-map.js 的主要功能是根据用户的 routes 配置的 path 、alias 以及 name 来生成对应的路由记录, 方便后续匹配对应.
path
alias
name
VueRouter 提供了 HTML5History、HashHistory 以及 AbstractHistory 三种方式, 根据不同的 mode 和环境来实例化 History. 所有的 History 类都是在 src/history/ 目录下, 并且都继承自 src/history/base.js:
HTML5History
HashHistory
AbstractHistory
mode
src/history/
src/history/base.js
// 获取私有的 Vue 实例 import {_Vue} from '../install' import {START, isSameRoute} from '../util/route' // ... import {inBrowser} from '../util/dom' // ... export class History{ // ... constructor(router: Router, base: ?string) { this.router = router this.base = normalizeBase(base) // 默认的当前路由 this.current = START this.pending = null this.ready = false this.readyCbs = [] this.readyErrorCbs = [] this.errorCbs = [] } // ... } // 格式化 base 值 function normalizeBase(base: ?string): string { if (!base) { if (inBrowser) { // 如果未传入 base 且在浏览器环境, 则获取 base 标签的属性 const baseEl = document.querySelector('base') base = (baseEl && baseEl.getAttribute('href')) || '/' // bugfix: https://github.com/vuejs/vue-router/releases/tag/v2.6.0 base = base.replace(/^https?:\/\/[^\/]+/, '') } else { // 非浏览器环境下的默认值 base = '/' } } // 确保 base 以 / 开始 if (base.charAt(0) !== '/') { base = '/' + base } // 去掉字符串结尾的 / return base.replace(/\/$/, '') }
到这, History 就实例化完成了, VueRouter 的实例化也完成了. 接下来看下 Vue.js 的实例化.
在启动 Vue.js 应用之前, 需要先对其进行实例化, 并传入 VueRouter 实例:
// 5.创建 Vue 实例, 启动应用 const app = new Vue({ router, ...Wrap }).$mount('#app');
在创建 Vue 实例时, 定义在 src/install.js 中的 mixin 会被调用:
src/install.js
mixin
// ... const isDef = v => v !== undefined // ... Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router // 初始化 router this._router.init(this) // 定义响应式的 _route 对象 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // ... }, destroyed () { // ... } }) // ...
在 beforeCreate 钩子中, 会判断实例化时 options 是否包含 router. router 在这有两个作用:
beforeCreate
router
router.init
对于第二点, 因为 mixin beforeCreate 是全局的, 其它非函数式组件(如 APP/Hello)渲染时, 该钩子会优先于组件内 beforeCreate (如果有)执行, 但 $options 并不会有 router 属性, 该属性只在 app 被实例化时传入.
mixin beforeCreate
$options
app
如果有则进行 router 的初始化工作.
// src/index.js // ... export default class VueRouter{ // ... // 实例属性 app: any; apps: Array<any>; //... // Router 初始化 init(app: any /* Vue component instance */){ // ... this.apps.push(app) // app 是否已经初始化 if (this.app) { return } // 实例赋值 this.app = app const history = this.history // 针对于 HTML5History 和 HashHistory 特殊处理, // 因为在这两种模式下才有可能存在进入时候的不是默认页, // 需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { // 设置 hashchange 监听 history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } // Route改变的回调监听 history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // ... }
从上述代码可以看出, 主要进行了 app 赋值, 针对于 HTML5History 和 HashHistory 特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页, 需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由, 此时就是通过调用 transitionTo 来达到目的. 注意: 这里在处理 HashHistory 时, 是在 route 切换完成之后再设置 hashchange 的监听, 这是为了修复 vuejs/vue-router#725 而做的. 因为如果钩子函数 beforeEnter 是异步的话, beforeEnter 钩子就会被触发两次. 因为在初始化时, 如果此时的 hash 值不是以 / 开头的话就会补上 #/, 这个过程会触发 hashchange 事件, 就会再走一次生命周期钩子, 也就意味着会再次调用 beforeEnter 钩子函数.
hash
transitionTo
hashchange
beforeEnter
#/
transitionTo 的第一个参数是当前的 location, 其实现在 src/history/base.js 中:
location
// ... export class History{ // ... transitionTo(location: RawLocation, onComplete?: Function, onAbort?: Function) { // 获取匹配的 Route 对象 const route = this.router.match(location, this.current) // 确认切换 this.confirmTransition(route, () => { // 更新 route this.updateRoute(route) onComplete && onComplete(route) // 分别调用子类的实现更新浏览器 url this.ensureURL() // 调用 ready 的回调 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) }) } }) } // 更新当前 route 对象 updateRoute(route: Route) { const prev = this.current this.current = route // 调用 listen 的回调 this.cb && this.cb(route) // 执行 afterEach 钩子 this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) } }
在 transitionTo 中, 首先通过调用 VueRouter 实例的 match 方法获取到和当前 location 对应的 route 对象:
match
route
// ... export default class VueRouter { // ... constructor(options: RouterOptions = {}) { // ... // 创建路由映射 this.matcher = createMatcher(options.routes || [], this) // ... } // 返回匹配的 Route match(raw: RawLocation, current?: Route, redirectedFrom?: Location): Route { return this.matcher.match(raw, current, redirectedFrom) } // ... }
matcher.match 的实现在 src/create-matcher.js 中:
matcher.match
// ... import {createRoute} from './util/route' import {createRouteMap} from './create-route-map' import {normalizeLocation} from './util/location' export function createMatcher(routes: Array<RouteConfig>, router: VueRouter): Matcher { // ... function match(raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location): Route { const location = normalizeLocation(raw, currentRoute, false, router) const {name} = location if (name) { // 根据 name 获取对应对应的记录 const record = nameMap[name] //... // 没有则创建 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] // 根据 path 回去记录 const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { // 匹配成功 创建 pathMap[path] 对应的路由 return _createRoute(record, location, redirectedFrom) } } } // 没有匹配就根据 location 创建新的路由 return _createRoute(null, location) } // ... function redirect(record: RouteRecord, location: Location): Route {} function alias(record: RouteRecord, location: Location, matchAs: string): Route {} // ... // 根据不同条件创建路由 function _createRoute(record: ?RouteRecord, location: Location, redirectedFrom?: Location): Route { if (record && record.redirect) { return redirect(record, redirectedFrom || location) } // matchAs 用于创建别名路由 if (record && record.matchAs) { return alias(record, location, record.matchAs) } return createRoute(record, location, redirectedFrom, router) } }
createRoute 的实现在 src/util/route.js 中:
createRoute
src/util/route.js
// ... export function createRoute(record: ?RouteRecord, location: Location, redirectedFrom?: ?Location, router?: VueRouter): Route { const stringifyQuery = router && router.options.stringifyQuery const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query: location.query || {}, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), // 根据记录层级的得到所有匹配的路由记录 matched: record ? formatMatch(record) : [] } if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery) } return Object.freeze(route) } // 起始路由 export const START = createRoute(null, { path: '/' }) function formatMatch(record: ?RouteRecord): Array<RouteRecord> { const res = [] while (record) { res.unshift(record) record = record.parent } return res } // ...
回到 src/history/base.js , 在 transitionTo 方法中获取到匹配的 route 之后, 就调用了 confirmTransition:
confirmTransition
// ... import {runQueue} from '../util/async' import {START, isSameRoute} from '../util/route' // ... export class History { // ... // 确认过渡 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() } // 交叉比跳转前的路由记录和将要跳转的路由记录 // 以便可以确切的知道 哪些组件需要更新 哪些不需要更新 const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched) // 待执行的各种钩子更新队列 const queue: Array<?NavigationGuard> = [].concat( // 提取组件的 beforeRouteLeave 钩子 extractLeaveGuards(deactivated), // 全局的 beforeEach 钩子 this.router.beforeHooks, // 提取组件的 beforeRouteUpdate 钩子 extractUpdateHooks(updated), // 组件的 beforeRouteEnter 钩子 activated.map(m => m.beforeEnter), // 异步组件处理 resolveAsyncComponents(activated) ) // 保存下一个路由 this.pending = route const iterator = (hook: NavigationGuard, next) => { // 不相等则终止 if (this.pending !== route) { return abort() } try { // 导航钩子 hook(route, current, (to: any) => { if (to === false || isError(to)) { // next(false) -> 终止导航 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: '/' }) -> 重定向 abort() if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // 路由跳转 next(to) } }) } catch (e) { abort(e) } } // 执行各种钩子队列 runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // 等待异步组件 OK 时,执行组件内的钩子 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() }) }) } }) }) } }
从上述代码可知, 整个过程就是执行组件的各种钩子以及处理异步组件问题. 再回到之前看的 init, 最后调用了 history.listen 方法:
init
history.listen
// Route改变的回调监听 history.listen(route => { this.apps.forEach((app) => { app._route = route }) })
listen 设置了 Route 改变之后的回调, 会在 confirmTransition 的 onComplete 回调中调用, 其作用就是更新下当前应用实例的 _route 值. 在前文的分析中, _route 属性被定义为一个 reactive 属性, 初始值是当前的路由对象:
listen
onComplete
_route
// ... // 初始化 router this._router.init(this) // 定义响应式的 _route 对象 Vue.util.defineReactive(this, '_route', this._router.history.current) // ...
history 的改变会去更新 _route, 进而触发 Vue 实例的更新机制, 调用 render 去重新渲染界面.
render
vue-router 的整体流程就分析到这了. 由于篇幅有限, 省略了很多细节, 但不影响对整个流程的了解, 后续会再针对具体的模块(组件/History 等)进行具体的分析.
下次可以安利一下react-router吗?
mark
传说中的用github 的 issue 写博客。 有markdown 也有评论。666。
match那一块,其实挺复杂的
在前端框架 React、Vue.js 和 Angular 三足鼎立的年代, Vue.js 因其易用、易学、学习成本低等特点已经成为了广大前端er的新宠, 而其对应的路由 vue-router 也是简单好用, 功能强大. 本文将结合 Vue.js 来分析
vue-router
的整体流程.目录结构
vue-router@2.6.0 的整体目录结构如下:
主要关注点就是
components
、history
目录以及create-matcher.js
、create-route-map.js
、index.js
、install.js
等文件. 下面以一个小 demo 来分析vue-router
的整体流程.入口
首先看 demo 入口的代码部分:
(2和4并无特定的顺序关系)
插件安装
在上述代码的第2步中, 利用了
Vue.js
的插件机制来安装vue-router
, 这有三个作用:$router
和$route
对象<router-view>
和<router-link>
组件Vue.js
通过use(plugin)
来安装插件时, 会调用 plugin 的install
方法, 如果没有该方法, 则将 plugin 本身作为函数来调用. 其实现如下:VueRouter 是在
src/index.js
中导出的, 提供了静态的install
方法:这是 Vue.js 插件的常规开发方式, 给 plugin 对象增加 install 方法, 然后在 install 中实现具体逻辑. 此外, 并作浏览器环境检测, 如果是在浏览器环境并且存在
window.Vue
就自动使用 plugin.浏览器环境的检测很简单:
install
作为一个单独的模块存在:这里导出一个私有的 Vue 引用的目的是: 插件不必将 Vue.js 作为一个依赖打包, 但插件的其它模块有可能要依赖 Vue 实例的一些方法, 其它模块可以从这里获取到 Vue 实例引用.
在
beforeCreate mixin
中, 在创建 Vue 实例时, 如果判断传入了 router(不传入 router, 在渲染router-view
组件时会因获取不到matched
属性而出错), 就将 router 赋值给私有属性_router
, 便于后续的初始化和 getter 定义.在 Vue.js 应用中, 所有组件都是 Vue 实例的扩展, 也就意味着所有的组件都可以访问到这个实例原型上定义的属性. 所以, VueRouter 将
$route
和$router
属性定义在了 Vue 实例的原型上.Router 实例化
在应用入口文件中, 对 VueRouter 进行了实例化, 并将其作为参数传给 Vue 实例的
options
. VueRouter 类的入口在src/index.js
:在实例化时, 主要作了两件事:
matcher
对象路由匹配
matcher
对象是由src/create-matcher.js
中的createMatcher
创建的:createMatcher
根据传入的routes
配置生成对应的路由 map, 然后直接返回一个matcher
对象.继续来看
src/create-route-map.js
中的createRouteMap
函数:cleanPath
的逻辑比较简单, 只是对双/
作正则替换从上述代码可以看出,
create-route-map.js
的主要功能是根据用户的routes
配置的path
、alias
以及name
来生成对应的路由记录, 方便后续匹配对应.History 实例化
VueRouter 提供了
HTML5History
、HashHistory
以及AbstractHistory
三种方式, 根据不同的mode
和环境来实例化 History. 所有的 History 类都是在src/history/
目录下, 并且都继承自src/history/base.js
:到这, History 就实例化完成了, VueRouter 的实例化也完成了. 接下来看下 Vue.js 的实例化.
Vue 实例化
在启动 Vue.js 应用之前, 需要先对其进行实例化, 并传入 VueRouter 实例:
在创建 Vue 实例时, 定义在
src/install.js
中的mixin
会被调用:在
beforeCreate
钩子中, 会判断实例化时options
是否包含router
.router
在这有两个作用:router-view
组件的渲染提供$route
router.init
只被调用一次对于第二点, 因为
mixin beforeCreate
是全局的, 其它非函数式组件(如 APP/Hello)渲染时, 该钩子会优先于组件内beforeCreate
(如果有)执行, 但$options
并不会有router
属性, 该属性只在app
被实例化时传入.如果有则进行
router
的初始化工作.从上述代码可以看出, 主要进行了
app
赋值, 针对于HTML5History
和HashHistory
特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页, 需要根据当前浏览器地址栏里的path
或者hash
来激活对应的路由, 此时就是通过调用transitionTo
来达到目的. 注意: 这里在处理HashHistory
时, 是在 route 切换完成之后再设置hashchange
的监听, 这是为了修复 vuejs/vue-router#725 而做的. 因为如果钩子函数beforeEnter
是异步的话,beforeEnter
钩子就会被触发两次. 因为在初始化时, 如果此时的hash
值不是以/
开头的话就会补上#/
, 这个过程会触发hashchange
事件, 就会再走一次生命周期钩子, 也就意味着会再次调用beforeEnter
钩子函数.transitionTo
的第一个参数是当前的location
, 其实现在src/history/base.js
中:在
transitionTo
中, 首先通过调用 VueRouter 实例的match
方法获取到和当前location
对应的route
对象:matcher.match
的实现在src/create-matcher.js
中:createRoute
的实现在src/util/route.js
中:回到
src/history/base.js
, 在transitionTo
方法中获取到匹配的route
之后, 就调用了confirmTransition
:从上述代码可知, 整个过程就是执行组件的各种钩子以及处理异步组件问题. 再回到之前看的
init
, 最后调用了history.listen
方法:listen
设置了 Route 改变之后的回调, 会在confirmTransition
的onComplete
回调中调用, 其作用就是更新下当前应用实例的_route
值. 在前文的分析中,_route
属性被定义为一个 reactive 属性, 初始值是当前的路由对象:history
的改变会去更新_route
, 进而触发 Vue 实例的更新机制, 调用render
去重新渲染界面.总结
vue-router
的整体流程就分析到这了. 由于篇幅有限, 省略了很多细节, 但不影响对整个流程的了解, 后续会再针对具体的模块(组件/History 等)进行具体的分析.