vue2视图切换:vue-router
vue
路由是单页面中视图切换的方案。
文件main.js:
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App";
Vue.use(VueRouter);
// 1. 定义组件。
const Foo = { template: "<div>foo</div>" };
const Bar = { template: "<div>bar</div>" };
// 2. 定义路由
const routes = [
{ path: "/foo", component: Foo },
{ path: "/bar", component: Bar }
];
// 3. 实例化`VueRouter`
const router = new VueRouter({
routes
});
// 4. 将`router`作为参数实例化`Vue`。
const app = new Vue({
el: "#app",
render(h) {
return h(App);
},
router
});
文件App.vue:
<template>
<div>
<div>
<!-- 使用 router-link 组件来导航. -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</div>
<!-- 路由出口 -->
<router-view></router-view>
</div>
</template>
我们让其跑在本地http://localhost:8080
上。
一、安装VueRouter
VueRouter
是Class
类:
export default class VueRouter {
// ...
}
VueRouter.install = install
install
定义在同级目录下的install.js文件中:
import View from './components/view'
import Link from './components/link'
export let _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({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
Vue.use
定义在initGlobalAPI(Vue)
中的initUse(Vue)
:
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)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
/**
* Convert an Array-like object to a real Array.
*/
export function toArray(list: any, start?: number): Array<any> {
start = start || 0;
let i = list.length - start;
const ret: Array<any> = new Array(i);
while (i--) {
ret[i] = list[i + start];
}
return ret;
}
当执行Vue.use(VueRouter)
时,获取到this.installedPlugins
,如果其中存在当前的plugin
则直接返回,否则获取除去前1
位(VueRouer
)后的参数数组,然后将this
(Vue
)补充至首位,就是将第一个参数由VueRouer
换成了Vue
。VueRouer
中的install
是function
类型,所以,再以VueRouer
为执行主体,Vue
为参数,执行plugin.install.apply(plugin, args)
,相当于执行install (Vue)
。
install(Vue)
中首次安装则会执行install.installed = true
和_Vue = Vue
,这里的_Vue
就可以在全局使用,如果进行第二次重复安装,则会执行if (install.installed && _Vue === Vue) return
进行终止操作。接下来会在Vue
中通过Vue.mixin
在全局options
中混入方法beforeCreate
和destroyed
,在Vue
原型上挂载$router
和$route
,通过Vue.component
注册组件RouterView
和RouterLink
,最后将合并策略中与路由先关的beforeRouteEnter
、beforeRouteLeave
和beforeRouteUpdate
都赋值为合并策略中的created
函数。
二、实例化VueRouter
export default class VueRouter {
// ...
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}`)
}
}
}
// 定义的match、init、beforeEach、beforeResolve、afterEach和onReady等各种方法...
}
可以看出VueRouter
实例化时将会生成app
、apps
、options
、beforeHooks
、resolveHooks
、afterHooks
、matcher
和mode
等属性构成的实例。先分别看匹配器matcher
、模式mode
和history
的获取。
1、matcher
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
// 这里定义了match、addRoute、getRoutes、addRoutes等方法
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
这里定义了match
、addRoute
、getRoutes
和addRoutes
并返回,其中都用到了对于路由的匹配、添加或者获取都是基于pathList
, pathMap
和nameMap
,首先来看createRouteMap(routes)
:
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 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, parentRoute)
})
// ...
return {
pathList,
pathMap,
nameMap
}
}
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
// ...
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
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 },
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) {
// ...
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
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}" }`
)
}
}
}
先看主流程,通过normalizePath(path, parent, pathToRegexpOptions.strict)
的方式处理当前路径,然后创建可以描述当前路由path
、components
、name
和parent
等属性的record
对象。如果有route.children
那么,通过递归的方式执行addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
,其中的参数record
将作为parent
的参数传递给下一个节点,作为record
中的parent
属性,这样就建立起了父子关系。
再看pathMap
中如果没有record.path
则进行推入,并且通过pathMap[record.path] = record
将path
和record
作为映射关系,而nameMap
是name
和record
的映射。
2、mode
实例化VueRouter
的过程中还将定义了mode
,有三种类型:hash
、history
和abstract
hash
,是默认mode
history
,如果supportsPushState
判断为false
即不支持pushState
,则降级为hash
export const supportsPushState =
inBrowser &&
(function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && typeof window.history.pushState === 'function'
})()
abstract
,在非浏览器场景下为abstract
mode
也各自对应history
不同的获取方式,最常用的为hash
模式,先看HashHistory
实例化。
3、history
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`、`push`和`replace`等方法
}
这里不管哪种mode
都继承于History
类:
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、push、replace和ensureURL等方法
constructor (router: Router, base: ?string) {
this.router = router
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 = []
}
// 还有`listen`、`onReady`、`onError`和`transitionTo`等方法
}
这里继承的过程类似于执行了History.call(this, router, base)
,最终创建了包含router
、base
和current
等属性的实例history
。
三、过程分析
1、初始化
通过脚手架启动项目,然后,在浏览器地址栏输入http://localhost:8080
时,地址栏链接会变成http://localhost:8080/#/
,app.vue页面中<router-view></router-view>
部分渲染成了<!---->
注释节点。
(1)url
修改
在实例化HashHistory
的过程中,会执行ensureSlash()
:
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 replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
export function replaceState (url?: string) {
pushState(url, true)
}
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
执行完const path = getHash()
后path
为""
,接着执行replaceHash('/')
,即执行replaceState(getUrl(path))
,其中的getUrl(path)
为http://localhost:8080/#/
。最终执行pushState('http://localhost:8080/#/', true)
,相当于执行浏览器中window.history.replaceState(stateCopy, '', 'http://localhost:8080/#/')
,实现了http://localhost:8080/
直接替换为http://localhost:8080/#/
而不在浏览器中留下任何记录。
(2)页面展示
页面中app.vue的render
为:
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
[
_c(
"div",
[
_c("router-link", { attrs: { to: "/foo" } }, [_vm._v("Go to Foo")]),
_vm._v(" "),
_c("router-link", { attrs: { to: "/bar" } }, [_vm._v("Go to Bar")])
],
1
),
_vm._v(" "),
_c("router-view")
],
1
)
}
当执行到_c("router-link", { attrs: { to: "/bar" } }, [_vm._v("Go to Bar")])
的时候,就执行到了组件router-view
中的render
函数:
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
const h = parent.$createElement
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 : {}
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
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()
}
}
const matched = route.matched[depth]
const component = matched && matched.components[name]
// render empty node if no matched route or no config component
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component
cache[name] = { component }
// attach instance registration hook
// this will be called in the instance's 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
}
}
// 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
}
// register instance in init hook
// in case kept-alive component be actived when routes changed
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
// if the route transition has already been confirmed then we weren't
// able to call the cbs during confirmation as the component was not
// registered yet, so we call it here.
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)
}
}
function fillPropsinData (component, data, route, configProps) {
// resolve props
let propsToPass = data.props = resolveProps(route, configProps)
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]
}
}
}
}
因为其中matched
为空,所以返回了空的注释节点。
当执行到_c("router-link", { attrs: { to: "/foo" } }, [_vm._v("Go to Foo")]),
的时候就执行到了组件router-link
中的render
函数中的以下代码:
// ...
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
}
// ...
if (Array.isArray(this.event)) {
this.event.forEach(e => {
on[e] = handler
})
} else {
on[this.event] = handler
}
// ...
也就是当执行到点击事件的时候,会执行到handler
函数,即router.push(location, noop)
。
在当前例子中初始化时最终渲染结果为:
2、切换
当点击Go to Foo
或者Go to Bar
的过程中,执行router.push(location, noop)
:
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)
}
其中执行完pushState(cleanPath(this.base + route.fullPath))
逻辑后,页面url
地址栏的链接会变成http://localhost:8080/#/foo
。
然后,在组件router-view
变成vNode
的时候,会执行到return h(component, data, children)
,当前例子中的路由/foo
的路由对应的组件component
是{ template: '<div>foo</div>' }
,最终的渲染结果就是:
总结
路由点击的过程会分两步:
url
的替换和真实组件的替换。url
替换会通过window.history.pushState
方法进行页面url
的替换,当前替换记录会存放在浏览器记录中。 然后,通过router-view
组件获取到点击路由对应的真实组件,在patch
阶段进行渲染。
转载自:https://juejin.cn/post/7141965586099077157