這篇文章你可以學習到:
了解什么是Vue的插件
學習b站大佬后做的筆記整理和源碼實現
使用官方的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 = false new 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;
有個人注釋版本:

// 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;
一步一步分析——從零開始
首先,有幾個問題
問題一:
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 = false new 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的插件
學習自:
-
插件內部為什么要實現一個install方法
vue的插件應該暴露出一個install方法,這個方法的e第一個參數是Vue構造器,第二個參數是一個可選的選項對象——這個是Vue官方對Vue插件的規范,
install函數可以做些什么?
install內部怎么實現的?
插件在install中到底做了什么?
經典三連問~
install在vue-router等插件中的處理
拋出問題:
-
為什么在項目中可以直接使用 $router 來獲取其中的值以及一些方法
-
為什么這些插件都要先用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
之前執行;
所以:
-
Vue-Router是在install函數使用了一個全局混入,在beforeCreate生命周期觸發的時候把this.$option.router掛載到Vue的原型上了,那么這樣就可以使用this.$router來調用router實例啦
-
那么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
所以因為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;