本文首發於我的部落格。歡迎點選以獲得更好的閱讀效果~
之前在寫fj-service-system的時候,遇到了一些問題。那就是我有些元件,比如Dialog
、Message
這樣的元件,是引入三方元件庫,比如element-ui
這樣的,還是自己實現一個?雖然它們有按需引入的功能,但是整體風格和我的整個系統不搭。於是就可以考慮自己手動實現這些簡單的元件了。
通常我們看Vue的一些文章的時候,我們能看到的通常是講Vue單檔案元件化開發頁面的。單一元件開發的文章相對就較少了。我在做fj-service-system專案的時候,發現其實單一元件開發也是很有意思的。可以寫寫記錄下來。因為寫的不是什麼ui框架,所以也只是一個記錄,沒有github倉庫,權且看程式碼吧。
主要講三種方式呼叫元件:
v-model
或者.sync
顯式控制元件顯示隱藏- 通過js程式碼呼叫
- 通過Vue指令呼叫
在寫元件的時候很多寫法、靈感來自於element-ui,感謝。
Dialog
我習慣把這個東西叫做對話方塊,實際上還有叫做modal(彈窗)元件的叫法。其實就是在頁面裡,彈出一個小視窗,這個小視窗裡的內容可以定製。通常可以用來做登入功能的對話方塊。
這種元件就很適合通過v-model
或者.sync
的方式來顯式的控制出現和消失。它可以直接寫在頁面裡,然後通過data去控制——這也是最符合Vue的設計思路的元件。
為此我們可以寫一個元件就叫做Dialog.vue
<template>
<div class="dialog">
<div class="dialog__wrapper" v-if="visble" @clcik="closeModal">
<div class="dialog">
<div class="dialog__header">
<div class="dialog__title">{{ title }}</div>
</div>
<div class="dialog__body">
<slot></slot>
</div>
<div class="dialog__footer">
<slot name="footer"></slot>
</div>
</div>
</div>
<div class="modal" v-show="visible"></div>
</div>
</template>
<script>
export default {
name: 'dialog',
props: {
title: String,
visible: {
type: Boolean,
default: false
}
},
methods: {
close() {
this.$emit('update:visible', false) // 傳遞關閉事件
},
closeModal(e) {
if (this.visible) {
document.querySelector('.dialog').contains(e.target) ? '' : this.close(); // 判斷點選的落點在不在dialog對話方塊內,如果在對話方塊外就呼叫this.close()方法關閉對話方塊
}
}
}
}
</script>
複製程式碼
CSS什麼的就不寫了,跟元件本身關係比較小。不過值得注意的是,上面的dialog__wrapper
這個class也是全屏的,透明的,主要用於獲取點選事件並鎖定點選的位置,通過DOM的Node.contains()
方法來判斷點選的位置是不是dialog本身,如果是點選到了dialog外面,比如半透明的modal
層那麼就派發關閉事件,把dialog給關閉掉。
當我們在外部要呼叫的時候,就可以如下呼叫:
<template>
<div class="xxx">
<dialog :visible.sync="visible"></dialog>
<button @click="openDialog"></button>
</div>
</template>
<script>
import Dialog from 'Dialog'
export default {
components: {
Dialog
},
data() {
return {
visible: false
}
},
methods: {
openDialog() {
this.visible = true // 通過data顯式控制dialog
}
}
}
</script>
複製程式碼
為了Dialog開啟和關閉好看點,你可試著加上<transition></transition>
元件配合上過渡效果,簡單的一點過渡動效也將會很好看。
Notice
這個元件類似於element-ui
的message(訊息提示)。它吸引我的最大的地方在於,它不是通過顯式的在頁面裡寫好元件的html結構通過v-model去呼叫的,而是通過在js裡通過形如this.$message()
這樣的方法呼叫的。這種方法雖然跟Vue的資料驅動的思想有所違背。不過不得不說在某些情況下真的特別方便。
對於Notice這種元件,一次只要提示幾個文字,給使用者簡單的訊息提示就行了。提示的資訊可能是多變的,甚至可以出現疊加的提示。如果通過第一種方式去呼叫,事先就得寫好html結構,這無疑是麻煩的做法,而且無法預知有多少訊息提示框。而通過js的方法呼叫的話,只需要考慮不同情況呼叫的文字、型別不同就可以了。
而之前的做法都是寫一個Vue檔案,然後通過components
屬性引入頁面,顯式寫入標籤呼叫的。那麼如何將元件通過js的方法去呼叫呢?
這裡的關鍵是Vue的extend
方法。
文件裡並沒有詳細給出extend
能這麼用,只是作為需要手動mount
的一個Vue的元件構造器說明了一下而已。
通過檢視element-ui
的原始碼,才算是理解了如何實現上述的功能。
首先依然是建立一個Notice.vue
的檔案
<template>
<div class="notice">
<div class="content">
{{ content }}
</div>
</div>
</template>
<script>
export default {
name: 'notice',
data () {
return {
visible: false,
content: '',
duration: 3000
}
},
methods: {
setTimer() {
setTimeout(() => {
this.close() // 3000ms之後呼叫關閉方法
}, this.duration)
},
close() {
this.visible = false
setTimeout(() => {
this.$destroy(true)
this.$el.parentNode.removeChild(this.$el) // 從DOM裡將這個元件移除
}, 500)
}
},
mounted() {
this.setTimer() // 掛載的時候就開始計時,3000ms後消失
}
}
</script>
複製程式碼
上面寫的東西跟普通的一個單檔案Vue元件沒有什麼太大的區別。不過區別就在於,沒有props了,那麼是如何通過外部來控制這個元件的顯隱呢?
所以還需要一個js檔案來接管這個元件,並呼叫extend
方法。同目錄下可以建立一個index.js
的檔案。
import Vue from 'vue'
const NoticeConstructor = Vue.extend(require('./Notice.vue').default) // 直接將Vue元件作為Vue.extend的引數。新版vue-loader升級後要求加上`.default`,感謝評論指出~
let nId = 1
const Notice = (content) => {
let id = 'notice-' + nId++
const NoticeInstance = new NoticeConstructor({
data: {
content: content
}
}) // 例項化一個帶有content內容的Notice
NoticeInstance.id = id
NoticeInstance.vm = NoticeInstance.$mount() // 掛載但是並未插入dom,是一個完整的Vue例項
NoticeInstance.vm.visible = true
NoticeInstance.dom = NoticeInstance.vm.$el
document.body.appendChild(NoticeInstance.dom) // 將dom插入body
NoticeInstance.dom.style.zIndex = nId + 1001 // 後插入的Notice元件z-index加一,保證能蓋在之前的上面
return NoticeInstance.vm
}
export default {
install: Vue => {
Vue.prototype.$notice = Notice // 將Notice元件暴露出去,並掛載在Vue的prototype上
}
}
複製程式碼
這個檔案裡我們能看到通過NoticeConstructor
我們能夠通過js的方式去控制一個元件的各種屬性。最後我們把它註冊進Vue的prototype上,這樣我們就可以在頁面內部使用形如this.$notice()
方法了,可以方便呼叫這個元件來寫做出簡單的通知提示效果了。
當然別忘了這個相當於一個Vue的外掛,所以需要去主js裡呼叫一下Vue.use()
方法:
// main.js
// ...
import Notice from 'notice/index.js'
Vue.use(Notice)
// ...
複製程式碼
Loading
在看element-ui
的時候,我也發現了一個很有意思的元件,是Loading
,用於給一些需要載入資料等待的元件套上一層載入中的樣式的。這個loading的呼叫方式,最方便的就是通過v-loading
這個指令,通過賦值的true/false
來控制Loading層的顯隱。這樣的呼叫方法當然也是很方便的。而且可以選擇整個頁面Loading或者某個元件Loading。這樣的開發體驗自然是很好的。
其實跟Notice的思路差不多,不過因為涉及到directive
,所以在邏輯上會相對複雜一點。
平時如果不涉及Vue的directive
的開發,可能是不會接觸到modifiers
、binding
等概念。參考文件
簡單說下,形如:v-loading.fullscreen="true"
這句話,v-loading
就是directive
,fullscreen
就是它的modifier
,true
就是binding
的value
值。所以,就是通過這樣簡單的一句話實現全屏的loading效果,並且當沒有fullscreen
修飾符的時候就是對擁有該指令的元素進行loading
效果。元件通過binding
的value
值來控制loading
的開啟和關閉。(類似於v-model
的效果)
其實loading也是一個實際的DOM節點,只不過要把它做成一個方便的指令還不是特別容易。
首先我們需要寫一下loading
的Vue元件。新建一個Loading.vue
檔案
<template>
<transition
name="loading"
@after-leave="handleAfterLeave">
<div
v-show="visible"
class="loading-mask"
:class={'fullscreen': fullscreen}>
<div class="loading">
...
</div>
<div class="loading-text" v-if="text">
{{ text }}
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'loading',
data () {
return {
visible: true,
fullscreen: true,
text: null
}
},
methods: {
handleAfterLeave() {
this.$emit('after-leave');
}
}
}
</script>
<style>
.loading-mask{
position: absolute; // 非全屏模式下,position是absolute
z-index: 10000;
background-color: rgba(255,235,215, .8);
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: opacity .3s;
}
.loading-mask.fullscreen{
position: fixed; // 全屏模式下,position是fixed
}
// ...
</style>
複製程式碼
Loading關鍵是實現兩個效果:
- 全屏loading,此時可以通過插入body下,然後將Loading的position改為fixed,插入body實現。
- 對所在的元素進行loading,此時需要對當前這個元素的的position修改:如果不是
absolute
的話,就將其修改為relatvie
,並插入當前元素下。此時Loading的position就會相對於當前元素進行絕對定位了。
所以在當前目錄下建立一個index.js
的檔案,用來宣告我們的directive
的邏輯。
import Vue from 'vue'
const LoadingConstructor = Vue.extend(require('./Loading.vue').default) // 新版vue-loader升級後要求加上`.default`,感謝評論指出~
export default {
install: Vue => {
Vue.directive('loading', { // 指令的關鍵
bind: (el, binding) => {
const loading = new LoadingConstructor({ // 例項化一個loading
el: document.createElement('div'),
data: {
text: el.getAttribute('loading-text'), // 通過loading-text屬性獲取loading的文字
fullscreen: !!binding.modifiers.fullscreen
}
})
el.instance = loading; // el.instance是個Vue例項
el.loading = loading.$el; // el.loading的DOM元素是loading.$el
el.loadingStyle = {};
toggleLoading(el, binding);
},
update: (el, binding) => {
el.instance.setText(el.getAttribute('loading-text'))
if(binding.oldValue !== binding.value) {
toggleLoading(el, binding)
}
},
unbind: (el, binding) => { // 解綁
if(el.domInserted) {
if(binding.modifiers.fullscreen) {
document.body.removeChild(el.loading);
}else {
el.loading &&
el.loading.parentNode &&
el.loading.parentNode.removeChild(el.loading);
}
}
}
})
const toggleLoading = (el, binding) => { // 用於控制Loading的出現與消失
if(binding.value) {
Vue.nextTick(() => {
if (binding.modifiers.fullscreen) { // 如果是全屏
el.originalPosition = document.body.style.position;
el.originalOverflow = document.body.style.overflow;
insertDom(document.body, el, binding); // 插入dom
} else {
el.originalPosition = el.style.position;
insertDom(el, el, binding); // 如果非全屏,插入元素自身
}
})
} else {
if (el.domVisible) {
el.instance.$on('after-leave', () => {
el.domVisible = false;
if (binding.modifiers.fullscreen && el.originalOverflow !== 'hidden') {
document.body.style.overflow = el.originalOverflow;
}
if (binding.modifiers.fullscreen) {
document.body.style.position = el.originalPosition;
} else {
el.style.position = el.originalPosition;
}
});
el.instance.visible = false;
}
}
}
const insertDom = (parent, el, binding) => { // 插入dom的邏輯
if(!el.domVisible) {
Object.keys(el.loadingStyle).forEach(property => {
el.loading.style[property] = el.loadingStyle[property];
});
if(el.originalPosition !== 'absolute') {
parent.style.position = 'relative'
}
if (binding.modifiers.fullscreen) {
parent.style.overflow = 'hidden'
}
el.domVisible = true;
parent.appendChild(el.loading) // 插入的是el.loading而不是el本身
Vue.nextTick(() => {
el.instance.visible = true;
});
el.domInserted = true;
}
}
}
}
複製程式碼
同樣,寫完整個邏輯,我們需要將其註冊到專案裡的Vue下:
// main.js
// ...
import Loading from 'loading/index.js'
Vue.use(Loading)
// ...
複製程式碼
至此我們已經可以使用形如
<div v-loading.fullscreen="loading" loading-text="正在載入中">
複製程式碼
這樣的方式來實現呼叫一個loading元件了。
總結
在用Vue寫我們的專案的時候,不管是寫頁面還是寫形如這樣的功能型元件,其實都是一件很有意思的事情。本文介紹的三種呼叫元件的方式,也是根據實際情況出發而實際操作、實現的。不同的元件通過不同的方式去呼叫,方便了開發人員,也能更好地對程式碼進行維護。當然也許還有其他的方式,我並沒有瞭解,也歡迎大家在評論裡指出!
最後再次感謝element-ui的原始碼給予的極大啟發。