手寫vue-router & 什麼是Vue外掛

gogocj發表於2021-10-30

博文分享

這篇文章你可以學習到:

  • 實現一個自己的vue-router

  • 瞭解什麼是Vue的外掛

 

 

學習b站大佬後做的筆記整理和原始碼實現

1.1.3一步一步帶你弄懂vue-router核心原理及實現嗶哩嗶哩bilibili

 

使用官方的Vue-router

通過vue-cli腳手架初始化一個專案

 

下載vue-router

ps: vue-cli腳手架生成的時候可以選擇:是否安裝vue-router

下面是手動安裝過程:

  • 就是npm install vue-router之後,通過import引入了

  • 然後通過Vue.use() 引入

  • 之後定義一個路由表routes

  • 然後new VueRouter 就可以得到一個例項

  • 新建了Home和About兩個元件

得到程式碼:

router/index.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/home'
import About from '@/components/about'
​
Vue.use(Router)
​
export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
})

 

匯入到main.js中

import Vue from 'vue'
import App from './App'
import router from './router'
​
Vue.config.productionTip = falsenew Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

 

在new Vue新增這個配置項

 

使用router-link和router-view

App.vue

<template>
  <div id="app">
    <router-link to="/">home</router-link>
    <router-link to="/about">about</router-link>
    <router-view/>
  </div>
</template>

 

效果:

 

 

 

自己寫一個vue-router

老規矩,先上原始碼

沒註釋版本:

手寫vue-router & 什麼是Vue外掛
let Vue;
class VueRouter {
    constructor(options) {
        this.$options = options;
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);
        window.addEventListener("hashchange", () => {
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}
​
VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
    
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    href: `#${this.to}`
                },
            },
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;
            
            const current = this.$router.current;
​
            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )
​
            if (route) component = route.component;
​
            return h(component);
        }
    })
}
export default VueRouter;
View Code

有個人註釋版本:

手寫vue-router & 什麼是Vue外掛
// 1、實現一個外掛
// 2、兩個元件
// Vue外掛怎麼寫
// 外掛要麼是function 要麼就是 物件
// 要求外掛必須要實現一個install方法,將來被vue呼叫的
let Vue; // 儲存Vue的建構函式,在外掛中要使用
class VueRouter {
    constructor(options) {
        this.$options = options;
        // 只有把current變成響應式資料之後,才可以修改之後重新執行router-view中的render渲染函式的
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);
​
        window.addEventListener("hashchange", () => {
            // 獲取#後面的東西
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}
​
VueRouter.install = (_Vue) => {
    Vue = _Vue;
​
    // 1、掛載$router屬性(這個獲取不到router/index.js中new 出來的VueRouter例項物件,
    // 因為Vue.use要更快指向,所以就在main.js中引入router,才能使用的
    // this.$router.push()
    // 全域性混入(延遲下面的邏輯到router建立完畢並且附加到選項上時才執行)
    Vue.mixin({
        beforeCreate() {
            // 注意此鉤子在每個元件建立例項的時候都會被呼叫
            // 判斷根例項是否有該選項
            if (this.$options.router) {
                /**
                 * 因為每一個Vue的元件例項,都會繼承Vue.prototype上面的方法,所以這樣就可以
                 * 在每一個元件裡面都可以通過this.$router來訪問到router/index.js中初始化的new VueRouter例項了
                 */
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
    
    // 實現兩個元件:router-link、router-view
    // <router-link to="/">Hone</router-link> 所以我們要把這個router-link標籤轉換成:<a href="/">Home</a>
    /**
     * 第二個引數其實是一個template,也就是一個渲染元件dom
     * 我們這裡使用的是渲染函式,也就是返回一個虛擬DOM
     */
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // 為了不重新更新頁面,這裡通過錨點
                    href: `#${this.to}`
                },
            },
            // 如果要獲取Home的話,可以是下面這樣
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;
​
            // 由於上面通過混入拿到了this.$router了,所以就可以獲取當前路由所對應的元件並將其渲染出來
            const current = this.$router.current;
​
            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )
​
            if (route) component = route.component;
​
            return h(component);
        }
    })
}
export default VueRouter;
 
View Code

 

一步一步分析——從零開始

首先,有幾個問題

問題一:

router/index.js中

import Router from 'vue-router'
​
Vue.use(Router)

 

我們知道,通過Vue.use( ) 是個Vue引入了一個外掛

