Open muwoo opened 6 years ago
因为我们用的比较多的是 vue 的 HashHistory。下面我们首先来介绍一下 HashHistory。我们知道,通过mode来确定使用 history的方式,如果当前mode = 'hash',则会执行:
mode
history
mode = 'hash'
this.history = new HashHistory(this, options.base, this.fallback)
this.fallback是用来判断当前mode = 'hash'是不是通过降级处理的:
this.fallback
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
接下来我们看看HashHistory的内部实现,首先是看一下 new HashHistory()的时候,实例化做了哪些事:
HashHistory
new HashHistory()
// 继承 History 基类 export class HashHistory extends History { constructor (router: VueRouter, base: ?string, fallback: boolean) { // 调用基类构造器 super(router, base) // 如果说是从 history 模式降级来的 // 需要做降级检查 if (fallback && this.checkFallback()) { // 如果降级 且 做了降级处理 则什么也不需要做 return } // 保证 hash 是以 / 开头 ensureSlash() } // ... } function checkFallback (base) { // 得到除去 base 的真正的 location 值 const location = getLocation(this.base) if (!/^\/#/.test(location)) { // 如果说此时的地址不是以 /# 开头的 // 需要做一次降级处理 降级为 hash 模式下应有的 /# 开头 window.location.replace( cleanPath(this.base + '/#' + location) ) return true } } // 保证 hash 以 / 开头 function ensureSlash (): boolean { // 得到 hash 值 const path = getHash() // 如果说是以 / 开头的 直接返回即可 if (path.charAt(0) === '/') { return true } // 不是的话 需要手工保证一次 替换 hash 值 replaceHash('/' + path) return false } export function getHash (): string { // 因为兼容性问题 这里没有直接使用 window.location.hash // 因为 Firefox decode hash 值 const href = window.location.href const index = href.indexOf('#') // 如果此时没有 # 则返回 '' // 否则 取得 # 后的所有内容 return index === -1 ? '' : href.slice(index + 1) }
可以看到在实例化过程中主要做两件事情:针对于不支持history api 的降级处理,以及保证默认进入的时候对应的 hash 值是以 / 开头的,如果不是则替换。
history api
如果细心点,可以发现这里并没有对 hashchange事件做处理。主要是因为这个问题:beforeEnter fire twice on root path ('/') after async next call。
hashchange
简要来说就是说如果在 beforeEnter 这样的钩子函数中是异步的话,beforeEnter 钩子就会被触发两次,原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/,这个过程会触发hashchange 事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter 钩子函数。
beforeEnter
还记得 init的时候,有这样的动作:
init
if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) }
如果history 是 HashHistory 的实例。则调用history的transitionTo方法。调用transitionTo的时候传入了3个参数,第一个是history.getCurrentLocation(),后面的都是setupHashListener。先来看一下getCurrentLocation:
transitionTo
history.getCurrentLocation()
setupHashListener
getCurrentLocation
getCurrentLocation () { return getHash() }
也就是返回了当前路径。接着是setupHashListener函数,其内部定义了history.setupListeners()的执行。后面我们在具体分析他所做的工作,我们现在只需要明白这几个参数的含义。 接下来我们来看一下transitionTo的实现:
history.setupListeners()
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) 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变量:
route
const route = this.router.match(location, this.current)
我们知道location代表了当前的 hash 路径。那么this.current又是什么呢?不要着急,我们找到this.current的定义:
location
this.current
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) } export const START = createRoute(null, { path: '/' }) this.current = START
this.current就是START,通过createRoute来创建返回。注意返回的是通过Object.freeze定义的只读对象 route。可以简单看一下大致返回的内容可能是这样的:
START
createRoute
Object.freeze
接着,我们会调用this.router.match方法,来获取route对象。来看一下match方法:
this.router.match
match
this.matcher = createMatcher(options.routes || [], this) match ( raw: RawLocation, current?: Route, redirectedFrom?: Location ): Route { return this.matcher.match(raw, current, redirectedFrom) }
大致能看出来 match函数执行this.macher对象的match方法调用。this.matcher对象通过createMatcher方法返回。看一下this.matcher.match方法:
this.macher
this.matcher
createMatcher
this.matcher.match
function match ( raw: RawLocation, // 目标url currentRoute?: Route, // 当前url对应的route对象 redirectedFrom?: Location // 重定向 ): Route { // 解析当前 url,得到 hash、path、query和name等信息 const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location // 如果是命名路由 if (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 = {} } // 复制 currentRoute.params 到 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] } } } // 如果存在 record 记录 if (record) { location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } } else if (location.path) { // 处理非命名路由 location.params = {} // 这里会遍历pathList,找到合适的record,因此命名路由的record查找效率更高 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) } } } // 没有匹配到的情况 return _createRoute(null, location) }
这里我们可能需要理解一下pathList、pathMap、nameMap这几个变量。他们是通过createRouteMap来创建的几个对象:
pathList
pathMap
nameMap
createRouteMap
const { pathList, pathMap, nameMap } = createRouteMap(routes)
routes 使我们定义的路由数组,可能是这样的:
const router = new VueRouter({ mode: 'history', base: __dirname, routes: [ { path: '/', name: 'home', component: Home }, { path: '/foo', name: 'foo', component: Foo }, { path: '/bar/:id', name: 'bar', component: Bar } ] })
而 createRouteMap主要作用便是处理传入的routes属性,整理成3个对象:
routes
所以 match的主要功能是通过目标路径匹配定义的route 数据,根据匹配到的记录,来进行_createRoute操作。而_createRoute会根据RouteRecord执行相关的路由操作,最后返回Route对象:
_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) } // 普通路由 return createRoute(record, location, redirectedFrom, router) }
现在我们知道了this.mather.match最终返回的就是Route对象。到这里,我们再回到之前所说的transitionTo方法:
this.mather.match
Route
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // 匹配目标url的route对象 const route = this.router.match(location, this.current) // 调用this.confirmTransition,执行路由转换 this.confirmTransition(route, () => { // ...跳转完成 this.updateRoute(route) onComplete && onComplete(route) this.ensureURL() // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { // ...处理异常 }) } }
得到正确的路由对象route后,我们开始跳转动作confirmTransition。接下来看看confirmTransition的主要操作
confirmTransition
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current // 定义中断处理 const abort = err => { // ... onAbort && onAbort(err) } // 同路由且 matched.length 相同 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), // 全局 router before hooks this.router.beforeHooks, // 得到组件 updated 钩子 extractUpdateHooks(updated), // 将要更新的路由的 beforeEnter 钩子 activated.map(m => m.beforeEnter), // 异步组件 resolveAsyncComponents(activated) ) this.pending = route // 每一个队列执行的 iterator 函数 const iterator = (hook: NavigationGuard, next) => { // ... } // 执行队列 leave 和 beforeEnter 相关钩子 runQueue(queue, iterator, () => { // ... }) }
这里有一个很关键的路由对象的 matched 实例,从上次的分析中可以知道它就是匹配到的路由记录的合集;这里从执行顺序上来看有这些 resolveQueue、extractLeaveGuards、extractUpdateHooks、resolveAsyncComponents、runQueue 关键方法。我们先来看看resolveQueue方法:
resolveQueue
extractLeaveGuards
extractUpdateHooks
resolveAsyncComponents
runQueue
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) } }
可以看出resolveQueue 就是交叉比对当前路由的路由记录和现在的这个路由的路由记录来确定出哪些组件需要更新,哪些需要激活,哪些组件被卸载。再执行其中的对应钩子函数。
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> { return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) } function extractGuards ( records: Array<RouteRecord>, name: string, bind: Function, reverse?: boolean ): Array<?Function> { const guards = flatMapComponents(records, (def, instance, match, key) => { // 获取组建的 beforeRouteLeave 钩子函数 const guard = extractGuard(def, name) if (guard) { return Array.isArray(guard) ? guard.map(guard => bind(guard, instance, match, key)) : bind(guard, instance, match, key) } }) return flatten(reverse ? guards.reverse() : guards) } function extractGuard ( def: Object | Function, key: string ): NavigationGuard | Array<NavigationGuard> { if (typeof def !== 'function') { // extend now so that global mixins are applied. def = _Vue.extend(def) } return def.options[key] } export function flatMapComponents ( matched: Array<RouteRecord>, fn: Function ): Array<?Function> { return flatten(matched.map(m => { // 遍历得到组建的 template, instance, macth,和组件名 return Object.keys(m.components).map(key => fn( m.components[key], m.instances[key], m, key )) })) } // 抹平数组得到一个一维数组 export function flatten (arr: Array<any>): Array<any> { return Array.prototype.concat.apply([], arr) }
总的来说 extractLeaveGuards的功能就是找到即将被销毁的路由组件的beforeRouteLeave钩子函数。处理成一个由深到浅的顺序组合的数组。接下来的extractUpdateHooks函数功能也是类似,主要是处理beforeRouteUpdate钩子函数。这里不再过多介绍了。
beforeRouteLeave
beforeRouteUpdate
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> { return extractGuards(updated, 'beforeRouteUpdate', bindGuard) }
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function { // 返回“异步”钩子函数 return (to, from, next) => { let hasAsync = false let pending = 0 let error = null flatMapComponents(matched, (def, _, match, key) => { // 这里假定说路由上定义的组件 是函数 但是没有 options // 就认为他是一个异步组件。 // 这里并没有使用 Vue 默认的异步机制的原因是我们希望在得到真正的异步组件之前 // 整个的路由导航是一直处于挂起状态 if (typeof def === 'function' && def.cid === undefined) { hasAsync = true // ... } }) if (!hasAsync) next() } }
这里主要是用来处理异步组建的问题,通过判断路由上定义的组件 是函数且没有 options来确定异步组件,然后在得到真正的异步组件之前将其路由挂起。
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) { const step = index => { // 如果全部执行完成则执行回调函数 cb if (index >= queue.length) { cb() } else { // 如果存在对应的函数 if (queue[index]) { // 这里的 fn 传过来的是个 iterator 函数 fn(queue[index], () => { // 执行队列中的下一个元素 step(index + 1) }) } else { // 执行队列中的下一个元素 step(index + 1) } } } // 默认执行钩子队列中的第一个数据 step(0) }
我们知道在confirmTransition中通过这样的方式来调度队列的执行:
runQueue(queue, iterator, () => { })
为runQueue函数 fn 参数传入了一个iterator函数。接下来我们看看iterator函数的执行:
iterator
this.pending = route const iterator = (hook: NavigationGuard, next) => { // 如果当前处理的路由,已经不等于 route 则终止处理 if (this.pending !== route) { return abort() } try { // hook 是queue 中的钩子函数,在这里执行 hook(route, current, (to: any) => { // 钩子函数外部执行的 next 方法 // next(false): 中断当前的导航。 // 如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮) // 那么 URL 地址会重置到 from 路由对应的地址。 if (to === false || isError(to)) { this.ensureURL(true) abort(to) } else if ( // next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。 // 当前的导航被中断,然后进行一个新的导航。 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 { // 当前钩子执行完成,移交给下一个钩子函数 // 注意这里的 next 指的是 runQueue 中传过的执行队列下一个方法函数: step(index + 1) next(to) } }) } catch (e) { abort(e) } }
我们来屡一下现在主要的流程:
next
大致流程便是这样,我们接下来看处理完整个钩子函数队列之后将要执行的回调是什么样的:
runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // 获取 beforeRouteEnter 钩子函数 const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) // 获取 beforeResolve 钩子函数 并合并生成另一个 queue const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { // 处理完,就不需要再次执行 if (this.pending !== route) { return abort() } // 清空 this.pending = null // 调用 onComplete 函数 onComplete(route) if (this.router.app) { // nextTick 执行 postEnterCbs 所有回调 this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) })
可以看到,处理完整个钩子函数队列之后将要执行的回调主要就是接入路由组件后期的钩子函数beforeRouteEnter和beforeResolve,并进行队列执行。一切处理完成后,开始执行transitionTo的回调函数onComplete:
beforeRouteEnter
beforeResolve
onComplete
this.confirmTransition(route, () => { // 更新 route this.updateRoute(route) // 执行 onComplete onComplete && onComplete(route) // 更新浏览器 url this.ensureURL() // 调用 ready 的回调 if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { // ... }) updateRoute (route: Route) { const prev = this.current // 当前路由更新 this.current = route // cb 执行 this.cb && this.cb(route) // 调用 afterEach 钩子 this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) }
可以看到,到这里,已经完成了对当前 route 的更新动作。我们之前已经分析了,在 install函数中设置了对route的数据劫持。此时会触发页面的重新渲染过程。还有一点需要注意,在完成路由的更新后,同时执行了onComplete && onComplete(route)。而这个便是在我们之前篇幅中介绍的setupHashListener:
install
onComplete && onComplete(route)
const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) setupListeners () { const router = this.router // 处理滚动 const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { setupScroll() } // 通过 supportsPushState 判断监听popstate 还是 hashchange window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { const current = this.current // 判断路由格式 if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } // 如果不支持 history 模式,则换成 hash 模式 if (!supportsPushState) { replaceHash(route.fullPath) } }) }) }
可以看到 setupListeners这里主要做了 2 件事情,一个是对路由切换滚动位置的处理,具体的可以参考这里滚动行为。另一个是对路由变动做了一次监听window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {})。
setupListeners
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {})
到这里,hash模式下的主要操作便差不多介绍完成了,接下来我们会去介绍history模式。
hash
参考: vue-router 源码分析-history
一脸懵逼的进来,学习中
满怀期待的进来,一脸懵圈的出去,,没看懂,,,哭了
因为我们用的比较多的是 vue 的 HashHistory。下面我们首先来介绍一下 HashHistory。我们知道,通过
mode
来确定使用history
的方式,如果当前mode = 'hash'
,则会执行:this.fallback
是用来判断当前mode = 'hash'
是不是通过降级处理的:接下来我们看看
HashHistory
的内部实现,首先是看一下new HashHistory()
的时候,实例化做了哪些事:constructor
可以看到在实例化过程中主要做两件事情:针对于不支持
history api
的降级处理,以及保证默认进入的时候对应的 hash 值是以 / 开头的,如果不是则替换。如果细心点,可以发现这里并没有对
hashchange
事件做处理。主要是因为这个问题:beforeEnter fire twice on root path ('/') after async next call。简要来说就是说如果在
beforeEnter
这样的钩子函数中是异步的话,beforeEnter
钩子就会被触发两次,原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/,这个过程会触发hashchange
事件,所以会再走一次生命周期钩子,也就意味着会再次调用beforeEnter
钩子函数。transitionTo
还记得
init
的时候,有这样的动作:如果
history
是HashHistory
的实例。则调用history
的transitionTo
方法。调用transitionTo
的时候传入了3个参数,第一个是history.getCurrentLocation()
,后面的都是setupHashListener
。先来看一下getCurrentLocation
:也就是返回了当前路径。接着是
setupHashListener
函数,其内部定义了history.setupListeners()
的执行。后面我们在具体分析他所做的工作,我们现在只需要明白这几个参数的含义。 接下来我们来看一下transitionTo
的实现:该函数执行的时候,先去定义了
route
变量:我们知道
location
代表了当前的 hash 路径。那么this.current
又是什么呢?不要着急,我们找到this.current
的定义:this.current
就是START
,通过createRoute
来创建返回。注意返回的是通过Object.freeze
定义的只读对象 route。可以简单看一下大致返回的内容可能是这样的:接着,我们会调用
this.router.match
方法,来获取route
对象。来看一下match
方法:大致能看出来
match
函数执行this.macher
对象的match
方法调用。this.matcher
对象通过createMatcher
方法返回。看一下this.matcher.match
方法:这里我们可能需要理解一下
pathList
、pathMap
、nameMap
这几个变量。他们是通过createRouteMap
来创建的几个对象:routes 使我们定义的路由数组,可能是这样的:
而
createRouteMap
主要作用便是处理传入的routes
属性,整理成3个对象:nameMap
pathList
pathMap
所以
match
的主要功能是通过目标路径匹配定义的route 数据,根据匹配到的记录,来进行_createRoute
操作。而_createRoute
会根据RouteRecord执行相关的路由操作,最后返回Route对象:现在我们知道了
this.mather.match
最终返回的就是Route
对象。到这里,我们再回到之前所说的transitionTo
方法:得到正确的路由对象
route
后,我们开始跳转动作confirmTransition
。接下来看看confirmTransition
的主要操作confirmTransition
这里有一个很关键的路由对象的 matched 实例,从上次的分析中可以知道它就是匹配到的路由记录的合集;这里从执行顺序上来看有这些
resolveQueue
、extractLeaveGuards
、extractUpdateHooks
、resolveAsyncComponents
、runQueue
关键方法。我们先来看看resolveQueue
方法:1. resolveQueue
可以看出
resolveQueue
就是交叉比对当前路由的路由记录和现在的这个路由的路由记录来确定出哪些组件需要更新,哪些需要激活,哪些组件被卸载。再执行其中的对应钩子函数。2. extractLeaveGuards/extractUpdateHooks
总的来说
extractLeaveGuards
的功能就是找到即将被销毁的路由组件的beforeRouteLeave
钩子函数。处理成一个由深到浅的顺序组合的数组。接下来的extractUpdateHooks
函数功能也是类似,主要是处理beforeRouteUpdate
钩子函数。这里不再过多介绍了。3. resolveAsyncComponents
这里主要是用来处理异步组建的问题,通过判断路由上定义的组件 是函数且没有 options来确定异步组件,然后在得到真正的异步组件之前将其路由挂起。
4. runQueue
我们知道在
confirmTransition
中通过这样的方式来调度队列的执行:为
runQueue
函数 fn 参数传入了一个iterator
函数。接下来我们看看iterator
函数的执行:我们来屡一下现在主要的流程:
transitionTo
函数,先得到需要跳转路由的 match 对象route
confirmTransition
函数confirmTransition
函数内部判断是否是需要跳转,如果不需要跳转,则直接中断返回confirmTransition
判断如果是需要跳转,则先得到钩子函数的任务队列 queuerunQueue
函数来批次执行任务队列中的每个方法。iterator
来构造迭代器由用户传入next
方法,确定执行的过程大致流程便是这样,我们接下来看处理完整个钩子函数队列之后将要执行的回调是什么样的:
可以看到,处理完整个钩子函数队列之后将要执行的回调主要就是接入路由组件后期的钩子函数
beforeRouteEnter
和beforeResolve
,并进行队列执行。一切处理完成后,开始执行transitionTo
的回调函数onComplete
:可以看到,到这里,已经完成了对当前 route 的更新动作。我们之前已经分析了,在
install
函数中设置了对route
的数据劫持。此时会触发页面的重新渲染过程。还有一点需要注意,在完成路由的更新后,同时执行了onComplete && onComplete(route)
。而这个便是在我们之前篇幅中介绍的setupHashListener
:可以看到
setupListeners
这里主要做了 2 件事情,一个是对路由切换滚动位置的处理,具体的可以参考这里滚动行为。另一个是对路由变动做了一次监听window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {})
。总结
到这里,
hash
模式下的主要操作便差不多介绍完成了,接下来我们会去介绍history
模式。参考: vue-router 源码分析-history