简介
路由的概念相信大部分同学并不陌生,我们在用 Vue
开发过实际项目的时候都会用到 Vue-Router
这个官方插件来帮我们解决路由的问题。它的作用就是根据不同的路径映射到不同的视图。本文不再讲述路由的基础使用和API
,不清楚的同学可以自行查阅官方文档vue-router3 对应 vue2 和 vue-router4 对应 vue3。今天我们从源码出发以vue-router 3.5.3
源码为例,一起来分析下Vue-Router
的具体实现。
由于篇幅原因,
vue-router
源码分析分上、中、下三篇文章讲解。
在上文中我者讲述路路由的一些前置知识,以及路由安装模块的分析。今天我们来讲讲路由的实例化到底都干了些什么事情。
实例化
实例化就是我们new VueRouter({routes})
的过程,我们来重点分析下VueRouter
的构造函数。
分析VueRouter构造函数
VueRouter
的构造函数在src/index.js
中。
// index.js
constructor (options: RouterOptions = {}) {
if (process.env.NODE_ENV !== 'production') {
warn(this instanceof VueRouter, `Router must be called with the new operator.`)
}
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.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}`)
}
}
}
构造函数参数
构造函数接收RouterOptions
参数,也就是我们new VueRouter({})
传递的参数。
具体参数意思如下:
export interface RouterOptions {
routes?: RouteConfig[] // 路由配置规则列表
mode?: RouterMode // 模式
fallback?: boolean // 是否启用回退到hash模式
base?: string // 路由base url
linkActiveClass?: string // router-link激活时类名
linkExactActiveClass?: string // router-link精准激活时类名
parseQuery?: (query: string) => Object // 自定义解析qs的方法
stringifyQuery?: (query: Object) => string // 自定义序列化qs的方法
scrollBehavior?: ( // 控制滚动行为
to: Route,
from: Route,
savedPosition: Position | void ) => PositionResult | Promise<PositionResult> | undefined | null
}
参数初始化
我们看到在最开始有些参数的初始化,这些参数到底是什么呢?
this.app
用来保存根 Vue
实例。
this.apps
用来保存持有 $options.router
属性的 Vue
实例。
this.options
保存传入的路由配置。
this.beforeHooks
、 this.resolveHooks
、this.afterHooks
表示一些钩子函数,我们之后会介绍。
this.fallback
表示在浏览器不支持 history.pushState
的情况下,根据传入的 fallback
配置参数,决定是否回退到hash
模式。
this.mode
表示路由创建的模式。
创建matcher
通过createMatcher(options.routes || [], this)
生成matcher
,这个matcher
对象就是前面聊的匹配器,负责url
匹配,它接收了routes
和router实例
。
这个非常重要,我们来重点分析。
分析create-matcher
// src/create-matcher.js
...
// Matcher数据结构,包含四个方法
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
addRoute: (parentNameOrRoute: string | RouteConfig, route?: RouteConfig) => void;
getRoutes: () => Array<RouteRecord>;
};
// createMatcher返回Matcher对象
export function createMatcher ( routes: Array<RouteConfig>, // 路由配置列表
router: VueRouter // VueRouter实例 ): Matcher {
// 创建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 批量添加路由
function addRoutes (routes) {
// 所以这里会重新调用createRouteMap方法
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 添加单个路由
function addRoute (parentOrRoute, route) {
...
}
// 获取路由关系数组
function getRoutes () {
return pathList.map(path => pathMap[path])
}
// 传入Location和Route,返回匹配的Route对象
function match ( raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location ): Route {
...
}
return {
match,
addRoute,
getRoutes,
addRoutes
}
...
}
matcher
对象不光定义了match、addRoute、addRoutes、getRoutes
四个方法供我们调用,而且在最开始通过createRouteMap()
方法创建了路由映射表RouteMap
。
这里的match
方法非常重要,后面我们会分析。
下面我们来看看createRouteMap()
方法。
分析createRouteMap
// src/create-route-map.js
// 创建路由映射map、添加路由记录
export function createRouteMap ( routes: Array<RouteConfig>, // 路由配置列表
oldPathList?: Array<string>, // 旧pathList
oldPathMap?: Dictionary<RouteRecord>, // 旧pathMap
oldNameMap?: Dictionary<RouteRecord>// 旧nameMap ): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 若旧的路由相关映射列表及map存在,则使用旧的初始化(借此实现添加路由功能)
// 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)
})
// 确保path:*永远在在最后
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
// 开发环境,提示非嵌套路由的path必须以/或者*开头
if (process.env.NODE_ENV === 'development') {
// warn if routes do not include leading slashes
const found = pathList
// check for missing leading slash
.filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')
if (found.length > 0) {
const pathNames = found.map(path => `- ${path}`).join('\n')
warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
}
}
return {
pathList,
pathMap,
nameMap
}
// 添加路由记录,更新pathList、pathMap、nameMap
function addRouteRecord ( pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string ) {
...
}
}
可以看到createRouteMap
返回一个对象,它包含pathList
、pathMap
和nameMap
-
pathList
数组中存储了routes
中的所有path
-
pathMap
对象维护的是path
和路由记录RouteRecord
的映射 -
nameMap
对象维护的是name
和路由记录RouteRecord
的映射
那么 RouteRecord
到底是什么,先来看一下它的数据结构:
export interface RouteRecord {
path: string // 规范化后的路径
regex: RegExp / 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
components: Dictionary<Component> // 保存路由组件,支持命名视图
instances: Dictionary<Vue> // 保存router-view实例
name?: string // 路由名
parent?: RouteRecord // 父路由
redirect?: RedirectOption // 重定向的路由配置对象
matchAs?: string // 别名路由需要使用
meta: RouteMeta // 路由元信息
beforeEnter?: ( // 路由钩子
route: Route,
redirect: (location: RawLocation) => void,
next: () => void ) => any
props: // 动态路由传参
| boolean
| Object
| RoutePropsFunction
| Dictionary<boolean | Object | RoutePropsFunction>
}
笔者的路由文件是
const routes = [
{
path: "/routertest",
name: "RouterTest",
component: () =>
import(/* webpackChunkName: "router" */ "@/views/Router.vue"),
},
]
我们分别输出pathList
、pathMap
和nameMap
来看一下
可以看到pathList
是一个path
数组,pathMap
和nameMap
是两个对象,对象的key
分别是path
和name,value
就是RouteRecord
对象。
接下来我们重点看看addRouteRecord()
方法。
分析addRouteRecord
// src/create-route-map.js
...
// 添加路由记录,更新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') {
// route.path不能为空
assert(path != null, `"path" is required in a route configuration.`)
// route.component不能为string
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(
path || name
)} cannot be a ` + `string id. Use an actual component instead.`
)
}
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
// 生成格式化后的path(子路由会拼接上父路由的path)
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 匹配规则是否大小写敏感?(默认值:false)
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 生成一条路由记录
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
components: route.components || { default: route.component }, // 保存路由组件,支持命名视图https://router.vuejs.org/zh/guide/essentials/named-views.html#命名视图
instances: {}, // 保存router-view实例
name, // name
parent, // 父实例
matchAs, // 别名路由需要使用
redirect: route.redirect, // 重定向的路由配置对象
beforeEnter: route.beforeEnter, // 路由独享的守卫
meta: route.meta || {}, // 元信息
props: // 动态路由传参;
route.props == null
? {}
: route.components // 命名视图的传参规则需要使用route.props指定的规则
? route.props
: { default: route.props }
}
// 处理有子路由情况
if (route.children) {
// 命名路由 && 未使用重定向 && 子路由配置对象path为''或/时,使用父路由的name跳转时,子路由将不会被渲染
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 // matchAs若有值,代表当前路由是别名路由,则需要单独生成别名路由的子路由,路径前缀需使用matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 若pathMap中不存在当前路径,则更新pathList和pathMap
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 处理别名;
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias] // alias支持string,和Array<String>
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
if (process.env.NODE_ENV !== 'production' && alias === path) { // alias的值和path重复,需要给提示
warn(
false,
`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
)
// skip in dev to make it work
continue
}
// 生成别名路由配置对象
const aliasRoute = {
path: alias,
children: route.children
}
// 添加别名路由记录
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute, // 别名路由
parent, // 当前路由的父路由,因为是给当前路由取了个别名,所以二者其实是有同个父路由的
record.path || '/' // matchAs,用来生成别名路由的子路由;
)
// ! 总结:当前路由设置了alias后,会单独为当前路由及其所有子路由生成路由记录,且子路由的path前缀为matchAs(即别名路由的path)
}
}
// 处理命名路由
if (name) {
// 更新nameMap
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
// 路由重名警告,相当于name有重复了
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
我们看到addRouteRecord()
参数有一个route
参数,它的类型是RouteConfig
,我们来看看它的数据结构。
interface RouteConfig = {
path: string,
component?: Component,
name?: string, // 命名路由
components?: { [name: string]: Component }, // 命名视图组件
redirect?: string | Location | Function,
props?: boolean | Object | Function,
alias?: string | Array<string>,
children?: Array<RouteConfig>, // 嵌套路由
beforeEnter?: (to: Route, from: Route, next: Function) => void,
meta?: any,
// 2.6.0+
caseSensitive?: boolean, // 匹配规则是否大小写敏感?(默认值:false)
pathToRegexpOptions?: Object // 编译正则的选项
}
这个RouteConfig
其实就是我们写代码的时候定义的routes
数组里面的对象。
addRouteRecord()
方法总结
-
首先会检查
route
对象path
和component
不能为空。 -
然后生成格式化后的
path
(子路由会拼接上父路由的path
) -
处理匹配规则是否大小写敏感,(默认值:
false
) -
生成一条路由记录。
-
如果有子路由,则会继续递归调用
addRouteRecord
,生成子路由记录。 -
若
pathMap
中不存在当前路径,则更新pathList
和pathMap
。 -
处理别名路由。当前路由设置了
alias
后,会单独为当前路由及其所有子路由生成路由记录,且子路由的path
前缀为matchAs
(即别名路由的path
)。 -
若
nameMap
中不存在当前名字,则会添加到nameMap
,如果route
对象没有name
则不会添加。所以nameMap
的长度不一定等于pathList
和pathMap
长度。
我们来重点看看别名和父子组件,笔者的路由文件是
const routes = [
{
path: "/father",
name: "Father",
component: () =>
import(/* webpackChunkName: "father" */ "../views/Father.vue"),
children: [
{
path: "fchild1",
name: "FChild1",
component: () =>
import(/* webpackChunkName: "father" */ "@/components/Fchild1.vue"),
children: [
{
path: "fchild1_1",
name: "FChild1_1",
component: () =>
import(
/* webpackChunkName: "father" */ "@/components/Fchild1_1.vue"
),
},
],
},
{
path: "fchild2",
name: "FChild2",
component: () =>
import(/* webpackChunkName: "father" */ "@/components/Fchild2.vue"),
},
],
},
{
path: "/routertest",
name: "RouterTest",
alias: "/router",
component: () =>
import(/* webpackChunkName: "router" */ "@/views/Router.vue"),
},
]
我们分别输出pathList
、pathMap
和nameMap
来看一下
可以看到每个子路由都生成了记录,并且取别名的生成了/routertest、/router
两条记录。
确定路由模式
路由模式平时都会只说两种,其实在vue-router
总共实现了 hash
、history
、abstract
3 种模式。
VueRouter
会根据options.mode
、options.fallback
、supportsPushState
、inBrowser
来确定最终的路由模式。
如果没有设置mode
就默认是hash
模式。
确定fallback
值,只有在用户设置了mode:history
并且当前环境不支持pushState
且用户没有主动声明不需要回退(没设置fallback
值位undefined
),此时this.fallback
才为true
,当fallback
为true
时会使用hash
模式。(简单理解就是如果不支持history
模式并且只要没设置fallback
为false
,就会启用hash
模式)
如果最后发现处于非浏览器环境,则会强制使用abstract
模式。
let mode = options.mode || 'hash'
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
实例化路由模式
根据mode
属性值来实例化不同的对象。
我们来看下源码
// index.js
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}`)
}
}
VueRouter
的三种路由模式,主要由下面的三个核心类实现
-
History
- 基础类
- 位于
src/history/base.js
-
HTML5History
- 用于支持
pushState
的浏览器 src/history/html5.js
- 用于支持
-
HashHistory
- 用于不支持
pushState
的浏览器 src/history/hash.js
- 用于不支持
-
AbstractHistory
- 用于非浏览器环境(服务端渲染)
src/history/abstract.js
HTML5History
、HashHistory
、AbstractHistory
三者都是继承于基础类History
。
三者不光能访问History类
的所有属性和方法,他们还都实现了基础类中声明的需要子类实现的5个接口(go、push、replace、ensureURL、getCurrentLocation
)
首先我们来看看History类
History类
// src/history/base.js
...
export class History {
router: Router
base: string
current: Route
pending: ?Route
cb: (r: Route) => void
ready: boolean
readyCbs: Array<Function>
readyErrorCbs: Array<Function>
errorCbs: Array<Function>
listeners: Array<Function>
cleanupListeners: Function
// 子类需要实现的方法
+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
// 格式化base,保证base是以/开头
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
}
}
可以看到,History
类构造函数中主要干了下面几件事
-
保存了
router
实例 -
规范化了
base
,确保base
是以/
开头 -
初始化了当前路由指向,默认指向
START
初始路由;在路由跳转时,this.current
代表的是from
。START
定义在src/utils/route.js
中,会返回一个默认Route
对象。 -
初始化了路由跳转时的下个路由
pending
,默认为null
;在路由跳转时,this.pending
代表的是to
。 -
初始化了一些回调相关的属性
关于Route
对象,我们来看看它的结构
export interface Route {
path: string
name?: string | null
hash: string
query: Dictionary<string | (string | null)[]>
params: Dictionary<string>
fullPath: string
matched: RouteRecord[]
redirectedFrom?: string
meta?: RouteMeta
}
再来看看HTML5History类
HTML5History类
// src/history/html5.js
...
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ?string) {
// 调用父类构造函数初始化
super(router, base)
this._startLocation = getLocation(this.base)
}
// 设置监听,主要是监听popstate方法来自动触发transitionTo
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
// 若支持scroll,初始化scroll相关逻辑
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
// 某些浏览器,会在打开页面时触发一次popstate
// 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新
// 所以需要避免
const location = getLocation(this.base)
if (this.current === START && location === this._startLocation) {
return
}
// 路由地址发生变化,则跳转,如需滚动则在跳转后处理滚动
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
}
// 监听popstate事件
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
// 可以看到 history模式go方法其实是调用的window.history.go(n)
go (n: number) {
window.history.go(n)
}
// push方法会主动调用transitionTo进行跳转
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// replace方法会主动调用transitionTo进行跳转
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
ensureURL (push?: boolean) {
if (getLocation(this.base) !== this.current.fullPath) {
const current = cleanPath(this.base + this.current.fullPath)
push ? pushState(current) : replaceState(current)
}
}
getCurrentLocation (): string {
return getLocation(this.base)
}
}
export function getLocation (base: string): string {
let path = window.location.pathname
const pathLowerCase = path.toLowerCase()
const baseLowerCase = base.toLowerCase()
// base="/a" shouldn't turn path="/app" into "/a/pp"
// https://github.com/vuejs/vue-router/issues/3555
// so we ensure the trailing slash in the base
if (base && ((pathLowerCase === baseLowerCase) ||
(pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
可以看到HTML5History
类主要干了如下几件事。
-
继承于
History类
,并调用父类构造函数初始化。 -
实现了
setupListeners
方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑,监听了popstate事件
,并在popstate
触发时自动调用transitionTo
方法。 -
实现了
go、push、replace
等方法,我们可以看到,history
模式其实就是使用的新的history api
。
// 可以看到 history模式go方法其实是调用的window.history.go(n)
go (n: number) {
window.history.go(n)
}
// push、replace调用的是util/push-state.js,里面实现了push和replace方法
// 实现原理也是使用的history api,并且在不支持history api的情况下使用location api
export function pushState (url?: string, replace?: boolean) {
...
const history = window.history
try {
if (replace) {
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
// 调用的 history.replaceState
history.replaceState(stateCopy, '', url)
} else {
// 调用的 history.pushState
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
总结
所以history
模式的原理就是在js
中路由的跳转(也就是使用push
和replace
方法)都是通过history
新的api
,history.pushState
和 history.replaceState
两个方法完成,通过这两个方法我们知道了路由的变化,然后根据路由映射关系来实现页面内容的更新。
对于直接点击浏览器的前进后退按钮或者js
调用 this.$router.go()
、this.$router.forward()
、this.$router.back()
、或者原生js
方法history.back()
、history.go()
、history.forward()
的,都会触发popstate
事件,通过监听这个事件我们就可以知道路由发生了哪些变化然后来实现更新页面内容。
注意history.pushState
和 history.replaceState
这两个方法并不会触发popstate
事件。
接下来我们再来看看HashHistory类
HashHistory类
//src/history/hash.js
...
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
// 事件优先使用 popstate
// 判断supportsPushState就是通过return window.history && typeof window.history.pushState === 'function'
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
// 其实也是优先使用history的pushState方法来实现,不支持再使用location修改hash值
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
)
}
// 其实也是优先使用history的replaceState方法来实现,不支持再使用location修改replace方法
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
// 也是使用的history go方法
go (n: number) {
window.history.go(n)
}
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {
return getHash()
}
}
function checkFallback (base) {
const location = getLocation(base)
if (!/^\/#/.test(location)) {
window.location.replace(cleanPath(base + '/#' + location))
return true
}
}
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
// 获取 # 后面的内容
export function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
let href = window.location.href
const index = href.indexOf('#')
// empty path
if (index < 0) return ''
href = href.slice(index + 1)
return href
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
可以看到HashHistory
类主要干了如下几件事。
-
继承于
History类
,并调用父类构造函数初始化。这里比HTML5History
多了回退操作,所以,需要将history
模式的url
替换成hash
模式,即添加上#
,这个逻辑是由checkFallback
实现的 -
实现了
setupListeners
方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑。 监听了popstate事件或hashchange事件
,并在相应事件触发时,调用transitionTo
方法实现跳转。
通过
const eventType = supportsPushState ? 'popstate' : 'hashchange'
我们可以发现就算是hash
模式优先使用的还是popstate
事件。
- 实现了
go、push、replace
等方法。
我们可以看到,hash
模式实现的push、replace
方法其实也是优先使用history
里面的方法,也就是新的history api
。
// 可以看到 hash 模式go方法其实是调用的window.history.go(n)
go (n: number) {
window.history.go(n)
}
// 在支持新的history api情况下优先使用history.pushState实现
// 否则使用location api
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
// 在支持新的history api情况下优先使用history.replaceState实现
// 否则使用location api
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
总结
在浏览器链接里面我们改变hash
值是不会重新向后台发送请求的,也就不会刷新页面。并且每次 hash
值的变化,还会触发hashchange
这个事件。
所以hash
模式的原理就是通过监听hashchange
事件,通过这个事件我们就可以知道 hash
值发生了哪些变化然后根据路由映射关系来实现页面内容的更新。(这里hash
值的变化不管是通过js
修改的还是直接点击浏览器的前进后退按钮都会触发hashchange
事件)
上面说的其实是在不支持history
新api
情况下的实现原理。如果是支持history
新api
情况下,hash
模式的实现其实是和history
模式一样的。
我们再来看看AbstractHistory类
AbstractHistory类
// src/history/abstract.js
...
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
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(
location,
route => {
this.stack = this.stack.slice(0, this.index).concat(route)
onComplete && onComplete(route)
},
onAbort
)
}
go (n: number) {
const targetIndex = this.index + n
if (targetIndex < 0 || targetIndex >= this.stack.length) {
return
}
const route = this.stack[targetIndex]
this.confirmTransition(
route,
() => {
const prev = this.current
this.index = targetIndex
this.updateRoute(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
},
err => {
if (isNavigationFailure(err, NavigationFailureType.duplicated)) {
this.index = targetIndex
}
}
)
}
getCurrentLocation () {
const current = this.stack[this.stack.length - 1]
return current ? current.fullPath : '/'
}
ensureURL () {
// noop
}
}
可以看到AbstractHistory
类主要干了如下几件事。
-
继承于
History类
,并调用父类构造函数初始化。并对index
、stack
做了初始化。前面说过,非浏览器环境,是没有历史记录栈的,所以使用index
、stack
来模拟历史记录栈。 -
实现了
go、push、replace
等方法。
总结
可以看到,abstract
模式并没有使用浏览器相关api
,所以它不依赖于浏览器环境。路由历史记录的存储都是使用数据结构来实现的。一般用服务端渲染。
如果发现没有浏览器的 API,路由会自动强制进入这个模式。
总结
那么到此为止,我们分析了 Vue-Router
的实例化过程,首先初始化了一些参数。然后创建了matcher
,并根据routes
初始化了路由映射表RouteMap
。最后确定路由模式来实例化路由。
后面我们再来分析初始化部分。