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