vue-router 源代码全流程分析「长文」
栏目: JavaScript · 发布时间: 5年前
内容简介:根据上述基础使用,我们大概可以梳理一个基本流程出来:下面我们就根据上述流程步骤,一步一步解析,首先
import Vue from 'vue'; import VueRouter from 'vue-router'; Vue.use(VueRouter); const Home = { template: '<div>home</div>' }; const Foo = { template: '<div>foo</div>' }; const Bar = { template: '<div>bar</div>' }; const Child = { template: '<div>Child</div>' }; const router = new VueRouter({ mode: 'history', // base: __dirname, base: '/', // 默认 ‘/’ routes: [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar, children: [{ path: 'child', component: Child }] } ] }); const template = ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <li><router-link to="/bar/child">/bar</router-link></li> </ul> <router-view class="view"></router-view> </div> `; new Vue({ router, template }).$mount('#app'); 复制代码
根据上述基础使用,我们大概可以梳理一个基本流程出来:
-
注册插件:
-
将
$router
和$route
注入所有启用路由的子组件。 -
安装
<router-view>
和<router-link>
。
-
将
- 定义路由组件。
-
new VueRouter(options)
创建一个路由器, 传入相关配置。 -
创建并挂载根实例,确保注入路由器。路由组件将在
<router-view>
中呈现。
下面我们就根据上述流程步骤,一步一步解析, vue-router
代码实现。
注册插件(vue-router)
首先 Vue.use(VueRouter);
这段代码间接执行 VueRouter
暴露的 install
方法,下面来看看 install
具体实现:(由于有完整的注释,故略去文字叙述)
install
import View from './components/view'; import Link from './components/link'; /** * 安装 Vue.js 插件 install 方法调用时,会将 Vue 作为参数传入。 * * @export * @param {*} Vue * @returns * */ export function install(Vue) { // 防止插件被多次安装 - 当 install 方法被同一个插件多次调用,插件将只会被安装一次。 if (install.installed) return; install.installed = true; // 在 Vue 原型上添加 $router 属性( VueRouter )并代理到 this.$root._router Object.defineProperty(Vue.prototype, '$router', { get() { return this.$root._router; } }); // 在 Vue 原型上添加 $route 属性( 当前路由对象 )并代理到 this.$root._route Object.defineProperty(Vue.prototype, '$route', { get() { return this.$root._route; } }); // 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。 Vue.mixin({ /** * 混入 Vue 创建前钩子 * 1.取传入 Vue 构造函数的路由配置参数并调用 init 方法。 * 2.在 Vue 根实例添加 _router 属性( VueRouter 实例) * 3.执行路由实例的 init 方法并传入 Vue 实例 * 4.把 ($route <=> _route) 处理为响应式的。 */ beforeCreate() { if (this.$options.router) { this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive(this, '_route', this._router.history.current); } } }); // 注册全局组件 Vue.component('router-view', View); Vue.component('router-link', Link); } 复制代码
上述 就是 vue-router
暴露给 Vue
的注册方法。这里特别说明一下: defineReactive Vue
构建响应式的核心方法。在研究注册的两个全局组件: <router-view>
和 <router-link>
之前,我们先讨论 VueRouter
构造函数,因为它们其中涉及到 VueRouter
的很多方法。
代码接着执行,接下来就是为 vue-router
装填配置并实例化。之后把实例化的结果传入 Vue
。
const router = new VueRouter({ // 选择路由模式 mode: 'history', // 应用的基路径。默认值: "/" 例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/". base: '/', // 默认 ‘/’ // 路由配置表 routes: [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar, children: [{ path: 'child', component: Child }] } ] }); new Vue({ router }).$mount('#app'); 复制代码
接下来我们就来看看定义路由的 VueRouter 构造函数。
定义路由:VueRouter 构造函数
/* @flow */ import { install } from './install'; import { createMatcher } from './create-matcher'; import { HashHistory } from './history/hash'; import { HTML5History } from './history/html5'; import { AbstractHistory } from './history/abstract'; import { inBrowser, supportsHistory } from './util/dom'; import { assert } from './util/warn'; export default class VueRouter { static install: () => void; app: any; // Vue 实例 options: RouterOptions; // 路由配置 mode: string; // 路由模式,默认 hash history: HashHistory | HTML5History | AbstractHistory; match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录。? fallback: boolean; // 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。 beforeHooks: Array<?NavigationGuard>; // 前置钩子集合 afterHooks: Array<?(to: Route, from: Route) => any>; // 后置钩子集合 constructor(options: RouterOptions = {}) { this.app = null; this.options = options; this.beforeHooks = []; this.afterHooks = []; this.match = createMatcher(options.routes || []); // 匹配器 /******* 确定路由模式 - 默认为 hash *******/ let mode = options.mode || 'hash'; // 如果传入的模式为 ·history· 在浏览器环境下不支持 history 模式,则强制回退到 hash 模式 this.fallback = mode === 'history' && !supportsHistory; if (this.fallback) { mode = 'hash'; } // 在非浏览器环境下,采用 abstract 模式 if (!inBrowser) { mode = 'abstract'; } this.mode = mode; } /** * 当前路由 * * @readonly * @type {?Route} * @memberof VueRouter */ get currentRoute(): ?Route { return this.history && this.history.current; } /** * 初始化 * @param {Any} app Vue component instance */ init(app: any) { // 断言有没有安装插件,如果没有抛出错误提示 assert( install.installed, `没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。` ); this.app = app; const { mode, options, fallback } = this; // 根据不同模式实例化不同基类,无效模式下抛出错误 switch (mode) { case 'history': this.history = new HTML5History(this, options.base); break; case 'hash': this.history = new HashHistory(this, options.base, fallback); break; case 'abstract': this.history = new AbstractHistory(this); break; default: assert(false, `invalid mode: ${mode}`); } // 调用 history 属性下 listen 方法? this.history.listen(route => { this.app._route = route; }); } /** * Router 实例方法 beforeEach 全局前置的导航守卫。 * 当一个导航触发时,全局前置守卫按照创建顺序调用。 * 守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。 * * @param {Function} fn (to, from, next) => {} * @memberof VueRouter * */ beforeEach(fn: Function) { this.beforeHooks.push(fn); } /** * Router 实例方法 afterEach 全局后置钩子 * * @param {Function} fn (to, from) => {} * @memberof VueRouter */ afterEach(fn: Function) { this.afterHooks.push(fn); } /** * 编程式导航 push 导航到对应的 location * 这个方法会向 history 栈添加一个新的记录, * 所以,当用户点击浏览器后退按钮时,则回到之前的 location。 * * @param {RawLocation} location * @memberof VueRouter */ push(location: RawLocation) { this.history.push(location); } /** * 编程式导航 replace 导航到对应的 location * 它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。 * * @param {RawLocation} location * @memberof VueRouter */ replace(location: RawLocation) { this.history.replace(location); } /** * 在 history 记录中向前或者后退多少步,类似 window.history.go(n)。 * * @param {number} n * @memberof VueRouter */ go(n: number) { this.history.go(n); } /** * 后退 * * @memberof VueRouter */ back() { this.go(-1); } /** * 前进 * * @memberof VueRouter */ forward() { this.go(1); } /** * 获取匹配到的组件列表 * * @returns {Array<any>} * @memberof VueRouter */ getMatchedComponents(): Array<any> { if (!this.currentRoute) { return []; } return [].concat.apply( [], this.currentRoute.matched.map(m => { return Object.keys(m.components).map(key => { return m.components[key]; }); }) ); } } // 添加 install 方法 VueRouter.install = install; // 在浏览器环境下且 Vue 构造函数存在的情况下调用 use 方法注册插件(插件预装) if (inBrowser && window.Vue) { window.Vue.use(VueRouter); } 复制代码
由上述代码实现分析,首先声明了一些属性、方法、和常用的 API
;接着添加 install
方法和执行 Vue.use
方法注册插件。
其中对相关代码做了详细的注释,无需再此重复论述,但其中有一些方法将在后面涉及时,做深入分析。继续分析前,这里需要先对这段代码进行解释:
this.match = createMatcher(options.routes || []); // 创建匹配器 复制代码
/** * 创建匹配器 * * @export * @param {Array<RouteConfig>} routes * @returns {Matcher} */ export function createMatcher(routes: Array<RouteConfig>): Matcher { const { pathMap, nameMap } = createRouteMap(routes); function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { ... 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 { ... } return match; } 复制代码
上述代码首先根据传入的路由配置表,创建新的映射表并从中解构出路径映射表、名称映射表,之后返回内建函数 match
。这里暂时先不对其内部实现做详细介绍,之后再被调用处详细论述。
初始化
我们知道在 VueRouter - install
注册插件这个方法时,混入了一个全局的生命周期函数 beforeCreate
, 代码如下:
export function install(Vue) { ... Vue.mixin({ beforeCreate() { if (this.$options.router) { this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive(this, '_route', this._router.history.current); } } }); ... } 复制代码
我们发现其中执行了 vue-router
提供的 init
方法并传入 Vue
组件实例。那么接下来我们就来看看 init
做了哪些事情。
init
方法简析
export default class VueRouter { static install: () => void; app: any; // vue 实例 options: RouterOptions; // 路由配置 mode: string; // 路由模式,默认 hash history: HashHistory | HTML5History | AbstractHistory; match: Matcher; // 一个数组,包含当前路由的所有嵌套路径片段的路由记录 。 fallback: boolean; // 回退 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true。 beforeHooks: Array<?NavigationGuard>; // 前置钩子集合 afterHooks: Array<?(to: Route, from: Route) => any>; // 后置钩子集合 constructor(options: RouterOptions = {}) { ... } ... /** * 初始化 * @param {Any} app Vue component instance */ init(app: any) { // 断言有没有安装插件,如果没有抛出错误提示 assert( install.installed, `没有安装。在创建根实例之前,请确保调用 Vue.use(VueRouter)。` ); this.app = app; const { mode, options, fallback } = this; // 根据不同模式实例化不同基类,无效模式下抛出错误 switch (mode) { case 'history': this.history = new HTML5History(this, options.base); break; case 'hash': this.history = new HashHistory(this, options.base, fallback); break; case 'abstract': this.history = new AbstractHistory(this); break; default: assert(false, `invalid mode: ${mode}`); } // 执行父类监听函数,注册回调 this.history.listen(route => { this.app._route = route; }); } ... } 复制代码
上述代码实现:
- 首先断言有没有安装插件,如果没有抛出错误提示。
-
在
VueRouter
上添加Vue
实例. - 根据不同模式实例化不同基类,无效模式下抛出错误
-
执行父类监听函数,注册回调:
-
在路由改变时替换
vue
实例上_route
当前匹配的路由对象属性 - 响应式值得改变,从而触发视图的重新渲染
-
在
<router-view>
中拿到匹配的路由对象,渲染匹配到的路由组件。完成跳转。
-
在路由改变时替换
根据上述基础示例,选用的 history
模式。我们先看一下该模式的实现。
HTML5History
/** * h5 - history 模式 * * @export * @class HTML5History * @extends {History} */ export class HTML5History extends History { constructor(router: VueRouter, base: ?string) { // 调用父类,并传入VueRouter路由实例和基础路径 super(router, base); // 跳转核心方法 跳转到跟=基础路径 this.transitionTo(getLocation(this.base)); const expectScroll = router.options.scrollBehavior; // 添加 popstate 监听函数 window.addEventListener('popstate', e => { _key = e.state && e.state.key; const current = this.current; this.transitionTo(getLocation(this.base), next => { if (expectScroll) { this.handleScroll(next, current, true); } }); }); // 若存在滚动行为配置,则添加 scroll 监听函数 if (expectScroll) { window.addEventListener('scroll', () => { saveScrollPosition(_key); }); } } /** * 前进对应步数 * * @param {number} n * @memberof HTML5History */ go(n: number) { // 通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面 window.history.go(n); } /** * 导航到不同的 location 向 history 栈添加一个新的记录 * * @param {RawLocation} location * @memberof HTML5History */ push(location: RawLocation) { // 拿到当前路由对象 const current = this.current; // 调用跳转核心方法 this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)); this.handleScroll(route, current, false); }); } /** * 导航到不同的 location 替换掉当前的 history 记录。 * * @param {RawLocation} location * @memberof HTML5History */ replace(location: RawLocation) { const current = this.current; this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)); this.handleScroll(route, current, false); }); } /** * 更新 URL * * @memberof HTML5History */ ensureURL() { if (getLocation(this.base) !== this.current.fullPath) { replaceState(cleanPath(this.base + this.current.fullPath)); } } /** * 处理页面切换时,滚动位置 * * @param {Route} to 将要去的路由对象 * @param {Route} from 当前路由对象 * @param {boolean} isPop 当且仅当 popstate 导航 (通过浏览器的 前进/后退 按钮触发) 时才可用 * @memberof HTML5History */ handleScroll(to: Route, from: Route, isPop: boolean) { const router = this.router; // 若当前 Vue 组件实例不存在,直接return if (!router.app) { return; } // 取路由 滚动行为 配置参数。 若不存在直接 return // 使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样 // 只在html5历史模式下可用; 默认没有滚动行为; 返回false以防止滚动. // { x: number, y: number } // { selector: string, offset? : { x: number, y: number }} const behavior = router.options.scrollBehavior; if (!behavior) { return; } // 断言 其必须是函数,否则抛出异常 assert(typeof behavior === 'function', `scrollBehavior must be a function`); // 等到重新渲染完成后再滚动 router.app.$nextTick(() => { // 获取滚动位置 let position = getScrollPosition(_key); // 获取回调返回的滚动位置的对象信息 const shouldScroll = behavior(to, from, isPop ? position : null); // 若不存在直接 return if (!shouldScroll) { return; } const isObject = typeof shouldScroll === 'object'; // 处理模拟“滚动到锚点”的行为 if (isObject && typeof shouldScroll.selector === 'string') { const el = document.querySelector(shouldScroll.selector); if (el) { position = getElementPosition(el); } else if (isValidPosition(shouldScroll)) { position = normalizePosition(shouldScroll); } } else if (isObject && isValidPosition(shouldScroll)) { position = normalizePosition(shouldScroll); } if (position) { // 把内容滚动到指定的坐标 window.scrollTo(position.x, position.y); } }); } } 复制代码
对 HTML5History
实现简析:
-
根据
init
方法model: history
实例化new HTML5History(this, options.base)
调用构造函数并传入VueRouter
和 应用的基路径。 -
构造函数
-
调用父类
super(router, base)
,并传入 VueRouter 路由实例和基础路径; - 调用核心过渡跳转方法 跳转到应用的基路径;
-
定义
popstate
监听函数, 并在回调里做相应跳转处理; -
若在实例化
VueRouter
传入滚动行为配置scrollBehavior
则添加滚动监听事件。回调为:滚动到上次标记位置点。
-
调用父类
-
定义相应
Router
实例方法;和一些父类里调用子类实现的方法。
对于其提供的方法有很详细的注释信息,故接下来我们直接来看看父类的实现,其它两种模式也是继承了这个基类 History
。
History
/** * History 基类 * * @export * @class History */ export class History { router: VueRouter; base: string; current: Route; pending: ?Route; cb: (r: Route) => void; // 以下这些方法由子类去实现 go: (n: number) => void; push: (loc: RawLocation) => void; replace: (loc: RawLocation) => void; ensureURL: () => void; // 更新URL constructor(router: VueRouter, base: ?string) { // VueRouter 实例 this.router = router // 应用的基路径 this.base = normalizeBase(base) // 从一个表示 “nowhere” 的 route 对象开始 this.current = START // 等待状态标志 this.pending = null } /** * 注册回调 * * @param {Function} cb * @memberof History */ listen(cb: Function) { this.cb = cb; } /** * 核心跳转方法 * * @param {RawLocation} location * @param {Function} [cb] * @memberof History */ transitionTo(location: RawLocation, cb?: Function) { ... } // 最终过渡 confirmTransition(route: Route, cb: Function) { ... } // 路由更新 updateRoute(route: Route) { ... } } /** * 规范化应用的基路径 * * @param {?string} base * @returns {string} */ function normalizeBase(base: ?string): string { if (!base) { if (inBrowser) { // respect <base> tag // HTML <base> 元素 指定用于一个文档中包含的所有相对 URL 的根 URL。一份中只能有一个 <base> 元素。 const baseEl = document.querySelector('base') base = baseEl ? baseEl.getAttribute('href') : '/' } else { base = '/' } } // 确保有开始斜杠 if (base.charAt(0) !== '/') { base = '/' + base } // 去除末尾斜杠 return base.replace(/\/$/, '') } 复制代码
-
上述是
History
基类中所有代码实现。同样是添加几个属性,和一些跳转所需的核心方法。这里只需要大概了解其内部大概实现,之后会详细论述。 -
回到
HTML5History
构造函数的执行代码this.transitionTo(getLocation(this.base))
,调用核心过渡跳转方法 跳转到应用的基路径。
核心跳转方法 transitionTo
根据上述例子初始化时调用( HTML5History - this.transitionTo(getLocation(this.base));
),这里的入参是: location: /, cb: undefined
/** * 核心跳转方法 * * @param {RawLocation} location 地址 * @param {Function} [cb] 回调 * @memberof History */ transitionTo(location: RawLocation, cb?: Function) { // 获取路由匹配信息,传入 location 和 current属性 const route = this.router.match(location, this.current) // 调用最终跳转方法,并传入路由对象信息,和回调 // 回调:更新路由,执行传入回调, 更新 URL this.confirmTransition(route, () => { this.updateRoute(route) cb && cb(route) this.ensureURL() }) } 复制代码
根据上述代码实现简析:
-
传入地址和
current
属性,current
属性在调用super
在父类里初始时被赋值为START
// vue-router/src/history/base.js // 从一个表示 “nowhere” 的 route 对象开始 this.current = START; 复制代码
START 代码实现及结果展示START
// 表示初始状态的起始路径 export const START = createRoute(null, { path: '/' }); /** * 创建一个路由对象 * * @export * @param {?RouteRecord} record * @param {Location} location * @param {Location} [redirectedFrom] * @returns {Route} */ export function createRoute( record: ?RouteRecord, location: Location, redirectedFrom?: Location ): Route { 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), matched: record ? formatMatch(record) : [] }; // 这里暂时不说 if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom); } return Object.freeze(route); } /** * 格式化匹配 * * @param {?RouteRecord} record * @returns {Array<RouteRecord>} */ function formatMatch(record: ?RouteRecord): Array<RouteRecord> { const res = []; while (record) { res.unshift(record); record = record.parent; } return res; } /** * 获取完整路径 * * @param {*} { path, query = {}, hash = '' } * @returns */ function getFullPath({ path, query = {}, hash = '' }) { return (path || '/') + stringifyQuery(query) + hash; } 复制代码
START 结果如下:
START = { fullPath: "/", hash: "", matched: [], meta: {}, name: null, params: {}, path: "/", query: {}, __proto__: Object, , } 复制代码
-
首先调用
VueRouter
类的match
属性, 该属性在初始化VueRouter
被赋值为一个方法。// vue-router/src/index.js this.match = createMatcher(options.routes || []); 复制代码
createMatcher 生成 match 的代码实现createMatcher
实现-
首先调用
createRouteMap
传入路由映射表,解构出路径、名称映射表 -
定义内建方法
match
redirect
_createRoute
最后 returnmatch
/** * 创建匹配器 * * @export * @param {Array<RouteConfig>} routes * @returns {Matcher} */ export function createMatcher(routes: Array<RouteConfig>): Matcher { const { pathMap, nameMap } = createRouteMap(routes); // 匹配函数 function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { ... 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 { ... } return match; } 复制代码
createRouteMap
- 首先创建 name、path 映射对象
- 对路由表内部每一项进行处理
- 最终返回包含 路径/名称的映射表
/** * 创建路由映射表 * * @export * @param {Array<RouteConfig>} routes * @returns {{ * pathMap: Dictionary<RouteRecord>, * nameMap: Dictionary<RouteRecord> * }} */ export function createRouteMap( routes: Array<RouteConfig> ): { pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord> } { const pathMap: Dictionary<RouteRecord> = Object.create(null); const nameMap: Dictionary<RouteRecord> = Object.create(null); // 对路由表内部每一项进行处理 routes.forEach(route => { addRouteRecord(pathMap, nameMap, route); }); return { pathMap, nameMap }; } 复制代码
addRouteRecord
/** * 添加路由记录 * * @param {Dictionary<RouteRecord>} pathMap 路径映射表 * @param {Dictionary<RouteRecord>} nameMap 名称映射表 * @param {RouteConfig} route 路由项 * @param {RouteRecord} [parent] 父路由项 * @param {string} [matchAs] */ function addRouteRecord( pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string ) { // 解构路径和名称,若路径不存在,则抛出异常 const { path, name } = route; assert(path != null, `在路由配置中需要“path”。`); // 定义路由记录构建选项 const record: RouteRecord = { path: normalizePath(path, parent), // 规范化之后的路径 components: route.components || { default: route.component }, // 路由组件 instances: {}, name, // 路由的名称 parent, // 父路由 matchAs, redirect: route.redirect, // 重定向 beforeEnter: route.beforeEnter, // 进入前钩子函数,形如:(to: Route, from: Route, next: Function) => void; meta: route.meta || {} // 路由元信息 }; // 是否存在嵌套路由 if (route.children) { // 如果路由已命名并具有默认子路由,则发出警告。 // 如果用户按名称导航到此路由,则不会呈现默认的子节点(GH问题#629)。 if (process.env.NODE_ENV !== 'production') { if ( route.name && route.children.some(child => /^\/?$/.test(child.path)) // 匹配空字符串 ) { warn( false, `命名路由'${route.name}'有一个默认的子路由。 当导航到这个命名路由(:to="{name: '${ route.name }'")时,将不会呈现默认的子路由。 从此路由中删除该名称,并使用已命名链接的默认子路由的名称。` ); } } // 若存在子路由,递归处理子路由表 route.children.forEach(child => { addRouteRecord(pathMap, nameMap, child, record); }); } // 是否存在别名配置 string | Array<string> if (route.alias) { // 处理数组情况 if (Array.isArray(route.alias)) { // 递归处理别名配置项 route.alias.forEach(alias => { addRouteRecord( pathMap, nameMap, { path: alias }, parent, record.path ); }); } else { addRouteRecord( pathMap, nameMap, { path: route.alias }, parent, record.path ); } } // 分别项路径,名称映射表里新增记录 pathMap[record.path] = record; if (name) nameMap[name] = record; } /** * 规范化路径 * * @param {string} path * @param {RouteRecord} [parent] * @returns {string} */ function normalizePath(path: string, parent?: RouteRecord): string { path = path.replace(/\/$/, ''); // 替换字符结尾为'/' => '' 如:'/foo/' => '/foo' if (path[0] === '/') return path; if (parent == null) return path; return cleanPath(`${parent.path}/${path}`); // 替换 '//' => '/' 如:'router//foo//' => 'router/foo/' } 复制代码
-
上述代码主要对路由配置的每一项进行处理,最终写入相应的路径,名称映射表。
-
上述基础示例代码路由配置项处理之后为:
pathMap:{ '': { beforeEnter: undefined, components: { default: { template: "<div>home</div>" }, __proto__: Object }, instances: {}, matchAs: undefined, meta: {}, name: undefined, parent: undefined, path: "", redirect: undefined, __proto__: Object, }, '/bar': { beforeEnter: undefined, components: { default: {template: "<div>bar</div>"}, __proto__: Object }, instances: {}, matchAs: undefined, meta: {}, name: undefined, parent: undefined, path: "/bar", redirect: undefined, __proto__: Object }, '/bar/child': { beforeEnter: undefined, components: { default: {template: "<div>Child</div>"}, __proto__: Object }, instances: {}, matchAs: undefined, meta: {}, name: undefined, parent: {path: "/bar", ... }, path: "/bar/child", redirect: undefined, __proto__: Object }, '/foo': { beforeEnter: undefined, components: { default: {template: "<div>foo</div>"}, __proto__: Object }, instances: {}, matchAs: undefined, meta: {}, name: undefined, parent: undefined, path: "/foo", redirect: undefined, __proto__: Object } } nameMap:{} 复制代码
知道了
const { pathMap, nameMap } = createRouteMap(routes);
解构的实现及结果,咱们继续看match
的代码实现function match( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute); const { name } = location; if (name) { // 若存在名称,从名称映射表中取对应记录 const record = nameMap[name]; if (record) { // 处理路径 location.path = fillParams( record.path, location.params, `named route "${name}"` ); return _createRoute(record, location, redirectedFrom); } } else if (location.path) { location.params = {}; for (const path in pathMap) { if (matchRoute(path, location.params, location.path)) { return _createRoute(pathMap[path], location, redirectedFrom); } } } // 没有匹配直接传入 null。 return _createRoute(null, location); } 复制代码
-
规范化目标路由的链接
normalizeLocation 代码实现normalizeLocation
/** * 规范化目标路由的链接 * * @export * @param {RawLocation} raw 目标路由的链接 * @param {Route} [current] 当前路由 * @param {boolean} [append] 是否在当前 (相对) 路径前添加基路径 * @returns {Location} */ export function normalizeLocation( raw: RawLocation, current?: Route, append?: boolean ): Location { // 处理目标路由的链接(to),我们知道其支持多种写法 // 'home' // { path: 'home' } // { path: `/user/${userId}` } // { name: 'user', params: { userId: 123 }} // { path: 'register', query: { plan: 'private' }} const next: Location = typeof raw === 'string' ? { path: raw } : raw; // 若已经被规范化或存在name属性直接返回 next if (next.name || next._normalized) { return next; } // 解析路径 返回 { path, query, hash } const parsedPath = parsePath(next.path || ''); // current.path - 字符串,对应当前路由的路径,总是解析为绝对路径 const basePath = (current && current.path) || '/'; // 获取最终路径地址 const path = parsedPath.path ? resolvePath(parsedPath.path, basePath, append) : (current && current.path) || '/'; // 获取查询参数 const query = resolveQuery(parsedPath.query, next.query); // 当前路由的 hash 值 (带 #) ,如果没有 hash 值,则为空字符串 let hash = next.hash || parsedPath.hash; if (hash && hash.charAt(0) !== '#') { hash = `#${hash}`; } return { _normalized: true, path, query, hash }; } 复制代码
-
normalizeLocation
所涉及的函数调用的代码实现parsePath 代码实现解析路径
/** * 解析路径 * * @export * @param {string} path * @returns {{ * path: string; * query: string; * hash: string; * }} */ export function parsePath( path: string ): { path: string, query: string, hash: string } { let hash = ''; let query = ''; // 是否存在 # const hashIndex = path.indexOf('#'); if (hashIndex >= 0) { hash = path.slice(hashIndex); // 截取 hash 值 path = path.slice(0, hashIndex); // 截取路径 } // 是否存在查询参数 const queryIndex = path.indexOf('?'); if (queryIndex >= 0) { query = path.slice(queryIndex + 1); // 截取参数 path = path.slice(0, queryIndex); // 截取路径 } return { path, query, hash }; } 复制代码
resolvePath 代码实现导出处理之后的路径地址
/** * 导出处理之后的路径地址 * * @export * @param {string} relative 相对路径 * @param {string} base 基础路径 * @param {boolean} [append] 是否在当前 (相对) 路径前添加基路径 * @returns {string} */ export function resolvePath( relative: string, base: string, append?: boolean ): string { if (relative.charAt(0) === '/') { return relative; } if (relative.charAt(0) === '?' || relative.charAt(0) === '#') { return base + relative; } // '/vue-router/releases' => ["", "vue-router", "releases"] const stack = base.split('/'); // 删除后段 // - 没有附加 // - 附加到尾随斜杠(最后一段为空) if (!append || !stack[stack.length - 1]) { stack.pop(); } // resolve 相对路径 // '/vue-router/releases'.replace(/^\//, '') => "vue-router/releases" // 'vue-router/releases'.split('/') => ["vue-router", "releases"] const segments = relative.replace(/^\//, '').split('/'); for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (segment === '.') { continue; } else if (segment === '..') { stack.pop(); } else { stack.push(segment); } } // 确保领先的削减 ensure leading slash if (stack[0] !== '') { stack.unshift(''); } return stack.join('/'); } 复制代码
resolveQuery 代码实现导出处理之后的路径地址
/** * 导出查询参数 * * @export * @param {?string} query * @param {Dictionary<string>} [extraQuery={}] * @returns {Dictionary<string>} */ export function resolveQuery( query: ?string, extraQuery: Dictionary<string> = {} ): Dictionary<string> { if (query) { let parsedQuery; try { parsedQuery = parseQuery(query); } catch (e) { warn(false, e.message); parsedQuery = {}; } for (const key in extraQuery) { parsedQuery[key] = extraQuery[key]; } return parsedQuery; } else { return extraQuery; } } /** * 解析查询参数 * * @param {string} query * @returns {Dictionary<string>} */ function parseQuery(query: string): Dictionary<string> { const res = Object.create(null); // 匹配 ?、#、& 开头的字符串 如:'?id=1'.match(/^(\?|#|&)/) => ["?", "?", index: 0, input: "?id=1", groups: undefined] // '?id=1&name=cllemon'.replace(/^(\?|#|&)/, '') => id=1&name=cllemon query = query.trim().replace(/^(\?|#|&)/, ''); if (!query) { return res; } // 如上例: => ["id=1", "name=cllemon"] query.split('&').forEach(param => { // 匹配 ”+“ // 如上例:"id=1" => ["id", "1"] const parts = param.replace(/\+/g, ' ').split('='); // 如上例:["id", "1"] => 'id' // 解码由 decode 等于 decodeURIComponent() 方法用于 encodeURIComponent 方法或者其它类似方法编码的部分统一资源标识符(URI)。 const key = decode(parts.shift()); // 如上例:["1"] const val = parts.length > 0 ? decode(parts.join('=')) : null; if (res[key] === undefined) { res[key] = val; } else if (Array.isArray(res[key])) { res[key].push(val); } else { res[key] = [res[key], val]; } }); return res; } 复制代码
-
-
fillParams
填充参数const regexpCompileCache: { [key: string]: Function } = Object.create(null); /** * 填充参数 * * @param {string} path * @param {?Object} params * @param {string} routeMsg * @returns {string} */ function fillParams( path: string, params: ?Object, routeMsg: string ): string { try { // 第三方库 path-to-regexp: 将路径字符串(例如`/user/:name`)转换为正则表达式 // compile : 用于将字符串转换为有效路径。 // 如: const toPath = Regexp.compile('/user/:id') // toPath({ id: 123 }) //=> "/user/123" const filler = regexpCompileCache[path] || (regexpCompileCache[path] = Regexp.compile(path)); return filler(params || {}, { pretty: true }); } catch (e) { assert(false, `missing param for ${routeMsg}: ${e.message}`); return ''; } } 复制代码
-
_createRoute
根据不同的配置项信息调用不同的路由创建处理函数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); } // createRoute 在 “START 代码实现及结果展示” 中以有论述 return createRoute(record, location, redirectedFrom); } 复制代码
-
首先调用
最终 route
(通过 match
函数返回的值) 值为:
route = { fullPath: '/', hash: '', matched: [ { beforeEnter: undefined, components: { default: { template: '<div>home</div>' } }, instances: {}, matchAs: undefined, meta: {}, name: undefined, parent: undefined, path: '', redirect: undefined } ], meta: {}, name: undefined, params: {}, path: '/', query: {}, __proto__: Object }; 复制代码
最终跳转方法 confirmTransition
/** * 确认跳转 * * @param {Route} route 目录路由信息 * @param {Function} cb * @memberof History */ confirmTransition(route: Route, cb: Function) { const current = this.current // 是不是相同路由 if (isSameRoute(route, current)) { this.ensureURL() return } const { 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, // enter guards beforeEnter: (to, from, next) => {} activated.map(m => m.beforeEnter), // 异步组件 resolveAsyncComponents(activated) ) this.pending = route // 迭代方法 const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) return // route: 即将要进入的目标 路由对象 current: 当前导航正要离开的路由 next: 调用该方法来 resolve 这个钩子 hook(route, current, (to: any) => { // to === false: 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。 if (to === false) { // next(false) -> abort navigation, ensure current URL this.ensureURL() } else if (typeof to === 'string' || typeof to === 'object') { // next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。 // next('/') or next({ path: '/' }) -> redirect this.push(to) } else { // 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。 // 确认转换并传递值 next(to) } }) } // 队列执行函数 runQueue(queue, iterator, () => { const postEnterCbs = [] // 在提取组件内的 enter 守卫之前,请等待异步组件被解析 runQueue(extractEnterGuards(activated, postEnterCbs), iterator, () => { if (this.pending === route) { this.pending = null cb(route) this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => cb()) }) } }) }) } 复制代码
-
到此,路由核心基类已经全部梳理完毕
-
上述代码逻辑很清晰,有详细的注释,这里直接略过文字描述,上述代码所涉及的函数调用 :point_down::
resolveQueue 的代码实现/** * 比对当前路由和目标路由,导出失活和激活路由信息 * * @param {Array<RouteRecord>} current * @param {Array<RouteRecord>} next * @returns {{ * activated: Array<RouteRecord>, * deactivated: Array<RouteRecord> * }} */ function resolveQueue( current: Array<RouteRecord>, next: Array<RouteRecord> ): { activated: Array<RouteRecord>, deactivated: Array<RouteRecord> } { let i; const max = Math.max(current.length, next.length); for (i = 0; i < max; i++) { if (current[i] !== next[i]) { break; } } return { activated: next.slice(i), deactivated: current.slice(i) }; } 复制代码
extractLeaveGuards 的代码实现/** * 提取离开的路由对象 * * @param {Array<RouteRecord>} matched * @returns {Array<?Function>} */ function extractLeaveGuards(matched: Array<RouteRecord>): Array<?Function> { // 返回反转之后的数组元素 return flatMapComponents(matched, (def, instance) => { // 提取匹配路由的组件路由守卫钩子函数 const guard = def && def.beforeRouteLeave; if (guard) { return function routeLeaveGuard() { return guard.apply(instance, arguments); }; } }).reverse(); } 复制代码
resolveAsyncComponents 的代码实现/** * 加载异步组件 * * @param {Array<RouteRecord>} matched * @returns {Array<?Function>} */ function resolveAsyncComponents( matched: Array<RouteRecord> ): Array<?Function> { return flatMapComponents(matched, (def, _, match, key) => { // 如果它是一个函数并且没有附加 Vue 选项, // 那么假设它是一个异步组件解析函数 // 我们没有使用 Vue 的默认异步解析机制 // 因为我们希望在解析传入组件之前停止导航。 if (typeof def === 'function' && !def.options) { return (to, from, next) => { const resolve = resolvedDef => { match.components[key] = resolvedDef; next(); }; const reject = reason => { warn(false, `Failed to resolve async component ${key}: ${reason}`); next(false); }; const res = def(resolve, reject); if (res && typeof res.then === 'function') { res.then(resolve, reject); } }; } }); } 复制代码
runQueue 的代码实现/** * 执行对列 * * @export * @param {Array<?NavigationGuard>} queue * @param {Function} fn * @param {Function} cb */ export function runQueue( queue: Array<?NavigationGuard>, fn: Function, cb: Function ) { const step = index => { if (index >= queue.length) { cb(); } else { if (queue[index]) { fn(queue[index], () => { step(index + 1); }); } else { step(index + 1); } } }; step(0); } 复制代码
extractEnterGuards 的代码实现/** * 提取进入的路由对象 * * @param {Array<RouteRecord>} matched * @param {Array<Function>} cbs * @returns {Array<?Function>} */ function extractEnterGuards( matched: Array<RouteRecord>, cbs: Array<Function> ): Array<?Function> { return flatMapComponents(matched, (def, _, match, key) => { // 提取组件内的守卫 // 如:beforeRouteEnter (to, from, next) {} // 在渲染该组件的对应路由被 confirm 前调用, 不!能!获取组件实例 `this`, 因为当守卫执行前,组件实例还没被创建. const guard = def && def.beforeRouteEnter; if (guard) { return function routeEnterGuard(to, from, next) { // 不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。 // 如:next(vm => { // 通过 `vm` 访问组件实例 }) return guard(to, from, cb => { next(cb); if (typeof cb === 'function') { cbs.push(() => { cb(match.instances[key]); }); } }); }; } }); } 复制代码
/** * 导航到不同的 location 向 history 栈添加一个新的记录 * * @param {RawLocation} location * @memberof HTML5History */ push(location: RawLocation) { // 拿到当前路由对象 const current = this.current // 调用跳转核心方法 this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) this.handleScroll(route, current, false) }) } 复制代码
-
最后我们以路由实例的跳转方法(
push
)来梳理一下路由跳转的过程:-
首先拿到当前路由信息。
-
调用
History
提供的transitionTo
过渡跳转的方法,传入要跳转的路径信息location
(由上文分析,此处是一个经过规范化之后的路径信息),以及一个参数为路由信息的回调函数(主要用来更新浏览器 URL 和处理用户滚动行为的方法「前文已分析」)。 -
transitionTo
方法上拿到目标路径匹配的路由信息,调用confirmTransition
并传入匹配路由信息及一个回调函数,该回调函数主要做:调用updateRoute
更新路由,执行transitionTo
内传入的回调,以及更新 URL。 -
confirmTransition
内首先过滤掉相同路由;然后调用resolveQueue
方法传入当前路由匹配信息及目标路由匹配信息解构出deactivated, activated
; 然后构造执行队列(处理全局钩子函数,异步组件解析等);然后定义执行方法;最后执行队列执行函数(具体实现看上述代码解析)。最终在队列内执行transitionTo
传入的回调,更新路由,执行监听函数:更新URL
;更新vue
根实例的_route
属性,由值的改变,进而触发视图的重新渲染 在 中拿到匹配的路由对象,渲染匹配到的路由组件。完成跳转。
-
注册的两个全局组件: <router-view>
和 <router-link>
-
<router-view>
组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view>
渲染的组件还可以内嵌自己的<router-view>
,根据嵌套路径,渲染嵌套组件。 -
Props: 默认值: "default"; 如果
<router-view>
设置了名称,则会渲染对应的路由配置中components
下的相应组件。
export default { name: 'router-view', functional: true, // functional 组件 props: { // 用于匹配渲染对应的路由配置中 components 下的相应组件 name: { type: String, default: 'default' } }, render(h, { props, children, parent, data }) { data.routerView = true; const route = parent.$route; const cache = parent._routerViewCache || (parent._routerViewCache = {}); let depth = 0; let inactive = false; // 是否失活 // 循环寻找父节点,找到当前组件嵌套深度 while (parent) { if (parent.$vnode && parent.$vnode.data.routerView) { depth++; } // 处理 keep-alive _inactive vue 内部属性 if (parent._inactive) { inactive = true; } parent = parent.$parent; } // 设置当前 router-view 所属层级 data.routerViewDepth = depth; // 获取对应层级的匹配项 const matched = route.matched[depth]; // 若不存在,直接渲染为空 if (!matched) { return h(); } // 若失活,直接从缓存中取 const component = inactive ? cache[props.name] : (cache[props.name] = matched.components[props.name]); // keep-alive 非失活组件, if (!inactive) { // 添加钩子函数,更新匹配的组件实例 (data.hook || (data.hook = {})).init = vnode => { debugger; matched.instances[props.name] = vnode.child; }; } return h(component, data, children); } }; 复制代码
-
<router-link>
组件支持用户在具有路由功能的应用中 (点击) 导航。 通过to
属性指定目标地址,默认渲染成带有正确链接的<a>
标签,可以通过配置tag
属性生成别的标签。 -
<router-link>
比起写死的<a href="...">
会好一些。 -
更多参阅
vue-router
文档。
/* @flow */ import { cleanPath } from '../util/path'; import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'; import { normalizeLocation } from '../util/location'; // 解决奇怪的 flow bug const toTypes: Array<Function> = [String, Object]; export default { name: 'router-link', props: { // 目标路由的链接 to: { type: toTypes, required: true }, // 标签名称 tag: { type: String, default: 'a' }, // 是否激活 exact: Boolean, // 是否在当前 (相对) 路径前添加基路径 如:/a => /b (true:/a/b; false: /b) append: Boolean, // true: 当点击时,会调用 router.replace() 而不是 router.push() 不会留下 history 记录 replace: Boolean, // 链接激活时使用的 CSS 类名 activeClass: String }, render(h: Function) { const router = this.$router; const current = this.$route; // 规范化 目标路由的链接 const to = normalizeLocation(this.to, current, this.append); const resolved = router.match(to); const fullPath = resolved.redirectedFrom || resolved.fullPath; const base = router.history.base; const href = base ? cleanPath(base + fullPath) : fullPath; const classes = {}; const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active'; const compareTarget = to.path ? createRoute(null, to) : resolved; classes[activeClass] = this.exact ? isSameRoute(current, compareTarget) : isIncludedRoute(current, compareTarget); const on = { click: e => { // 阻止浏览器默认行为 防止a跳转 e.preventDefault(); if (this.replace) { router.replace(to); } else { router.push(to); } } }; const data: any = { class: classes }; if (this.tag === 'a') { data.on = on; data.attrs = { href }; } else { // find the first <a> child and apply listener and href const a = findAnchor(this.$slots.default); if (a) { const aData = a.data || (a.data = {}); aData.on = on; const aAttrs = aData.attrs || (aData.attrs = {}); aAttrs.href = href; } } return h(this.tag, data, this.$slots.default); } }; function findAnchor(children) { if (children) { let child; for (let i = 0; i < children.length; i++) { child = children[i]; if (child.tag === 'a') { return child; } if (child.children && (child = findAnchor(child.children))) { return child; } } } } 复制代码
结语
最后的最后,
Hash
和
Abstract
两种模式都是依托于 History
基类去实现,这里就不做深入分析了。
若对此感兴趣请参阅
vue-router
。
更多细节部分,建议直接把
vue-router
项目拉下来,本地跑起来,根据 vue-router
提供的用例分析,一定收获满满。
以上所述就是小编给大家介绍的《vue-router 源代码全流程分析「长文」》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。