淺析vue-router源碼並嘗試實現一個簡單的前端路由

前言

做爲vue生態圈中的重要一員:vue-router已經成爲了前端工程師要掌握的基本技能之一。本文拋開了vue-router的平常使用,從源碼入手,一塊兒學習下源碼並本身嘗試實現一個本身的vue-router。閱讀過程當中,若有不足之處,懇請斧正。html

本文共2000餘字,閱讀本篇大概須要15分鐘。若有不足之處,懇請斧正

此處推薦一篇以前實現一個本身的vuex的文章,可與本篇搭配觀看

從0到1手寫一個vuex
前端

源碼淺析

首先來看下源碼目錄結構: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 ---------------------------- 路由裝載文件
複製代碼

入口開始分析

vue-router實例

從入口文件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

  1. 初始化路由模式
  2. 根據傳入的routes參數生成路由狀態表
  3. 獲取當前路由對象
  4. 初始化路由函數
  5. 註冊Hooks等事件
  6. 添加install裝載函數

install註冊函數

上述暴露出的router類中掛載了一個install方法,這裏咱們對其作一個簡要的分析(這也是咱們下面實現一個本身路由的思惟引導)。在咱們引入vue-router而且實例化它的時候,vue-router內部幫助咱們將router實例裝載入vue的實例中,這樣咱們才能夠在組件中能夠直接使用router-link、router-view等組件。以及直接訪問this.$router、this.$route等全局變量,這裏主要歸功於install.js幫助實現這一個過程,主要分如下幾個步驟:git

  1. 首先引入vue-router後須要利用beforeCreate生命週期進行裝載它,用來初始化_routerRoot,_router,_route等數據,
  2. 同時設置全局訪問變量$router$router
  3. 完成router-linkrouter-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也新增了pushStatepopstate來幫助咱們無感知刷新瀏覽器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

開始實現本身的vue-router


實現install裝載方法

首先咱們須要初始化一下咱們項目結構,新建simple-vue-router.js,根據上面分析,這裏咱們須要暴露出一個router類,其中須要包含一個install方法

let Vue // 用於保存vue實例
class VueRouter(){ // router類
    
}
function install(_vue){ // 裝載函數
    
}
export default {
    VueRouter,
    install
}
複製代碼

其中的install須要實現如下幾點功能

  1. 初始化_routerRoot_router_route等數據,
  2. 設置全局訪問變量$router$router
  3. 完成router-linkrouter-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類中的功能。按照上文源碼中的分析,咱們須要實現如下幾點功能:

  1. 生成根據傳入的rotues參數生成,路由狀態表。即如若傳入參數爲
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 
}
複製代碼
  1. 定義當前路由變量,經過劫持進行實時渲染對應組件
  2. 定義一個函數,具體實現不一樣模式應對應使用的處理方法

具體代碼以下

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原理的大門,後面的就要靠你們本身堅持繼續深刻學習了。

參考資料

vue-router官方源碼
手寫vue-router源碼

相關文章
相關標籤/搜索