做爲
vue
生態圈中的重要一員:vue-router
已經成爲了前端工程師要掌握的基本技能之一。本文拋開了vue-router
的平常使用,從源碼入手,一塊兒學習下源碼並本身嘗試實現一個本身的vue-router
。閱讀過程當中,若有不足之處,懇請斧正。html
本文共2000餘字,閱讀本篇大概須要15分鐘。若有不足之處,懇請斧正
首先來看下源碼目錄結構:vue
// 路徑:node_modules/vue-router ├── dist ---------------------------------- 構建後文件的輸出目錄 ├── package.json -------------------------- 不解釋 ├── src ----------------------------------- 包含主要源碼 │ ├── components --------------------------- 路由組件 │ │ ├── link.js ---------------- router-link組件 │ │ ├── view.js -- router-view組件 │ ├── history -------------------------- 路由模式 │ │ ├── abstract.js ------------------------ abstract路由模式 │ │ ├── base.js ----------------------- history路由模式 │ │ ├── errors.js ------------------ 錯誤類處理 │ │ ├── hash.js ---------------------- hash路由模式 │ │ ├── html5.js -------------------------- HTML5History模式封裝 │ ├── util ---------------------------- 工具類功能封裝 │ ├── create-matcher.js ------------------------- 路由映射表 │ ├── create-route-map.js ------------------------------- 建立路由映射狀態樹 │ ├── index.js ---------------------------- 主入口文件 | ├── install.js ---------------------------- 路由裝載文件 複製代碼
從入口文件index.js
中咱們能夠看到暴露出了一個VueRouter
類,這個就是咱們在 vue
項目中引入 vue-router
的時候所用到的new Router()
其中具體內部代碼以下(爲了方便閱讀,省略部分代碼)html5
export default class VueRouter { constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode 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}`) } } } init () {} function registerHook (){} go(){} push(){} VueRouter.install = install VueRouter.version = '__VERSION__' if (inBrowser && window.Vue) { window.Vue.use(VueRouter) } 複製代碼
從入口文件中咱們能夠看出裏面包含了如下幾個主要幾個步驟:node
routes
參數生成路由狀態表Hooks
等事件install
裝載函數上述暴露出的
router
類中掛載了一個install
方法,這裏咱們對其作一個簡要的分析(這也是咱們下面實現一個本身路由的思惟引導)。在咱們引入vue-router
而且實例化它的時候,vue-router
內部幫助咱們將router
實例裝載入vue
的實例中,這樣咱們才能夠在組件中能夠直接使用router-link、router-view
等組件。以及直接訪問this.$router、this.$route
等全局變量,這裏主要歸功於install.js
幫助實現這一個過程,主要分如下幾個步驟:git
vue-router
後須要利用beforeCreate
生命週期進行裝載它,用來初始化_routerRoot,_router,_route
等數據,$router
和$router
router-link
和 router-view
兩個組件的註冊在源碼install.js
中能夠體現github
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 // 混入vue實例中 Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } }) // 設置全局訪問變量$router Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 註冊組件 Vue.component('RouterView', View) Vue.component('RouterLink', Link) } 複製代碼
上述咱們對vue-router
的源碼作了一個粗略的脈絡梳理,下面咱們將實現一個簡化版的vue-router
。在此以前咱們須要簡單的瞭解一些知識點vue-router
咱們都知道vue-router
提供一個mode
參數配置,咱們能夠設置history
或者是hash
兩種參數背後的實現原理也各不相同vuex
hash
的實現原理json
http://localhost:8080/#login
#
符號自己以及它後面的字符稱之爲hash
,可經過window.location.hash
屬性讀取。H5新增了一個hashchange
來幫助咱們監聽瀏覽器連接的hash
值變化。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>老 yuan</title> </head> <body> <h1 id="app"></h1> <a href="#/jin">掘金</a> <a href="#/sn">黃小蟲</a> <script> window.addEventListener('hashchange',()=>{ app.innerHTML = location.hash.slice(2) }) </script> </body> </html> 複製代碼
history
的實現原理
http://localhost:8080/login
一樣H5
也新增了pushState
和popstate
來幫助咱們無感知刷新瀏覽器url
<body> <h1 id="app"></h1> <a onclick="to('jin')">掘金</a> <a onclick="to('sn')">黃小蟲</a> <script > function to(pathname) { history.pushState({},null,pathname) app.innerHTML = pathname } window.addEventListener('popstate',()=>{ to(location.pathname) }) </script> </body> 複製代碼
在咱們理清楚無感知刷新url的原理後,咱們要基於這些原理封裝出一個vue-router
首先咱們須要初始化一下咱們項目結構,新建simple-vue-router.js
,根據上面分析,這裏咱們須要暴露出一個router
類,其中須要包含一個install
方法
let Vue // 用於保存vue實例 class VueRouter(){ // router類 } function install(_vue){ // 裝載函數 } export default { VueRouter, install } 複製代碼
其中的install
須要實現如下幾點功能
_routerRoot
,_router
,_route
等數據,$router
和$router
router-link
和 router-view
兩個組件的註冊代碼以下:
let Vue // 用於保存vue實例 class VueRouter { // router類 } VueRouter.install = function(_vue) { // 裝載函數 //每一個組件都有 this.$router / this.$route 因此要mixin一下 Vue = _vue // 在每一個組件中均可以獲取到 this.$router與this.$route,這裏進行混入vue實例中 Vue.mixin({ beforeCreate() { // 若是是根組件則 if (this.$options && this.$options.router) { this._root = this //把當前vue實例保存到_root上 this._router = this.$options.router // 把router的實例掛載在_router上 } else { // 若是是子組件則去繼承父組件的實例(這樣全部組件共享一個router實例) this._root = this.$parent._root } // 定義router實例 當訪問this.$router時即返回router實例 Object.defineProperty(this, '$router', { get() { return this._root._router } }) // 定義route 當訪問this.$route時即返回當前頁面路由信息 Object.defineProperty(this, '$route', { get() { return {} } }) } }) // 全局註冊 router的兩個組件 Vue.component('router-link', { render(h) {} }) Vue.component('router-view', { render(h) {} }) } export default VueRouter 複製代碼
router
類上述實現了install
方法幫助咱們將router
掛載在vue
實例中,接下來咱們須要完善一下router
類中的功能。按照上文源碼中的分析,咱們須要實現如下幾點功能:
routes:[ { path: '/', name: 'index', component: index }, { path: '/login', name: 'login', component: login }, { path: '/learn', name: 'learn', component: learn }, ] 複製代碼
將其用path爲key,component爲value的規律格式化爲
{ '/':index, '/login':login, '/learn':learn } 複製代碼
具體代碼以下
let Vue // 用於保存vue實例 class VueRouter { // router類 constructor(options) { // 默認爲hash模式 this.mode = options.mode || 'hash' this.routes = options.routes || [] // 路由映射表 this.routeMaps = this.generateMap(this.routes) // 當前路由 this.currentHistory = new historyRoute() // 初始化路由函數 this.initRoute() } generateMap(routes) { return routes.reduce((prev, current) => { prev[current.path] = current.component return prev }, {}) } initRoute() { // 這裏僅處理hash模式與history模式 if (this.mode === 'hash') { // 先判斷用戶打開時url中有沒有hash,沒有重定向到#/ location.hash ? '' : (location.hash = '/') // 監控瀏覽器load事件,改變當前存儲的路由變量 window.addEventListener('load', () => { this.currentHistory.current = location.hash.slice(1) }) window.addEventListener('hashchange', () => { this.currentHistory.current = location.hash.slice(1) }) } else { location.pathname ? '' : (location.pathname = '/') window.addEventListener('load', () => { this.currentHistory.current = location.pathname }) window.addEventListener('popstate', () => { this.currentHistory.current = location.pathname }) } } } class historyRoute { constructor() { this.current = null } } VueRouter.install = function(_vue) { // 省略部分代碼 } export default VueRouter 複製代碼
在構建完router
類以後,咱們發現還存在一個問題,那就是當前路由狀態currentHistory.current
仍是靜態的,當咱們改變當前路由的時候頁面並不會顯示對應模板。這裏咱們能夠利用vue
自身的雙向綁定機制實現
具體代碼以下
let Vue // 用於保存vue實例 class VueRouter { // router類 constructor(options) { // 默認爲hash模式 this.mode = options.mode || 'hash' this.routes = options.routes || [] // 路由映射表 this.routeMaps = this.generateMap(this.routes) // 當前路由 this.currentHistory = new historyRoute() // 初始化路由函數 this.initRoute() } generateMap(routes) { return routes.reduce((prev, current) => { prev[current.path] = current.component return prev }, {}) } initRoute() { // 這裏僅處理hash模式與history模式 if (this.mode === 'hash') { // 先判斷用戶打開時url中有沒有hash,沒有重定向到#/ location.hash ? '' : (location.hash = '/') // 監控瀏覽器load事件,改變當前存儲的路由變量 window.addEventListener('load', () => { this.currentHistory.current = location.hash.slice(1) }) window.addEventListener('hashchange', () => { this.currentHistory.current = location.hash.slice(1) }) } else { location.pathname ? '' : (location.pathname = '/') window.addEventListener('load', () => { this.currentHistory.current = location.pathname }) window.addEventListener('popstate', () => { this.currentHistory.current = location.pathname }) } } } class historyRoute { constructor() { this.current = null } } VueRouter.install = function(_vue) { // 裝載函數 //每一個組件都有 this.$router / this.$route 因此要mixin一下 Vue = _vue // 在每一個組件中均可以獲取到 this.$router與this.$route,這裏進行混入vue實例中 Vue.mixin({ beforeCreate() { // 若是是根組件則 if (this.$options && this.$options.router) { this._root = this //把當前vue實例保存到_root上 this._router = this.$options.router // 把router的實例掛載在_router上 //利用vue工具庫對當前路由進行劫持 Vue.util.defineReactive(this,'route',this._router.currentHistory) } else { // 若是是子組件則去繼承父組件的實例(這樣全部組件共享一個router實例) this._root = this.$parent._root } // 定義router實例 當訪問this.$router時即返回router實例 Object.defineProperty(this, '$router', { get() { return this._root._router } }) // 定義route 當訪問this.$route時即返回當前頁面路由信息 Object.defineProperty(this, '$route', { get() { return { // 當前路由 current: this._root._router.history.current } } }) } }) // 全局註冊 router的兩個組件 Vue.component('router-link', { props: { to: String, tag: String }, methods: { handleClick(event) { // 阻止a標籤默認跳轉 event && event.preventDefault && event.preventDefault() let mode = this._self._root._router.mode let path = this.to this._self._root._router.currentHistory.current = path if (mode === 'hash') { window.history.pushState({}, '', '#/' + path.slice(1)) } else { window.history.pushState({}, '', path.slice(1)) } } }, render(h) { let mode = this._self._root._router.mode let tag = this.tag || 'a' return ( <tag on-click={this.handleClick} href={mode === 'hash' ? `#${this.to}` : this.to}> {this.$slots.default} </tag> ) } }) Vue.component('router-view', { render(h) { // 這裏的current經過上面的劫持已是動態了 let current = this._self._root._router.currentHistory.current let routeMap = this._self._root._router.routeMaps return h(routeMap[current]) // 動態渲染對應組件 } }) } export default VueRouter 複製代碼
到此爲止,一個簡單的路由管理器已經完成。實際上相對vue-router
來講仍是缺乏了不少諸如導航守衛、動態路由等功能。千里之行始於足下,本篇文章旨在經過簡要分析vue-router
的原理以及實踐一個簡單的路由器幫助你們走進vue-router
原理的大門,後面的就要靠你們本身堅持繼續深刻學習了。
參考資料