那麼這個外掛vue-router 內部做了什麼?

 

問題二:

router/index.js中

import Router from 'vue-router'
​
export default new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
})

 

  • 初始化了一個引入的vue-router外掛物件

  • 括號裡面傳入的是一個{ } 物件,其實就是一個配置項

    • 配置項裡面包含了一個routes路由表

之後在main.js中

import Vue from 'vue'
import App from './App'
import router from './router'
​
Vue.config.productionTip = falsenew Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

 

在new Vue例項的時候,把匯出的router作為了配置項傳入,這個又是為什麼?

 

問題三:router-link 和 router-view

  • 在元件中使用router-link元件實現路由跳轉

  • 使用router-view元件作為路由的出口

那麼,這兩個元件內部是怎麼樣實現的呢?

為什麼,其他元件都是要在Component裡面宣告才可以使用的,但是這兩個元件直接使用,就說明這兩個元件肯定在某個地方進行了全域性註冊

 

擴充:大概的思路:

其實在jquery中是這樣實現:就是監聽當前雜湊值hash的變換 或者是 history的變化,就可以得到一個觸發的事件,然後就可以拿到當前的地址了(就是要跳轉的地址),然後通過這個地址,就可以到我們router/index.js中定義的路由表,也就是匹配path,得到component,這樣就可以拿到元件了,然後就要拿到真實的DOM,,然後追加到我們的router-view裡面,也就是把之前的router-view裡面的內容清空掉,然後把最新的DOM壓入到router-view中進行顯示的,這個就是一個很典型的dom操作

但是vue中有一個新東西:Vue的響應式原理,所以就可以用響應式來監聽路由的變化

 

什麼是Vue的外掛

學習自:深入理解Vue的外掛機制與install詳細vue.js指令碼之家 (jb51.net)

  • 外掛內部為什麼要實現一個install方法

vue的外掛應該暴露出一個install方法,這個方法的e第一個引數是Vue構造器,第二個引數是一個可選的選項物件——這個是Vue官方對Vue外掛的規範,

install函式可以做些什麼?

install內部怎麼實現的?

外掛在install中到底做了什麼?

經典三連問~

 

install在vue-router等外掛中的處理

丟擲問題:

  1. 為什麼在專案中可以直接使用 $router 來獲取其中的值以及一些方法

  2. 為什麼這些外掛都要先用Vue.use 引入,然後才建立例項,並且之後在Vue例項中引入

 

使用vue-router舉例

class Router {
    constructor(options) {
        ...
    }
}
​
Router.install = function(_Vue) {
​
    _Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                _Vue.prototype.$router = this.$options.router
            }
        }
    })
​
}
​
export default Router;
  • _Vue.mixin全域性混入是什麼呢?相當於在所有的元件中混入這個方法;

  • beforeCreate是什麼呢?當然是Vue的一個生命週期,在create之前執行;

 

所以:

  1. Vue-Router是在install函式使用了一個全域性混入,在beforeCreate生命週期觸發的時候把this.$option.router掛載到Vue的原型上了,那麼這樣就可以使用this.$router來呼叫router例項啦

  2. 那麼this.$options.router又是什麼

    • 全域性混入中的this.$options是我們在 在main.js中 new Vue({})的時候 { } 大括號裡面傳入的配置項,所以我們main.js傳入的router,在這裡就可以通過this.$options.router來獲取到我們在router/index.js中new的vue-router例項了

    為什麼要這樣設計:因為在router/index.js中

    import Vue from 'vue'
    import Router from 'vue-router'
    import Home from '@/components/home'
    import About from '@/components/about'
    ​
    Vue.use(Router)
    ​
    export default new Router({
      routes: [
        {
          path: '/',
          name: 'Home',
          component: Home
        },
        {
          path: '/about',
          name: 'About',
          component: About
        }
      ]
    })

     

    是先執行了Vue.use 之後再進行new vue-router物件的操作,所以如果要在外掛的install中使用到這個vue-router例項的話,就要把例項傳入到main.js的new Vue({})配置項裡面,這樣的話,我們就可以用依賴注入的方式,把new Router({})裡面定義的路由表獲取到了,

    我們把 Vue.prototype.$router = this.$options.router; 所以其他元件就可以通過this.$router獲取訪問到我們定義的路由表了,所以為什麼可以用this.$router.push()新增路由,一部分的原因就是,this.$router路由表是一個陣列,所以可以通過push操作的

 

  • Vue.use的時候主要呼叫了 外掛內部的install方法,並把Vue例項作為了引數進行傳入

     

