vue-router 简介
Vue-Router 的能力十分强大,它支持 hash、history、abstract 3 种路由方式,提供了 <router-link> 和 <router-view> 2 种组件,还提供了简单的路由配置和一系列好用的 API。
简单使用流程
import Vue from vue
import VueRouter from vue-router
import App from ./App
Vue.use(VueRouter)
// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
const Foo = { template: <div>foo</div> }
const Bar = { template: <div>bar</div> }
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
const routes = [
{ path: /foo, component: Foo },
{ path: /bar, component: Bar },
]
// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes,
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
el: #app,
render(h) {
return h(App)
},
router,
})
路由的注册
Vue-Router 本质上就是一个路由的插件,vue 中用 Vue.use 来注册插件
Vue.use(plugin)
参数:{ Object | Function } plugin 可以是对象也可以是函数.
安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。调用 install 方法时,会将 Vue 作为参数传入。install 方法被同一个插件多次调用时,插件也只会被安装一次。
插件的类型,可以是 install 方法,也可以是一个包含 install 方法的对象。插件只能被安装一次,保证插件列表中不能有重复的插件。
// vue/src/core/global-api/use.js (vue.use源码)
export function initUse(Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 接受一个 plugin 参数,可以是对象或者是函数
const installedPlugins =
this._installedPlugins || (this._installedPlugins = [])
// _installedPlugins 数组,它存储所有注册过的 plugin
if (installedPlugins.indexOf(plugin) > -1) {
// 判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
// toArray方法我们在就是将类数组转成真正的数组。使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
if (typeof plugin.install === function) {
plugin.install.apply(plugin, args)
} else if (typeof plugin === function) {
plugin.apply(null, args)
}
// 由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。
installedPlugins.push(plugin)
// 最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册
return this
}
}
当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去 import Vue 了。
路由安装 intall 方法
在路由源码的 src/install.js 文件中
import View from ./components/view
import Link from ./components/link
export let _Vue
export function install(Vue) {
// Vue.use()的时候会把vue传入第一个参数,这里的vue参数就是vue中传入的
// 重复判断 如果installed是true说明已经注册过了 直接返回
if (install.installed && _Vue === Vue) return
install.installed = true
// 内部需要一个变量来存Vue ,因为作为 Vue 的插件对 Vue 对象是有依赖的
_Vue = Vue
const isDef = (v) => v !== undefined
// 为router-view组件关联路由组件
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode // 而这个方法只在router-view组件中存在,router-view组件定义在(../components/view.js)
// 所以,如果vm的父节点为router-view,则为router-view关联当前vm,即将当前vm做为router-view的路由组件
// 调用vm.$options._parentVnode.data.registerRouteInstance方法
if (
isDef(i) &&
isDef((i = i.data)) &&
isDef((i = i.registerRouteInstance))
) {
i(vm, callVal)
}
}
// mixin的作用是将mixin的内容混合到Vue的初始参数options中
Vue.mixin({
// this === new Vue({router:router}) === Vue根实例
// 为什么是beforeCreate而不是created呢?因为如果是在created操作的话,$options已经初始化好了。
beforeCreate() {
// 如果判断当前组件是根组件的话,就将我们传入的router和_root挂在到根组件实例上。判断是否使用了vue-router插件
if (isDef(this.$options.router)) {
this._routerRoot = this
// 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次
this._router = this.$options.router
this._router.init(this)
// 响应式定义_route属性,保证_route发生变化时,组件(router-view)会重新渲染
Vue.util.defineReactive(this, _route, this._router.history.current)
} else {
// 如果判断当前组件是子组件的话,就将我们_root根组件挂载到子组件。注意是引用的复制,因此每个组件都拥有了同一个_root根组件挂载在它身上。
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 为router-view组件关联路由组件
registerInstance(this, this)
},
destroyed() {
// 取消router - view和路由组件的关联
registerInstance(this)
},
})
Object.defineProperty(Vue.prototype, $router, {
// 将$router挂载到组件实例上 this.$router
get() {
return this._routerRoot._router
},
})
// $router是VueRouter的实例对象,$route是当前路由对象,也就是说$route是$router的一个属性
Object.defineProperty(Vue.prototype, $route, {
// 每个组件访问到的this.$route,其实最后访问的都是Vue根实例的_route
get() {
return this._routerRoot._route
},
})
// 注册router-view、router-link全局组件
Vue.component(RouterView, View)
Vue.component(RouterLink, Link)
const strats = Vue.config.optionMergeStrategies
//设置路由组件守卫的合并策略
strats.beforeRouteEnter =
strats.beforeRouteLeave =
strats.beforeRouteUpdate =
strats.created
}
当用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行实例 VueRouter 上的 install 函数,所以注册方法完成了以下事情:
避免重复安装:通过添加 installed 为 true 标识来判断是否重复安装存储 Vue 后续依赖使用:install 方法被调用时, Vue.use()的时候会把 vue 传入第一个参数,Vue 会被赋值给事先定义好的_Vue 变量注册了一个全局混入:注册了两个生命周期钩子 beforeCreate 和 destroyed,因为是 vue 的 mixin 方法,所以注册时不会调用,只有 vue 执行钩子时调用添加实例属性、方法:在 Vue 原型上注入$router、$route 属性,方便在 vue 实例中通过 this.$router、this.$route 快捷访问注册 router-view、router-link 全局组件设置路由组件守卫的合并策略
VueRouter 的构造函数
src/index.js 看下 VueRouter 构造函数
先看构造函数的 constructor 构造函数 内部的实现
export default class VueRouter {
static install: () => void
static version: string
static isNavigationFailure: Function
static NavigationFailureType: any
static START_LOCATION: Route
app: any
apps: Array<any>
ready: boolean
readyCbs: Array<Function>
options: RouterOptions
mode: string
history: HashHistory | HTML5History | AbstractHistory
matcher: Matcher
fallback: boolean
beforeHooks: Array<?NavigationGuard>
resolveHooks: Array<?NavigationGuard>
afterHooks: Array<?AfterNavigationHook>
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 = [] // VueRouter支持多实例
this.options = options //接收的参数
this.beforeHooks = [] // 接收 beforeEach hook
this.resolveHooks = [] // 接收 beforeResolve hook
this.afterHooks = [] // 接收 afterEach hook
this.matcher = createMatcher(options.routes || [], this)
// 路由匹配器,创建路由matcher对象,传入routes路由配置列表及VueRouter实例
let mode = options.mode || hash // 默认hash模式
this.fallback =
mode === history && !supportsPushState && options.fallback !== false
// 表示在浏览器不支持 history.pushState 的情况下,根据传入的 fallback 配置参数,决定是否回退到hash模式
if (this.fallback) {
// 如果可以回退就是hash模式
mode = hash
}
// 非浏览器环境,强制使用abstract模式
if (!inBrowser) {
mode = abstract
}
this.mode = mode
switch (mode) {
// 根据不同mode,实例化不同history实例
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 构造函数主要干了下面几件事:
接收 RouterOptions 类型的参数 options,也就是我们通常配置的路由信息表,主要是 mode,routes 等;对一些属性赋予了初值,保存 vue 实例,接收全局导航守卫(beforeEach、beforeResolve、afterEach)的数组做了初始化创建了路由匹配器,路由matcher对象,传入routes路由配置列表及VueRouter实例确定路由模式,先设置默认值,如果options.mode没有配置的话默认就是hash模式;接下来fallback判断 如果是mode:history模式 但是不支持pushState,回退到hash模式;非浏览器模式下强制abstract模式根据路由模式生成不同的History实例
匹配器 matcher
在构造函数内
this.matcher = createMatcher(options.routes || [], this)
createMatcher的实现在src/create-matcher.js中
// matcher 的数据结构
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; //匹配方法
addRoutes: (routes: Array<RouteConfig>) => void; // 添加Route方法
getRoutes: () => Array<RouteRecord>;
};
// Matcher工厂函数
export function createMatcher (
routes: Array<RouteConfig>, // 路由配置列表
router: VueRouter // VueRouter实例
): Matcher {
// 创建一个路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 添加路由
function addRoutes (routes) {
// 由于传入pathList, pathMap, nameMap了,所以createRouteMap方法会执行添加逻辑
createRouteMap(routes, pathList, pathMap, nameMap)
}
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)
}
// ......
return {
match,
getRoutes,
addRoutes
}
}
1. createMatcher 接收 2 个参数,一个是 router,它是我们 new VueRouter 返回的实例,一个是 routes,它是用户定义的路由配置.
2. 返回一个Matcher对象;Matcher对象包含一个用于匹配的match方法和一个动态添加路由的addRoutes方法(match,addRoutes)
3. 而这两个方法都声明在createMatcher内部,由于闭包特性,它能访问到createMatcher作用域的所有变量
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
parentRoute?: RouteRecord
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 若旧的路由相关映射列表及map存在,则使用旧的初始化(借此实现添加路由功能)
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历路由配置对象,生成/添加路由记录
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// 确保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
}
}
createRouteMap 函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,
pathList 存储所有的 path,
pathMap 表示一个 path 到 RouteRecord 的映射关系,
nameMap 表示 name 到 RouteRecord 的映射关系
createRouteMap的逻辑:
1. 先判断路由相关映射表是否已经存在,若存在则使用,否则新建(这就实现了createRouteMap创建/新增的双重功能)
2. 然后遍历routes,依次为每个route调用addRouteRecord生成一个RouteRecord并更新pathList、pathMap和nameMap
3. 由于pathList在后续逻辑会用来遍历匹配,为了性能,所以需要将path:*放置到pathList的最后
4. 最后检查非嵌套路由的path是否是以/或者*开头
addRouteRecord
这个方法主要是创建路由记录并更新路由映射表,在同个文件内
// 添加路由记录,更新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.`
)
warn(
// eslint-disable-next-line no-control-regex
!/[^\u0000-\u007F]+/.test(path),
`Route with path "${path}" contains unencoded characters, make sure ` +
`your path is correctly encoded before passing it to the router. Use ` +
`encodeURI to encode static segments of your path.`
)
}
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,
// 利用path-to-regexp包生成用来匹配path的增强正则对象,可以用来匹配动态路由
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// 保存路由组件,支持命名视图
components: route.components || { default: route.component },
alias: route.alias //别名
? typeof route.alias === string
? [route.alias]
: route.alias
: [],
instances: {},
enteredCbs: {},
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) {
//命名路由 && 未使用重定向 && 子路由配置对象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
? 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]
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
if (process.env.NODE_ENV !== production && 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
)
}
}
// 处理命名路由
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}" }`
)
}
}
}
1. 检查了路由规则中的path和component
2. 生成path-to-regexp的选项pathToRegexpOptions
3. 格式化path,如果是嵌套路由,则会追加上父路由的path
4. 生成路由记录
5. 处理嵌套路由,递归生成子路由记录
6. 更新pathList、pathMap
7. 处理别名路由,生成别名路由记录
8. 处理命名路由,更新nameMap
路由模式
vueRouter采用两种路由模式,一种hash+hashChange(hash),另一种利用History API的pushState+popState(history)
hash:利用hash改变时页面不会刷新并会触发hashChange这个特性来实现前端路由
history:利用了HTML5 History API 的pushState方法和popState事件来实现前端路由
回到VueRouter 的构造函数中可以看到:
switch (mode) {
case history:
// 用于支持pushState的浏览器src/history/html5.js
this.history = new HTML5History(this, options.base)
break
case hash:
// 用于不支持pushState的浏览器src/history/hash.js
this.history = new HashHistory(this, options.base, this.fallback)
break
case abstract:
// 用于非浏览器环境(服务端渲染)src/history/abstract.js
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== production) {
assert(false, `invalid mode: ${mode}`)
}
}
HTML5History、HashHistory、AbstractHistory三者都是继承于基础类History(src/history/base.js );
三者不光能访问History类的所有属性和方法,他们还都实现了基础类中声明的需要子类实现的5个接口(go、push、replace、ensureURL、getCurrentLocation)
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
// implemented by sub-classes
// 需要子类(HTML5History、HashHistory)实现的方法
+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) // 格式化base,保证base是以/开头
// start with a route object that stands for "nowhere"
this.current = START // 当前指向的route对象,默认为START;即from
this.pending = null // 记录将要跳转的route;即to
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
}
// ......
}
export const START = createRoute(null, {
path: /
})
- 保存了router实例
- 规范化了base,确保base是以/开头
- 初始化了当前路由指向,默认只想START初始路由;在路由跳转时,this.current代表的是from
- 初始化了路由跳转时的下个路由,默认为null;在路由跳转时,this.pending代表的是to
- 初始化了一些回调相关的属性
history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法,匹配到新线路后又做了哪些事情:
1. transitionTo 首先根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径 let route = this.router.match(location, this.current)
2. 这样就创建了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current
3. 拿到新的路径后,那么接下来就会执行 confirmTransition 方法去做真正的切换
HTML5History类
继承自History类,来自src/history/html5.js文件
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ?string) {
// 初始化父类History
super(router, base)
// 获取初始location
this._startLocation = getLocation(this.base)
}
setupListeners () {
// 监听任务队列,如果没有就返回
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
// 检测是否需要支持scroll
const supportsScroll = supportsPushState && expectScroll
// 若支持scroll,初始化scroll相关逻辑
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
// 获取当前路径
const current = this.current
//获取location
const location = getLocation(this.base)
//某些浏览器,会在打开页面时触发一次popstate
// 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新
// 所以需要避免,如果路径是初始值并且初始location值时候需要返回
if (this.current === START && location === this._startLocation) {
return
}
// 路由地址发生变化,则跳转,并在跳转后处理滚动,这里的transitionTo就是继承父History的方法
this.transitionTo(location, route => {
if (supportsScroll) {
// 如果有滚动需要调用滚动方法
handleScroll(router, route, current, true)
}
})
}
window.addEventListener(popstate, handleRoutingEvent)
// 点击浏览器的前进、后退按钮等,会触发 popstate 事件,然后加入事件队列中
this.listeners.push(() => {
window.removeEventListener(popstate, handleRoutingEvent)
})
}
// 其他history方法
go (n: number) {
window.history.go(n)
}
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)
}
// ......
}
1. 他是继承于History类,所以在构造函数中调用了父类构造函数(super(router,base))
2. 检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑
3. 监听了popstate事件,并在popstate触发时,调用transitionTo方法实现跳转
4. 某些浏览器下,打开页面会触发一次popstate,此时如果路由组件是异步的,就会出现popstate事件触发了,但异步组件还没解析完成,最后导致route没有更新
HashHistory类
继承自History类,位于src/history/hash.js
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// fallback只有在指明了mode为history,但是浏览器又不支持popstate,用户手动指明了fallback为true时,才为true,其它情况为false
// 如果需要回退,则将url换为hash模式(/#开头)
// this.base来自父类
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// this is delayed until the app mounts
// to avoid the hashchange listener being fired too early
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) {
// 不支持则是hash,直接替换路径
replaceHash(route.fullPath)
}
})
}
// 判断浏览器是否支持supportsPushState,来选择是popstate事件或者是hashchange
const eventType = supportsPushState ? popstate : hashchange
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
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
)
}
// ......
}
// 检查回退,将url转换为hash模式(添加/#)
function checkFallback (base) {
const location = getLocation(base)
if (!/^\/#/.test(location)) {
window.location.replace(cleanPath(base + /# + location))
return true
}
}
// 确保url是以/开头
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === /) {
return true
}
replaceHash(/ + path)
return false
}
// 替换hash记录
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
检查了fallback,看是否需要回退,传入的fallback只有在用户设置了history且又不支持pushState并且启用了回退时才为true
所以,此时,需要将history模式的url替换成hash模式,即添加上#,这个逻辑是由checkFallback实现的
如果不是fallback,则直接调用ensureSlash,确保url是以/开头的
初始化过程
在安装注册阶段install内,全局混入beforeCreate勾子触发后调用router实例的init方法并传入Vue根实例,完成初始化流程,该初始化只会调用一次
Vue.mixin({
beforeCreate () {
// 判断是否使用了vue-router插件
if (isDef(this.$options.router)) {
...
this._router = this.$options.router // 保存VueRouter实例,this.$options.router仅存在于Vue根实例上,其它Vue组件不包含此属性,所以下面的初始化,只会执行一次
// beforeCreate hook被触发时,调用
this._router.init(this) // 初始化VueRouter实例,并传入Vue根实例
} else {
...
}
...
}
})
init方法
在构造函数VueRouter中
// 初始化,app为Vue根实例,在全局注册时候传入vue实例this
init (app: any /* Vue component instance */) {
// 开发环境,确保已经安装VueRouter
process.env.NODE_ENV !== production &&
assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app) // 保存实例
// 绑定destroyed hook,避免内存泄露
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已经存在,则不需要重复初始化history 的事件监听
if (this.app) {
return
}
this.app = app
const history = this.history
// PS:这里是3.5.5版本路由,老版本,这里是HTML5History和HashHistory分别处理;
// 若是HTML5History类,则直接调用父类的transitionTo方法,跳转到当前location
// 若是HashHistory,在调用父类的transitionTo方法后,并传入onComplete、onAbort回调
if (history instanceof HTML5History || history instanceof HashHistory) {
// 3.5.5版本路由
// 如果是HTML5History或者HashHistory都会统一执行父类history上的的transitionTo方法,并且会传入三个参数
const handleInitialScroll = routeOrError => {
const from = history.current
// 获取滚动参数属性
const expectScroll = this.options.scrollBehavior
// 判断浏览器是否支持history模式并且是否带了滚动属性的(history才支持滚动)
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && fullPath in routeOrError) {
// 调用滚动的方法
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
// 统一执行history父类上的改变路由方法,然后去执行各自对应的方法,hash和history不同
history.transitionTo(
history.getCurrentLocation(),//hash是获取带#的路径,history是获取/的路径
setupListeners,
setupListeners
)
}
// 调用父类的listen方法,添加回调;
// 回调会在父类的updateRoute方法被调用时触发,重新为app._route赋值
// 由于app._route被定义为响应式,所以app._route发生变化,依赖app._route的组件(route-view组件)都会被重新渲染
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
// 父类history中
// 路由跳转
transitionTo (
location: RawLocation, // 原始location,一个url或者是一个Location interface(自定义形状,在types/router.d.ts中定义)
onComplete?: Function, // 跳转成功回调
onAbort?: Function// 跳转失败回调
) {
...
}
init过程:
- 检查了VueRouter是否已经安装
- 保存了挂载router实例的vue实例(VueRouter支持多实例嵌套,所以存在this.apps来保存持有router实例的vue实例)
- 注册了一个一次性钩子destroyed,在destroyed时,卸载this.app,避免内存泄露
- 检查了this.app,避免重复事件监听
- 根据history类型,调用transitionTo跳转到不同的初始页面
transitionTo 方法:
1. 首个参数是需要解析的地址,第二是跳转成功回调,第三个是跳转失败回调
2. 可以看到传入的成功、失败回调都是setupListeners函数,setupListeners函数内部调用了history.setupListeners方法,hsah和history都有这个方法
组件(link和view)
当我们点击 router-link 的时候,实际上最终会执行 router.push,如下:
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)
}
}
然后再执行相应history(hash或者history上的push方法)
router-view
路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件,它的定义在src/components/view.js 中;
export default {
name: RouterView,
functional: true, //函数化组件
props: {
name: {
type: String,
default: default
}
},
render (_, { props, children, parent, data }) {
// parent:是使用vue-router所在组件(父),可以取到父组件的上下文,解析具名插曹
// 标记routerView组件
data.routerView = true
//函数式组件没有data,没有组件实例。因此使用了父组件中的$createElement函数,用以渲染组件。
const h = parent.$createElement
// 组件上的name属性
const name = props.name
// 获取当前的路径
const route = parent.$route
// 做了一层缓存
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
// 代表深度,用于路由嵌套问题中
let depth = 0
let inactive = false
// 从当前组件一直遍历到最外层的根组件
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
// 获取vnode的data
//router-view属性标记当前vnode组件是否为路由组件
if (vnodeData.routerView) {
depth++
}
//在keepAlive中处于非激活状态
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
// 由内而外循环
parent = parent.$parent
}
data.routerViewDepth = depth
// 当组件被keepAlive缓存时
if (inactive) {
// 读取缓存数据
const cachedData = cache[name]
// 读取缓存组件
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
// 渲染缓存组件
return h(cachedComponent, data, children)
} else {
// render previous empty view
return h()
}
}
// matched是个数组,获取matched深度就是层级关系
const matched = route.matched[depth]
// 对没有name属性的router-view而言默认就是name为defult
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
// 找不到对应组件和record路由信息的就清空缓存
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component换成组件
cache[name] = { component }
// attach instance registration hook
// this will be called in the instances injected lifecycle hooks
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
// 在keepaliva激活时候调用init钩子
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
handleRouteEntered(route)
}
const configProps = matched.props && matched.props[name]
// save route and configProps in cache
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
// 渲染组件
return h(component, data, children)
}
}
》的全部内容,本文网址:https://www.7ca.cn/baike/60026.shtml,如对您有帮助可以分享给好友,谢谢。