内容简介:路由这个概念最开始是在后端出现的,以前使用模板引擎开发页面的时候经常会看到这样的路径:有时还会有带.asp或.html的路径,这就是所谓的SSR(Server Side Render),通过服务端渲染,直接返回页面。其响应过程是这样的
路由这个概念最开始是在后端出现的,以前使用模板引擎开发页面的时候经常会看到这样的路径:
http://hometown.xxx.edu.cn/bbs/forum.php
有时还会有带.asp或.html的路径,这就是所谓的SSR(Server Side Render),通过服务端渲染,直接返回页面。
其响应过程是这样的
1.浏览器发出请求
2.服务器监听到80端口(或443)有请求过来,并解析url路径
3.根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)
4.浏览器根据数据包的Content-Type来决定如何解析数据
简单来说路由就是用来跟后端服务器进行交互的一种方式,通过不同的路径,来请求不同的资源,请求不同的页面是路由的其中一种功能。就像路由器在网络层中扮演的角色一样,肩负着将数据包正确导向目的地址的重任,只不过在这里变成了客户端浏览器的指路人,所谓的前端路由,指的是一种能力,即:
不依赖于服务器,根据不同的URL渲染不同的页面
前端路由与后端路由
在 Ajax
还没有诞生的时候,路由的工作是交给后端来完成的,当进行页面切换的时候,浏览器会发送不同的 URL
请求,服务器接收到浏览器的请求时,通过解析不同的 URL
去拼接需要的 Html
或模板,然后将结果返回到浏览器端进行渲染。
服务器端路由同样是有利亦有弊。它的好处是安全性更高,更严格得控制页面的展现。这在某些场景中是很有用的,譬如下单支付流程,每一步只有在上一步成功执行之后才能抵达。这在服务器端可以为每一步流程添加验证机制,只有验证通过才返回正确的页面。那么前端路由不能实现每一步的验证?自然不是,姑且相信你的代码可以写的很严谨,保证正常情况下流程不会错,但是另一个不得不面对的事实是:前端是毫无安全性可言的。用户可以肆意修改代码来进入不同的流程,你可能会为此添加不少的处理逻辑。相较之下,当然是后端控制页面的进入权限更为安全和简便。
另一方面,后端路由无疑增加了服务器端的负荷,并且需要reload页面,用户体验其实不佳。
前端路由的出现
在 90s 年代初,大多数的网页都是通过直接返回 HTML
的,用户的每次更新操作都需要重新刷新页面。及其影响交互体验,随着网络的发展,迫切需要一种方案来改善这种情况。
1996,微软首先提出 iframe 标签, iframe
带来了异步加载和请求元素的概念,随后在 1998 年,微软的 Outloook Web App 团队提出 Ajax
的基本概念(XMLHttpRequest的前身),并在 IE5
通过 ActiveX
来实现了这项技术。在微软实现这个概念后,其他浏览器比如 Mozilia
, Safari
, Opera
相继以 XMLHttpRequest
来实现 Ajax
。(:sob: 兼容问题从此出现,话说微软命名真喜欢用X,MFC源码一大堆。。)不过在 IE7 发布时,微软选择了妥协,兼容了 XMLHttpRequest
的实现。
有了 Ajax
后,用户交互就不用每次都刷新页面,体验带来了极大的提升。
但真正让这项技术发扬光大的,(。・∀・)ノ゙还是后来的 Google Map,它的出现向人们展现了 Ajax
的真正魅力,释放了众多开发人员的想象力,让其不仅仅局限于简单的数据和页面交互,为后来异步交互体验方式的繁荣发展带来了根基。
而异步交互体验的更高级版本就是我们熟知的 SPA
, SPA
不单单在页面交互上做到了不刷新,而且在页面之间跳转也做到了不刷新,为了做到这一点,就促使了前端路由的诞生。
前端路由的实现方式
前端路由其实只要解决两个问题:
- 在页面不刷新的前提下实现url变化
-
捕捉到url的变化,以便执行页面替换逻辑
在 2014 年之前,大家是通过 hash 来实现路由,url hash 就是类似于:http://www.xxx.com/#/login
这种 #。后面 hash
值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面。另外每次 hash
值的变化,还会触发 hashchange
这个事件,通过这个事件我们就可以知道 hash
值发生了哪些变化。然后我们便可以监听 hashchange
来实现更新页面部分内容的操作:
function matchAndUpdate () {
// todo 匹配 hash 做 dom 更新操作
}
window.addEventListener('hashchange', matchAndUpdate)
后来,因为 HTML5
标准发布。多了两个 API, pushState
和 replaceState
,通过这两个 API
可以改变 url
地址且不会发送请求。同时还有 popstate
事件。通过这些就能用另一种方式来实现前端路由了,但原理都是跟 hash
实现相同的。用了 HTML5
的实现,单页路由的 url
就不会多出一个#,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面:
function matchAndUpdate () {
// todo 匹配路径 做 dom 更新操作
}
window.addEventListener('popstate', matchAndUpdate)
Vue-Router的实现方式
Vue-Router
跟 Vuex
一样都是通过 Vue.use
这个全局 API
来注册的,这个方法定义在 vue/src/core/global-api/use.js
:
/* @flow */
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
Vue.use
接受一个 plugin
参数,并且维护了一个 _installedPlugins
数组,它存储所有注册过的 plugin
;如果 plugin
是一个对象,会判断 plugin
有没有定义 install
方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue
;如果 plugin
是一个函数,它会被作为 install
方法,最后把 plugin
存储到 installedPlugins
数组里面, Vue
的这种插件注册的机制有一个好处就是我们不需要额外的去 import Vue
了。
路由的注册
Vue-Router
的入口在 src/index.js
,其中 install
方法定义在 src/install.js
,可以看一下 src
下面的目录结构:
├── components
│ ├── link.js
│ └── view.js
├── create-matcher.js
├── create-route-map.js
├── history
│ ├── abstract.js
│ ├── base.js
│ ├── hash.js
│ └── html5.js
├── index.js
├── install.js
└── util
├── async.js
├── dom.js
├── location.js
├── misc.js
├── params.js
├── path.js
├── push-state.js
├── query.js
├── resolve-components.js
├── route.js
├── scroll.js
└── warn.js
简单看下 install
的流程:
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 确保Vue-Router只被install一次
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
// 在beforeCreate钩子里面初始化路由
beforeCreate () {
// 根组件的$options上才有router对象
if (isDef(this.$options.router)) {
// 设置根路由
this._routerRoot = this
// 获取到根组件上的router实例
this._router = this.$options.router
// 路由初始化
this._router.init(this)
// 为_route属性实现双向绑定
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 获取父组件的_routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 注册<router-view></router-view>实例的钩子
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 方便全局通过this.$router获取路由实例
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 方便全局通过this.$route获取路由对象
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册全局组件<router-view/>和<router-link/>
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 使用和created相同的合并策略
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
首先通过设立一个 installed
标志位来确保 Vue-Router
只被安装一次,然后通过变量 _Vue
承载传入的 Vue
实例,然后利用 Vue.mixin
向每一个 Vue
实例注册 beforeCreate
和 destroyed
钩子函数。
在 beforeCreate
函数里面,如果是根组件,将根组件赋值给 this._routerRoot
,获取根组件的路由实例之后执行 init
初始化函数,然后调用 Vue
的 defineReactive
将 _route
变为响应式对象,如果不是根组件则获取父组件的 _routerRoot
属性。
在 beforeCreate
函数的最后部分和 destroyed
函数里面都执行了 registerInstance
函数,这个函数是注册 <router-view>
实例的钩子函数,根据传入参数的个数来决定是注册还是取消注册,函数的定义在 src/components/view.js
里面:
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
回到 install.js
,紧接着,为了让我们能够全局的使用 this.$router
和 this.$route
在 Vue
原型上定义了对应的 get
方法,然后通过 Vue.component
注册了全局组件 注册全局组件<router-view/>
和 和<router-link/>
,最后定义了一些钩子函数的使用策略,这就是整个 Vue-Router
的安装过程。
路由的实例化
先看一下 Vue-Router
的构造函数,当我们 new
一个 Vue-Router
的时候都干了些什么:
/* @flow */
import { install } from './install'
import { START } from './util/route'
import { assert } from './util/warn'
import { inBrowser } from './util/dom'
import { cleanPath } from './util/path'
import { createMatcher } from './create-matcher'
import { normalizeLocation } from './util/location'
import { supportsPushState } from './util/push-state'
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 {
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 = {}) {
// 根Vue实例
this.app = null
// 存储含有this.$options.router属性的Vue实例
this.apps = []
// 传入路由的配置
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 创建路由匹配对象
this.matcher = createMatcher(options.routes || [], this)
// 默认为hash模式
let mode = options.mode || 'hash'
// 当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式。默认值为 true
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根据mode采用不同的路由方式
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
get currentRoute (): ?Route {
return this.history && this.history.current
}
init (app: any /* Vue component instance */) {
// 在初始化Vue-Router之前必须先通过Vue.use(VueRouter)注册
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
this.app = app
const history = this.history
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
})
})
}
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
onError (errorCb: Function) {
this.history.onError(errorCb)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
go (n: number) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
getMatchedComponents (to?: RawLocation | Route): Array<any> {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply([], route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
}))
}
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
// for backwards compat
normalizedTo: Location,
resolved: Route
} {
current = current || this.history.current
const location = normalizeLocation(
to,
current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
// for backwards compat
normalizedTo: location,
resolved: route
}
}
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
function registerHook (list: Array<any>, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
function createHref (base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
VueRouter.install = install
VueRouter.version = '__VERSION__'
// 通过link标签引用js的实行自动注册
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
构造函数里面定义了一些属性,其中 this.app
表示根 Vue
的实例, this.apps
存储含有 this.$options.router
属性的Vue实例,初始化 Vue-Router
后传入的配置都会存储在 this.options
, this.beforeHooks
、 this.resolveHooks
、 this.afterHooks
用来存储钩子函数, this.matcher
是路由匹配后返回的对象, this.fallback
会根据配置的 mode
参数以及浏览器支持度来决定给是否回退到 hash
模式, this.mode
就是路由创建的模式,这里提供 hash
、 history
、 abstract
三种模式, this.history
表示根据不同的路由模式来创建的路由 history
的具体实现方式。
实例化 Vue-Router
之后会返回它的实例 router
,我们在使用 Vue-Router
的时候需要在初始化 Vue
的时候传入这个 router
属性:
new Vue({
el: '#app',
router,
render: h => h(App)
})
这个时候会把 router
属性配置到 this.$options
,回想到 install.js
里面在 beforeCreate
钩子函数里面执行的方法:
beforeCreate () {
// 根组件的$options上才有router对象
if (isDef(this.$options.router)) {
// 设置根路由
this._routerRoot = this
// 获取到根组件上的router实例
this._router = this.$options.router
// 路由初始化
this._router.init(this)
// ....
}
所以这个时候会执行 init
方法:
init (app: any /* Vue component instance */) {
// 在初始化Vue-Router之前必须先通过Vue.use(VueRouter)注册
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 存储app实例
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
this.app = app
const history = this.history
// 根据history实现的方式不同执行不同的逻辑
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
})
})
}
init
其实没干很多事情,首先把传入的 Vue
实例存储到 apps
数组中,然后把 this.history
赋值给一个本地变量,根据 this.history
实现方式的不同执行不同的逻辑,最后通过 history
的回调更新路由对象也就是 this.$route
。
无论 this.history
是基于 history
还是 hash
实现的,最后都会调用 transitionTo
方法,这个方法定义在 src/history/base.js
:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
// ...
}
实际上就是调用 match
方法:
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
那我们可以先把上面的逻辑放一边,先了解一下 matchers
的构建,相关的源码在 src/create-matcher.js
:
match的实现
/* @flow */
import type VueRouter from './index'
import { resolvePath } from './util/path'
import { assert, warn } from './util/warn'
import { createRoute } from './util/route'
import { fillParams } from './util/params'
import { createRouteMap } from './create-route-map'
import { normalizeLocation } from './util/location'
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 {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 添加路由路径关系映射
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 根据raw和currentRoute计算出新的location
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// 如果是命名路由,取出对应的路由record
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 = {}
}
// 赋值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 redirect (
record: RouteRecord,
location: Location
): Route {
const originalRedirect = record.redirect
let redirect = typeof originalRedirect === 'function'
? originalRedirect(createRoute(record, location, null, router))
: originalRedirect
if (typeof redirect === 'string') {
redirect = { path: redirect }
}
if (!redirect || typeof redirect !== 'object') {
if (process.env.NODE_ENV !== 'production') {
warn(
false, `invalid redirect option: ${JSON.stringify(redirect)}`
)
}
return _createRoute(null, location)
}
const re: Object = redirect
const { name, path } = re
let { query, hash, params } = location
query = re.hasOwnProperty('query') ? re.query : query
hash = re.hasOwnProperty('hash') ? re.hash : hash
params = re.hasOwnProperty('params') ? re.params : params
if (name) {
// resolved named direct
const targetRecord = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
assert(targetRecord, `redirect failed: named route "${name}" not found.`)
}
return match({
_normalized: true,
name,
query,
hash,
params
}, undefined, location)
} else if (path) {
// 1. resolve relative redirect
const rawPath = resolveRecordPath(path, record)
// 2. resolve params
const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)
// 3. rematch with existing query and hash
return match({
_normalized: true,
path: resolvedPath,
query,
hash
}, undefined, location)
} else {
if (process.env.NODE_ENV !== 'production') {
warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)
}
return _createRoute(null, location)
}
}
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {
const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)
const aliasedMatch = match({
_normalized: true,
path: aliasedPath
})
if (aliasedMatch) {
const matched = aliasedMatch.matched
const aliasedRecord = matched[matched.length - 1]
location.params = aliasedMatch.params
return _createRoute(aliasedRecord, location)
}
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)
}
return {
match,
addRoutes
}
}
function matchRoute (
regex: RouteRegExp,
path: string,
params: Object
): boolean {
const m = path.match(regex)
if (!m) {
return false
} else if (!params) {
return true
}
for (let i = 1, len = m.length; i < len; ++i) {
const key = regex.keys[i - 1]
const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
if (key) {
// Fix #1994: using * with props: true generates a param named 0
params[key.name || 'pathMatch'] = val
}
}
return true
}
function resolveRecordPath (path: string, record: RouteRecord): string {
return resolvePath(path, record.parent ? record.parent.path : '/', true)
}
createMatcher
接受两个参数,第一个是初始化路由的配置对象 routes
,第二个是我们的路由实例 router
,首先会跑一个 createRouteMap
的逻辑,这个方法的作用是创建一个路由映射,这个方法定义在 src/create-route-map.js
:
/* @flow */
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>;
} {
// 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') {
// path不能为空并且component的值必须用一个组件名而不是一个string字符串
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.`
)
}
// pathToRegexpOptions是编译正则的选项
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// normalize path
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
// caseSensitive匹配规则是否大小写敏感?(默认值:false)
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,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: 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
)
})
}
// 添加path记录以及建立path和记录的对应关系
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 如果配置了命名路由,给name和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 compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp {
const regex = Regexp(path, [], pathToRegexpOptions)
if (process.env.NODE_ENV !== 'production') {
const keys: any = Object.create(null)
regex.keys.forEach(key => {
// 有重复的动态路径参数
warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`)
keys[key.name] = true
})
}
return regex
}
function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string {
// 替换根路由路径
if (!strict) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
// 将//的路径替换成/
return cleanPath(`${parent.path}/${path}`)
}
这个方法的作用是将路由配置转换成一组组映射关系表,返回一个对象:
return {
pathList,
pathMap,
nameMap
}
其中 pathList
存储了所有的 path
, pathMap
表示了 path
到 RouteRecord
对象的一一映射关系, nameMap
则表示了 name
到 RouteRecord
对象的映射关系, RouteRecord
对象是对路由配置参数 routes
每一项进行遍历后调用 addRouteRecord
方法生成的一条记录,方法定义如下:
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') {
// path不能为空并且component的值必须用一个组件名而不是一个string字符串
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.`
)
}
// pathToRegexpOptions是编译正则的选项
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// normalize path
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
// caseSensitive匹配规则是否大小写敏感?(默认值:false)
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,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: 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
)
})
}
// 添加path记录以及建立path和记录的对应关系
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 如果配置了命名路由,给name和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}" }`
)
}
}
}
这个方法首先会对 path
使用 normalizePath
进行规范化处理,我们看一下这个方法:
function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string {
// 替换根路由路径
if (!strict) path = path.replace(/\/$/, '')
// 说明是一级路径,直接返回
if (path[0] === '/') return path
if (parent == null) return path
// 将//的路径替换成/并且拼接父路由的路径
return cleanPath(`${parent.path}/${path}`)
}
主要作用就是生成多层路由的具体路径,然后根据已有参数构建 RouteRecord
:
const record: RouteRecord = {
path: normalizedPath,
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 }
}
这里解释下 regex
这个参数,用到了 path-to-regexp
这个库,这个库可以把路径转换为正则表达式,举个栗子:
const keys = []
const regexp = pathToRegexp('/foo/:bar', keys)
// regexp = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
用这个库作为路径匹配引擎是为了实现可选的动态路径参数、匹配零个或多个、一个或多个,甚至是自定义正则匹配。
紧接着判断是否配置了子路由,然后循环调用 addRouteRecord
这个方法,并把当前的 record
作为 parent
,如果设置了路由别名,也会给别名添加一份 record
,最后就是更新映射表,返回一个 Array
对象以及两个 Dictionary
对象。
回到 create-matcher.js
,它对外暴露了两个方法: addRoutes
和 match
,分别用于动态添加路由配置以及返回一个路由的路径,先看 addRoutes
:
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
其实就是在现有的 pathList
、 pathMap
、 nameMap
上动态添加一条新纪录,这几个都是引用类型,执行 addRoutes
之后都会被修改。
match
函数相对复杂一点,接受三个参数,第一个参数可以为 string
也可以是一个 Location
对象,第二个参数表示当前的路由路径,第三个参数也是 Location
对象,跟重定向有关:
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 根据raw和currentRoute计算出新的location
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// 如果是命名路由,取出对应的路由record
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 = {}
}
// 赋值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)
}
一开始会执行 normalizeLocation
方法,返回一个新的 location
,看一眼 normalizeLocation
的实现:
export function normalizeLocation (
raw: RawLocation,
current: ?Route,
append: ?boolean,
router: ?VueRouter
): Location {
let next: Location = typeof raw === 'string' ? { path: raw } : raw
// named target
if (next._normalized) {
// 已经normalized的直接返回
return next
} else if (next.name) {
// 如果是命名路由,返回一份备份
return extend({}, raw)
}
// relative params
// 没有path,但是有params和current
if (!next.path && next.params && current) {
next = extend({}, next)
next._normalized = true
// 拿到params
const params: any = extend(extend({}, current.params), next.params)
if (current.name) {
next.name = current.name
next.params = params
} else if (current.matched.length) {
const rawPath = current.matched[current.matched.length - 1].path
// 根据rawPath和params计算出当前path
next.path = fillParams(rawPath, params, `path ${current.path}`)
} else if (process.env.NODE_ENV !== 'production') {
warn(false, `relative params navigation requires a current route.`)
}
return next
}
// 将path拆分成path、hash和query
const parsedPath = parsePath(next.path || '')
const basePath = (current && current.path) || '/'
// 返回最后拼接完成好的路径,append用于判断是否在当前 (相对) 路径前添加基路径
const path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath
// 解析query parseQuery是提供自定义查询字符串的解析/反解析函数,用于覆盖默认行为
const query = resolveQuery(
parsedPath.query,
next.query,
router && router.options.parseQuery
)
// 路由的hash值
let hash = next.hash || parsedPath.hash
if (hash && hash.charAt(0) !== '#') {
hash = `#${hash}`
}
return {
_normalized: true,
path,
query,
hash
}
}
这个方法首先会判断当前的 RawLocation
是否已经经过 _normalized
处理,是的话直接返回,否则的话继续判断当前 Location
是否有 name
字段,有的话通过 extend
方法拷贝一份 raw
对象直接返回,这个 extend
方法的实现很简单:
export function extend (a, b) {
for (const key in b) {
a[key] = b[key]
}
return a
}
当上面的情况都不满足,接着进入下一个判断条件,如果有当前 Route
信息,有 params
但是没有 path
的情况,首先会设置 _normalized
标志位,然后对 params
参数进行合并处理,然后继续分为两种情况处理,分别是 current
有 name
与否,前者的话会直接将 current
的 name
和拼接后的 params
赋值给 next
后直接返回,后者的话会从路由记录里面找到最新的一条记录的 path
,调用 fillParams
方法根据 rawPath
和 params
计算当前 path
,看一下 fillParams
对相对路径的处理:
/* @flow */
import { warn } from './warn'
import Regexp from 'path-to-regexp'
// $flow-disable-line
const regexpCompileCache: {
[key: string]: Function
} = Object.create(null)
export function fillParams (
path: string,
params: ?Object,
routeMsg: string
): string {
params = params || {}
try {
const filler =
regexpCompileCache[path] ||
(regexpCompileCache[path] = Regexp.compile(path))
// 如果param中有名为pathMatch的key将他设置为{0, params[patchMatch]}的键值对
// Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }}
if (params.pathMatch) params[0] = params.pathMatch
// 将动态路径参数替换成正式参数
return filler(params, { pretty: true })
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
warn(false, `missing param for ${routeMsg}: ${e.message}`)
}
return ''
} finally {
// delete the 0 if it was added
delete params[0]
}
}
其实这里就是把形如 {zapId: 1}
的params参数通过 Regexp.compile
生成的方法拼接到 path
后面,就像这样:
const toPath = pathToRegexp.compile('/user/:id')
toPath({ id: 123 }) //=> "/user/123"
回到 create-matcher.js
,计算出新的 location
之后对命名路由和非命名路由进行了不同的处理,如果 name
存在,从 nameMap
字典里面匹配出对应的 record
,如果 record
不存在通过 _createRoute
生成一条新的 record
直接返回,否则取出这条 record
里面的 params
的 key
组成的数组,将 currentRoute
里面的 params
以 key
/ value
的形式不重复地存入名为 location.params
的一个对象里面,最后依然是通过 fillParams
拼接 params
参数到路径尾部,通过 _createRoute
方法创建一条新路径返回。
反之,如果是非命名路由,会通过 pathList
返回 path
对应的 record
,然后通过 createRoute
方法判断是否能够匹配到路由信息,是的话也会通过 _createRoute
生成一条新路径返回。接下来只要搞懂 matchRoute
和 _createRoute
干了什么就行了,先看 matchRoute
:
function matchRoute (
regex: RouteRegExp,
path: string,
params: Object
): boolean {
const m = path.match(regex)
if (!m) {
return false
} else if (!params) {
return true
}
for (let i = 1, len = m.length; i < len; ++i) {
const key = regex.keys[i - 1]
const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
if (key) {
// Fix #1994: using * with props: true generates a param named 0
params[key.name || 'pathMatch'] = val
}
}
return true
}
其实就是通过 match
方法做判断,如果没匹配到直接返回 false
,如果传入了 params
会将 path
里面的 params
以 key
/ value
形式存入,这个传入的 params
在这里是 location.params
所以和前面做的是一样的操作。
接着看 _createRoute
方法,这个方法也在 create_matcher.js
内部:
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)
}
无论是否设置了 redirect
还是 alias
最后都会重新调用 _createRoute
,所以这里直接看最后的 createRoute
方法:
/* @flow */
import type VueRouter from '../index'
import { stringifyQuery } from './query'
const trailingSlashRE = /\/?$/
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
// 提供自定义查询字符串的解析/反解析函数。覆盖默认行为
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery), // 完整路径
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
function clone (value) {
if (Array.isArray(value)) {
return value.map(clone)
} else if (value && typeof value === 'object') {
const res = {}
for (const key in value) {
res[key] = clone(value[key])
}
return res
} else {
return value
}
}
// the starting route that represents the initial state
export const START = createRoute(null, {
path: '/'
})
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
function getFullPath (
{ path, query = {}, hash = '' },
_stringifyQuery
): string {
const stringify = _stringifyQuery || stringifyQuery
return (path || '/') + stringify(query) + hash
}
export function isSameRoute (a: Route, b: ?Route): boolean {
if (b === START) {
return a === b
} else if (!b) {
return false
} else if (a.path && b.path) {
return (
a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query)
)
} else if (a.name && b.name) {
return (
a.name === b.name &&
a.hash === b.hash &&
isObjectEqual(a.query, b.query) &&
isObjectEqual(a.params, b.params)
)
} else {
return false
}
}
function isObjectEqual (a = {}, b = {}): boolean {
// handle null value #1566
if (!a || !b) return a === b
const aKeys = Object.keys(a)
const bKeys = Object.keys(b)
if (aKeys.length !== bKeys.length) {
return false
}
return aKeys.every(key => {
const aVal = a[key]
const bVal = b[key]
// check nested equality
if (typeof aVal === 'object' && typeof bVal === 'object') {
return isObjectEqual(aVal, bVal)
}
return String(aVal) === String(bVal)
})
}
export function isIncludedRoute (current: Route, target: Route): boolean {
return (
current.path.replace(trailingSlashRE, '/').indexOf(
target.path.replace(trailingSlashRE, '/')
) === 0 &&
(!target.hash || current.hash === target.hash) &&
queryIncludes(current.query, target.query)
)
}
function queryIncludes (current: Dictionary<string>, target: Dictionary<string>): boolean {
for (const key in target) {
if (!(key in current)) {
return false
}
}
return true
}
通过传入的 record
和 location
创建一个不可被修改的 Route
对象,其中有个 matched
属性通过 formatMatch
方法构建:
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
通过循环不断地查找当前 record
的 parent
,然后返回这条线上所有的 record
组成的数组。
路由的跳转
无论是 Hash
路由还是 History
路由,在初始化的时候都会通过 transitionTo
方法跳转到初始路径,这个方法也是我们切换路由路径时候使用的方法,下面分析一下该方法的实现过程:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 通过location和current返回初始化Route信息
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
// 执行onComplete回调
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) })
}
})
}
初始化调用的时候,拿到的是初始化的 Route
,通过 confirmTransition
方法进行实际的跳转操作,同时对跳转成功和失败两种情况都设置了回调函数,看一下这个函数的定义:
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
if (isError(err)) {
// 将所有的错误信息都存入errorCbs
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
) {
// ensureURL在不同的路由实现方式里面该方法的实现不一样
this.ensureURL()
// 如果设置了abort方法这里直接调用
return abort()
}
// 拿到路径的变化部分以及遗弃部分和升级部分
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
// 维持一个对应路径变化的导航守卫的钩子组成的List
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
// 定义一个迭代器
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
// 执行beforeRouteEnter钩子函数
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() })
})
}
})
})
}
首先定义了一个 abort
函数用于处理路由跳转失败的情况以及执行 onAbort
回调,然后判断如果要跳转的路由和当前路由是同一个的话,直接调用 this.ensureURL()
和 abort()
,这个 ensureURL
在不同的路由实现方式里面该方法的实现不一样,最终都是做了路由跳转的操作,紧接着通过 resolveQueue
方法拿到三个数组,分别存储着固定的部分,遗弃的部分以及更新的部分,这个方法的实现如下:
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: 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 {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
路由从 current
变为 next
,两个路径的公共部分就是 next.slice(0, i)
, next.slice(0, i)
就是路径需要更新的部分,而 current.slice(i)
就是路径需要变化的部分。
拿到三个数组之后会构造一个 queue
队列,里面存储了路径变化要执行的钩子函数,也就是官方说的路由守卫,会按次序执行一些诸如 beforeRouteLeave
、 beforeRouteUpdate
等方法,下面还会定义一个迭代器,这个迭代器会根据传入的 ro
参数来决定执行 abort
还是 next
方法,最后执行 runQueue
方法来执行这个队列,这个方法的定义如下:
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)
}
这里的 fn
其实就是 iterator
里面的 next
函数,只有执行了 next
函数 index
才会 +1
,才会进行管道中的下一个钩子,如果全部钩子执行完了,则导航的状态会变成 confirmed
(确认的)。
最后可以看一下 Vue-Router
里面对导航的完整解析流程:
导航被触发。
在失活的组件里调用离开守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
总结
传统的路由实现是通过对路径的切换做到的,对于 Vue-Router
而言,路由模块的本质 就是建立起url和页面之间的映射关系。路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url
,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据.
参考链接: Vue核心解密
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
精通JavaScript+jQuery
曾顺 编著 / 人民邮电出版社 / 2008-9 / 59.00元
随着Ajax技术的不断风靡,其核心技术JavaScript越来越受到人们的关注,各种JavaScript的框架层出不穷。jQuery作为JavaScript框架的优秀代表,为广大开发者提供了诸多便利。 本书从介绍JavaScript的基础知识开始,围绕标准Web的各项技术予以展开,通过大量实例对JavaScript、CSS、DOM、Ajax等 Web关键技术进行深入浅出的分析,主要内容包括J......一起来看看 《精通JavaScript+jQuery》 这本书的介绍吧!