源码下载:github.com/vuejs/vue-r…
1. 简述
vue-router基于Vue组件化的概念,使用VueRouter对vue进行组件化的路由管理。也就是说,路由变化时,使用组件替换的方式更新页面展示。替换的地方,就是router-view组件所在地方,router-view组件是一个functional组件,根据路径和match算法,找到要渲染的组件进行渲染。这种渲染只有路径变化才会发生。
引起路由(route)变化主要有以下几种情况: (1) 浏览器上前进、后退按钮 (2) 页面刷新 (3) 使用router调用push、pop、replace、go、back、forward等方法,主动改变路由 (4) 浏览器url输入栏更改location (5) router-link组件点击触发
VueRouter实例化后,才具有路由功能,router和route是两个重要的概念,都可称作路由,容易混淆,这里将router称作路由管理器,route称作路由。router是VueRouter的实例,具有路由管理功能,也就是说管理route。route是随着location(url)变化而变化的,一般来说route与组件的关系是1-n,具体由路由配置表项routes配置决定。路由(router)的配置过程是:
(1) Vue.use(VueRouter)
(2) 创建路由实例router,用于整个vue项目的路由管理
router = new VueRouter({
mode:...
routes:[
{
path:...
component:...
children:...
},
...
]
})
(3) main.js中将router注入到根实例
new Vue({
...
router,
...
})
复制代码
2. 目录结构
目录结构如下,
3. index.js——VueRouter类
index.js定义了VueRouter类。
3.1 构造函数
constructor (options: RouterOptions = {}) {
this.app = null //vue实例
this.apps = [] //存放正在被使用的组件(vue实例),只有destroyed掉的
组件,才会从这里移除)
this.options = options //vueRouter实例化时,传来的参数,也就是
router.js中配置项的内容
this.beforeHooks = [] //存放各组件的全局beforeEach钩子
this.resolveHooks = [] //存放各组件的全局beforeResolve钩子
this.afterHooks = [] //存放各组件的全局afterEach钩子
this.matcher = createMatcher(options.routes || [], this) //由
createMatcher生成的matcher,里面有一些匹配相关方法
let mode = options.mode || 'hash' //模式
//模式的回退或者兼容方式,若设置的mode是history,而js运行平台不支持
supportsPushState 方法,自动回退到hash模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
//若不在浏览器环境下,强制使用abstract模式
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
//不同模式,使用对应模式Histroty管理器去管理history
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}`)
}
}
}
复制代码
3.2 vueRouter内部方法
3.2.1 match
输入参数raw,current,redirectedFrom,结果返回匹配route
match (
raw: RawLocation, // /user/4739284722这种形式,类似route的path
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
复制代码
3.2.2 currentRoute
是一个get方法,用于获取当前history.current,也就是当前route,包括path、component、meta等。
get currentRoute (): ?Route {
return this.history && this.history.current
}
复制代码
3.2.3 init
init (app: any /* Vue component instance */) {
// assert是个断言,测试install.install是否为真,为真,则说明vueRouter已经安装了
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
//将vue实例推到apps列表中,install里面最初是将vue根实例推进去的
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
// app被destroyed时候,会$emit ‘hook:destroyed’事件,监听这个事件,执行下面方法
// 从apps 里将app移除
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) {
return
}
// 新增一个history,并添加route监听器
//并根据不同路由模式进行跳转。hashHistory需要监听hashchange和popshate两个事件,而html5History监听popstate事件。
this.app = app
const history = this.history
if (history instanceof HTML5History) {
//HTML5History在constructor中包含了监听方法,因此这里不需要像
//HashHistory那样setupListner。
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
//将apps中的组件的_route全部更新至最新的
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
复制代码
3.2.4 全局路由钩子
在路由切换的时候被调用,可以自定义fn。可以在main.js中使用VueRoute实例router进行调用。比如:
router.beforeEach((to,from,next)=>{})
复制代码
//将回调方法fn注册到beforeHooks里。registerHook会返回,fn执行后的callback方法,功能是将fn从beforeHooks删除。
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
//将回调方法fn注册到resolveHooks里。registerHook会返回,fn执行后的callback方法,功能是将fn从resolveHooks删除。
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
//将回调方法fn注册到afterHooks里。registerHook会返回,fn执行后的callback方法,功能是将fn从afterHooks删除。
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
复制代码
3.2.5 registerHook
将callback(参数fn)插入list,返回一个方法,方法实现的是从list中删除fn。也就是在callback执行后,通过调用这个方法,可以将fn从list中移除。
function registerHook (list: Array, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
复制代码
3.2.6 主动改变路由的功能
通常我们在组件内$router.push调用的就是这里的push方法。 其中onReady方法,添加一个回调函数,它会在首次路由跳转完成时被调用。此方法通常用于等待异步的导航钩子完成,比如在进行服务端渲染的时候,示例代码如下: return new Promise((resolve, reject) => { const {app, router} = createApp() router.push(context.url) // 主动去推,服务端 router.onReady(() => {// 页面刷新,检测路由是否匹配 const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject(new Error('no component matched')) } resolve(app) }) })
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
//报错
onError (errorCb: Function) {
this.history.onError(errorCb)
}
//新增路由跳转
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
//路由替换
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
//前进n条路由
go (n: number) {
this.history.go(n)
}
//回退一步
back () {
this.go(-1)
}
//前进一步
forward () {
this.go(1)
}
复制代码
3.2.7 getMatchedComponents & resolve
getMatchedComponents 根据路径或者路由获取匹配的组件,返回目标位置或是当前路由匹配的组件数组(是组件的定义或构造类,不是实例)。通常在服务端渲染的数据预加载时使用到。里面的组件满足:前一个组件嵌套着下一个组件的。参考下图:
getMatchedComponents (to?: RawLocation | Route): Array {
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]
})
}))
}
//一般用于解析目标位置
//返回包含如下属性的对象
{
location:Location;
route:Route;
href:string;
}
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
}
}
复制代码
3.2.8 createHref&addRoutes
//建立路由在浏览器中显示格式
function createHref (base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
//动态新增路由
addRoutes (routes: Array) {
this.matcher.addRoutes(routes)
复制代码
4.install.js
VueRouter的一个方法,Vue.use(插件)来注册插件的时候会找到插件的install方法进行执行。install主要有以下几个目的: (1)子组件通过从父组件获取router实例,从而可以在组件内进行路由切换、在路由钩子内设置callback、获取当前route等操作,所有router提供的接口均可以调用。 (2)设置代理,组件内部使用this.route等同于获得当前_route (3)设置_route设置数据劫持,也就是在数值变化时,可以notify到watcher (4)注册路由实例,router-view中注册route。 (5)注册全局组件RouterView、RouterLink
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
// 从父节点拿到registerRouteInstance,注册路由实例
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
//mixin,这个每个组件在实例话的时候,都会在对应的钩子里运行这部分代码
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
//调用初始化init方法
this._router.init(this)
// 设置_route为Reactive
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
//子组件从父组件获取routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
//设置代理 this.$router === this._routerRoot._router,组件内部通过
//this.$router调用_router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
//组件内部通过this.$route调用_route
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
//注册RouterView、RouterLink组件
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
}
复制代码
5. base.js
这个文件定义了History类,VueRouter中的history,根据mode,可能是HTML5History、HashHistory或Abstract实例,其中HTML5History、HashHistory等都是继承自History。History提供了一些路由操作的基本方法,如下:
- Listen //listen callback
- onReady //监听路由是否ready,ready时,将所有cb装进readyCbs列表
- onError
- transitionTo //路由的跳转,会判断跳转to的路径是否在路由表中,是,才进行组件替换,调用confirmTransition。
5.1 constructor
constructor (router: Router, base: ?string) {
this.router = router //当前router
this.base = normalizeBase(base) //获取路由base
// start with a route object that stands for "nowhere"
this.current = START //由createRoute生成的基础路由,path:'/'
this.pending = null
this.ready = false //history状态,路由是否已经更新好
this.readyCbs = [] //ready状态下的callbacks
this.readyErrorCbs = [] //
this.errorCbs = [] //
}
复制代码
6. HashHistroy
contructor中由fallback的判断与执行方法。
- go //调用window.history.go(n)方法
- push
- replace
- ensureUrl //导航栏url替换,当组件被替换后,会调用此方法改变location。至此route切换完成。ready 状态为 true。然后readyCbs被一次执行。根据参数push为true,执行pushHash,false执行replaceHash。
- getCurrentLocation://获取当前location,返回的是hash值
- setupListeners:hashHitory需要同时监听hashchange和popstate事件,前者来自#hash中的hash变化触发,后者来自浏览器的back,go按钮的触发。根据hash进行切换。
其中看下setupListeners这个方法:
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
//ensureSlash路径是否正确,path的第一为是否是'/'
if (!ensureSlash()) {
return
}
//transitionTo调用的父类History下的跳转方法,跳转后路径会进行hash化。
this.transitionTo(getHash(), route => {
//根据router配置的scrollBehavior,进行滚动到last pos。
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}
//判断supportsPushState(浏览器对'pushState'是否支持),true时调用
//push-state.js中的pushState方法。反之直接将当前url中hash替换。
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
//判断supportsPushState(浏览器对'pushState'是否支持),true时调用
//push-state.js中的replaceState方法。反之直接将当前url替换。
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
复制代码
7. Html5History
- go
- push
- replace
- ensureUrl // 与HashHistory的对应方法类型,根据参数push为true,执行 pushState,false执行replaceState。HashHistory相比多了一步 supportsPushState的判断。
- getCurrentLocation
html5History下只需监听浏览器的‘popstate’事件,根据传来的参数获取location,然后进行路由跳转即可,这部分在constructor中包含,html5History实例化的时候就会执行。
constructor (router: Router, base: ?string) {
super(router, base)
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
const initLocation = getLocation(this.base)
window.addEventListener('popstate', e => {
const current = this.current
// Avoiding first `popstate` event dispatched in some browsers but first
// history route not updated since async guard at the same time.
const location = getLocation(this.base)
if (this.current === START && location === initLocation) {
return
}
this.transitionTo(location, route => {
if (supportsScroll) {
handleScroll(router, route, current, true)
}
})
})
}
复制代码
8. Hash&History两种mode比较
- url的美观性,hash模式中,一定要有#与路由path隔开,history的url更美观
- hash模式下,页面刷新还是能正常现在页面,而history存在刷新404的问题,需要与后端配合设置静态资源根路径的问题。
- hash模式window监听hashchange和popstate两种事件,而history只监听popshate事件。
- history模式,实际向后端提出了http请求,而hash模式没有,只是hash变化,不与后端有任何请求。