這是一篇集合了從如何查看 vue-router源碼(v3.1.3),到 vue-router源碼解析,以及擴(kuò)展了相關(guān)涉及的知識(shí)點(diǎn),科普了完整的導(dǎo)航解析流程圖,一時(shí)讀不完,建議收藏。
如何查看vue-router源碼
查看源碼的方法有很多,下面是我自己讀vue-router源碼的兩種方法,大家都是怎么查看源碼的,歡迎在評(píng)論區(qū)留言。
查看vue-router源碼 方法一:
- 下載好
vue-router源碼,安裝好依賴。 - 找到
build/config.js修改module.exports,只保留es,其它的注釋。
module.exports = [
{
file: resolve('dist/vue-router.esm.js'),
format: 'es'
}
].map(genConfig)
- 在根目錄下創(chuàng)建一個(gè)
auto-running.js文件,用于監(jiān)聽src文件的改變的腳本,監(jiān)聽到vue-router源碼變更就從新構(gòu)建vue-router執(zhí)行node auto-running.js命令。auto-running.js的代碼如下:
const { exec } = require('child_process')
const fs = require('fs')
let building = false
fs.watch('./src', {
recursive: true
}, (event, filename) => {
if (building) {
return
} else {
building = true
console.log('start: building ...')
exec('npm run build', (err, stdout, stderr) => {
if (err) {
console.log(err)
} else {
console.log('end: building: ', stdout)
}
building = false
})
}
})
4.執(zhí)行 npm run dev命令,將 vue-router 跑起來(lái)
查看vue-router源碼方法二:
一般項(xiàng)目中的node_modules的vue-router的src不全 不方便查看源碼;
所以需要自己下載一個(gè)vue-router的完整版,看到哪里不清楚了,就去vue-router的node_modules的 dist>vue-router.esm.js 文件里去打debugger。
為什么要在vue-router.esm.js文件里打點(diǎn)而不是vue-router.js;是因?yàn)閣ebpack在進(jìn)行打包的時(shí)候用的是esm.js文件。
為什么要在esm.js文件中打debugger
在vue-router源碼的 dist/目錄,有很多不同的構(gòu)建版本。
| 版本 | UMD | Common JS | ES Module(基于構(gòu)建工具使用) | ES Modules(直接用于瀏覽器) |
|---|---|---|---|---|
| 完整版 | vue-router.js | vue-router.common.js | vue-router.esm.js | vue-router.esm.browser.js |
| 完整版(生產(chǎn)環(huán)境) | vue-router.min.js | vue-router.esm.browser.min.js |
- 完整版:同時(shí)包含編譯器和運(yùn)行時(shí)的版本
- UMD:UMD版本可以通過
<script>標(biāo)簽直接用在瀏覽器中。 - CommonJS: CommonJS版本用來(lái)配合老的打包工具比如webpack1。
- ES Module: 有兩個(gè)ES Modules構(gòu)建文件:
- 為打包工具提供的ESM,ESM被設(shè)計(jì)為可以被靜態(tài)分析,打包工具可以利用這一點(diǎn)來(lái)進(jìn)行“tree-shaking”。
- 為瀏覽器提供的ESM,在現(xiàn)代瀏覽器中通過
<script type="module">直接導(dǎo)入
現(xiàn)在清楚為什么要在esm.js文件中打點(diǎn),因?yàn)閑sm文件為打包工具提供的esm,打包工具可以進(jìn)行“tree-shaking”。
vue-router項(xiàng)目src的目錄樹
.
├── components
│ ├── link.js
│ └── view.js
├── create-matcher.js
├── create-route-map.js
├── history
│ ├── abstract.js
│ ├── base.js
│ ├── errors.js
│ ├── hash.js
│ └── html5.js
├── index.js
├── install.js
└── util
├── async.js
├── dom.js
├── location.js
├── misc.js
├── params.js
├── path.js
├── push-state.js
├── query.js
├── resolve-components.js
├── route.js
├── scroll.js
├── state-key.js
└── warn.js
vue-router的使用
vue-router 是vue的插件,其使用方式跟普通的vue插件類似都需要按照、插件和注冊(cè)。
vue-router的基礎(chǔ)使用在 vue-router 項(xiàng)目中 examples/basic,注意代碼注釋。
// 0.在模塊化工程中使用,導(dǎo)入Vue和VueRouter
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1. 插件的使用,必須通過Vue.use()明確地安裝路由
// 在全局注入了兩個(gè)組件 <router-view> 和 <router-link>,
// 并且在全局注入 $router 和 $route,
// 可以在實(shí)例化的所有的vue組件中使用 $router路由實(shí)例、$route當(dāng)前路由對(duì)象
Vue.use(VueRouter)
// 2. 定義路由組件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const Unicode = { template: '<div>unicode</div>' }
// 3. 創(chuàng)建路由實(shí)例 實(shí)例接收了一個(gè)對(duì)象參數(shù),
// 參數(shù)mode:路由模式,
// 參數(shù)routes路由配置 將組件映射到路由
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
{ path: '/é', component: Unicode }
]
})
// 4. 創(chuàng)建和掛載根實(shí)例
// 通過router參數(shù)注入到vue里 讓整個(gè)應(yīng)用都有路由參數(shù)
// 在應(yīng)用中通過組件<router-view>,進(jìn)行路由切換
// template里有寫特殊用法 我們晚點(diǎn)討論
new Vue({
router,
data: () => ({ n: 0 }),
template: `
<div id="app">
<h1>Basic</h1>
<ul>
<!-- 使用 router-link 創(chuàng)建a標(biāo)簽來(lái)定義導(dǎo)航鏈接. to屬性為執(zhí)行鏈接-->
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<!-- 通過tag屬性可以指定渲染的標(biāo)簽 這里是li標(biāo)簽 event自定義了事件-->
<router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']">
<a>/bar</a>
</router-link>
<li><router-link to="/é">/é</router-link></li>
<li><router-link to="/é?t=%25?">/é?t=%?</router-link></li>
<li><router-link to="/é#%25?">/é#%25?</router-link></li>
<!-- router-link可以作為slot,插入內(nèi)容,如果內(nèi)容中有a標(biāo)簽,會(huì)把to屬性的鏈接給內(nèi)部的a標(biāo)簽 -->
<router-link to="/foo" v-slot="props">
<li :class="[props.isActive && 'active', props.isExactActive && 'exact-active']">
<a :href="props.href" @click="props.navigate">{{ props.route.path }} (with v-slot).</a>
</li>
</router-link>
</ul>
<button id="navigate-btn" @click="navigateAndIncrement">On Success</button>
<pre id="counter">{{ n }}</pre>
<pre id="query-t">{{ $route.query.t }}</pre>
<pre id="hash">{{ $route.hash }}</pre>
<!-- 路由匹配到的組件將渲染在這里 -->
<router-view class="view"></router-view>
</div>
`,
methods: {
navigateAndIncrement () {
const increment = () => this.n++
// 路由注冊(cè)后,我們可以在Vue實(shí)例內(nèi)部通過 this.$router 訪問路由實(shí)例,
// 通過 this.$route 訪問當(dāng)前路由
if (this.$route.path === '/') {
// this.$router.push 會(huì)向history棧添加一個(gè)新的記錄
// <router-link>內(nèi)部也是調(diào)用來(lái) router.push,實(shí)現(xiàn)原理相同
this.$router.push('/foo', increment)
} else {
this.$router.push('/', increment)
}
}
}
}).$mount('#app')
使用 this.$router 的原因是并不想用戶在每個(gè)獨(dú)立需要封裝路由的組件中都導(dǎo)入路由。<router-view> 是最頂層的出口,渲染最高級(jí)路由匹配的組件,要在嵌套的出口中渲染組件,需要在 VueRouter 的參數(shù)中使用 children 配置。
注入路由和路由實(shí)例化都干了點(diǎn)啥
Vue提供了插件注冊(cè)機(jī)制是,每個(gè)插件都需要實(shí)現(xiàn)一個(gè)靜態(tài)的 install方法,當(dāng)執(zhí)行 Vue.use 注冊(cè)插件的時(shí)候,就會(huì)執(zhí)行 install 方法,該方法執(zhí)行的時(shí)候第一個(gè)參數(shù)強(qiáng)制是 Vue對(duì)象。
為什么install的插件方法第一個(gè)參數(shù)是Vue
Vue插件的策略,編寫插件的時(shí)候就不需要inport Vue了,在注冊(cè)插件的時(shí)候,給插件強(qiáng)制插入一個(gè)參數(shù)就是 Vue 實(shí)例。
vue-router注入的時(shí)候時(shí)候,install了什么
// 引入install方法
import { install } from './install'
export default class VueRouter {
// 在VueRouter類中定義install靜態(tài)方法
static install: () => void;
}
// 給VueRouter.install復(fù)制
VueRouter.install = install
// 以鏈接的形式引入vue-router插件 直接注冊(cè)vue-router
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
在 vue-router源碼中,入口文件是 src/index.js,其中定義了 VueRouter 類,在VueRouter類中定義靜態(tài)方法 install,它定義在 src/install.js中。
src/install.js文件中路由注冊(cè)的時(shí)候install了什么
import View from './components/view'
import Link from './components/link'
// 導(dǎo)出Vue實(shí)例
export let _Vue
// install 方法 當(dāng)Vue.use(vueRouter)時(shí) 相當(dāng)于 Vue.use(vueRouter.install())
export function install (Vue) {
// vue-router注冊(cè)處理 只注冊(cè)一次即可
if (install.installed && _Vue === Vue) return
install.installed = true
// 保存Vue實(shí)例,方便其它插件文件使用
_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)
}
}
/**
* 注冊(cè)vue-router的時(shí)候,給所有的vue組件混入兩個(gè)生命周期beforeCreate、destroyed
* 在beforeCreated中初始化vue-router,并將_route響應(yīng)式
*/
Vue.mixin({
beforeCreate () {
// 如果vue的實(shí)例的自定義屬性有router的話,把vue實(shí)例掛在到vue實(shí)例的_routerRoot上
if (isDef(this.$options.router)) {
// 給大佬遞貓 把自己遞大佬
this._routerRoot = this
// 把VueRouter實(shí)例掛載到_router上
this._router = this.$options.router
// 初始化vue-router,init為核心方法,init定義在src/index.js中,晚些再看
this._router.init(this)
// 將當(dāng)前的route對(duì)象 隱式掛到當(dāng)前組件的data上,使其成為響應(yīng)式變量。
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 找爸爸,自身沒有_routerRoot,找其父組件的_routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
/**
* 給Vue添加實(shí)例對(duì)象$router和$route
* $router為router實(shí)例
* $route為當(dāng)前的route
*/
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
/**
* 注入兩個(gè)全局組件
* <router-view>
* <router-link>
*/
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
/**
* Vue.config 是一個(gè)對(duì)象,包含了Vue的全局配置
* 將vue-router的hook進(jìn)行Vue的合并策略
*/
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
為了保證 VueRouter 只執(zhí)行一次,當(dāng)執(zhí)行 install 邏輯的時(shí)候添加一個(gè)標(biāo)識(shí) installed。用一個(gè)全局變量保存Vue,方便VueRouter插件各處對(duì)Vue的使用。這個(gè)思想就很好,以后自己寫Vue插件的時(shí)候就可以存一個(gè)全局的 _Vue。
VueRouter安裝的核心是通過 mixin,向應(yīng)用的所有組件混入 beforeCreate 和 destroyed鉤子函數(shù)。在beforeCreate鉤子函數(shù)中,定義了私有屬性_routerRoot 和 _router。
- _routerRoot: 將Vue實(shí)例賦值給_routerRoot,相當(dāng)于把Vue跟實(shí)例掛載到每個(gè)組件的_routerRoot的屬性上,通過
$parent._routerRoot的方式,讓所有組件都能擁有_routerRoot始終指向根Vue實(shí)例。 - _router:通過
this.$options.router方式,讓每個(gè)vue組件都能拿到VueRouter實(shí)例
用Vue的defineReactive方法把 _route 變成響應(yīng)式對(duì)象。this._router.init() 初始化了router,init方法在 src/index.js中,init方法很重要,后面介紹。registerInstance 也是后面介紹。
然后給Vue的原型上掛載了兩個(gè)對(duì)象屬性 $router 和 $route,在應(yīng)用的所有組件實(shí)例上都可以訪問 this.$router 和 this.$route,this.$router 是路由實(shí)例,對(duì)外暴露了像this.$router.push、this.$router.replace等很多api方法,this.$route包含了當(dāng)前路由的所有信息。是很有用的兩個(gè)方法。
后面通過 Vue.component 方法定義了全局的 <router-link> 和 <router-view> 兩個(gè)組件。<router-link>類似于a標(biāo)簽,<router-view> 是路由出口,在 <router-view> 切換路由渲染不同Vue組件。
最后定義了路由守衛(wèi)的合并策略,采用了Vue的合并策略。
小結(jié)
Vue插件需要提供 install 方法,用于插件的注入。VueRouter安裝時(shí)會(huì)給應(yīng)用的所有組件注入 beforeCreate 和 destoryed 鉤子函數(shù)。在 beforeCreate 中定義一些私有變量,初始化了路由。全局注冊(cè)了兩個(gè)組件和兩個(gè)api。
那么問題來(lái)了,初始化路由都干了啥
VueRouter類定義很多屬性和方法,我們先看看初始化路由方法 init。初始化路由的代碼是 this._router.init(this),init接收了Vue實(shí)例,下面的app就是Vue實(shí)例。注釋寫的很詳細(xì)了,這里就不文字?jǐn)⑹隽恕?/p>
init (app: any /* Vue component instance */) {
// vueRouter可能會(huì)實(shí)例化多次 apps用于存放多個(gè)vueRouter實(shí)例
this.apps.push(app)
// 保證VueRouter只初始化一次,如果初始化了就終止后續(xù)邏輯
if (this.app) {
return
}
// 將vue實(shí)例掛載到vueRouter上,router掛載到Vue實(shí)例上,哈 給大佬遞貓
this.app = app
// history是vueRouter維護(hù)的全局變量,很重要
const history = this.history
// 針對(duì)不同路由模式做不同的處理 transitionTo是history的核心方法,后面再細(xì)看
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 路由全局監(jiān)聽,維護(hù)當(dāng)前的route
// 因?yàn)開route在install執(zhí)行時(shí)定義為響應(yīng)式屬性,
// 當(dāng)route變更時(shí)_route更新,后面的視圖更新渲染就是依賴于_route
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
接下來(lái)看看 new VueRouter 時(shí)constructor做了什么。
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 創(chuàng)建 matcher 匹配函數(shù),createMatcher函數(shù)返回一個(gè)對(duì)象 {match, addRoutes} 很重要
this.matcher = createMatcher(options.routes || [], this)
// 默認(rèn)hash模式
let mode = options.mode || 'hash'
// h5的history有兼容性 對(duì)history做降級(jí)處理
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 不同的mode,實(shí)例化不同的History類, 后面的this.history就是History的實(shí)例
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}`)
}
}
}
constructor 的 options 是實(shí)例化路由是的傳參,通常是一個(gè)對(duì)象 {routes, mode: 'history'}, routes是必傳參數(shù),mode默認(rèn)是hash模式。vueRouter還定義了哪些東西呢。
...
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
// 獲取當(dāng)前的路由
get currentRoute (): ?Route {
return this.history && this.history.current
}
init(options) { ... }
beforeEach(fn) { ... }
beforeResolve(fn) { ... }
afterEach(fn) { ... }
onReady(cb) { ... }
push(location) { ... }
replace(location) { ... }
back() { ... }
go(n) { ... }
forward() { ... }
// 獲取匹配到的路由組件
getMatchedComponents (to?: RawLocation | Route): Array<any> {
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]
})
}))
}
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
在實(shí)例化的時(shí)候,vueRouter仿照history定義了一些api:push、replace、back、go、forward,還定義了路由匹配器、添加router動(dòng)態(tài)更新方法等。
小結(jié)
install的時(shí)候先執(zhí)行init方法,然后實(shí)例化vueRouter的時(shí)候定義一些屬性和方法。init執(zhí)行的時(shí)候通過 history.transitionTo 做路由過渡。matcher 路由匹配器是后面路由切換,路由和組件匹配的核心函數(shù)。所以...en
matcher了解一下吧
在VueRouter對(duì)象中有以下代碼:
// 路由匹配器,createMatcher函數(shù)返回一個(gè)對(duì)象 {match, addRoutes}
this.matcher = createMatcher(options.routes || [], this)
...
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
...
const route = this.match(location, current)
我們可以觀察到 route 對(duì)象通過 this.match() 獲取,match 又是通過 this.matcher.match(),而 this.matcher 是通過 createMatcher 函數(shù)處理。接下來(lái)我們?nèi)タ纯碿reateMatcher函數(shù)的實(shí)現(xiàn)。
createMatcher
createMatcher 相關(guān)的實(shí)現(xiàn)都在 src/create-matcher.js中。
/**
* 創(chuàng)建createMatcher
* @param {*} routes 路由配置
* @param {*} router 路由實(shí)例
*
* 返回一個(gè)對(duì)象 {
* match, // 當(dāng)前路由的match
* addRoutes // 更新路由配置
* }
*/
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
...
return {
match,
addRoutes
}
}
createMatcher 接收2個(gè)參數(shù),routes 是用戶定義的路由配置,router 是 new VueRouter 返回的實(shí)例。routes 是一個(gè)定義了路由配置的數(shù)組,通過 createRouteMap 函數(shù)處理為 pathList, pathMap, nameMap,返回了一個(gè)對(duì)象 {match, addRoutes} 。也就是說(shuō) matcher 是一個(gè)對(duì)象,它對(duì)外暴露了 match 和 addRoutes 方法。
一會(huì)我們先了解下 pathList, pathMap, nameMap分別是什么,稍后在來(lái)看createRouteMap的實(shí)現(xiàn)。
- pathList:路由路徑數(shù)組,存儲(chǔ)所有的path
- pathMap:路由路徑與路由記錄的映射表,表示一個(gè)path到RouteRecord的映射關(guān)系
- nameMap:路由名稱與路由記錄的映射表,表示name到RouteRecord的映射關(guān)系
RouteRecord
那么路由記錄是什么樣子的?
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
RouteRecord 是一個(gè)對(duì)象,包含了一條路由的所有信息: 路徑、路由正則、路徑對(duì)應(yīng)的組件數(shù)組、組件實(shí)例、路由名稱等等。
createRouteMap
createRouteMap 函數(shù)的實(shí)現(xiàn)在 src/create-route-map中:
/**
*
* @param {*} routes 用戶路由配置
* @param {*} oldPathList 老pathList
* @param {*} oldPathMap 老pathMap
* @param {*} oldNameMap 老nameMap
*/
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// pathList被用于控制路由匹配優(yōu)先級(jí)
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)
})
// 確保通配符路由總是在最后
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
...
return {
pathList,
pathMap,
nameMap
}
}
createRouteMap 函數(shù)主要是把用戶的路由匹配轉(zhuǎn)換成一張路由映射表,后面路由切換就是依據(jù)這幾個(gè)映射表。routes 為每一個(gè) route 執(zhí)行 addRouteRecord 方法生成一條記錄,記錄在上面展示過了,我們來(lái)看看是如何生成一條記錄的。
addRouteRecord
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
//...
// 先創(chuàng)建一條路由記錄
const record: RouteRecord = { ... }
// 如果該路由記錄 嵌套路由的話 就循環(huán)遍歷解析嵌套路由
if (route.children) {
// ...
// 通過遞歸的方式來(lái)深度遍歷,并把當(dāng)前的record作為parent傳入
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 如果有多個(gè)相同的路徑,只有第一個(gè)起作用,后面的會(huì)被忽略
// 對(duì)解析好的路由進(jìn)行記錄,為pathList、pathMap添加一條記錄
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// ...
}
addRouteRecord 函數(shù),先創(chuàng)建一條路由記錄對(duì)象。如果當(dāng)前的路由記錄有嵌套路由的話,就循環(huán)遍歷繼續(xù)創(chuàng)建路由記錄,并按照路徑和路由名稱進(jìn)行路由記錄映射。這樣所有的路由記錄都被記錄了。整個(gè)RouteRecord就是一個(gè)樹型結(jié)構(gòu),其中 parent 表示父的 RouteRecord。
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
// ...
}
如果我們?cè)诼酚膳渲弥性O(shè)置了 name,會(huì)給 nameMap添加一條記錄。createRouteMap 方法執(zhí)行后,我們就可以得到路由的完整記錄,并且得到path、name對(duì)應(yīng)的路由映射。通過path 和 name 能在 pathMap 和 nameMap快速查到對(duì)應(yīng)的 RouteRecord。
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
//...
return {
match,
addRoutes
}
}
還記得 createMatcher 的返回值中有個(gè) match,接下里我們看 match的實(shí)現(xiàn)。
match
/**
*
* @param {*} raw 是RawLocation類型 是個(gè)url字符串或者RawLocation對(duì)象
* @param {*} currentRoute 當(dāng)前的route
* @param {*} redirectedFrom 重定向 (不是重要,可忽略)
*/
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// location 是一個(gè)對(duì)象類似于
// {"_normalized":true,"path":"/","query":{},"hash":""}
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 如果有路由名稱 就進(jìn)行nameMap映射
// 獲取到路由記錄 處理路由params 返回一個(gè)_createRoute處理的東西
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)
// 如果路由配置了 path,到pathList和PathMap里匹配到路由記錄
// 如果符合matchRoute 就返回_createRoute處理的東西
} 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)
}
}
}
// 通過_createRoute返回一個(gè)東西
return _createRoute(null, location)
}
match 方法接收路徑、但前路由、重定向,主要是根據(jù)傳入的raw 和 currentRoute處理,返回的是 _createRoute()。來(lái)看看 _createRoute返回了什么,就知道 match返回了什么了。
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
_createRoute 函數(shù)根據(jù)有是否有路由重定向、路由重命名做不同的處理。其中redirect 函數(shù)和 alias 函數(shù)最后還是調(diào)用了 _createRoute,最后都是調(diào)用了 createRoute。而來(lái)自于 util/route。
/**
*
* @param {*} record 一般為null
* @param {*} location 路由對(duì)象
* @param {*} redirectedFrom 重定向
* @param {*} router vueRouter實(shí)例
*/
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 = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
// 凍結(jié)route 一旦創(chuàng)建不可改變
return Object.freeze(route)
}
createRoute 可以根據(jù) record 和 location 創(chuàng)建出來(lái)最終返回 Route 對(duì)象,并且外部不可以修改,只能訪問。Route 對(duì)象中有一個(gè)非常重要的屬性是 matched,它是通過 formatMatch(record) 計(jì)算的:
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
通過 record 循環(huán)向上找 parent,直到找到最外層,并把所有的 record 都push到一個(gè)數(shù)組中,最終飯后就是一個(gè) record 數(shù)組,這個(gè) matched 為后面的渲染組件提供了重要的作用。
小結(jié)
matcher的主流程就是通過createMatcher 返回一個(gè)對(duì)象 {match, addRoutes}, addRoutes 是動(dòng)態(tài)添加路由用的,平時(shí)使用頻率比較低,match 很重要,返回一個(gè)路由對(duì)象,這個(gè)路由對(duì)象上記錄當(dāng)前路由的基本信息,以及路徑匹配的路由記錄,為路徑切換、組件渲染提供了依據(jù)。那路徑是怎么切換的,又是怎么渲染組件的呢。喝杯誰(shuí),我們繼續(xù)繼續(xù)往下看。
路徑切換
還記得 vue-router 初始化的時(shí)候,調(diào)用了 init 方法,在 init方法里針對(duì)不同的路由模式最后都調(diào)用了 history.transitionTo,進(jìn)行路由初始化匹配。包括 history.push 、history.replace的底層都是調(diào)用了它。它就是路由切換的方法,很重要。它的實(shí)現(xiàn)在 src/history/base.js,我們來(lái)看看。
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
// 調(diào)用 match方法得到匹配的 route對(duì)象
const route = this.router.match(location, this.current)
// 過渡處理
this.confirmTransition(
route,
() => {
// 更新當(dāng)前的 route 對(duì)象
this.updateRoute(route)
onComplete && onComplete(route)
// 更新url地址 hash模式更新hash值 history模式通過pushState/replaceState來(lái)更新
this.ensureURL()
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}
transitionTo 可以接收三個(gè)參數(shù) location、onComplete、onAbort,分別是目標(biāo)路徑、路經(jīng)切換成功的回調(diào)、路徑切換失敗的回調(diào)。transitionTo 函數(shù)主要做了兩件事:首先根據(jù)目標(biāo)路徑 location 和當(dāng)前的路由對(duì)象通過 this.router.match方法去匹配到目標(biāo) route 對(duì)象。route是這個(gè)樣子的:
const route = {
fullPath: "/detail/394"
hash: ""
matched: [{…}]
meta: {title: "工單詳情"}
name: "detail"
params: {id: "394"}
path: "/detail/394"
query: {}
}
一個(gè)包含了目標(biāo)路由基本信息的對(duì)象。然后執(zhí)行 confirmTransition方法進(jìn)行真正的路由切換。因?yàn)橛幸恍┊惒浇M件,所以回有一些異步操作。具體的實(shí)現(xiàn):
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
// ...
onAbort && onAbort(err)
}
// 如果當(dāng)前路由和之前路由相同 確認(rèn)url 直接return
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(new NavigationDuplicated(route))
}
// 通過異步隊(duì)列來(lái)交叉對(duì)比當(dāng)前路由的路由記錄和現(xiàn)在的這個(gè)路由的路由記錄
// 為了能準(zhǔn)確得到父子路由更新的情況下可以確切的知道 哪些組件需要更新 哪些不需要更新
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 在異步隊(duì)列中執(zhí)行響應(yīng)的勾子函數(shù)
// 通過 queue 這個(gè)數(shù)組保存相應(yīng)的路由鉤子函數(shù)
const queue: Array<?NavigationGuard> = [].concat(
// leave 的勾子
extractLeaveGuards(deactivated),
// 全局的 before 的勾子
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// 將要更新的路由的 beforeEnter勾子
activated.map(m => m.beforeEnter),
// 異步組件
resolveAsyncComponents(activated)
)
this.pending = route
// 隊(duì)列執(zhí)行的iterator函數(shù)
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
// 如果有導(dǎo)航鉤子,就需要調(diào)用next(),否則回調(diào)不執(zhí)行,導(dǎo)航將無(wú)法繼續(xù)
next(to)
}
})
} catch (e) {
abort(e)
}
}
// runQueue 執(zhí)行隊(duì)列 以一種遞歸回調(diào)的方式來(lái)啟動(dòng)異步函數(shù)隊(duì)列的執(zhí)行
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 組件內(nèi)的鉤子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
// 在上次的隊(duì)列執(zhí)行完成后再執(zhí)行組件內(nèi)的鉤子
// 因?yàn)樾枰犬惒浇M件以及是否OK的情況下才能執(zhí)行
runQueue(queue, iterator, () => {
// 確保期間還是當(dāng)前路由
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
}
查看目標(biāo)路由 route 和當(dāng)前前路由 current 是否相同,如果相同就調(diào)用 this.ensureUrl 和 abort。
// ensureUrl todo
接下來(lái)執(zhí)行了 resolveQueue函數(shù),這個(gè)函數(shù)要好好看看:
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
resolveQueue函數(shù)接收兩個(gè)參數(shù):當(dāng)前路由的 matched 和目標(biāo)路由的 matched,matched 是個(gè)數(shù)組。通過遍歷對(duì)比兩遍的路由記錄數(shù)組,當(dāng)有一個(gè)路由記錄不一樣的時(shí)候就記錄這個(gè)位置,并終止遍歷。對(duì)于 next 從0到i和current都是一樣的,從i口開始不同,next 從i之后為 activated部分,current從i之后為 deactivated部分,相同部分為 updated,由 resolveQueue 處理之后就能得到路由變更需要更改的部分。緊接著就可以根據(jù)路由的變更執(zhí)行一系列的鉤子函數(shù)。完整的導(dǎo)航解析流程有12步,后面會(huì)出一篇vue-router路由切換的內(nèi)部實(shí)現(xiàn)文章。盡情期待
!
路由改變路由組件是如何渲染的
路由的變更之后,路由組件隨之的渲染都是在 <router-view> 組件,它的定義在 src/components/view.js中。
router-view 組件
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
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
if (inactive) {
return h(cache[name], data, children)
}
const matched = route.matched[depth]
if (!matched) {
cache[name] = null
return h()
}
const component = cache[name] = matched.components[name]
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
}
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
propsToPass = data.props = extend({}, propsToPass)
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]
}
}
}
return h(component, data, children)
}
}
<router-view>是一個(gè)渲染函數(shù),它的渲染是用了Vue的 render 函數(shù),它接收兩個(gè)參數(shù),第一個(gè)是Vue實(shí)例,第二個(gè)是一個(gè)context,通過對(duì)象解析的方式可以拿到 props、children、parent、data,供創(chuàng)建 <router-view> 使用。
router-link 組件
支持用戶在具有路由功能的組件里使用,通過使用 to 屬性指定目標(biāo)地址,默認(rèn)渲染成 <a>標(biāo)簽,支持通過 tag 自定義標(biāo)簽和插槽。
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
const data: any = {
class: classes
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
const a = findAnchor(this.$slots.default)
if (a) {
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
<router-link>的特點(diǎn):
-
history模式和hash模式的標(biāo)簽一致,針對(duì)不支持history的模式會(huì)自動(dòng)降級(jí)為hash模式。 - 可進(jìn)行路由守衛(wèi),不從新加載頁(yè)面
<router-link> 的實(shí)現(xiàn)也是基于 render 函數(shù)。內(nèi)部實(shí)現(xiàn)也是通過 history.push() 和 history.replace() 實(shí)現(xiàn)的。
路徑變化是路由中最重要的功能:路由始終會(huì)維護(hù)當(dāng)前的線路,;欲嘔切換的時(shí)候會(huì)把當(dāng)前線路切換到目標(biāo)線路,切換過程中會(huì)執(zhí)行一些列的導(dǎo)航守衛(wèi)鉤子函數(shù),會(huì)更改url, 渲染對(duì)應(yīng)的組件,切換完畢后會(huì)把目標(biāo)線路更新替換為當(dāng)前線路,作為下一次路徑切換的依據(jù)。
知識(shí)補(bǔ)充
hash模式和history模式的區(qū)別
vue-router 默認(rèn)是hash模式,使用hash模式時(shí),變更URL,頁(yè)面不會(huì)重新加載,這種模式從ie6就有了,是一種很穩(wěn)定的路由模式。但是hash的URL上有個(gè) # 號(hào),看上去很丑,后來(lái)HTML5出來(lái)后,有了history模式。
history 模式通過 history.pushState來(lái)完成url的跳轉(zhuǎn)而無(wú)須重新加載頁(yè)面,解決了hash模式很臭的問題。但是老瀏覽器不兼容history模式,有些時(shí)候我們不得不使用hash模式,來(lái)做向下兼容。
history 模式,如果訪問一個(gè)不存在的頁(yè)面時(shí)就會(huì)返回 404,為了解決這個(gè)問題,需要后臺(tái)做配置支持:當(dāng)URL匹配不到任何靜態(tài)資源的時(shí)候,返回一個(gè)index.html頁(yè)面?;蛘咴诼酚膳渲美锾砑右粋€(gè)統(tǒng)一配置的錯(cuò)誤頁(yè)。
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '*',
component: NotFoundComponent
}
]
})
Vue Router 的 query 與 params 的使用和區(qū)別
在 vue-router中有兩個(gè)概念 query和params,一開始的時(shí)候我對(duì)它們分不清,相信也有人分不清。這里做個(gè)匯總,方便記憶理解。
- query的使用
// 帶查詢參數(shù),變成 /register?plan=private
router.push({ path: 'register', query: {plan: 'private'}})
- params的配置和調(diào)用
- 路由配置,使用params傳參數(shù),使用name
{
path: '/detail/:id',
name: 'detail',
component: Detail,
}
- 調(diào)用
this.$router.push進(jìn)行params傳參,使用name,前提需要在路由配置里設(shè)置過名稱。
this.$router.push({
name: 'detail',
params: {
id: '2019'
}
})
- params接收參數(shù)
const { id } = this.$route.params
query通常與path使用。query帶查詢參數(shù),params路徑參數(shù)。如果提供了path,params會(huì)被忽略。
// params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
導(dǎo)航守衛(wèi)
導(dǎo)航 表示路由正在發(fā)生變化,vue-router 提供的導(dǎo)航守衛(wèi)主要用來(lái)通過跳轉(zhuǎn)或者取消的方式守衛(wèi)導(dǎo)航。導(dǎo)航守衛(wèi)分為三種:全局守衛(wèi)、單個(gè)路由守衛(wèi)和組件內(nèi)的守衛(wèi)。
全局守衛(wèi):
- 全局前置守衛(wèi) beforeEach (to, from, next)
- 全局解析守衛(wèi) beforeResolve (to, from, next)
- 全局后置鉤子 afterEach (to, from)
單個(gè)路由守衛(wèi):
- 路由前置守衛(wèi) beforeEnter (to, from, next)
組件內(nèi)的守衛(wèi):
- 渲染組件的對(duì)應(yīng)路由被confirm前 beforeRouterEnter (to, from, next) next可以是函數(shù),因?yàn)樵撌匦l(wèi)不能獲取組件實(shí)例,新組件還沒被創(chuàng)建
- 路由改變,該組件被復(fù)用時(shí)調(diào)用 (to, from, next)
- 導(dǎo)航離開該組件對(duì)應(yīng)路由時(shí)調(diào)用 beforeRouteLeave
完整的導(dǎo)航解析流程圖
- 導(dǎo)航被觸發(fā)
- 在失活的組件里調(diào)用離開守衛(wèi)
beforeRouteLeave - 調(diào)用全局的
beforeEach守衛(wèi) - 在重用的組件里調(diào)用
beforeRouteUpdate守衛(wèi)(2.2+) - 在路由配置里調(diào)用
beforeEnter - 解析異步路由組件
- 在被激活的組件里調(diào)用
beforeRouteEnter - 調(diào)用全局的
beforeResolve守衛(wèi) - 導(dǎo)航被確認(rèn)
- 調(diào)用全局的
afterEach鉤子 - 觸發(fā)DOM更新
- 用創(chuàng)建好的實(shí)例調(diào)用
beforeRouterEnter守衛(wèi)中傳給next的回調(diào)函數(shù)