外掛install在vue中的內部實現

下面是Vue.use的原始碼

export function initUse (Vue: GlobalAPI) {
    // 註冊一個掛載在例項上的use方法
    Vue.use = function (plugin: Function | Object) {
        // 初始化當前外掛的陣列
        const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
        // 如果這個外掛已經被註冊過了,那就不作處理
        if (installedPlugins.indexOf(plugin) > -1) {
​
            return this
​
        }
​
        ...
        
        // 重點來了哦!!!
        if (typeof plugin.install === 'function') {
        // 當外掛中install是一個函式的時候,呼叫install方法,指向外掛,並把一眾引數傳入
            plugin.install.apply(plugin, args)
​
        } else if (typeof plugin === 'function') {
        // 當外掛本身就是一個函式的時候,把它當做install方法,指向外掛,並把一眾引數傳入
            plugin.apply(null, args)
​
        }
        
        // 將外掛放入外掛陣列中
        installedPlugins.push(plugin)
​
        return this
    }
}

看到這裡大家對外掛應該有了一定的認識了,堅持!!

 

開始實現

  • 首先:因為router/index 初始化了外掛的例項,所以該外掛可以用一個class表示,並且還要實現一個install方法

class VueRouter {
​
}
​
VueRouter.install = (_Vue) => {
​
}

 

上面也說了,外掛的install方法,第一個引數就是Vue例項本身

優化

後面其他地方也要用到vue例項的,所以我們就在外掛宣告一個全域性的vue,用來儲存這個傳入的vue例項

並且:也是一個保證外掛和vue的獨立性,有了這個操作之後,當我們打包該外掛的時候,就不會把vue也打包到外掛了

並且把從new Vue({router})的配置項router,掛載到Vue例項原型物件上

let Vue; 
class VueRouter {
​
}
​
VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    })
}

 

不僅如此,我們還在install函式中,實現了兩個元件 router-link 和 router-view

原理:

<router-link to="/">Home</router-link> 所以我們要把這個router-link標籤轉換成:Home

  • 接收一個to屬性

  • 並且返回的是一個render渲染函式,也就是返回一個虛擬DOM

 

那麼怎麼獲得router-link中間的文字Home呢?

擴充:Vue.$slots

img

所以因為router-link裡面只有home文字,所以可以直接通過 vue.$slots.default獲取即可了

 

let Vue;
class VueRouter {
​
}
​
VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
​
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // 為了不重新更新頁面,這裡通過錨點
                    href: `#${this.to}`
                },
            },
            // 如果要獲取Home的話,可以是下面這樣
                this.$slots.default
            );
        }
    });
}

 

上面就是router-link具體實現了

 

下面是router-view實現

原理:獲取到當前路由,並從路由表找到對應的component並進行渲染

注意:我們在install方法中,通過全域性混入,把在router/index.js中例項化的vue-router例項,掛載到了vue原型物件上的$router上了

  • 那麼:我們就可以在元件中通過this.$router來獲取到我們的例項化元件

 

下面就要實現:該外掛的類class怎麼實現

 

我們在router/index.js中,通過

new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/about',
      name: 'About',
      component: About
    }
  ]
}) 

 

傳入了一個路由表,作為這個外掛例項的配置項

所以就可以在該類的建構函式中,通過引數獲取到這個配置項了,為了可以在其他元件中獲取到路由表,我們把配置項掛載到該類本身

class VueRouter {
    constructor(options) {
        this.$options = options
    }
}

 

為什麼要這樣做?

這樣的話,在router-view這些元件中

就可以通過 this.$router.$options訪問到我們在router/index裡面new的vue-router類中傳入的配置項裡面的路由表了

 

