今日心血来潮,想起我们使用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