赞
踩
在使用 vue 开发单页面(SPA)应用时,vue-router 是必不可少的技术;它的本质是监听 URL 的变化然后匹配路由规则显示相应的页面,并且不刷新页面;简单的说就是,更新视图但不重新请求页面。
下面通过源码来看一下 vue-router 的整个实现流程:
github地址:https://github.com/vuejs/vue-router
当前版本:
vue 2.6.11
vue-router 3.5.1
components:这里面是两个组件 router-view 和 router-link
history:这个是路由模式(mode),有三种方式
util:这里是路由的功能函数和类
create-matcher 和 create-router-map 是路由解析和生成配置表
index:VueRouter类,也是整个插件的入口
install:提供插件安装方法
vue 在使用路由时需要调用 Vue.use(plugin) 方法进行注册,代码在 vue 源码里面 src/core/global-api/use.js文件里主要作用两个:
1、缓存判断是否已经注册过,避免重复注册
2、使用插件的 install 方法或者直接运行插件来注册
3、这里使用了 flow 的语法,在编译时对 js 变量进行类型检查,缩短调式时间减少类型错误引起的 bug
// 初始化use
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)
// 调用插件的 install 方法或者直接运行插件,以实现插件的 install
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
注册路由的时候需要调用路由的 install 方法,代码在 vue-router 源码的 src/install.js 文件里,是 vue-router 的安装程序,该方法做了下面四件事:
1、缓存判断是否已经安装过,避免重复安装
2、使用 Vue.mixin 混入 beforeCreate 和 destroyed 钩子函数,这样在 Vue 生命周期阶段就会被调用
3、通过 Vue.prototype 定义 router 和 route 属性,方便所有组件使用
4、全局注册 router-view 和 router-link 组件;router-link 用于触发路由的变化,router-view 用于触发对应路由视图的变化
import View from './components/view'
import Link from './components/link'
export let _Vue
// Vue.use安装插件时候需要暴露的install方法
export function install (Vue) {
// 判断是否已安装过,安装过直接 return 出来,没安装执行安装程序
if (install.installed && _Vue === Vue) return
install.installed = true
// 把Vue赋值给全局变量
_Vue = Vue
// 判断是否已定义
const isDef = v => v !== undefined
//通过registerRouteInstance方法注册router实例
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 混淆进Vue实例,在boforeCreate与destroyed钩子上混淆
Vue.mixin({
beforeCreate () {
// 在option上面存在router则代表是根组件
if (isDef(this.$options.router)) {
// 根路由设置为自己
this._routerRoot = this
// 保存router
this._router = this.$options.router
// VueRouter对象的init方法
this._router.init(this)
// Vue内部方法,为对象defineProperty上在变化时通知的属性,实现响应式
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根组件则直接从父组件中获取,用于 router-view 层级判断
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 通过registerRouteInstance方法注册router实例
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
//在Vue.prototype挂载属性,可以通过 this.$router、this.$route 来访问 Vue.prototype 上的 _router、_route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册router-view以及router-link组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 该对象保存了两个option合并的规则
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
安装路由插件之后,会对 VueRouter 进行实例化然后将其传入 Vue 实例的 options 中,在 vue-router 源码的 src/index.js 文件里;下面是 VueRouter 的构造函数。
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 路由匹配对象
this.matcher = createMatcher(options.routes || [], this)
// 根据 mode 采取不同的路由方式
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}`)
}
}
}
构造函数有两个重要的东西:
1、创建路由匹配对象 matcher(核心)
这里用到的 createMatcher 函数在 src/create-matcher.js 文件里,其作用是创建路由映射表,然后使用闭包的方法让 addRoutes 和 match 函数能够使用路由映射表的几个对象,最后返回一个 Matcher 对象。
//Matcher 的数据结构
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
//createMatcher 具体实现
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
//创建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {...}
//路由匹配
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {...}
function redirect (
record: RouteRecord,
location: Location
): Route {...}
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {...}
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {...}
return {
match,
addRoutes
}
}
createMatcher()有两个参数routes表示创建VueRouter新对象传入的routes配置信息,router表示VueRouter实例。
createRouteMap() 的作用就是对当前开发者传入的 options.routes 进行路由映射化处理,并得到了三个路由容器 pathList、pathMap、nameMap,方法在 src/create-route-map.js 文件里
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
// 创建映射表
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)
})
// 确保通配符在最后
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
}
}
createRouteMap 函数返回三个属性 —— pathList、pathMap、nameMap。然后通过 addRouteRecord 函数去向这三个属性中增添数据。
下面是 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') {
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.`
)
}
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
//格式化url 替换 /
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
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) {
// 递归路由配置的 children 属性,添加路由记录
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只有第一个会起作用,后面的都会忽略
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = 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}" }`
)
}
}
}
2、根据 mode 采取不同的路由方式
1、vue-router 一共有三种路由模式(mode):hash、history、abstract,其中 abstract 是在非浏览器环境下使用的路由模式,如 weex
2、默认是 hash 模式,如果传入的是 history 模式,但是当前环境不支持则会降级为 hash 模式
3、如果当前环境是非浏览器环境,则强制使用 abstract 模式
4、模式匹配成功则会进行对应的初始化操作
当根组件调用 beforeCreate 钩子函数的时候会执行路由初始化代码,代码在 src/index 文件下面,是路由实例提供的一个方法。
/* 初始化 */
init (app: any /* Vue component instance */) {
/* 未安装就调用init会抛出异常 */
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
/* 将当前vm实例保存在app中 */
this.apps.push(app)
// main app already initialized.
/* 已存在说明已经被init过了,直接返回 */
if (this.app) {
return
}
/* this.app保存当前vm实例 */
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
})
})
}
路由初始化会进行路由跳转,改变 URL 后渲染对应的组件。路由跳转的核心的 history 的 transitionTo 方法。
在 src/history/base 文件下 History 实例提供的一个方法。
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 根据跳转的 location 得到新的route
const route = this.router.match(location, this.current)
//确认切换路由
this.confirmTransition(route, () => {
//更新 route
this.updateRoute(route)
//添加 hashChange 监听
onComplete && onComplete(route)
//更新 URL
this.ensureURL()
//只执行一次 ready 回掉
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) })
}
})
}
在 transitionTo 方法中先调用match方法得到新的路由对象,然后调用 confirmTransition 方法是处理导航守卫的逻辑。
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
//中断跳转路由函数
const abort = err => {
if (isError(err)) {
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
) {
this.ensureURL()
return abort()
}
//对比路由,解析出可复用的组件、失活的组件、需要渲染的组件
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
//导航守卫数组
const queue: Array<?NavigationGuard> = [].concat(
//失活的组件钩子
extractLeaveGuards(deactivated),
//全局的 beforeEach 钩子
this.router.beforeHooks,
//在当前路由改变,但是该组件被复用时调用
extractUpdateHooks(updated),
//需要渲染组件 enter 守卫钩子
activated.map(m => m.beforeEnter),
//解析异步路由组件
resolveAsyncComponents(activated)
)
//保存路由
this.pending = route
//迭代器,用于执行 queue 里面的导航守卫钩子
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: '/' }) -> 重定向
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
//上一次队列执行完成之后再执行组件内的钩子
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() })
})
}
})
})
}
通过 next 进行导航守卫的回调迭代,所以如果在代码中使用了路由钩子函数,那么就必须在最后调用 next(),否则回调不执行,导航将无法继续
在路由切换的时候,vue-router 会调用 push、go 等方法实现视图与地址 url 的同步。
1、主动触发
点击事件跳转页面,触发 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。
// src/history/hash.js
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
push 方法先检测当前浏览器是否支持 html5的 History API,如果支持则调用此 API进行 href的修改,否则直接对window.location.hash进行赋值
2、改变地址栏 url,然后视图同步
在路由初始化的时候会添加事件 setupHashListener 来监听 hashchange 或 popstate;当路由变化时,会触发对应的 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。
// src/history/hash.js
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
在 src/components/ 文件夹下
1、router-view:组件挂载
该组件是无状态 (没有 data ) 和无实例 (没有 this 上下文)的(功能组件)函数式组件。其通过路由匹配获取到对应的组件实例,通过 h函数动态生成组件,如果当前路由没有匹配到任何组件,则渲染一个注释节点。
export default {
name: 'RouterView',
/*
https://cn.vuejs.org/v2/api/#functional
使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
*/
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// 标记位,标记是route-view组件
data.routerView = true
// 直接使用父组件的createElement函数
const h = parent.$createElement
// props的name,默认'default'
const name = props.name
// option中的VueRouter对象
const route = parent.$route
// 在parent上建立一个缓存对象
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// 记录组件深度
let depth = 0
// 标记是否是待用(非alive状态)
let inactive = false
// _routerRoot中中存放了根组件的实例,这边循环向上级访问,直到访问到根组件,得到depth深度
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
// 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态)
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
// 存放route-view组件的深度
data.routerViewDepth = depth
// 如果inactive为true说明在keep-alive组件中,直接从缓存中取
if (inactive) {
return h(cache[name], data, children)
}
const matched = route.matched[depth]
// 如果没有匹配到的路由,则渲染一个空节点
if (!matched) {
cache[name] = null
return h()
}
// 从成功匹配到的路由中取出组件
const component = cache[name] = matched.components[name]
// 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用
data.registerRouteInstance = (vm, val) => {
// 第二个值不存在的时候为注销
// 获取组件实例
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
//这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val赋给instances
matched.instances[name] = val
}
}
// also register instance in prepatch hook
// in case the same component instance is reused across different routes
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// resolve props
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
return h(component, data, children)
}
}
主要作用就是拿到匹配的组件进行渲染。
2、router-link:路由跳转
router-link在执行 render函数的时候,会根据当前的路由状态,给渲染出来的active元素添加 class,所以你可以借助此给active路由元素设置样式等
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
// Support global empty active class
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
// 当触发这些路由切换事件时,会调用相应的方法来切换路由刷新视图:
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
const data: any = {
class: classes
}
// 渲染出 <a> 标签,然后添加 href 属性和点击事件
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
// find the first <a> child and apply listener and href
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
// doesn't have <a> child, apply listener to self
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
页面渲染
1、Vue.use(Router) 注册
2、注册时调用 install 方法混入生命周期,定义 router 和 route 属性,注册 router-view 和 router-link 组件
3、生成 router 实例,根据配置数组(传入的routes)生成路由配置记录表,根据不同模式生成监控路由变化的History对象
4、生成 vue 实例,将 router 实例挂载到 vue 实例上面,挂载的时候 router 会执行最开始混入的生命周期函数
5、初始化结束,显示默认页面
路由点击更新
1、 router-link 绑定 click 方法,触发 history.push 或 history.replace ,从而触发 history.transitionTo 方法
2、ransitionTo 用于处理路由转换,其中包含了 updateRoute 用于更新 _route
3、在 beforeCreate 中有劫持 _route 的方法,当 _route 变化后,触发 router-view 的变化
地址变化路由更新
1、HashHistory 和 HTML5History 会分别监控 hashchange 和 popstate 来对路由变化作对用的处理
2、HashHistory 和 HTML5History 捕获到变化后会对应执行 push 或 replace 方法,从而调用 transitionTo
3、然后更新 _route 触发 router-view 的变化
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。