VueRouter 是 Vue.js的路由管理器,包含的功能如下:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制
- 带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为 基本用法:
// 引入Vue 和 VueRouter
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const component = {
template: '<div></div>'
}
// 初始化路由
const router = new VueRouter({
mode: 'hash', // hash history abstract(node)
routes: [
{
path: '/', // 常规路由
component
},
{
path: '/news/:id', // 动态路由
component
},
{
path: '/category', // 嵌套路由
component: {
template: '<router-view></router-view>'
},
children: [ // 子路由
{
path: 'news',
component
}
]
},
{
path: '/redirect', // 路由重定向
redirect: '/home'
},
{
path: '*', // 通配符
component
},
]
});
// 将 router 添加到 Vue的 $options 上
new Vue({
router
}).$mount('#app')
我们就上面的基本结构进行分析,VueRouter 是如何工作的;
Vue.use(VueRouter)
Vue.use是Vue用来安装第三方插件的函数, 源码在之前的文章# Vue2源码阅读——global-api中讲过,这里不再赘述; 我们就看下 VueRouter是如何安装到Vue上的;
export function install (Vue) {
// 如果安装过直接返回
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
Vue.mixin({
beforeCreate () {
// 我们在 创建 Vue实例的时候, 将 VueRouter的实例 添加到 Vue实例的$options上
// 这一行只有创建Vue的时候会触发, 因为这个只存在Vue上
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this) // 调用 VueRouter实例的 init 进行初始化
Vue.util.defineReactive(this, '_route', this._router.history.current) // 定义 _route 为响应式
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 将 VueRouter的实例 添加到 Vue的原型$router上 这就是我们为什么在 Vue组件中 可以 使用 this.$router的关键
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// 将 上面定义的 _route 添加到 Vue的原型$route上
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册全局组件 RouterView 和 RouterLink
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 合并策略 采用 strats.created的合并策略, 组成数组 而不是 直接覆盖
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
我们整理一下
- 新增混入
mixin注册了,beforeCreate和destroyed两个生命周期函数 - 在
Vue的beforeCreate生命周期函数中 调用的VueRouter实例的初始化函数 - 新增了一个响应式属性
_route - 在Vue的原型链上 定义了两个只读属性,
$router和$route,$router返回的就是VueRouter实例,$route返回的就是 前面定义的 响应式的_route, 这样我们可以在 组件中watch$route的变化 - 注册了
RouterView和RouterLink两个全局组件 - 设置了
beforeRouteEnter,beforeRouteLeave和beforeRouteUpdate(组件内导航守卫) 的合并策略
VueRouter的参数
我们在是示例中提供了mode 和 routes这两个选项,还有其他选项:
| 名称 | 类型 | 是否可选 | 说明 |
|---|---|---|---|
| routes | Array<RouteConfig> | ✅ | 路由参数,RouteConfig具体参数查看下表 |
| mode | string | ✅ | hash , history, abstract (Node.js 环境), 如何 fallback 为True并且 浏览器不支持 history.pushState,那么自动回退到 hash 模式 |
| fallback | boolean | ✅ | 当浏览器不支持 history.pushState控制路由是否应该回退到 hash 模式 |
| base | string | ✅ | 应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/" |
| linkActiveClass | string | ✅ | 全局配置 <router-link> 默认的激活的 class |
| linkExactActiveClass | string | ✅ | 全局配置 <router-link> 默认的精确激活的class |
| parseQuery | function | ✅ | 提供自定义查询字符串的解析/反解析函数。覆盖默认行为 |
| stringifyQuery | function | ✅ | 同 parseQuery 只不过返回值是string |
| scrollBehavior | function | ✅ | 使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置 |
RouteConfig
| 名称 | 类型 | 是否可选 | 说明 |
|---|---|---|---|
| path | string | ✅ | 路径 |
| name | string | ✅ | 命名路由 |
| component | Component | ✅ | 组件 |
| components | {[name: string]:Component} | ✅ | 命名视图组件 |
| redirect | string | Location | Function | ✅ | 重定向 |
| alias | string | Array<string> | ✅ | 别名 |
| children | Array<RouteConfig> | ✅ | 子路由 |
| beforeEnter | function | ✅ | 路由守卫 |
| meta | any | ✅ | 元信息 |
| props | boolean | Object | Function | ✅ | 属性 |
| caseSensitive | boolean | ✅ | 是否忽略大小写 |
| pathToRegexpOptions | {sensitive?:boolean, strict?: boolean, end?: boolean} | ✅ | 编译正则的选项 |
new VueRouter
源码文件定位: src/index.js
基本结构
### 构造函数
```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 = [] // beforeEach 的钩子函数
this.resolveHooks = [] // beforeResolve 的钩子函数
this.afterHooks = [] // afterEach 的 钩子函数
this.matcher = createMatcher(options.routes || [], this) // 创建路由匹配器
let mode = options.mode || 'hash' // 默认 ‘hash’
// 如果浏览器不支持 history.pushState是否回退到hash模式
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 如果不是浏览器, 那么设置为 abstract 模式
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}`)
}
}
}
在初始化的时候, VueRouter 做了以下事情:
- 将
routes选项 通过createMatcher创建一个 路由匹配器(后序会讲到), 该路由匹配器 返回了 一个对象, 是不是感觉眼熟, 是不是有点肯定了?对, VueRouter 上的addRoute,getRoutes,addRoutes,match就是 调用的这个匹配器返回的对象
{
match,
addRoute,
getRoutes,
addRoutes
}
- 设置
mode的值 - 根据
mode初始化对应的history实例
init
init (app: any /* Vue component instance */) {
this.apps.push(app)
// set up app destroyed handler
// 当 app 被销毁时, 将app从当前apps中移除掉,否则会引起 内存泄漏
// 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
if (!this.app) this.history.teardown()
})
// 如果已经初始化了,直接返回
// main app previously initialized
// return as we don't need to set up new history listener
if (this.app) {
return
}
this.app = app
// 对history进行配置 (后面会讲到)
const history = this.history
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
match
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
currentRoute
获取当前路由
get currentRoute (): ?Route {
return this.history && this.history.current
}
beforeEach
注册 beforeEach 钩子函数
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
beforeResolve
注册 beforeResolve 钩子函数
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
afterEach
注册 afterEach 钩子函数
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
onReady
注册一个回调, 在路由完成初始导航的时候调用
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
onError
注册一个回调, 该回调会在路由导航过程中出错时被调用
onError (errorCb: Function) {
this.history.onError(errorCb)
}
push
导航到不同的 URL, 这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL;
提示:该方法的参数可以是一个字符串路径,或者一个描述地址的对象
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
-
如果 不传入
onComplete和onAbort,那么返回的Promise,push('xxx').then(() => {}) -
否则采用 回调
push('xxx', () => {}, () => {})
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)
}
}
replace
replace 和 push 一样,但是 不会向history插入记录
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
go
这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步
go (n: number) {
this.history.go(n)
}
back
这个方法的意思是在 history 记录中后退1步
back () {
this.go(-1)
}
forward
这个方法的意思是在 history 记录中前进1步
forward () {
this.go(1)
}
getMatchedComponents
返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用,比如:
// category 路由参数
{
path: '/category',
name: 'category',
components: [
{
default: component,
name: {
template: '<div>NameComponent</div>'
},
component: {
template: '<div>ComponentComponent</div>'
},
a: {
template: '<div>AComponent</div>'
}
}
]
}
// 获取 /category下的所有的 组件
getMatchedComponents('/category')
// a: {template: '<div>AComponent</div>'}
// component: {template: '<div>ComponentComponent</div>'}
// default: {template: '<div>12321312</div>'}
// name: {template: '<div>NameComponent</div>'}
getMatchedComponents (to?: RawLocation | Route): Array<any> {
// 如果 to 不存在 则获取当前的路由
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
解析目标位置 (格式和 <router-link> 的 to prop 一样)。
current是当前默认的路由 (通常你不需要改变它)append允许你在current路由上附加路径 如同router-link
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
}
}
getRoutes
获取所有活跃的路由记录列表
getRoutes () {
return this.matcher.getRoutes()
}
addRoute
添加一条新的路由规则记录 或者 作为现有路由的子路由。如果该路由规则有 name,并且已经存在一个与之相同的名字,则会覆盖它。
addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
this.matcher.addRoute(parentOrRoute, route)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
addRoutes
动态添加更多的路由规则, 已废弃 使用 addRoute
addRoutes (routes: Array<RouteConfig>) {
if (process.env.NODE_ENV !== 'production') {
warn(false, 'router.addRoutes() is deprecated and has been removed in Vue Router 4. Use router.addRoute() instead.')
}
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
可以看到上面的其实 VueRouter 只是起到了 代理的作用,核心的 还是 依靠 history 和 matcher;
- 通过
history切换路由 - 通过
matcher管理路由和路由组件
Matcher
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 将 routes 传入到 createRouteMap 生成 pathList, pathMap, nameMap
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function addRoute (parentOrRoute, route) {
const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
// $flow-disable-line
createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
// add aliases of parent
if (parent && parent.alias.length) {
createRouteMap(
// $flow-disable-line route is defined if parent is
parent.alias.map(alias => ({ path: alias, children: [route] })),
pathList,
pathMap,
nameMap,
parent
)
}
}
function getRoutes () {
return pathList.map(path => pathMap[path])
}
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
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 = {}
}
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]
}
}
}
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,
addRoute,
getRoutes,
addRoutes
}
}
const { pathList, pathMap, nameMap } = createRouteMap(routes)
- pathList: 是包含所有路由的路径数组
['', '/news/:id', '/category/news', '/category', '/redirect', '*']
- pathMap: 是处理过后的路由的对象
{
'': RouteRecord,
'/news/:id': RouteRecord,
'/category/news': RouteRecord,
'/category': RouteRecord,
'/redirect': RouteRecord,
'*': RouteRecord
}
// RouteRecord的格式:
{
path,
regex, // 经过 path-to-regexp处理后的路由正则
components, // 路由组件, 如果 route.components存在, 则使用 route.components 否则 将 {default: route.component}
alias, // 别名
instances ,
enteredCbs,
name,
parent,
matchAs,
redirect,
beforeEnter,
meta,
props
}
- nameMap: 所有包含命名路由的对象, 与 pathMap 一样, 只不过 key 值变成了 route.name
addRoute 和 addRoutes
动态添加路由,其实都是调用了。createRouteMap方法
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function addRoute (parentOrRoute, route) {
const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined
// $flow-disable-line
createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent)
// add aliases of parent
if (parent && parent.alias.length) {
createRouteMap(
// $flow-disable-line route is defined if parent is
parent.alias.map(alias => ({ path: alias, children: [route] })),
pathList,
pathMap,
nameMap,
parent
)
}
}
getRoutes
获取所有路由,pathList 就是上面提到的 路由路径的 数组, 结合 pathMap
function getRoutes () {
return pathList.map(path => pathMap[path])
}
match
路由匹配
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 标准化路由
const location = normalizeLocation(raw, currentRoute, false, router)
// 路由是否包含name,也就是否是是 命名路由
const { name } = location
if (name) {
const record = nameMap[name] // 获取 路由记录
if (!record) return _createRoute(null, location) // 如果不存在 返回空路由
// 获取路由的params 比如/:id 或者 /:id?
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
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]
}
}
}
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)
}
History
因为有三个 不同的模块,我后面会 单独写一篇文章进行解析