在實際開發工程化vue項目時,基本都是使用單文件組件形式,即每個.vue文件都是一個組件。最基本的就是UI組件了,我們一般會根據自己的喜好和項目的需要選擇一個成熟的組件庫來使用,iView組件庫、餓了麼的Element組件庫都是很優秀很全面的組件庫,雖然都能滿足我們的需求,可衆口難調,總有一些特殊的需求不能滿足,這個時候就需要我們自己去開發組件實現特殊的需求了。而且我們的目標不僅僅是使用輪子,更要有造輪子的能力。接下來我們就以常用的彈窗Modal組件爲例一步一坑的搞一搞vue單文件組件開發。
一個vue組件除了HTML和css結構,最重要的三個部分是slot、props和events,所有的vue組件都逃不離這3個部分。
目標
Modal組件很常見,就是彈出一個帶有遮罩的對話框,我們這一次的目標就以iView組件庫的Modal爲例吧:
1、第一步:基礎-單文件組件模式
首先我們先創建一個vue工程,還不清楚的朋友可自行看一下vue官網教程搭建一個。單文件組件顧名思義一個.vue文件就是一個組件,所以我們新建一個Modal.vue
文件表示我們的目標Modal組件,再創建一個父組件HelloWorld.vue
來調用這個Modal組件,父組件是HelloWorld.vue
子組件是Modal.vue
代碼分別爲:
HelloWorld.vue
:
<template>
<div>
<button @click="toggleModal">打開Modal對話框</button>
<Modal v-show="showModal"></Modal>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
data () {
return {
showModal:false
}
},
components:{
'Modal':Modal //聲明組件
},
methods:{
toggleModal() {
this.showModal = !this.showModal; //切換showModal 的值來切換彈出與收起
},
}
}
</script>
<style>
</style>
Modal.vue
:
<template>
<div>
我是Modal裏的內容
</div>
</template>
<style>
</style>
<script>
export default {
name: 'Modal',
props: { //props裏面準備寫上父組件HelloWorld要傳進來的數據
},
data() {
return {
}
},
methods: {
}
}
</script>
我們這裏爲了更純粹的介紹組件間的關係就把HelloWorld.vue通過 vue-router
配置成首頁,小夥伴們大可根據自己工程的情況而定,反正就是一父一子的關係。運行命令npm run dev
(不熟悉的小夥伴可以再回頭看看vue官網的教程把工程跑起來,可以看到組件間的調用關係就完成了,當然她還不是個真正的彈出框,但卻是所有vue組件的基礎:
2、第二步:組件個性化
有了第一步單文件組件結構的基礎,接下來就可以在這基礎上創造出各種組件來,想做Button組件就做Button組件、想做Loading組件就做Loading組件、想做Modal組件就做Modal組件,直到把所有用到的組件都做完了,就形成了自己的組件庫,然後再打包託管到npm上...額慢慢來。接下來花一點時間讓Modal.vue看上去有她該有的樣子:
2.1Modal組件結構搭建
可以看到一個iView的Modal最基本的內容有
遮罩層、彈出框、header頭部、body內容區和footer尾部
5個部分,各自都有自己的樣式,接下來我們就要爲Modal.vue組件加上這樣的HTML結構和css樣式,Modal.vue代碼更新爲:Modal.vue
:
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<h3>我是一個Modal的標題</h3>
</div>
<div class="modal-body">
<p>我是一個Modal的內容</p>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">關閉</button>
<button type="button" class="btn-confirm" @click="confirm">確認</button>
</div>
</div>
</div>
</template>
<style>
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background-color: #fff;
box-shadow: 2px 2px 20px 1px;
overflow-x:auto;
display: flex;
flex-direction: column;
border-radius: 16px;
width: 700px;
}
.modal-header {
border-bottom: 1px solid #eee;
color: #313131;
justify-content: space-between;
padding: 15px;
display: flex;
}
.modal-footer {
border-top: 1px solid #eee;
justify-content: flex-end;
padding: 15px;
display: flex;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close, .btn-confirm {
border-radius: 8px;
margin-left:16px;
width:56px;
height: 36px;
border:none;
cursor: pointer;
}
.btn-close {
color: #313131;
background-color:transparent;
}
.btn-confirm {
color: #fff;
background-color: #2d8cf0;
}
</style>
<script>
export default {
name: 'Modal',
props: {
},
data() {
return {
}
},
methods: {
closeSelf() {
}
}
}
</script>
先看一下更新後Modal組件的樣子:
2.2 組件通信
再仔細看一下Modal.vue組件的代碼,發現有兩個問題。
1、一是我們這裏爲彈出框寫了一個width=700px,顯然用戶每打開一個Modal都是700寬,這樣做組件的可重用性就太低了。接下來要做到寬度能由父組件來決定。
2、二是Modal組件裏的關閉Button事件還沒起作用,它的本意是點擊它就關閉整個彈出框。接下來要做到實現此事件。
藉由上面這兩個問題,我們就要用到組件通信了,在vue組件中,父組件給子組件傳數據是單向數據流,父組件通過定義屬性的方式傳遞,子組件用props
接收,如父組件傳一個width參數給子組件:
父組件:<Modal width="700" ></Modal>
子組件:props: { width:{ type:[Number,String], default:520 } }
而子組件向父組件傳遞數據的方式爲事件傳遞,如要在子組件關閉Modal彈出框,則要傳遞一個雙方約定的事件名給父組件,如:
父組件:<Modal @on-cancel="cancel" ></Modal>
子組件:this.$emit('on-cancel');
接下來就更新一下HelloWorld.vue和Modal.vue的代碼,來解決這兩個問題,實現父組件自定義彈出框寬度爲200px,以及關閉彈出框功能
,更新代碼如下:
HelloWorld.vue
:
<template>
<div>
<button @click="toggleModal">打開Modal對話框</button>
<!--定義width屬性,和on-cancel事件-->
<Modal
v-show="showModal"
width="200"
@on-cancel="cancel"
></Modal>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
data () {
return {
showModal:false
}
},
components:{
'Modal':Modal
},
methods:{
toggleModal() {
this.showModal = !this.showModal;
},
//響應on-cancel事件,來把彈出框關閉
cancel() {
this.showModal = false;
}
}
}
</script>
<style>
</style>
Modal.vue
:
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<h3>我是一個Modal的標題</h3>
</div>
<div class="modal-body">
<p>我是一個Modal的內容</p>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">關閉</button>
<button type="button" class="btn-confirm" @click="confirm">確認</button>
</div>
</div>
</div>
</template>
<style>
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background-color: #fff;
box-shadow: 2px 2px 20px 1px;
overflow-x:auto;
display: flex;
flex-direction: column;
border-radius: 16px;
}
.modal-header {
border-bottom: 1px solid #eee;
color: #313131;
justify-content: space-between;
padding: 15px;
display: flex;
}
.modal-footer {
border-top: 1px solid #eee;
justify-content: flex-end;
padding: 15px;
display: flex;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close, .btn-confirm {
border-radius: 8px;
margin-left:16px;
width:56px;
height: 36px;
border:none;
cursor: pointer;
}
.btn-close {
color: #313131;
background-color:transparent;
}
.btn-confirm {
color: #fff;
background-color: #2d8cf0;
}
</style>
<script>
export default {
name: 'Modal',
//接收父組件傳遞的width屬性
props: {
width:{
type:[Number,String],//類型檢測
default:300 //父組件沒傳width時的默認值
}
},
data() {
return {
}
},
computed:{
//計算屬性來響應width屬性,實時綁定到相應DOM元素的style上
mainStyles() {
let style = {};
style.width = `${parseInt(this.width)}px`;
return style;
}
},
methods: {
//響應關閉按鈕點擊事件,通過$emit api通知父組件執行父組件的on-cancel方法
closeSelf() {
this.$emit('on-cancel');
}
}
}
</script>
新增的代碼在圖中都做了註釋說明,此時就可看到效果:
可以看到iView的Modal組件定義了很多屬性和事件,都是日積月累不斷優化而來的,我們的例子只寫了一個width屬性和on-cancel事件,但其他的基本是大同小異,套路掌握了都可以一一實現的。
iView中Modal的props、events一覽
:2.3 slot插槽
props、events都實現了,三部分中就剩slot了。slot的作用也是爲了解決組件可重用的問題的。
props解決的是組件參數的傳遞、events解決的是組件事件的傳遞、slot解決的就是組件內容的傳遞。
首先發現問題,例子中Modal的標題和body裏的內容是寫死的一個<h3>一個<p>標籤,但真正使用起來彈出框裏的內容都是自定義的五花八門的,一個p標籤是搞不定的。接下來我們再更新一下代碼,Modal.vue的改動爲把<h3>標籤替換成了一個name=header
的<slot>插槽,body裏的<p>標籤替換成了一個name=body
的<slot>插槽(這裏用的是具名slot插槽,單個插槽、作用於插槽等就不展開了還請小夥伴們查看vue官網文檔):
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<slot name="header">我是子組件定義的header</slot>
</div>
<div class="modal-body">
<slot name="body">我是子組件定義的body</slot>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">關閉</button>
<button type="button" class="btn-confirm" >確認</button>
</div>
</div>
</div>
</template>
HelloWorld.vue的改動爲,在<Modal>裏定義了一個具有屬性slot
的div,slot的值爲header
和<slot name="header">
對應,div裏是一個圖片和一個<h3>標題。並且這裏特意只定義了一個具有slot的div:
<template>
<div>
<button @click="toggleModal">打開Modal對話框</button>
<Modal v-show="showModal" width="700" @on-cancel="cancel">
<div slot="header">
<div class="myHeader">
<img src="../assets/logo.png" width="40px" height="40px"/>
<h3>我是父組件定義的標題</h3>
</div>
</div>
</Modal>
</div>
</template>
前面說到slot解決的組件內容的傳遞,它就好像是子組件定義一個佔位符,父組件有對應的內容傳進來就替換掉它,沒有傳就默認顯示子組件自己定義的內容,所以上面代碼運行起來會是:
完整的HelloWorld.vue和Modal.vue代碼如下:
HelloWorld.vue
:
<template>
<div>
<button @click="toggleModal">打開Modal對話框</button>
<Modal v-show="showModal" width="700" @on-cancel="cancel">
<div slot="header">
<div class="myHeader">
<img src="../assets/logo.png" width="40px" height="40px"/>
<h3>我是父組件定義的標題</h3>
</div>
</div>
</Modal>
</div>
</template>
<script>
import Modal from './Modal.vue'
export default {
data () {
return {
showModal:false
}
},
components:{
'Modal':Modal
},
methods:{
toggleModal() {
this.showModal = !this.showModal;
},
cancel() {
this.showModal = false;
}
}
}
</script>
<style>
.myHeader{
justify-content: flex-start;
padding: 15px;
display: flex;
}
</style>
Modal.vue
:
<template>
<div class="modal-backdrop">
<div class="modal" :style="mainStyles">
<div class="modal-header">
<slot name="header">我是子組件定義的header</slot>
</div>
<div class="modal-body">
<slot name="body">我是子組件定義的body</slot>
</div>
<div class="modal-footer">
<button type="button" class="btn-close" @click="closeSelf">關閉</button>
<button type="button" class="btn-confirm" >確認</button>
</div>
</div>
</div>
</template>
<style>
.modal-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0,0,0,.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background-color: #fff;
box-shadow: 2px 2px 20px 1px;
overflow-x:auto;
display: flex;
flex-direction: column;
border-radius: 16px;
}
.modal-header {
border-bottom: 1px solid #eee;
color: #313131;
justify-content: space-between;
padding: 15px;
display: flex;
}
.modal-footer {
border-top: 1px solid #eee;
justify-content: flex-end;
padding: 15px;
display: flex;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close, .btn-confirm {
border-radius: 8px;
margin-left:16px;
width:56px;
height: 36px;
border:none;
cursor: pointer;
}
.btn-close {
color: #313131;
background-color:transparent;
}
.btn-confirm {
color: #fff;
background-color: #2d8cf0;
}
</style>
<script>
export default {
name: 'Modal',
props: {
width:{
type:[Number,String],
default:300
}
},
data() {
return {
}
},
computed:{
mainStyles() {
let style = {};
style.width = `${parseInt(this.width)}px`;
return style;
}
},
methods: {
closeSelf() {
this.$emit('on-cancel');
}
}
}
</script>
3、後續
有幾點需要說明一下。首先,毋庸置疑的是幾乎所有的vue組件都是圍繞着props、slot和events這三大件,每個部分都有不少的內容需要學習使用,就像前面說的slot還有單個插槽、作用域插槽等等,events的api也不止parent、dispatch等等針對各種情形應運而生的。其次,看到iViewUI組件庫源碼的小夥伴會覺得我們這個例子的代碼和它的代碼有出入,這是肯定的,例子總是單薄的。最後,我發現在寫HTML結構和css時漏了一個transtions過渡效果,不過無傷大雅,週五了加班狗先閃爲敬~~