7種組件通信方式隨你選
組件通信是 Vue 的核心知識都毒,掌握這幾個知識點,面試開發(fā)一點問題都沒有账劲。
props/@on+$emit
用于實現(xiàn)父子組件間通信戳护。通過 props 可以把父組件的消息傳遞給子組件:
// parent.vue
<child :title="title"></child>
// child.vue
props: {
title: {
type: String,
default: '',
}
}
這樣一來 this.title 就直接拿到從父組件中傳過來的 title 的值了。注意瀑焦,你不應(yīng)該在子組件內(nèi)部直接改變 prop腌且,這里就不多贅述,可以直接看官網(wǎng)介紹榛瓮。
而通過 @on+$emit 組合可以實現(xiàn)子組件給父組件傳遞信息:
// parent.vue
<child @changeTitle="changeTitle"></child>
// child.vue
this.$emit('changeTitle', 'bubuzou.com')
attrs和listeners
Vue_2.4 中新增的 listeners 可以進(jìn)行跨級的組件通信铺董。$attrs 包含了父級作用域中不作為 prop 的屬性綁定(class 和 style 除外),好像聽起來有些不好理解禀晓?沒事精续,看下代碼就知道是什么意思了:
// 父組件 index.vue
<list class="list-box" title="標(biāo)題" desc="描述" :list="list"></list>
// 子組件 list.vue
props: {
list: [],
},
mounted() {
console.log(this.$attrs) // {title: "標(biāo)題", desc: "描述"}
}
在上面的父組件 index.vue 中我們給子組件 list.vue 傳遞了4個參數(shù)坝锰,但是在子組件內(nèi)部 props 里只定義了一個 list,那么此時 this.$attrs 的值是什么呢重付?首先要去除 props 中已經(jīng)綁定了的顷级,然后再去除 class 和 style,最后剩下 title 和 desc 結(jié)果和打印的是一致的确垫」保基于上面代碼的基礎(chǔ)上,我們在給 list.vue 中加一個子組件:
// 子組件 list.vue
<detail v-bind="$attrs"></detial>
// 孫子組件 detail.vue
// 不定義props森爽,直接打印 $attrs
mounted() {
console.log(this.$attrs) // {title: "標(biāo)題", desc: "描述"}
}
在子組件中我們定義了一個 v-bind="$attrs" 可以把父級傳過來的參數(shù)恨豁,去除 props爬迟、class 和 style 之后剩下的繼續(xù)往下級傳遞付呕,這樣就實現(xiàn)了跨級的組件通信徽职。
listeners 用類似的操作方式可以進(jìn)行跨級的事件傳遞埂伦,實現(xiàn)子到父的通信沾谜。listeners" 傳遞到子組件內(nèi)部涩僻。
// 父組件 index.vue
<list @change="change" @update.native="update"></list>
// 子組件 list.vue
<detail v-on="$listeners"></detail>
// 孫子組件 detail.vue
mounted() {
this.$listeners.change()
this.$listeners.update() // TypeError: this.$listeners.update is not a function
}
provide/inject組合拳
provide/inject 組合以允許一個祖先組件向其所有子孫后代注入一個依賴嵌巷,可以注入屬性和方法室抽,從而實現(xiàn)跨級父子組件通信坪圾。在開發(fā)高階組件和組件庫的時候尤其好用兽泄。
// 父組件 index.vue
data() {
return {
title: 'bubuzou.com',
}
}
provide() {
return {
detail: {
title: this.title,
change: (val) => {
console.log( val )
}
}
}
}
// 孫子組件 detail.vue
inject: ['detail'],
mounted() {
console.log(this.detail.title) // bubuzou.com
this.detail.title = 'hello world' // 雖然值被改變了病梢,但是父組件中 title 并不會重新渲染
this.detail.change('改變后的值') // 執(zhí)行這句后將打域涯啊:改變后的值
}
provide 和 inject 的綁定對于原始類型來說并不是可響應(yīng)的钮热。這是刻意為之的。然而飒责,如果你傳入了一個可監(jiān)聽的對象宏蛉,那么其對象的 property 還是可響應(yīng)的。這也就是為什么在孫子組件中改變了 title蚌讼,但是父組件不會重新渲染的原因篡石。
EventBus
以上三種方式都是只能從父到子方向或者子到父方向進(jìn)行組件的通信凰萨,而我就比較牛逼了??胖眷,我還能進(jìn)行兄弟組件之間的通信,甚至任意2個組件間通信冶忱。利用 Vue 實例實現(xiàn)一個 EventBus 進(jìn)行信息的發(fā)布和訂閱囚枪,可以實現(xiàn)在任意2個組件之間通信链沼。有兩種寫法都可以初始化一個 eventBus 對象:
通過導(dǎo)出一個 Vue 實例括勺,然后再需要的地方引入:
// eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()
使用 EventBus 訂閱和發(fā)布消息:
import {EventBus} from '../utils/eventBus.js'
// 訂閱處
EventBus.$on('update', val => {})
// 發(fā)布處
EventBus.$emit('update', '更新信息')
在 main.js 中初始化一個全局的事件總線:
// main.js
Vue.prototype.$eventBus = new Vue()
使用:
// 需要訂閱的地方
this.$eventBus.$on('update', val => {})
// 需要發(fā)布信息的地方
this.$eventBus.$emit('update', '更新信息')
如果想要移除事件監(jiān)聽朝刊,可以這樣來:
this.$eventBus.$off('update', {})
上面介紹了兩種寫法拾氓,推薦使用第二種全局定義的方式咙鞍,可以避免在多處導(dǎo)入 EventBus 對象续滋。這種組件通信方式只要訂閱和發(fā)布的順序得當(dāng)疲酌,且事件名稱保持唯一性朗恳,理論上可以在任何 2 個組件之間進(jìn)行通信粥诫,相當(dāng)?shù)膹?qiáng)大崭庸。但是方法雖好,可不要濫用镰踏,建議只用于簡單搀玖、少量業(yè)務(wù)的項目中灌诅,如果在一個大型繁雜的項目中無休止的使用該方法猜拾,將會導(dǎo)致項目難以維護(hù)挎袜。
Vuex進(jìn)行全局的數(shù)據(jù)管理
Vuex 是一個專門服務(wù)于 Vue.js 應(yīng)用的狀態(tài)管理工具盯仪。適用于中大型應(yīng)用全景。Vuex 中有一些專有概念需要先了解下:
State:用于數(shù)據(jù)的存儲爸黄,是 store 中的唯一數(shù)據(jù)源;
Getter:類似于計算屬性梆奈,就是對 State 中的數(shù)據(jù)進(jìn)行二次的處理亩钟,比如篩選和對多個數(shù)據(jù)進(jìn)行求值等清酥;
Mutation:類似事件,是改變 Store 中數(shù)據(jù)的唯一途徑,只能進(jìn)行同步操作;
Action:類似 Mutation荸频,通過提交 Mutation 來改變數(shù)據(jù)旭从,而不直接操作 State和悦,可以進(jìn)行異步操作鸽素;
Module:當(dāng)業(yè)務(wù)復(fù)雜的時候馍忽,可以把 store 分成多個模塊遭笋,便于維護(hù)瓦呼;
對于這幾個概念有各種對應(yīng)的 map 輔助函數(shù)用來簡化操作吵血,比如 mapState偷溺,如下三種寫法其實是一個意思挫掏,都是為了從 state 中獲取數(shù)據(jù)尉共,并且通過計算屬性返回給組件使用袄友。
computed: {
count() {
return this.$store.state.count
},
...mapState({
count: state => state.count
}),
...mapState(['count']),
},
又比如 mapMutations剧蚣, 以下兩種函數(shù)的定義方式要實現(xiàn)的功能是一樣的,都是要提交一個 mutation 去改變 state 中的數(shù)據(jù):
methods: {
increment() {
this.$store.commit('increment')
},
...mapMutations(['increment']),
}
接下來就用一個極簡的例子來展示 Vuex 中任意2個組件間的狀態(tài)管理饶碘。1馒吴、 新建 store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++
},
decrement(state) {
state.count--
}
},
})
2豪治、 創(chuàng)建一個帶 store 的 Vue 實例
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './utils/store'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
3扯罐、 任意組件 A 實現(xiàn)點擊遞增
<template>
<p @click="increment">click to increment:{{count}}</p>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
export default {
computed: {
...mapState(['count'])
},
methods: {
...mapMutations(['increment'])
},
}
</script>
4、 任意組件 B 實現(xiàn)點擊遞減
<template>
<p @click="decrement">click to decrement:{{count}}</p>
</template>
<script>
import {mapState, mapMutations} from 'vuex'
export default {
computed: {
...mapState(['count'])
},
methods: {
...mapMutations(['decrement'])
},
}
</script>
以上只是用最簡單的 vuex 配置去實現(xiàn)組件通信齿椅,當(dāng)然真實項目中的配置肯定會更復(fù)雜涣脚,比如需要對 State 數(shù)據(jù)進(jìn)行二次篩選會用到 Getter遣蚀,然后如果需要異步的提交那么需要使用 Action芭梯,再比如如果模塊很多玖喘,可以將 store 分模塊進(jìn)行狀態(tài)管理累奈。對于 Vuex 更多復(fù)雜的操作還是建議去看Vuex 官方文檔澎媒,然后多寫例子波桩。
Vue.observable實現(xiàn)mini vuex
這是一個 Vue2.6 中新增的 API镐躲,用來讓一個對象可以響應(yīng)。我們可以利用這個特點來實現(xiàn)一個小型的狀態(tài)管理器。
// store.js
import Vue from 'vue'
export const state = Vue.observable({
count: 0,
})
export const mutations = {
increment() {
state.count++
}
decrement() {
state.count--
}
}
// parent.vue
<template>
<p>{{ count }}</p>
</template>
<script>
import { state } from '../store'
export default {
computed: {
count() {
return state.count
}
}
}
</script>
// child.vue
import { mutations } from '../store'
export default {
methods: {
handleClick() {
mutations.increment()
}
}
}
refs/children/parent/root
通過給子組件定義 ref 屬性可以使用 $refs 來直接操作子組件的方法和屬性桥滨。
<child ref="list"></child>
比如子組件有一個 getList 方法齐媒,可以通過如下方式進(jìn)行調(diào)用喻括,實現(xiàn)父到子的通信:
this.$refs.list.getList()
除了 $refs 外贫奠,其他3個都是自 Vue 實例創(chuàng)建后就會自動包含的屬性唤崭,使用和上面的類似谢肾。
6類可以掌握的修飾符
表單修飾符
表單類的修飾符都是和 v-model 搭配使用的芦疏,比如:v-model.lazy酸茴、v-model.trim 以及 v-model.number 等薪捍。
.lazy:對表單輸入的結(jié)果進(jìn)行延遲響應(yīng),通常和 v-model 搭配使用与倡。正常情況下在 input 里輸入內(nèi)容會在 p 標(biāo)簽里實時的展示出來纺座,但是加上 .lazy 后則需要在輸入框失去焦點的時候才觸發(fā)響應(yīng)净响。
<input type="text" v-model.lazy="name" />
<p>{{ name }}</p>
.trim:過濾輸入內(nèi)容的首尾空格,這個和直接拿到字符串然后通過 str.trim() 去除字符串首尾空格是一個意思赞别。
.number:如果輸入的第一個字符是數(shù)字仿滔,那就只能輸入數(shù)字崎页,否則他輸入的就是普通字符串飒焦。
事件修飾符
Vue 的事件修飾符是專門為 v-on 設(shè)計的牺荠,可以這樣使用:@click.stop="handleClick"志电,還能串聯(lián)使用:@click.stop.prevent="handleClick"挑辆。
<div @click="doDiv">
click div
<p @click="doP">click p</p>
</div>
.stop:阻止事件冒泡鱼蝉,和原生 event.stopPropagation() 是一樣的效果魁亦。如上代碼洁奈,當(dāng)點擊 p 標(biāo)簽的時候绞灼,div 上的點擊事件也會觸發(fā)低矮,加上 .stop 后事件就不會往父級傳遞,那父級的事件就不會觸發(fā)了昨悼。
.prevent:阻止默認(rèn)事件率触,和原生的 event.preventDefault() 是一樣的效果葱蝗。比如一個帶有 href 的鏈接上添加了點擊事件,那么事件觸發(fā)的時候也會觸發(fā)鏈接的跳轉(zhuǎn)陆馁,但是加上 .prevent 后就不會觸發(fā)鏈接跳轉(zhuǎn)了。
.capture:默認(rèn)的事件流是:捕獲階段-目標(biāo)階段-冒泡階段击狮,即事件從最具體目標(biāo)元素開始觸發(fā)彪蓬,然后往上冒泡档冬。而加上 .capture 后則是反過來桃纯,外層元素先觸發(fā)事件态坦,然后往深層傳遞伞梯。
.self:只觸發(fā)自身的事件谜诫,不會傳遞到父級喻旷,和 .stop 的作用有點類似。
.once:只會觸發(fā)一次該事件伟阔。
.passive:當(dāng)頁面滾動的時候就會一直觸發(fā) onScroll 事件皱炉,這個其實是存在性能問題的合搅,尤其是在移動端灾部,當(dāng)給他加上 .passive 后觸發(fā)的就不會那么頻繁了惯退。
.native:現(xiàn)在在組件上使用 v-on 只會監(jiān)聽自定義事件 (組件用 $emit 觸發(fā)的事件)催跪。如果要監(jiān)聽根元素的原生事件懊蒸,可以使用 .native 修飾符骑丸,比如如下的 el-input通危,如果不加 .native 當(dāng)回車的時候就不會觸發(fā) search 函數(shù)黄鳍。
<el-input type="text" v-model="name" @keyup.enter.native="search"></el-input>
?
串聯(lián)使用事件修飾符的時候框沟,需要注意其順序忍燥,同樣2個修飾符進(jìn)行串聯(lián)使用梅垄,順序不同,結(jié)果大不一樣欲鹏。@click.prevent.self 會阻止所有的點擊事件赔嚎,而 @click.self.prevent 只會阻止對自身元素的點擊尤误。
?
鼠標(biāo)按鈕修飾符
.left:鼠標(biāo)左鍵點擊损晤;
.right:鼠標(biāo)右鍵點擊红竭;
.middle:鼠標(biāo)中鍵點擊德崭;
鍵盤按鍵修飾符
Vue 提供了一些常用的按鍵碼:
.enter
.tab
.delete (捕獲“刪除”和“退格”鍵)
.esc
.space
.up
.down
.left
.right
另外眉厨,你也可以直接將 KeyboardEvent.key 暴露的任意有效按鍵名轉(zhuǎn)換為 kebab-case 來作為修飾符憾股,比如可以通過如下的代碼來查看具體按鍵的鍵名是什么:
<input @keyup="onKeyUp">
onKeyUp(event) {
console.log(event.key) // 比如鍵盤的方向鍵向下就是 ArrowDown
}
.exact修飾符
.exact 修飾符允許你控制由精確的系統(tǒng)修飾符組合觸發(fā)的事件服球。
<!-- 即使 Alt 或 Shift 被一同按下時也會觸發(fā) -->
<button v-on:click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的時候才觸發(fā) -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>
<!-- 沒有任何系統(tǒng)修飾符被按下的時候才觸發(fā) -->
<button v-on:click.exact="onClick">A</button>
.sync修飾符
.sync 修飾符常被用于子組件更新父組件數(shù)據(jù)斩熊。直接看下面的代碼:
// parent.vue
<child :title.sync="title"></child>
// child.vue
this.$emit('update:title', 'hello')
子組件可以直接通過 update:title 的形式進(jìn)行更新父組件中聲明了 .sync 的 prop粉渠。上面父組件中的寫法其實是下面這種寫法的簡寫:
<child :title="title" @update:title="title = $event"></child>
注意帶有 .sync 修飾符的 v-bind 不能和表達(dá)式一起使用
如果需要設(shè)置多個 prop霸株,比如:
<child :name.sync="name" :age.sync="age" :sex.sync="sex"></child>
可以通過 v-bind.sync 簡寫成這樣:
<child v-bind.sync="person"></child>
person: {
name: 'bubuzou',
age: 21,
sex: 'male',
}
Vue 內(nèi)部會自行進(jìn)行解析把 person 對象里的每個屬性都作為獨立的 prop 傳遞進(jìn)去去件,各自添加用于更新的 v-on 監(jiān)聽器倔叼。而從子組件進(jìn)行更新的時候還是保持不變缀雳,比如:
this.$emit('update:name', 'hello')
6種方式編寫可復(fù)用模塊
今天需求評審了一個需求梢睛,需要實現(xiàn)一個詳情頁,這個詳情頁普通用戶和管理員都能進(jìn)去深碱,但是展示的數(shù)據(jù)有稍有不同敷硅,但絕大部分是一樣的愉阎;最主要的區(qū)別是詳情對于普通用戶是純展示榜旦,而對于管理員要求能夠編輯溅呢,然后管理員還有一些別的按鈕權(quán)限等咐旧。需求看到這里铣墨,如果在排期的時候把用戶的詳情分給開發(fā)A做伊约,而把管理員的詳情分給B去做,那這樣做的結(jié)果就是開發(fā)A寫了一個詳情頁肉盹,開發(fā)B寫了一個詳情頁上忍,這在開發(fā)階段窍蓝、提測后的修改 bug 階段以及后期迭代階段吓笙,都需要同時維護(hù)這 2 個文件面睛,浪費了時間浪費了人力,所以你可以從中意識到編寫可復(fù)用模塊的重要性土涝。
而 Vue 作者尤大為了讓開發(fā)者更好的編寫可復(fù)用模塊但壮,提供了很多的手段蜡饵,比如:組件溯祸、自定義指令您没、渲染函數(shù)胆绊、插件以及過濾器等压状。
組件
組件是 Vue 中最精髓的地方种冬,也是我們平時編寫可復(fù)用模塊最常用的手段娱两,但是由于這塊內(nèi)容篇幅很多十兢,所以不在這里展開旱物,后續(xù)會寫相關(guān)的內(nèi)容進(jìn)行詳述宵呛。
使用混入mixins
什么是混入呢宝穗?從代碼結(jié)構(gòu)上來看,混入其實就是半個組件虎忌,一個 Vue 組件可以包括 template膜蠢、script 和 style 三部分挑围,而混入其實就是 script 里面的內(nèi)容杉辙。一個混入對象包含任意組件選項蜘矢,比如 data品腹、methods舞吭、computed羡鸥、watch 惧浴、生命周期鉤子函數(shù)衷旅、甚至是 mixins 自己等,混入被設(shè)計出來就是旨在提高代碼的靈活性叙量、可復(fù)用性绞佩。
什么時候應(yīng)該使用混入呢品山?當(dāng)可復(fù)用邏輯只是 JS 代碼層面的肘交,而無 template 的時候就可以考慮用混入了涯呻。比如需要記錄用戶在頁面的停留的時間复罐,那我們就可以把這段邏輯抽出來放在 mixins 里:
// mixins.js
export const statMixin = {
methods: {
enterPage() {},
leavePage() {},
},
mounted() {
this.enterPage()
},
beforeDestroyed() {
this.leavePage()
}
}
然后在需要統(tǒng)計頁面停留時間的地方加上:
import { statMixin } from '../common/mixins'
export default {
mixins: [statMixin]
}
使用混入的時候要注意和組件選項的合并規(guī)則效诅,可以分為如下三類:
data 將進(jìn)行遞歸合并乱投,對于鍵名沖突的以組件數(shù)據(jù)為準(zhǔn):
// mixinA 的 data
data() {
obj: {
name: 'bubuzou',
},
}
// component A
export default {
mixins: [mixinA],
data(){
obj: {
name: 'hello',
age: 21
},
},
mounted() {
console.log( this.obj ) // { name: 'bubuzou', 'age': 21 }
}
}
對于生命周期鉤子函數(shù)將會合并成一個數(shù)組戚炫,混入對象的鉤子將先被執(zhí)行:
// mixin A
const mixinA = {
created() {
console.log( '第一個執(zhí)行' )
}
}
// mixin B
const mixinB = {
mixins: [mixinA]
created() {
console.log( '第二個執(zhí)行' )
}
}
// component A
export default {
mixins: [mixinB]
created() {
console.log( '最后一個執(zhí)行' )
}
}
值為對象的選項嘹悼,例如 methods、components 和 directives萌腿,將被合并為同一個對象抖苦。兩個對象鍵名沖突時,取組件對象的鍵值對峦筒。
自定義指令
除了 Vue 內(nèi)置的一些指令比如 v-model物喷、v-if 等峦失,Vue 還允許我們自定義指令尉辑。在 Vue2.0 中隧魄,代碼復(fù)用和抽象的主要形式是組件隘蝎。然而末贾,有的情況下拱撵,你仍然需要對普通 DOM 元素進(jìn)行底層操作拴测,這時候就會用到自定義指令集索。比如我們可以通過自定義一個指令來控制按鈕的權(quán)限。我們期望設(shè)計一個如下形式的指令來控制按鈕權(quán)限:
<button v-auth="['user']">提交</button>
通過在按鈕的指令里傳入一組權(quán)限妆距,如果該按鈕只有 admin 權(quán)限才可以提交娱据,而我們傳入一個別的權(quán)限中剩,比如 user结啼,那這個按鈕就不應(yīng)該顯示了郊愧。接下來我們?nèi)プ砸粋€全局的指令:
// auth.js
const AUTH_LIST = ['admin']
function checkAuth(auths) {
return AUTH_LIST.some(item => auths.includes(item))
}
function install(Vue, options = {}) {
Vue.directive('auth', {
inserted(el, binding) {
if (!checkAuth(binding.value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
})
}
export default { install }
然后我們需要在 main.js 里通過安裝插件的方式來啟用這個指令:
import Auth from './utils/auth'
Vue.use(Auth)
使用渲染函數(shù)
這里將使用渲染函數(shù)實現(xiàn)上面介紹過的的權(quán)限按鈕动分。使用方式如下红选,把需要控制權(quán)限的按鈕包在權(quán)限組件 authority 里面喇肋,如果有該權(quán)限就顯示蝶防,沒有就不顯示间学。
<authority :auth="['admin']">
<button>提交</button>
</authority>
然后我們用渲染函數(shù)去實現(xiàn)一個 authority 組件:
<script>
const AUTH_LIST = ['admin', 'user', 'org']
function checkAuth(auths) {
return AUTH_LIST.some(item => auths.includes(item))
}
export default {
functional: true,
props: {
auth: {
type: Array,
required: true
}
},
render(h, context) {
const { props, scopedSlots} = context
return checkAuth(props.auth) ? scopedSlots.default() : null
}
}
</script>
全局注冊這個組件:
// main.js
import Authority from './components/authority'
Vue.component('authority', Authority)
使用過濾器
Vue 提供了自定義過濾器的功能详羡,主要應(yīng)用場景是想要將數(shù)據(jù)以某種格式展示出來实柠,而原始數(shù)據(jù)又不符合這種格式的時候窒盐。比如有一組關(guān)于人的數(shù)據(jù)蟹漓,如下:
[{
name: '張茂',
population: 'young',
}, {
name: '王麗',
population: 'middle',
}, {
name: '郝鵬程',
population: 'child',
}]
其中有一項是關(guān)于按照年齡劃分的群體類型 population牧牢,而它是用 code 進(jìn)行標(biāo)識的塔鳍,我們希望在展示的時候能夠顯示成對應(yīng)的中文意思轮纫,比如 young 顯示成青年掌唾。那我們就可以定義一個如下的局部過濾器:
export default {
filters: {
popuFilters(value) {
if (!value) { return '未知' }
let index = ['child', 'lad', 'young', 'middle', 'wrinkly'].indexOf(value)
return index > 0 && ['兒童', '少年', '青年', '中年', '老年'][index] || '未知'
}
}
}
使用過濾器的時候只要在 template 中這樣使用即可:
<p>{{ item.population | popuFilters }}</p>
自定義插件
在某些情況下糯彬,我們封裝的內(nèi)容可能不需要使用者對其內(nèi)部代碼結(jié)構(gòu)進(jìn)行了解撩扒,其只需要熟悉我們提供出來的相應(yīng)方法和 api 即可搓谆,這需要我們更系統(tǒng)性的將公用部分邏輯封裝成插件泉手,來為項目添加全局功能斩萌,比如常見的 loading 功能术裸、彈框功能等袭艺。
開發(fā) Vue 的插件應(yīng)該暴露一個 install 方法猾编。這個方法的第一個參數(shù)是 Vue 構(gòu)造器答倡,第二個參數(shù)是一個可選的選項對象瘪撇【蠹龋可以通過如下4種方式來自定義插件:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 邏輯...
}
// 2. 添加全局資源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 邏輯...
}
...
})
// 3. 注入組件選項
Vue.mixin({
created: function () {
// 邏輯...
}
...
})
// 4. 添加實例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 邏輯...
}
}
然后需要在入口文件渤涌,比如 main.js 中注冊插件:
import MyPlugin from './plugins/plugins.js'
Vue.use(MyPlugin)
3種方式手寫優(yōu)雅代碼
平時寫項目的時候我們都是在第一時間完成需求功能的開發(fā)实蓬、提測修改 bug 等安皱,然后開開心心的等待著發(fā)布生產(chǎn)以為沒啥事情了酌伊。其實回過頭來細(xì)細(xì)的看我們平時寫的代碼腺晾,可能會發(fā)現(xiàn)很多地方都是值得優(yōu)化的悯蝉,比如對于很多重復(fù)性很強(qiáng)的代碼鼻由,比如對于某些寫得很繁雜的地方蕉世。優(yōu)雅的代碼可以化機(jī)械為自動狠轻、化繁為簡向楼,看人開了如沐春風(fēng)湖蜕,心情大好。這里列了幾個在 Vue 中一定會遇到的問題评也,然后通過優(yōu)雅的方式進(jìn)行解決盗迟。
自動化導(dǎo)入模塊
在開發(fā)一個稍微大點的項目的時候诈乒,會習(xí)慣將路由按照模塊來劃分怕磨,然后就可能會出現(xiàn)如下這種代碼:
// router.js
import Vue from 'vue'
import Router from 'vue-router'
// 導(dǎo)入了一大堆路由文件
import mediator from './mediator'
import judges from './judges'
import disputeMediation from './disputeMediation'
import onlineMediation from './onlineMediation'
import useraction from './useraction'
import organcenter from './organcenter'
import admin from './admin'
let routeList = []
routeList.push(mediator, judges, disputeMediation, onlineMediation, useraction, organcenter, admin)
export default new Router({
mode: 'history',
routes: routeList,
})
其實真實的遠(yuǎn)遠(yuǎn)不止這么點肠鲫,就我本地項目而言就有20幾個路由文件导饲,寫了一大堆的導(dǎo)入代碼渣锦,顯得很臃腫袋毙,更無奈的是每當(dāng)需要新增一個路由模塊听盖,還得再次 import 再次 push皆看,那么有沒有什么辦法可以解決這個問題呢腰吟?答案自然是有的毛雇。
利用 webpack 的 require.context 就可以很優(yōu)雅的解決這個問題禾乘,使用語法如下:
require.context(
directory, // 搜索的目錄
useSubdirectories = true, // 是否搜索子目錄
regExp = /^\.\/.*$/, // 匹配的目標(biāo)文件格式
mode = 'sync' // 同步還是異步
)
有了這個語法始藕,我們就能很容易的寫出下面的代碼:
import Vue from 'vue'
import Router from 'vue-router'
let routeList = []
let importAll = require.context('@/publicResource/router', false, /\.js$/)
importAll.keys().map(path => {
// 因為 index.js 也在 @/publicResource/router 目錄下伍派,所以需要排除
if (!path.includes('index.js')) {
//兼容處理:.default 獲取 ES6 規(guī)范暴露的內(nèi)容; 后者獲取 commonJS 規(guī)范暴露的內(nèi)容
let router = importAll(path).default || importAll(path)
routeList(router)
}
})
export default new Router({
mode: 'history',
routes: routeList,
})
其實不僅僅只是用在導(dǎo)入路由模塊這里诉植,對于項目里任何需要導(dǎo)入大量本地模塊的地方都可以使用這種方式來解決。
模塊化注冊插件
相信寫 Vue 的同學(xué)們都知道 element-ui 這個組件庫舌稀,在使用這個組件庫的時候大部分都是只使用某些個別的組件壁查,所以基本上都是按需引入需要的組件睡腿,然后就有如下一堆 Vue.use() 的代碼:
// main.js
import Vue from 'vue'
import {
Input,
Radio,
RadioGroup,
Checkbox,
CheckboxGroup,
Select
// 還有很多組件
} from 'element-ui'
Vue.use(Input)
Vue.use(Radio)
Vue.use(RadioGroup)
Vue.use(Checkbox)
Vue.use(CheckboxGroup)
Vue.use(Select)
這樣寫是沒任何問題的席怪,就是看著不夠簡潔舒服挂捻,那更優(yōu)雅的做法是把這塊邏輯抽到一個文件里细层,然后通過注冊插件的方式來使用他們:
// elementComponent.js
import {
Input,
Radio,
RadioGroup,
Checkbox,
CheckboxGroup,
Select
// 還有很多組件
} from 'element-ui'
const components = {
Input,
Radio,
RadioGroup,
Checkbox,
CheckboxGroup,
Select
}
function install(Vue){
Object.keys(components).forEach(key => Vue.use(components[key]))
}
export default { install }
然后在 main.js 里使用這個插件:
// main.js
import Vue from 'vue'
import elementComponent from './config/elementComponent'
Vue.use(elementComponent)
優(yōu)雅導(dǎo)出請求接口
不知道大伙是如何定義請求接口的疫赎,就我目前這個項目而言捧搞,是這么做的:
// api.js
import http from './config/httpServer.js'
/* 登入頁面獲取公鑰 */
export const getPublicKey = (data) => {
return http({ url: '/userGateway/user/getPublicKey' }, data)
}
// 用戶登錄
export const login = data => {
return http({ url: '/userGateway/userSentry/login' }, data)
}
// 驗證碼登錄
export const loginByCode = data => {
return http({ url: '/userGateway/userSentry/loginByCode' }, data)
}
在組件中使用接口:
<script>
import { getPublicKey } from './config/api.js'
export default {
mounted() {
getPublicKey().then(res => {
// xxx
}).catch(err => {
// xxx
})
}
}
</script>
這一切都很正常胎撇,但晚树,我們這個項目總共有200多個接口爵憎,按照上面這種定義方式的話,一個接口定義加上空行需要占用 5 行刑棵,所以如果把全部接口都定義到這個 api.js 里需要占用 1000 行左右蛉签,看了實在讓人心很慌呀碍舍。所以覺得應(yīng)該這個地方應(yīng)該可以優(yōu)化一下乒验。
/userGateway/user/getPublicKey
上面這是一個后端給接口路徑蒂阱,斜桿把這個路徑劃分成 3 個子串录煤,而最后一個子串必定是唯一的妈踊,所以我們可以從中做文章廊营。于是乎就有了下面的代碼:
// api.js
const apiList = [
'/userGateway/user/getPublicKey', // 登入頁面獲取公鑰
'/userGateway/userSentry/login', // 用戶登錄
'/userGateway/userSentry/loginByCode', // 驗證碼登錄
]
let apiName, API = {}
apiList.forEach(path => {
// 使用正則取到接口路徑的最后一個子串露筒,比如: getPublicKey
apiName = /(?<=\/)[^/]+$/.exec(path)[0]
API[apiName] = (data) => {
return http({url: path}, data)
}
})
export { API }
這樣大概就把定義一個接口需要占用 5 行縮小到只需要 1 行了慎式,大大減小了文件內(nèi)容瘪吏。在瀏覽這個文件的時候掌眠,我的鼠標(biāo)滾輪也不會一直在滾滾滾了蓝丙。
如果是這樣定義接口的話,那在使用的時候還需要做點變化的:
<script>
import { API } from './config/api.js'
export default {
mounted() {
API.getPublicKey().then(res => {
// xxx
}).catch(err => {
// xxx
})
}
}
</script>
4種$event傳參方式
在進(jìn)行實際項目開發(fā)的時候經(jīng)常會需要通過事件傳遞參數(shù)装畅,這里總結(jié)了4種應(yīng)用場景掠兄。
用于組件通信
比如子組件通過 event 接收到從子組件傳遞過來的參數(shù):
// 子組件
<button @click="$emit('changeText', '18px')">點擊加大字號</button>
// 父組件
<blog-post @changeText="changeText('article', $event)"></blog-post>
changeText(type, value) {
console.log(type, value) // 'article' '18px'
}
如果子組件傳遞過來的參數(shù)有多個婿牍,這個時候用 $event 就不太行了等脂,此時可以用 arguments 代替:
// 子組件
<button @click="$emit('changeText', 'red', '18px')">點擊改變樣式</button>
// 父組件
<blog-post @changeText="changeText(...arguments, 'article')"></blog-post>
changeText(...value) {
console.log( value ) // ['red', '18px', 'article']
}
傳遞原生DOM事件對象
比如我們需要獲取到當(dāng)前的點擊元素上遥,就可以通過給點擊事件傳遞 $event 參數(shù):
<button @click="submit('first', $event)">提交</button>
submit(type, event) {
const target = event.target.tagName
}
用于第三方類庫事件回調(diào)
比如有一個組件里使用了好幾個 element-ui 的分頁組件粉楚,每個分頁都有一個 current-change 事件模软,用來處理當(dāng)分頁改變之后的事情燃异,這樣的話我們就需要寫多個回調(diào)函數(shù)特铝,但是如果用以下方式,我們就也可以只寫一個函數(shù)稻轨,通過 type 來判斷是哪個分頁的回調(diào)殴俱,而 $event 則用來傳遞 current-change 回調(diào)默認(rèn)的參數(shù):
// 頁面列表的分頁
<el-pagination
@current-change="changePage('main', $event)">
</el-pagination>
// 彈窗A列表的分頁
<el-pagination
@current-change="changePage('modalA', $event)">
</el-pagination>
// 彈窗B列表的分頁
<el-pagination
@current-change="changePage('modalB', $event)">
</el-pagination>
changePage(type, page) {
const types = ['main', 'modalA', 'modalB']
types[type] && (this[types[type]].pageIndex = page) && this.getList(type)
}
使用箭頭函數(shù)處理
對于第三種場景,使用第三方類庫組件的時候汽摹,需要給事件回調(diào)增加額外的參數(shù)逼泣,如果默認(rèn)的回調(diào)參數(shù)只有1個那么我們就可以使用上面的那種方式拉庶,但是如果回調(diào)參數(shù)有多個的話氏仗,用 $event 就不好處理了皆尔,可以使用箭頭函數(shù)谣旁。比如文件上傳的時候榄审,有個 on-change 屬性搁进,當(dāng)文件變化的時候就會觸發(fā)回調(diào)饼问,正常情況下我們這樣寫是沒問題的:
<el-upload :on-change="changeFile">
<el-button>上傳</el-button>
</el-upload>
changeFile(file, fileList) {}
但是如果一個組件里有多個文件上傳,而我們又不想寫多個 changeFile揭斧,那就需要傳遞額外的參數(shù) type 了 :
<el-upload :on-change="(file, fileList) => changeFile('org', file, fileList)">
<el-button>上傳</el-button>
</el-upload>
changeFile(type, file, fileList) {}
3種深入watch的用法
立即執(zhí)行
watch 是 Vue 中的偵聽器莱革,可以偵聽一個 Vue 實例上的數(shù)據(jù),當(dāng)數(shù)據(jù)變動的時候讹开,就會觸發(fā)該偵聽器盅视。所以他的應(yīng)用場景就是:當(dāng)某個數(shù)據(jù)變動后需要做什么的時候就可以使用 watch 啦。對于 watch旦万,平常我們寫得最多的估計是如下這種寫法:
watch: {
list: function(val) {
this.getMsg()
}
}
如果我們希望組件初始化的時候就執(zhí)行一次 getMsg 方法闹击,可以直接在 mounted 里調(diào)用:
mounted() {
this.getMsg()
}
其實贺归,還有一種更加簡便的寫法,通過給 watch 設(shè)置 immediate: true 踱葛,即可:
watch: {
list: {
handler(val) { // 注意別寫錯成 handle
this.getMsg()
},
immediate: true
}
}
深度監(jiān)聽
偵聽器對于屬性變更后會自動調(diào)用一次,但是僅限于該屬性本身,如果變更的是屬性的屬性商蕴,則不會觸發(fā)偵聽回調(diào),如果想要實現(xiàn)這個功能可以給 watch 加上 'deep: true' 即可:
watch: {
obj: {
handler(val) { // do something },
deep: true
}
},
mounted() {
this.obj.name = 'bubuzou' // 將觸發(fā) handler
}
多個handlers
實際上,watch 可以設(shè)置為數(shù)組例书,支持類型為 String坟奥、Object 和 Function晒喷。觸發(fā)后,多個處理函數(shù)都將被調(diào)用雨效。
watch: {
obj: [
'print',
{
handler: 'print',
deep: true
},
function(val, oldValue) {
console.log(val)
}
]
},
methods: {
print() {
console.log(this.obj)
}
}
5個其他開發(fā)小技巧
掌握 Vue 的開發(fā)小技巧唉地,在一些特定的場景下真的很管用极颓,這里列了一些常用的小技巧狂秘。
函數(shù)式組件實現(xiàn)零時變量
我們在使用插槽的時候破衔,知道有一個叫做插槽 prop 的知識,今天我們用他和函數(shù)式組件結(jié)合在一塊,實現(xiàn)一個零時變量的組件:
// tempvar.vue
<script>
export default {
functional: true,
render(h, context) {
const { props, scopedSlots} = context
return scopedSlots.default && scopedSlots.default(props || {})
}
}
</script>
定義好了函數(shù)式組件,我們就可以在需要的地方引入且使用他:
<template>
<tempvar
:var1="`hello ${user.name}`"
:var2="user.age ? user.age : '18'">
<template v-slot="{var1, var2}">
姓名: {{ var1 }}
年齡:{{ var2 }}
</template>
</tempvar>
</template>
<script>
import tempvar from '@/components/tempvar.vue'
export default {
data() {
return {
obj: {
name: 'bubuzou',
age: 12,
},
}
}
components: {
tempvar
}
}
</script>
可能細(xì)心的小伙伴發(fā)現(xiàn)了尘吗,要把名字前加個 hello黔宛、默認(rèn)年齡設(shè)置為 18 用計算屬性就可以了呀?為啥還要搞那么復(fù)雜案淋,專門用一個函數(shù)式組件去實現(xiàn)呢宦棺?其實這個小技巧還是很有必要存在的,當(dāng)許多組件都有這種數(shù)據(jù)的重新計算的時候白华,如果沒有使用這個技巧,那么就需要寫很多很多的計算屬性,而有了函數(shù)式組件 tempvar 后,只需要在組件里引入他澡为,然后寫插槽就好了谷徙。就相當(dāng)于把寫計算屬性的功夫花在了寫插槽上了谋旦∷┕拢總而言之摊鸡,兩種方式都可以實現(xiàn)類似的屬性計算功能,該怎么選,隨你喜歡啦。
調(diào)試template(不推薦)
在開發(fā)調(diào)試的時候經(jīng)常會需要通過 console.log 來打印出某個數(shù)據(jù)對象來查看其內(nèi)部的結(jié)構(gòu)或者字段值,但是這樣做肯定不必在 template 里將其輸出更直接。比如有這樣一個數(shù)據(jù):
obj: {
name: 'bubuzou',
age: 21,
}
在模板中展示:
<p>{{ obj }}</p>
頁面渲染完成后會看到:
{ "name": "bubuzou", "age": 21 }
對于這樣的渲染結(jié)果雖然沒什么問題,但是如果這個 obj 是層級很深且字段很多的數(shù)據(jù),顯示出來就會一堆數(shù)據(jù)砸在一塊,絲毫沒有閱讀體驗。
因此基于這個背景,我們可以將 console.log 掛載在 Vue 的實例原型上:
// main.js
Vue.prototype.$log = window.console.log
然后就可以開開心心在模板中使用他了:
<p>{{ $log( obj ) }}</p>
這樣會在瀏覽器控制臺輸出當(dāng)前的數(shù)據(jù)對象,在顯示效果上和 console.log 直接打印別無二致。
但說了這么多,使用 Vue 進(jìn)行開發(fā)調(diào)試還是強(qiáng)烈推薦官方的vue-devtools 工具,誰用誰知道。
監(jiān)聽子組件的鉤子函數(shù)
通常如果我們想在子組件鉤子函數(shù)觸發(fā)的時候通知父組件,我們可以這樣做:
// parent.vue
<child @mounted="doSomething"></child>
// child.vue
this.$emit('mounted')
其實還有一種更加簡單的寫法,那就是使用 hookEvent:
<child @hook:mounted="doSomething"></child>
鉤子函數(shù)除了以上用法,還可以通過動態(tài)注冊做一些別的事情,比如組件銷毀前進(jìn)行資源的釋放:
mounted() {
let setIntervalId = setInterval(() => {
console.log(888);
}, 1000)
this.$once("hook:beforeDestroy", () => {
clearInterval(setIntervalId)
setIntervalId = null
})
}
路由參數(shù)解耦
參數(shù)解耦,啥意思呢?別著急,我們先來看比如對于這么一串路由:
const router = [{
path: '/home/:type/:id',
name: 'Home',
component: Home,
}]
當(dāng)前頁面的路徑是 http://xxx/detail/preview/21?sex=male谦炒,平時我們寫代碼的時候或多或少的會寫出這種代碼还蹲,在組件里使用 $route 給組件傳參數(shù):
mounted() {
if (this.$route.params.type === 'preview') {
this.isPreview = true
} else {
this.isPreview = false
}
this.id = this.$route.params.id
this.sex = this.$route.query.sex
}
這樣子寫本身沒什么問題锅论,就是會使得組件和路由高度耦合藻懒,讓組件只能在含有特定 URL 的頁面中使用鄙早,限制了組件的通用性弥虐。其實颖对,我們可以通過 props 傳參训堆,來解耦路由參數(shù)鲁沥,將上面的路由配置改成如下:
const router = [{
path: '/home/:type/:id',
name: 'Home',
component: Home,
props: (route) => ({
type: route.params.type,
id: route.params.id,
sex: route.query.sex,
})
}]
然后在組件 props 加上參數(shù):
props: ['type', 'id', 'sex']
組件里使用參數(shù)的時候就不需要用 this.$route,而是可以直接 this.type 即可。這樣一來,這個組件就可以在任何地方使用了。
深度作用選擇器
當(dāng)給 style 加上 scoped,頁面渲染完成后會給 html 和 css 選擇器加上哈希值用于表示唯一性:
<div class="home" data-v-fae5bece>
<button data-v-fae5bece class="el-button el-button-primary">提交</button>
</div>
.home .el-button[data-v-fae5bece] {
font-size: 20px;
}
對于在 style 中被加了 scoped 的組件赃蛛,其樣式將只能作用于組件內(nèi)部歧蒋,不會對其子組件造成影響。比如有這樣一個組件:
// 父組件
<div class="home">
<el-button type="primary">父按鈕</button>
<child></child>
</div>
<style lang="scss" scoped>
.home .el-button {
font-size: 20px;
}
</style>
// 子組件
<div class="child">
<el-button type="primary">子按鈕</button>
</div>
當(dāng)頁面渲染出來后奥秆,會是如下結(jié)果:
<div class="home" data-v-fae5bece>
<button data-v-fae5bece class="el-button el-button-primary">父按鈕</button>
<div class="child" data-v-fae5bece>
<button class="el-button el-button-primary">子按鈕</button>
</div>
</div>
根據(jù)上面的 html囊榜,我們可以看到 .home .el-button[data-v-fae5bece] 這個選擇器作用不到子按鈕這個 button孔庭。
在實際項目中芽淡,我們有時候需要讓父組件的樣式能作用到子組件,即使父組件的 style 上加了 scoped,那這個時候就需要用到深度作用選擇器 >>>认境,比如在剛剛的例子上可以給父組件樣式加上深度作用選擇器。
深度作用選擇器會被 Vue Loader 處理铜秆,且只能在有預(yù)處理器的地方使用客扎。由于某些預(yù)處理器比如 Sass 不能正確解析 >>>袱吆,所以我們可以使用它的別名:/deep/ 或 ::v-deep 來替代。
<style lang="scss" scoped>
.home {
/deep/ .el-button {
font-size: 20px;
}
}
</style>
加上深度作用選擇器后,選擇器會由原來的:
.home .el-button[data-v-fae5bece] {}
變成如下的:
.home[data-v-fae5bece] .el-button {}