手寫vue-router & 什么是Vue插件


博文分享

這篇文章你可以學習到:

  • 實現一個自己的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

老規矩,先上源碼

沒注釋版本:

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

有個人注釋版本:

// 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) || "/";
        })
    }
}

 

所以完整代碼就是:

// 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的,還有就是路由攔截器這些。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM