Vue-Router原理剖析之hash模式(一)

一、起步需求分析

從添加router插件開始,在執行了vue add router命令後,項目目錄中會增加一個router目錄並在main.js中導入router選項。

先從router目錄下的index.js開始:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

// 註冊VueRouter插件
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  routes
})

// 導出router實例
export default router

從router的index.js文件可以看出,這裏做了三件事:

  1. 註冊了VueRouter插件
  2. 實例化了一個VueRouter類並將route配置傳入
  3. 導出VueRouter實例

在main.js文件中,vue-rotuer的修改:

  1. 導入router目錄下的index.js
  2. 在Vue實例化時將VueRouter實例當成選項傳入

表面上Vue-Router就做了這些事情,但在我們平常開發時,我們可以在每個組件中訪問$router,而在表面上我們並沒有發現Vue-Router在main.js中將$router掛載到Vue.prototype上,那說明**$router是在Vue-Router內部掛載**的。

還有一個就是我們平時在任意組件中都可以使用<router-link><router-view>組件,而這並不是Vue自身的組件,說明Vue-Router還聲明瞭兩個全局組件

所以這裏可以轉換成Vue-Router實現的起步需求就是實現一個Vue插件,該插件起步的功能有:

  • 掛載$rotuer
  • 聲明兩個全局組件router-linkrouter-view
  • 實現一個VueRouter類,實現hash模式下路由跳轉

二、需求實現

先創建一個vue-router.js文件,在文件中創建一個VueRouter類

2.1 實現掛載$router

掛載$router我們很容易想到在install方法去掛載,但這裏有個問題,就是在執行install方法時,Vue還未實例化,這裏並不能拿到router實例,Vue-Router的解析方法是在執行install方法時全局混入beforeCreate生命週期方法,等這個生命週期方法執行時,Vue是已經實例化了,就可以拿到router實例了。

新建vue-router.js

class VueRouter {
    
}

