前言
Vue.js憑藉簡潔高效易用的特點迅速被前端開源社區接受,並藉助weex覆蓋移動開發場景,逐步演變成一個完整的生態圈,未來充滿想象。而組件化的出現是爲了確保代碼高內聚低耦合並實現高效複用,從而提升開發效率和降低維護成本。Vue.js提供了組件化解決方案,本篇文章將結合開發實踐先從零開始探討組件的意義以及設計組件需要考慮的問題,再向前邁一步,講解Vue.js組件的各種開發方式及對比。
0
從零開始,我們爲什麼需要組件?
換句話說,組件解決了什麼問題?回想我們剛接觸web開發時,通過<link>
標籤引入css,通過<script>
標籤引入js就能輕鬆實現簡單的靜態頁面,而隨着用戶需求的逐步提高,web項目在開發人員規模和代碼量兩個維度上會越來越龐大,自然會產生以下問題:
- 代碼耦合(不同人寫的代碼相互影響,css覆蓋,js變量作用域引起的覆蓋等)
- 代碼冗餘(項目裏面有大量爲快速實現需求而拷貝的重複代碼)
爲了解決這些問題,需要做到 ①限制作用域 ②抽離公共代碼,這不正是組件在做的事情嘛,因此我們可以認爲組件化是軟件工程變得複雜後的一種解決方案。具體這種解決方案是怎麼演變來了,可以參考什麼叫組件化開發? - aloo的回答 - 知乎。
既然組件是爲了解決代碼耦合和代碼複用的問題,那我們在設計一個組件的時候,需要逐步思考以下的問題:
- 複用:抽象出需要被複用的代碼(例如一個容器,盛載不同的內容,而不需要再設計一個新的容器去盛載新的內容)
- 複用:怎麼複用抽離後的代碼(模塊化引用、webpack打包)
- 解耦:組件與同級組件間如何保持獨立(消息通信、事件回調)
- 解耦:父子組件怎麼通信(單向數據流、消息通信)
解決方案始於問題,也止於新的問題,方案總在變化,理解背後方案背後解決的問題能加深我們對方案的認識,也有助於我們解決新的問題。
1
從0到1,如何動手打造一個合適的組件?
第一個簡單的Vue組件
前面簡單瞭解了組件的出現是爲了解決哪些具體問題,這個時候是不是好奇Vue的組件怎麼寫?學習資料首推Vue官方文檔-組件,既簡單又全面。爲了保持閱讀連貫性,這裏舉個簡單的例子。沒接觸過Vue的同學建議先去看Vue官方文檔-入門。
<!-- /components/hello.vue -->
<template>
<div>{{ msg }}</div>
</template>
<script>
export default {
data() {
return { msg: 'Hello Vue.js' }
}
}
</script>
<style scoped>
div { font-size: 20px; }
</style>
<!-- App.vue -->
<template>
<!-- 組件使用 -->
<hello></hello>
</template>
<script>
// 組件引用
import Hello from '/components/hello.vue';
export default {
// 組件註冊
components: {
Hello
},
}
</script>
上面我們實現了一個Hello
組件來顯示”Hello Vue.js”,Hello
組件採用單文件的形式(藉助webpack打包,具體配置可參考Vue腳手架工具:vue-cli生成的項目)來組合組件相關的模板、樣式以及邏輯,實現代碼層面的高內聚。這時候我們就能在任意地方使用Hello
組件(需引用,局部/全局註冊,使用)。
Vue組件的實現方式
這裏假設大家通過瀏覽Vue官方文檔-組件後瞭解了:
現在是不是特別想動手寫自己第一個組件,用於實際業務中?在接觸到新的技術時,我也有這種想法和衝動,但寫碼實在太自由了,實現同樣一個小功能,可以有千萬種寫法(誇張),從多種寫法中挑選出大家認可的,實際業務場景檢驗過的寫法,就叫“最佳實踐”,因此在動手寫碼前,不妨來看看Vue.js組件的“最佳實踐”或者說主流寫法,這常常會幫助我們省去一些填坑的時間。
在閱讀一些博客後,我決定去看餓了麼Vue組件庫 mint-ui源碼,最後總結出Vue組件的使用場景以及對應的實現方式,如下表:
序號 | 使用場景 | 實現方式 | 舉例 |
---|---|---|---|
1 | 不需要返回js引用對象,純粹是渲染內容 | 單Vue文件,例如上面的Hello 組件 |
Button |
2-1 | 需要返回js引用對象,例如控制彈窗開關 | 單Vue文件 + js封裝對外接口 | Dialog |
2-2 | 需要返回js引用對象,例如控制彈窗開關 | 單Vue文件 + refs引用 | 局部Dialog |
2-3 | 需要返回js引用對象,例如控制彈層 | 單Vue文件 + refs引用 + 掛載引用對象到全局 | 全局Toast |
下面就逐一詳細地和大家講解各種使用場景和對應的實現方式。
PS:在這裏特別感謝《Vue組件的三種調用方式》這篇博客給了我啓發,不要先去寫組件再考慮怎麼使用組件,而應該從使用場景出發去設計組件。
場景1 - 不需要返回js引用對象,純粹是渲染內容
這應該是開發中最普遍的場景,三部曲:①import引用組件 ②components註冊組件 ③<template>
中使用組件,並通過數據傳遞和事件綁定將組件需要的信息傳遞過去,完事。這裏舉一個比上面的Hello
組件複雜一些的例子:
①業務需求
上面是某個業務需求的視覺稿,咋一看是四個不同樣式的卡片,但還是能找到一些共同點:①背景色一致;②陰影一致;③右上角有幾種角標;④卡片兩側有缺口;我們對這四個共同點進一步整理和抽象:這是一個背景爲白色、帶陰影、右上角有不同角標、兩側可能有缺口的容器。下面就是愉快的編碼時間啦。
②具體實現
<!--
功能說明:
1. 抽象了陰影,間距,右上角提示
2. 提供content插槽
使用說明:
1. 接口
status: default、unqualified、expired、gift
semiCircle: true、flase
2. template
<card-container :status="status" :semiCircle="true">
<div slot="content"></div>
</card-container>
-->
<template>
<div class="card-container">
<slot name="content"></slot>
<div v-if="status === 'default'"></div>
<div v-else-if="status === 'unqualified'" class="tips unqualified">未達領取資格</div>
<div v-else-if="status === 'expired'" class="tips expired">已過期</div>
<div v-else-if="status === 'gift'" class="tips gift">平臺贈送</div>
<div v-if="semiCircle">
<div class="semi-circle"></div>
<div class="semi-circle right"></div>
</div>
</div>
</template>
<script>
export default {
data () {
return {}
},
props: ['status', 'semiCircle']
}
</script>
<style scoped>
/* 樣式不是重點,忽略 */
</style>
卡片容器實現比較簡單,這裏使用了插槽 - slot來將內容裝載在容器內,再通過props傳參來控制容器自身的形態。
③分析
“不需要返回js引用對象,純粹是渲染內容”覆蓋了業務絕大部分場景。如果不嫌麻煩,應該也能覆蓋所有的場景。例如,一個Toast,我們期望是Toast.show('hello world')
的方式調用,其實也能這樣寫:
<!-- /components/toast.vue -->
<template>
<div v-if="show">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {}
},
props: ['show', 'msg']
}
</script>
<!-- App.vue -->
<template>
<!-- 組件使用 -->
<toast :show="toastShow" :msg="toastMsg"></toast>
</template>
<script>
// 組件引用
import Toast from '/components/toast.vue';
export default {
data() {
return {
toastMsg: '',
toastShow: false
};
},
// 組件註冊
components: {
Toast
},
methods: {
showToast: function(msg) {
this.toastMsg = msg;
this.toastShow = true;
}
}
}
</script>
雖然上面同樣實現了Toast.show('hello world')
的需求,但試想我們在多個頁面裏都需要用到Toast,每個頁面都需要配置toastMsg
、toastShow
變量,showToast
方法,遠沒有Toast.show('hello world')
用得爽。下面我們來探討新的解決方案。
場景2-1 - 需要返回js引用對象: 單Vue文件 + js封裝對外接口
上面已經用代碼示例和大家展示用配置的方法來實現一個彈窗的開關過程是多麼“傻”。有些時候我們需要操作對應的組件對象來完成一些交互,這裏還是以Toast.show('hello world')
爲目標,看看怎麼去滿足這種業務場景。
①Show me the code
<!-- /components/toast.vue 單Vue文件實現組件邏輯-->
<template>
<div v-if="showView">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
showView: false,
msg: ''
}
},
methods: {
show: function(msg) {
this.msg = msg;
this.showView = true;
},
close: function() {
this.showView = false;
}
}
}
</script>
/* /components/toast.js 對上面的Vue組件進行封裝,對外暴露接口 */
import Toast from '/components/toast.vue';
export default {
// install爲Vue定義的插件引入規範
// 具體可以參考:https://cn.vuejs.org/v2/guide/plugins.html
install(Vue) {
// 由單Vue組件擴展成組件構造器
const constructor = Vue.extend(Toast);
function toast(msg) {
// 實例化
let toast = new constructor();
toast.show(msg);
// 掛載到DOM樹上,沒有考慮DOM節點複用的問題
if (!toast.$el) {
// 掛載虛擬DOM
let vm = toast.$mount();
document.querySelector('body').appendChild(vm.$el);
}
// 如果需要進一步操作,可返回組件實例
return toast;
}
// 將toast構造方法掛載在Vue上
Vue.toast = toast;
}
}
<!-- 使用組件 -->
<template>
<div></div>
</template>
<script>
import Vue from 'vue'
import Toast from './index.js'
// 全局註冊
Vue.use(Toast)
export default {
mounted: function() {
const toast = Vue.toast('Hello world');
toast.close();
}
}
</script>
②基本思路 - 從開源項目源碼中總結得來:
- 用單Vue文件的形式組織組件內容,實現組件邏輯;
- 新建一個js文件,對前面的Vue組件進行封裝,將構造函數掛載在Vue對象中:
- 利用Vue.extent將Vue組件擴展成組件構造器;
- 設計構造函數(新建實例,將構造函數的參數傳給實例,將實例掛載到真實DOM節點上,返回實例對象以便後續操作)
- 將構造函數掛載在Vue對象中
- 利用Vue插件機制全局註冊組件;
- 在需要用到組件的時候,調用步驟2中掛載在Vue對象中的構造函數;
按照上面的操作,我們已經能全局使用Vue.toast('Hello world')
。有時候我們的.vue
文件裏並沒有import Vue from 'vue'
,因此我們可以更改一下上面的步驟2,將構造函數掛載在window對象中,調用方法就變成toast('Hello world')
。
至此,我們已經知道如何返回組件實例操作對象供交互使用,但回顧一下整個解決方案,我們需要額外多寫一個封裝組件對外暴露構造方法的js文件,對於Vue新人來說這個過程會稍微有些複雜。
怎麼理解複雜的事情?抽象是一個好方法。
③額外的思考
剝去外殼,直擊本質,使用組件其實是將組件掛載到視圖上並與組件進行邏輯交互的一個過程。基於這個本質,我們只用考慮兩個問題:
- 怎麼掛載組件?
- 怎麼進行邏輯交互?
由這個思考,針對場景2(需要返回js引用對象),產生了下面2-2、2-3兩種稍微不一樣的方案。
場景2-2 - 需要返回js引用對象: 單Vue文件 + refs引用
Vue在<template>
標籤裏已經給我們提供掛載組件的能力,同時利用VNode引用 - ref我們能獲取到組件實例,有了組件實例,就能調用組件的方法進行各種交互。稍微改造一下之前的代碼:
<!-- /components/toast.vue 單Vue文件實現組件邏輯-->
<template>
<div v-if="showView">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
showView: false,
msg: ''
}
},
methods: {
show: function(msg) {
this.msg = msg;
this.showView = true;
},
close: function() {
this.showView = false;
}
}
}
</script>
<!-- App.vue -->
<template>
<!-- 組件使用 -->
<toast ref='myToast'></toast>
</template>
<script>
// 組件引用
import Toast from '/components/toast.vue';
export default {
// 組件註冊
components: {
Toast
},
mounted: function() {
// 獲取組件引用
const toast = this.$refs.myToast;
// 調用組件方法實現邏輯交互
toast.show('Hello world');
toast.close();
}
}
</script>
利用<template>
來掛載組件省去了我們使用Vue API來構建組件,掛載組件的麻煩,同時通過ref引用我們能快速獲取組件實例,完成需要的邏輯交互。需要注意的是ref只能獲取子組件的引用,因此只能在當前掛載作用域使用,在別的文件裏訪問不了this.refs.myToast
,更進一步,如果我們需要一個全局使用的組件呢?。
場景2-3 - 需要返回js引用對象: 單Vue文件 + refs引用 + 全局掛載
說不定大家都已經猜到了,把上面的this.refs.myToast
掛載在window對象不就實現了全局調用接口嗎?代碼和上面及其相似就不展示了,這裏再溫習一遍操作流程:
- 在根節點(通常是App.vue)中import引用Toast組件;
- 在根節點中
<template>
掛載<toast ref='myToast'></toast>
; - 通過
this.refs.myToast
獲取組件引用; - 將組件引用掛載到window對象供全局使用;
自此,大家可以開始結合自己的需求,挑選合適的方案來設計適合自己的組件了,歡迎來到組件化時代。
最後
剛開始寫的時候博客標題是《Vue.js組件開發從0到1到100》,後來寫着寫着發現從0到1內容就很多了,故拆開,後續會有《Vue.js組件開發從1到100》,分享內容可能有:
1. 組件的基本邏輯複用 - mixin
2. 你並不總需要組件 - 指令
3. 組件庫的技術架構
4. 高階組件 - 內置的transition組件分析
5. 複雜項目中的狀態管理
6. 組件與SSR
歡迎大家關注。
好久不見,我又回來啦。過去17年探索了很多新的領域,希望18年能收收心,一專多長,加速成長。期待與大家共同進步。