今日心血来潮,想起我们使用Vue开发单页面项目基本会用到 vue-router 路由插件,通过改变Url,在不刷新页面的情况下,更新页面视图。那么 vue-router 它是怎么实现路由跳转页面的呢?
好吧,没人理我就自己玩:joy:。我(们)先来回忆下路由的配置:
router/index.js
import Vue from 'vue'
import Router from 'vue- router '
Vue.use(Router)
//声明路由表
const routes = [
{
name: 'login',
path: '/login',
component: () => import('@/views/login/index')
},
{
name: 'register',
path: '/register',
component: () => import('@/views/register/index')
}
]
export default new Router({
routes
})
main.js引入
import router from './router'
new Vue({
el: '#app',
router,
render: h => h(App)
})
App.vue使用路由组件
<template>
<div id="app">
<router-view />
</div>
</template>
目前vue-router提供路由跳转的方法有:
- router.push 添加新路由
- router.replace 替换当前路由
- router.go 跳转到指定索引路由
- router.back 返回上一个路由
- router.forward 跳转下一个路由
以及常用的 <view-link to=”/login”>去登录</view-link>
好了,vue-router路由的使用回忆完了,脑海是否存在一下问题?
- Vue.use(Router)时做了什么事情?
- <router-view />组件是怎么来的?
- <router-link />组件是怎么来的?
- router路由提供的编程式导航是怎么实现的?
- 浏览器Url地址发生变化时怎么渲染对应组件的?
我们知道,Vue中使用Vue-router的时候,实际是引入一个提供路由功能的插件,既然是插件,那么它就会向外提供一些方法供开发者使用。下面我们就针对上述的疑问一步步揭开谜底。
Vue.use(Router)时做了什么事情?
用户执行Vue.use的时候,其实是执行vue-router插件的 install 方法,并且把Vue的实例作为参数传递进去。
注:在Vue定义,只要插件中提供 install 方法就可以被Vue作为Vue.use(xx)来使用。翻看Vue-router源码的 src/install.js 文件,我们就可以看到下面这样的代码:
可以看到这个文件向外提供了 install 的方法。方法里面使用了Vue实例,并在实例中使用了 mixin 。那么在 mixin 中做了什么事呢?
- 在 beforeCreate 生命周期中判断 this.$options.router 是否存在,这个东西就是我们在main.js文件中 new Vue({}) 创建路由实例时传入的touter对象。
- 在Vue实例中指定_routerRoot缓存下自身
- 在Vue实例中指定_router缓存传入的router路由实例
- 路由实例调用 init 方法,参数为 Vue实例
- 通过Vue的 defineReactive 方法将_route变成响应式,指向当前路由的URL。
- 劫持数据_route,一旦_route数据发生变化后,通知router-view执行render方法
我们再来看看 src/util/toute.js 文件中创建路由的方法。
export Function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = { // 添加一个route对象
name: location.name || (record && record.name), // 路由表 配置的name属性
meta: (record && record.meta) || {}, // 路由表配置的meta对象
path: location.path || '/', // 路由表配置的path属性
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route) // 最后将route对象冻结并返回(即不允许新增属性)
}
方法参数解析:
- record:路由记录信息
- location:需要跳转的路由地址(包含path、query、 hash 和params的对象)
- router:router实例
<router-view />和<router-link />组件怎么来的?
你可能会注意到,我们在App.vue页面中会使用<router-view>和<router-link />组件,但是我们并没有手动引入和注册这两个组件,其实是vue-router内部帮我们去全局注册了组件。
还是刚才那个 install.js 文件
import View from './components/view'
import Link from './components/link'
...
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
会看到这个几行代码。没错了,就是在执行 install 方法的时候就在Vue注册了组件了。
router路由提供的编程式导航是怎么实现的?
说到这里就要翻到 src/index.js 文件了。这里写了一个 VueRouter 类,VueRouter里面实现了编程式导航功能以及在 constructor 中看到了 mode 选项的配置。
从这也就知道了默认的路由渲染模式是 hash ,其中出现的 options 就是路由的配置。
接着往下走,来到第167行,会看到如下代码:
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 (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 (n: number) {
this.history.go(n)
}
back () {
this.go(-)
}
forward () {
this.go()
}
如此你的代码就可以这么写啦
router.push(location, onComplete?, onAbort?) router.replace(location, onComplete?, onAbort?) router.go(n) router.back() router.forward()
浏览器Url地址发生变化时怎么渲染对应组件的?
我们需要知道的是,当浏览器地址发生变化时:
❝
HashHistory和HTML5History会分别监控hashchange和popstate来对路由变化作对应的处理。HashHistory和HTML5History捕获到变化后会对应执行push或replace方法,从而调用transitionTo来对路由变化作对应的处理。
❞
上面提到在 install 方法的 mixin 中,会监听_route数据变化,一旦_route数据发生变化后,通知router-view执行render方法。这里就要回到刚才注册<router-view>组件那里去了。
翻到 sec/components/view.js 就能看到刚才注册的组件 render 函数啦
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
data.routerView = true
const h = parent.$createElement
// 得到渲染的组件
const name = props.name
// route 对象
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth =
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
if (inactive) {
// 非 keepalive 模式下 每次都需要设置钩子
// 进而更新(赋值&销毁)匹配了的实例元素
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
return h()
}
}
const matched = route.matched[depth]
const component = matched && matched.components[name]
if (!matched || !component) {
cache[name] = null
return h()
}
cache[name] = { component }
data.registerRouteInstance = (vm, val) => {
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
}
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]
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
}
}
最后做个总结就是:
- 向外暴露 install 和 router ,接着初始化路由。
- 内部注册<router-view>和<router-link>组件。
- 设置变量保存当前路由地址,监听hash变化,切换当前组件,然后 render 渲染对应的组件(hash模式)
END