class VueRouter {
    constructor(options) {
        this.$options = options
        this.current = window.location.hash.slice(1) || "/";
        window.addEventListener("hashchange", () => {
            // 獲取#後面的東西
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

  

初始化current,並通過onhashchange來監聽路由的變化,並賦值給current

通過slice(1)是為了獲取到#後面的值

 

這樣的話,就可以實現router-view元件了

let Vue;
class VueRouter {
    constructor(options) {
        this.$options = options
        this.current = window.location.hash.slice(1) || "/";
        window.addEventListener("hashchange", () => {
            // 獲取#後面的東西
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}
​
VueRouter.install = (_Vue) => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate() {
            if (this.$options.router) {
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
​
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // 為了不重新更新頁面,這裡通過錨點
                    href: `#${this.to}`
                },
            },
            // 如果要獲取Home的話,可以是下面這樣
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;
​
            // 由於上面通過混入拿到了this.$router了,所以就可以獲取當前路由所對應的元件並將其渲染出來
            const current = this.$router.current;
​
            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )
​
            if (route) component = route.component;
​
            return h(component);
        }
    })  
}

 

所以目前程式碼是這樣的

 

但是,我們可以發現current改變了,router-view不變,這是因為此時的current並不是一個響應式資料,所以current變化的時候,router-view裡面的render函式並不會再次執行並重新渲染

所以下面就要對class類裡面的current變成是響應式資料了

 

擴充:Vue.util.defineReactive

Vue.util.defineReactive(obj,key,value,fn)

obj: 目標物件,

key: 目標物件屬性;

value: 屬性值

fn: 只在node除錯環境下set時呼叫

其實底層就是一個Object.defineProperty()

依賴通過dep收集,通過Observer類,新增ob屬性

class VueRouter {
    constructor(options) {
        this.$options = options;
        // 只有把current變成響應式資料之後,才可以修改之後重新執行router-view中的render渲染函式的
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);
​
        window.addEventListener("hashchange", () => {
            // 獲取#後面的東西
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}

 

所以完整程式碼就是:

手寫vue-router & 什麼是Vue外掛
// 1、實現一個外掛
// 2、兩個元件
// Vue外掛怎麼寫
// 外掛要麼是function 要麼就是 物件
// 要求外掛必須要實現一個install方法,將來被vue呼叫的
let Vue; // 儲存Vue的建構函式,在外掛中要使用
class VueRouter {
    constructor(options) {
        this.$options = options;
        // 只有把current變成響應式資料之後,才可以修改之後重新執行router-view中的render渲染函式的
        let initial = window.location.hash.slice(1) || "/";
        Vue.util.defineReactive(this, "current", initial);
​
        window.addEventListener("hashchange", () => {
            // 獲取#後面的東西
            this.current = window.location.hash.slice(1) || "/";
        })
    }
}
​
VueRouter.install = (_Vue) => {
    Vue = _Vue;
​
    // 1、掛載$router屬性(這個獲取不到router/index.js中new 出來的VueRouter例項物件,
    // 因為Vue.use要更快指向,所以就在main.js中引入router,才能使用的
    // this.$router.push()
    // 全域性混入(延遲下面的邏輯到router建立完畢並且附加到選項上時才執行)
    Vue.mixin({
        beforeCreate() {
            // 注意此鉤子在每個元件建立例項的時候都會被呼叫
            // 判斷根例項是否有該選項
            if (this.$options.router) {
                /**
                 * 因為每一個Vue的元件例項,都會繼承Vue.prototype上面的方法,所以這樣就可以
                 * 在每一個元件裡面都可以通過this.$router來訪問到router/index.js中初始化的new VueRouter例項了
                 */
                Vue.prototype.$router = this.$options.router;
            }
        }
    });
    
    // 實現兩個元件:router-link、router-view
    // <router-link to="/">Hone</router-link> 所以我們要把這個router-link標籤轉換成:<a href="/">Home</a>
    /**
     * 第二個引數其實是一個template,也就是一個渲染元件dom
     * 我們這裡使用的是渲染函式,也就是返回一個虛擬DOM
     */
    Vue.component("router-link", {
        props: {
            to: {
                type: String,
                required: true,
            }
        },
        render(h) {
            return h("a",
            {
                attrs: {
                    // 為了不重新更新頁面,這裡通過錨點
                    href: `#${this.to}`
                },
            },
            // 如果要獲取Home的話,可以是下面這樣
                this.$slots.default
            );
        }
    });
    Vue.component("router-view", {
        render(h) {
            let component = null;
​
            // 由於上面通過混入拿到了this.$router了,所以就可以獲取當前路由所對應的元件並將其渲染出來
            const current = this.$router.current;
​
            const route = this.$router.$options.routes.find(
                (route) => route.path === current
            )
​
            if (route) component = route.component;
​
            return h(component);
        }
    })
}
export default VueRouter;
View Code

 

後面的一些優化,比如通過mode來改變模式(history、hash)上面是預設用了hash的,還有就是路由攔截器這些。

相關文章