這篇文章你可以學習到:
瞭解什麼是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;