系列文章目录
1、vue-router源码解析(一)
2、vue-router源码解析(二) —— install
3、vue-router源码解析(三) —— History
前言
上一篇简单介绍了下vue-router的挂载过程,本篇详细解析下VueRoute的三种路由模式~
一、index.js中的History初始化
VueRouter 对象是在 src/index.js 中暴露出来的,它在实例初始化时,初始化了History对象:
// index.js
// 引入history中的HashHistory,HTML5History,AbstractHistory模块
import {
HashHistory } from './history/hash'
import {
HTML5History } from './history/html5'
import {
AbstractHistory } from './history/abstract'
// 定义VueRouter对象
export default class VueRouter {
constructor (options: RouterOptions = {
}) {
...
let mode = options.mode || 'hash' // 默认是hash模式
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
// 降级处理,不支持History模式则使用hash模式
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash': // 传入fallback
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}`)
}
}
}
...
}
- 在VueRouter实例初始化中,mode得到用户传入的路由模式值,默认是
hash
。支持三种模式:hash、history、abstract
- 接着判定当为history模式时,当前环境是否支持HTML5 history API,若不支持则fallback=true,降级处理,并且使用hash模式:
if (this.fallback) { mode='hash' }
- 判定当前环境是否是浏览器环境,若不是,则默认使用
abstract
抽象路由模式,这种抽象模式,通过数组来模拟浏览器操作栈。 - 根据不同的mode,初始化不同的History实例,hash模式需传入
this.fallback
来判断降级处理情况。因为要针对这种降级情况做特殊的URL处理。后续history/hash.js会讲到。
二、History目录
├── history // 路由模式相关
│ ├── abstract.js // 非浏览器环境下的,抽象路由模式
│ ├── base.js // 定义History基类
│ ├── hash.js // hash模式,#
│ └── html5.js // html5 history模式
HashHistory、HTML5History、AbstractHistory
实例都 继承自src/history/base.js 中的 History
类的
1、base.js
export class History {
router: Router //vueRouter对象
base: string //基准路径
current: Route //当前的route对象
pending: ?Route // 正在跳转的route对象,阻塞状态
cb: (r: Route) => void // 每一次路由跳转的回调,会触发routeview的渲染
ready: boolean // 就绪状态
readyCbs: Array<Function> // 就绪状态的回调数组
readyErrorCbs: Array<Function> // 就绪时产生错误的回调数组。
errorCbs: Array<Function> // 错误的回调数组
listeners: Array<Function>
cleanupListeners: Function
// 以下方法均在子类中实现(hashHistory,HTML5History,AbstractHistory)
+go: (n: number) => void
+push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
+replace: (
loc: RawLocation,
onComplete?: Function,
onAbort?: Function
) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
+setupListeners: Function
constructor (router: Router, base: ?string) {
this.router = router
this.base = normalizeBase(base) // 返回基准路径
// start with a route object that stands for "nowhere"
this.current = START // 当前路由对象,import {START} from '../util/route'
...
}
// 注册监听
listen (cb: Function) {
this.cb = cb
}
// transitionTo方法,是对路由跳转的封装,onComplete是成功的回调,onAbort是失败的回调
transitionTo (location: RawLocation,onComplete?,onAbort?){
...
}
// confirmTransition方法,是确认跳转
confirmTransition (location: RawLocation,onComplete?,onAbort?){
...
}
// 更新路由,并执行listen 的 cb 方法, 更改_route变量,触发视图更新
updateRoute (route: Route) {
this.current = route // 更新 current route
this.cb && this.cb(route)
}
...
}
this.current = START
赋予current属性为一个route对象的初始状态:START在src/util/route.js中有定义,createRoute函数在route.js中也有定义,返回一个Route对象。
export const START = createRoute(null, {
path: '/'
})
我们所用到的route对象,都是通过createRoute
方法返回。可以看到我们用route时常用到的name
, meta
,path
,hash
,params
等属性
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)
}
2、hash.js
下面看一下HashHistory对象
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// 判定是否是从history模式降级而来,若是降级模式,更改URL(自动添加#号)
if (fallback && checkFallback(this.base)) {
return
}
// 保证 hash 是以 / 开头,所以访问127.0.0.1时,会自动替换为127.0.0.1/#/
ensureSlash()
}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
setupListeners () {
...}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
...}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
...}
go (n: number) {
window.history.go(n)
}
ensureURL (push?: boolean) {
...}
// 获取当前hash值
getCurrentLocation () {
...}
}
- HashHistory在初始化中继承于History父类,在初始化中,继承了父类的相关属性,判定了是否是从history模式降级而来,对URL做了相关处理。
- 分别具体实现了父类的
setupListeners
push
replace
go
ensureURL
getCurrentLocation
方法。
重点看一下我们经常用到的push()方法
我们使用vue-router跳转路由时使用:this.$router.push()
。可见在VueRouter对象中会有一个push方法:(index.js)
export default class VueRouter {
...
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
}
以上可以看出,router.push()
最终会使用this.history.push()
方法跳转路由。来看一下HashHistory中push()方法:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const {
current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath) //
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
- push()方法也主要是用到了
transitionTo()
方法跳转路由,transitionTo()是在base.js中History基类中有定义,HashHistory也继承了此方法。 - 在调用transitionTo()方法,路由跳转完成之后,执行
pushHash(route.fullPath)
,这里做了容错处理,判定是否存在html5 history API,若支持用history.pushState()操作浏览器历史记录,否则用window.location.hash = path
替换文档。注意:调用history.pushState()方法不会触发 popstate 事件,popstate只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在JS中调用 history.back()、history.forward()、history.go() 方法)。
function pushHash (path) {
if (supportsPushState) {
// 判定是否存在html5 history API
pushState(getUrl(path))// 使用pushState或者window.location.hash替换文档
} else {
window.location.hash = path
}
}
- 查看 transitionTo 方法,主要是调用了
confirmTransition()
方法。
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
this.pending = route
const abort = err => {
// 定义取消函数
...
onAbort && onAbort(err)
}
// 如果目标路由与当前路由相同,取消跳转
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
if (
isSameRoute(route, current) &&
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
return abort(createNavigationDuplicatedError(current, route))
}
// 根据当前路由对象和匹配的路由:返回更新的路由、激活的路由、停用的路由
const {
updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 定义钩子队列
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
// 定义迭代器
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
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(createNavigationRedirectedError(current, route))
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, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
}
大致是几个步骤:
- 如果目标路由与当前路由相同,取消跳转
- 定义钩子队列,依次为:
组件导航守卫 beforeRouteLeave -> 全局导航守卫 beforeHooks -> 组件导航守卫 beforeRouteUpdate -> 目标路由的 beforeEnter -> 处理异步组件 resolveAsyncComponents - 定义迭代器
- 执行钩子队列
3、html5.js
HTML5History类的实现方式与HashHistory的思路大致一样。不再详细赘述。
4、abstract.js
AbstractHistory类,也同样继承实现了History类中几个路由跳转方法。但由于此模式一般用于非浏览器环境,没有history 相关操作API,通过this.stack
数组来模拟操作历史栈。
export class AbstractHistory extends History {
index: number
stack: Array<Route>
constructor (router: Router, base: ?string) {
super(router, base)
this.stack = [] // 初始化模拟记录栈
this.index = -1 // 当前活动的栈的位置
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(
location,
route => {
// 更新历史栈信息
this.stack = this.stack.slice(0, this.index + 1).concat(route)
this.index++ // 更新当前所处位置
onComplete && onComplete(route)
},
onAbort
)
}
...
}
总结
三种路由方式可以让前端不需要请求服务器,完成页面的局部刷新。