前端路由是咱們前端開發平常開發中常常碰到的概念,在下在平常使用中知其然也好奇着因此然,所以對 vue-router 的源碼進行了一些閱讀,也汲取了社區的一些文章優秀的思想,於本文記錄總結做爲本身思考的輸出,本人水平有限,歡迎留言討論~javascript
目標 vue-rouer 版本:3.0.2
html
vue-router源碼註釋:vue-router-analysis前端
聲明:文章中源碼的語法都使用 Flow,而且源碼根據須要都有刪節(爲了避免被迷糊 @_@),若是要看完整版的請進入上面的 github地址 ~vue
本文是系列文章,連接見底部 ~java
感興趣的同窗能夠加文末的微信羣,一塊兒討論吧~node
若是你對這些尚未了解的話,能夠看一下本文末尾的推介閱讀。git
首先咱們來看看文件結構:es6
.
├── build // 打包相關配置
├── scripts // 構建相關
├── dist // 構建後文件目錄
├── docs // 項目文檔
├── docs-gitbook // gitbook配置
├── examples // 示例代碼,調試的時候使用
├── flow // Flow 聲明
├── src // 源碼目錄
│ ├── components // 公共組件
│ ├── history // 路由類實現
│ ├── util // 相關工具庫
│ ├── create-matcher.js // 根據傳入的配置對象建立路由映射表
│ ├── create-route-map.js // 根據routes配置對象建立路由映射表
│ ├── index.js // 主入口
│ └── install.js // VueRouter裝載入口
├── test // 測試文件
└── types // TypeScript 聲明
複製代碼
咱們主要關注的就是 src
中的內容。github
按照慣例,首先從 package.json
看起,這裏有兩個命令值得注意一下:vue-router
{
"scripts": {
"dev:dist": "rollup -wm -c build/rollup.dev.config.js",
"build": "node build/build.js"
}
}
複製代碼
dev:dist
用配置文件 rollup.dev.config.js
生成 dist
目錄下方便開發調試相關生成文件,對應於下面的配置環境 development
;
build
是用 node
運行 build/build.js
生成正式的文件,包括 es6
、commonjs
、IIFE
方式的導出文件和壓縮以後的導出文件;
這兩種方式都是使用 build/configs.js
這個配置文件來生成的,其中有一段語義化比較不錯的代碼挺有意思,跟 Vue 的配置生成文件比較相似:
// vue-router/build/configs.js
module.exports = [{ // 打包出口
file: resolve('dist/vue-router.js'),
format: 'umd',
env: 'development'
},{
file: resolve('dist/vue-router.min.js'),
format: 'umd',
env: 'production'
},{
file: resolve('dist/vue-router.common.js'),
format: 'cjs'
},{
file: resolve('dist/vue-router.esm.js'),
format: 'es'
}
].map(genConfig)
function genConfig (opts) {
const config = {
input: {
input: resolve('src/index.js'), // 打包入口
plugins: [...]
},
output: {
file: opts.file,
format: opts.format,
banner,
name: 'VueRouter'
}
}
return config
}
複製代碼
能夠清晰的看到 rollup
打包的出口和入口,入口是 src/index.js
文件,而出口就是上面那部分的配置,env
是開發/生產環境標記,format
爲編譯輸出的方式:
那麼正式輸出是使用 build
方式,咱們能夠從 src/index.js
看起
// src/index.js
import { install } from './install'
export default class VueRouter { ... }
VueRouter.install = install
複製代碼
首先這個文件導出了一個類 VueRouter
,這個就是咱們在 Vue 項目中引入 vue-router 的時候 Vue.use(VueRouter)
所用到的,而 Vue.use
的主要做用就是找註冊插件上的 install
方法並執行,往下看最後一行,從一個 install.js
文件中導出的 install 被賦給了 VueRouter.install
,這就是 Vue.use
中執行所用到的 install
方法。
能夠簡單看一下 Vue 中 Vue.use
這個方法是如何實現的:
// vue/src/core/global-api/use.js
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// ... 省略一些判重操做
const args = toArray(arguments, 1)
args.unshift(this) // 注意這個this,是vue對象
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
}
return this
}
}
複製代碼
上面能夠看到 Vue.use
這個方法就是執行待註冊插件上的 install
方法,並將這個插件實例保存起來。值得注意的是 install
方法執行時的第一個參數是經過 unshift
推入的 this
,所以 install
執行時能夠拿到 Vue 對象。
對應上一小節,這裏的 plugin.install
就是 VueRouter.install
。
接以前,看一下 install.js
裏面是如何進行路由插件的註冊:
// vue-router/src/install.js
/* vue-router 的註冊過程 Vue.use(VueRouter) */
export function install(Vue) {
_Vue = Vue // 這樣拿到 Vue 不會由於 import 帶來的打包體積增長
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode // 至少存在一個 VueComponent 時, _parentVnode 屬性才存在
// registerRouteInstance 在 src/components/view.js
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// new Vue 時或者建立新組件時,在 beforeCreate 鉤子中調用
Vue.mixin({
beforeCreate() {
if (isDef(this.$options.router)) { // 組件是否存在$options.router,該對象只在根組件上有
this._routerRoot = this // 這裏的this是根vue實例
this._router = this.$options.router // VueRouter實例
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 組件實例纔會進入,經過$parent一級級獲取_routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed() {
registerInstance(this)
}
})
// 全部實例中 this.$router 等同於訪問 this._routerRoot._router
Object.defineProperty(Vue.prototype, '$router', {
get() { return this._routerRoot._router }
})
// 全部實例中 this.$route 等同於訪問 this._routerRoot._route
Object.defineProperty(Vue.prototype, '$route', {
get() { return this._routerRoot._route }
})
Vue.component('RouterView', View) // 註冊公共組件 router-view
Vue.component('RouterLink', Link) // 註冊公共組件 router-link
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
複製代碼
install
方法主要分爲幾個部分:
Vue.mixin
在 beforeCreate
、 destroyed
的時候將一些路由方法掛載到每一個 vue 實例中methods
等地方能夠經過 this.$router
、this.$route
訪問到相關信息router-view
、router-link
Vue.mixin
將定義的兩個鉤子在組件 extend
的時候合併到該組件的 options
中,從而註冊到每一個組件實例。看看 beforeCreate
,一開始訪問了一個 this.$options.router
這個是 Vue 項目裏面 app.js
中的 new Vue({ router })
這裏傳入的這個 router,固然也只有在 new Vue
這時纔會傳入 router,也就是說 this.$options.router
只有根實例上纔有。這個傳入 router 究竟是什麼呢,咱們看看它的使用方式就知道了:
const router = new VueRouter({
mode: 'hash',
routes: [{ path: '/', component: Home },
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }]
})
new Vue({
router,
template: `<div id="app"></div>`
}).$mount('#app')
複製代碼
能夠看到這個 this.$options.router
也就是 Vue 實例中的 this._route
其實就是 VueRouter 的實例。
剩下的一頓眼花繚亂的操做,是爲了在每一個 Vue 組件實例中均可以經過 _routerRoot
訪問根 Vue 實例,其上的 _route
、_router
被賦到 Vue 的原型上,這樣每一個 Vue 的實例中均可以經過 this.$route
、this.$router
訪問到掛載在根實例 _routerRoot
上的 _route
、_router
,後面用 Vue 上的響應式化方法 defineReactive
來將 _route
響應式化,另外在根組件上用 this._router.init()
進行了初始化操做。
隨便找個 Vue 組件,打印一下其上的 _routerRoot
:
能夠看到這是 Vue 的根組件。
在以前咱們已經看過 src/index.js
了,這裏來詳細看一下 VueRouter 這個類
// vue-router/src/index.js
export default class VueRouter {
constructor(options: RouterOptions = {}) { }
/* install 方法會調用 init 來初始化 */
init(app: any /* Vue組件實例 */) { }
/* createMatcher 方法返回的 match 方法 */
match(raw: RawLocation, current?: Route, redirectedFrom?: Location) { }
/* 當前路由對象 */
get currentRoute() { }
/* 註冊 beforeHooks 事件 */
beforeEach(fn: Function): Function { }
/* 註冊 resolveHooks 事件 */
beforeResolve(fn: Function): Function { }
/* 註冊 afterHooks 事件 */
afterEach(fn: Function): Function { }
/* onReady 事件 */
onReady(cb: Function, errorCb?: Function) { }
/* onError 事件 */
onError(errorCb: Function) { }
/* 調用 transitionTo 跳轉路由 */
push(location: RawLocation, onComplete?: Function, onAbort?: Function) { }
/* 調用 transitionTo 跳轉路由 */
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) { }
/* 跳轉到指定歷史記錄 */
go(n: number) { }
/* 後退 */
back() { }
/* 前進 */
forward() { }
/* 獲取路由匹配的組件 */
getMatchedComponents(to?: RawLocation | Route) { }
/* 根據路由對象返回瀏覽器路徑等信息 */
resolve(to: RawLocation, current?: Route, append?: boolean) { }
/* 動態添加路由 */
addRoutes(routes: Array<RouteConfig>) { }
}
複製代碼
VueRouter 類中除了一坨實例方法以外,主要關注的是它的構造函數和初始化方法 init
。
首先看看構造函數,其中的 mode
表明路由建立的模式,由用戶配置與應用場景決定,主要有三種 History、Hash、Abstract,前兩種咱們已經很熟悉了,Abstract 表明非瀏覽器環境,好比 Node、weex 等;this.history
主要是路由的具體實例。實現以下:
// vue-router/src/index.js
export default class VueRouter {
constructor(options: RouterOptions = {}) {
this.matcher = createMatcher(options.routes || [], this) // 添加路由匹配器
let mode = options.mode || 'hash' // 路由匹配方式,默認爲hash
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) { mode = 'hash' } // 若是不支持history則退化爲hash
if (!inBrowser) { mode = 'abstract' } // 非瀏覽器環境強制abstract,好比node中
this.mode = mode
switch (mode) { // 外觀模式
case 'history': // history 方式
this.history = new HTML5History(this, options.base)
break
case 'hash': // hash 方式
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract': // abstract 方式
this.history = new AbstractHistory(this, options.base)
break
default: ...
}
}
}
複製代碼
init
初始化方法是在 install 時的 Vue.mixin
所註冊的 beforeCreate
鉤子中調用的,能夠翻上去看看;調用方式是 this._router.init(this)
,由於是在 Vue.mixin
裏調用,因此這個 this 是當前的 Vue 實例。另外初始化方法須要負責從任一個路徑跳轉到項目中時的路由初始化,以 Hash 模式爲例,此時尚未對相關事件進行綁定,所以在第一次執行的時候就要進行事件綁定與 popstate
、hashchange
事件觸發,而後手動觸發一次路由跳轉。實現以下:
// vue-router/src/index.js
export default class VueRouter {
/* install 方法會調用 init 來初始化 */
init(app: any /* Vue組件實例 */) {
const history = this.history
if (history instanceof HTML5History) {
// 調用 history 實例的 transitionTo 方法
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners() // 設置 popstate/hashchange 事件監聽
}
history.transitionTo( // 調用 history 實例的 transitionTo 方法
history.getCurrentLocation(), // 瀏覽器 window 地址的 hash 值
setupHashListener, // 成功回調
setupHashListener // 失敗回調
)
}
}
}
複製代碼
除此以外,VueRouter 還有不少實例方法,用來實現各類功能的,剩下的將在系列文章分享 ~
本文是系列文章,隨後會更新後面的部分,共同進步~
網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~
推介閱讀:
- H5 History Api - MDN
- ECMAScript 6 入門 - 阮一峯
- JS 靜態類型檢查工具 Flow - SegmentFault 思否
- JS 外觀模式 - SegmentFault 思否
- 前端路由跳轉基本原理 - 掘金
參考:
PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~
另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~