// Vue插件需要聲明一個install方法
VueRouter.install = function (Vue) {
    // 全局混入beforeCreate生命週期方法
    Vue.mixin({
        beforeCreate() {
            // 只要在Vue根實例中掛載,利用只有根實例選項上有router屬性找到根實例
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
}

2.2 聲明兩個全局組件

首先,我們要清楚在hash模式下,<router-link><router-view>組件到底有什麼用。

<router-link to="/about">About</router-link>

<router-view></router-view>

<router-link>組件接收一個to屬性,並將標籤中內容渲染,從瀏覽器中可以看出其實就是一個a標籤,to屬性改變的是a標籤的href屬性。

<router-view>組件的功能是在路由發生改變時,將對應路由的組件渲染到其中。

2.2.1 <router-link>組件

新建link.js

// 導出router-link組件配置對象
export default {
    name: 'router-link',
    props: {
        to: {
            type: String,
            default: ''
        }
    },
    render (h) {
        // 利用渲染函數,生成一個a標籤,href屬性值如 /#/about,將默認插槽作爲子元素
        return h('a', {attrs: {href: '#' + this.to}}, this.$slots.default)
    }
}

2.2.2 <router-view>組件

新建view.js

// 導出router-view組件配置對象
export default {
    name: 'router-view',
    render (h) {
        // component存放當前路由對應的組件
        let component = null
        
        return h(component)
    }
}

2.3 完善VueRouter類,實現hash模式下路由跳轉

因爲這是是用hash值的改變來實現地址修改,但其實頁面並不會刷新,這裏就需要手動去觸發渲染

vue-router.js完善

import Link from './link'
import View from './view'

let Vue = null

class VueRouter {
    constructor (options) {
        // 保存路由選項
        this.$options = options
        
        // 聲明一個響應式屬性,存放當前路由路徑
        Vue.util.defineReactive(this, 'currentPath', '/')
        
        // 監聽初始化和hash值變化時,改變當前路由路徑
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        window.addEventListener('load', this.onHashChange.bind(this))
        
        // 創建路由map,保存路由表
        this.routeMap = {}
        this.$options.routes.forEach(route => {
            this.routeMap[route.path] = route
        })
    }
    
    onHashChange () {
        // 這裏只是先簡單將 # 後的路徑賦值給currentPath
        this.currentPath = window.location.hash.slice(1)
    }
}

// Vue插件需要聲明一個install方法
VueRouter.install = function (_Vue) {
    // 保存Vue引用
    Vue = _Vue
    // 全局混入beforeCreate生命週期方法
    Vue.mixin({
        beforeCreate() {
            // 只要在Vue根實例中掛載,利用只有根實例選項上有router屬性找到根實例
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    
    // 註冊全局組件
    Vue.component('router-link', Link)
    Vue.component('router-view', View)
}

完善view.js

// 導出router-view組件配置對象
export default {
    name: 'router-view',
    render (h) {
        // component存放當前路由對應的組件
        let component = null
        
        // 因爲currentPath是響應式數據,在當前組件的render方法中使用該屬性後,當currentPath改變,render會重新執行
        // 根據$router上的currentPath的變化,動態匹配組件
        const {routeMap, currentPath} = this.$router
        component = routeMap[currentPath] ? routeMap[currentPath].component : null
        
        return h(component)
    }
}

三、實現嵌套路由

嵌套路由在路由配置時多了children數組,在父路由組件中需要添加<router-view>組件。

實現嵌套路由的原理就是:

  1. 把匹配hash路徑的路由及子路由push到matched數組中
  2. 利用當前匹配的<router-view>組件的深度得到depth,即可在matched中匹配到對應的路由

vue-router.js修改

import Link from './link'
import View from './view'

let Vue = null

class VueRouter {
    constructor (options) {
        // 保存路由選項
        this.$options = options
        
        // 聲明一個響應式數據,存放當前路由路徑
        // Vue.util.defineReactive(this, 'currentPath', '/')
        
        // 將matched聲明爲一個響應式數據,存放當前匹配的路由數組
        Vue.util.defineReactive(this, 'matched', [])
        
        // 監聽初始化和hash值變化時,改變當前路由路徑
        window.addEventListener('hashchange', this.onHashChange.bind(this))
        window.addEventListener('load', this.onHashChange.bind(this))
        
        // 創建路由map,保存路由表
        // this.routeMap = {}
        // this.$options.routes.forEach(route => {
        //    this.routeMap[route.path] = route
        // })
    }
    
    onHashChange () {
        // 這裏只是先簡單將 # 後的路徑賦值給currentPath
        // this.currentPath = window.location.hash.slice(1)
        
        // 獲取當前hash路徑
        this.current = window.location.hash.slice(1)
        
        this.matched = []
        // 匹配hash路徑對應的路由
        this.match()
    }
    
    match (routes) {
        routes = routes || this.$options.routes
        
        // 遍歷路由選項
        for (let route of routes) {
            if (route.path === '/' && this.current === '/') {
                this.matched.push(route)
                return
            }
            
            // 匹配嵌套路由的情況
            if (route.path !== '/' && this.current.indexOf(route.path) !== -1) {
                this.matched.push(route)
                if (route.children) {
                    this.match(route.children)
                }
                return
            }
        }
    }
}

// Vue插件需要聲明一個install方法
VueRouter.install = function (_Vue) {
    // 保存Vue引用
    Vue = _Vue
    // 全局混入beforeCreate生命週期方法
    Vue.mixin({
        beforeCreate() {
            // 只要在Vue根實例中掛載,利用只有根實例選項上有router屬性找到根實例
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router
            }
        }
    })
    
    // 註冊全局組件
    Vue.component('router-link', Link)
    Vue.component('router-view', View)
}

view.js修改

// 導出router-view組件配置對象
export default {
    name: 'router-view',
    render (h) {
        // 標記當前組件爲routerView
        this.$vnode.data.routerView = true
        
        // component存放當前路由對應的組件
        let component = null
        // 當前router-view組件的深度
        let depth = 0
        let parent = this.$parent
        
        // 向上遍歷是否還存在router-view組件,得到當前router-view的深度
        while(parent) {
            if (parent.$vnode.data && parent.$vnode.data.routerView) {
                depth++
            }  
            parent = parent.$parent
        }
        
        // 取出匹配路由的組件
        const matched = this.$router.matched[depth]
        component = matched && matched.component
        
        
        // 因爲currentPath是響應式數據,在當前組件的render方法中使用該屬性後,當currentPath改變,render會重新執行
        // 根據$router上的currentPath的變化,動態匹配組件
        // const {routeMap, currentPath} = this.$router
        // component = routeMap[currentPath] ? routeMap[currentPath].component : null
        
        return h(component)
    }
}

這裏的實現並沒有像官方一樣使用函數式組件

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章