Vue中你不知道但卻很實用的黑科技
最近數月一直投身于 iView 的開源工作中,完成了大大小小 30 多個 UI 組件,在 Vue 組件化開發中積累了不少經驗。其中也有很多帶有技巧性和黑科技的組件,這些特性有的是 Vue 文檔中提到但卻容易被忽略的,有的更是沒有寫在文檔里,今天就說說 Vue 組件的高級玩法。
目錄
- 遞歸組件
- 自定義組件使用 v-model
- 使用 $compile() 在指定上下文中手動編譯組件
- 內聯模板 inline-template
- 隱式創建 Vue 實例
遞歸組件
遞歸組件在文檔中有介紹,只要給組件指定一個 name 字段,就可以在該組件遞歸地調用自己,例如:
var iview = Vue.extend({
name: 'iview',
template:
'<div>' +
// 遞歸地調用它自己
'<iview></iview>' +
'</div>'
})
這種用法在業務中并不常見,在 iView 的級聯選擇組件中使用了該特性
效果如下圖所示:
圖中每一列是一個組件( caspanel.vue ),一開始想到用 v-for 來渲染列表,但后面發現擴展性極低,而且隨著功能的豐富,實現起來很困難,處理的邏輯很多,于是改寫成了遞歸組件:
<ul v-if="data && data.length" :class="[prefixCls + '-menu']">
<Casitem
v-for="item in data"
:prefix-cls="prefixCls"
:data.sync="item"
:tmp-item="tmpItem"
@click.stop="handleClickItem(item)"
@mouseenter.stop="handleHoverItem(item)"></Casitem>
</ul><Caspanel v-if="sublist && sublist.length" :prefix-cls="prefixCls" :data.sync="sublist" :disabled="disabled" :trigger="trigger" :change-on-select="changeOnSelect"></Caspanel>
props 比較多,可以忽略,但其中關鍵的兩個是 data 和 sublist ,即當前列數據和子集的數據,因為預先不知道有多少下級,所以只需傳遞下級數據給組件本身,如果為空時,遞歸就結束了,Vue 這樣設計的確很精妙。
注:該方法在 Vue 1.x 和 2.x 中都支持。
自定義組件使用 v-model
我們知道, v-model 是在表單類元素上進行雙向綁定時使用的,比如:
<template>
<input type="text" v-model="data">
{{ data }}
</template>
<script>
export default {
data () {
return {
data: ''
}
}
}
</script>
這時 data 就是雙向綁定的,輸入的內容會實時顯示在頁面上。在 Vue 1.x 中,自定義組件可以使用 props 的 .sync 雙向綁定,比如:
<my-component :data.sync="data"></my-component>
在 Vue 2.x 中,可以直接在自定義組件上使用 v-model 了,比如:
<my-component v-model="data"></my-component>
在組件 my-component 中,通過 this.$emit('input') 就可以改變data的值了。
雖然 Vue 1.x 中無法這樣使用,但是如果你的組件的模板外層是 input 、 select 、 textarea 等支持綁定 v-model 特性的元素,也是可以使用的,比如 my-component 的代碼是:
<template>
<input type="text">
</template>
那也可以使用上面2.x的寫法。
使用 $compile() 在指定上下文中手動編譯組件
注:該方法是在 Vue 1.x 中的使用介紹,官方文檔并沒有給出該方法的任何說明,不可過多依賴此方法。
使用 $compile() 方法,可以在任何一個指定的上下文(Vue實例)上手動編譯組件,該方法在 iView 新發布的表格組件 Table 中有使用: 由于表格的列配置是通過一個 Object 傳入 props 的,因此不能像 slot 那樣自動編譯帶有 Vue 代碼的部分,因為傳入的都是字符串,比如:
{
render (row) {
return `<i-button>${row.name}</i-button>`
}
}
render函數最終返回一個字符串,里面含有一個自定義組件 i-button,如果直接用 {{{ }}} 顯示,i-button 是不會被編譯的,那為了實現在單元格內支持渲染自定義組件,就用到了 $compile() 方法。
比如我們在組件的父級編譯:
// 代碼片段
const template = this.render(this.row); // 通過上面的render函數得到字符串
const div = document.createElement('div');
div.innerHTML = template;
this.$parent.$compile(div); // 在父級上下文編譯組件
this.$el.appendChild(cell); // 將編譯后的html插入當前組件
這樣一來, i-button 就被編譯了。
在某些時候使用 $compile() 確實能帶來益處,不過也會遇到很多問題值得思考:
- 這樣編譯容易把作用域搞混,所以要知道是在哪個Vue實例上編譯的;
- 手動編譯后,也需要在合適的時候使用 $destroy() 手動銷毀;
- 有時候容易重復編譯,所以要記得保存當前編譯實例的id,這里可以通過 Vue 組件的 _uid 來唯一標識(每個Vue實例都會有一個遞增的id,可以通過 this._uid 獲取)
另外,Vue 1.x 文檔也有提到另一個 $mount() 方法,可以實現類似的效果,在 Vue 2.x 文檔中,有 Vue.compile() 方法,用于在render函數中編譯模板字符串,讀者可以結合來看。
內聯模板 inline-template
內聯模板并不是什么新鮮東西,文檔中也有說明,只是平時幾乎用不到,所以也容易忽略。簡短解說,就是把組件的 slot 當做這個組件的模板來使用,這樣更為靈活:
<!-- 父組件: -->
<my-component inline-template>
{{ data }}
</my-component>
<!-- 子組件 -->
<script>
export default {
data () {
return {
data: ''
}
}
}
</script>
因為使用了 inline-template 內聯模板,所以子組件不需要 <template> 來聲明模板,這時它的模板直接是從 slot 來的 {{ data }} ,而這個 data 所在的上下文,是子組件的,并不是父組件的,所以,在使用內聯模板時,最容易產生的誤區就是混淆作用域。
隱式創建 Vue 實例
在 webpack 中,我們都是用 .vue 單文件的模式來開發,每個文件即一個組件,在需要的地方通過 components: {} 來使用組件。
比如我們需要一個提示框組件,可能會在父級中這樣寫:
<template>
<Message>這是提示標題</Message>
</template>
<script>
import Message from '../components/message.vue';
export default {
components: { Message }
}
</script>
這樣寫沒有任何問題,但從使用角度想,我們其實并不期望這樣來用,反而原生的 window.alert('這是提示標題') 這樣使用起來更靈活,那這時很多人可能就用原生 JS 拼字符串寫一個函數了,這也沒問題,不過如果你的提示框組件比較復雜,而且多處復用,這種方法還是不友好的,體現不到 Vue 的價值。
iView 在開發全局提示組件(Message)、通知提醒組件(Notice)、對話框組件(Modal)時,內部都是使用 Vue 來渲染,但卻是 JS 來隱式地創建這些實例,這樣我們就可以像 Message.info('標題') 這樣使用,但其內部還是通過 Vue 來管理。
下面我們來看一下具體實現:
上圖是最終效果圖,這部分 .vue 代碼比較簡單,相信大家都能寫出這樣一個組件來,所以直接說創建實例的部分,先看下核心代碼:
import Notification from './notification.vue';
import Vue from 'vue';
import { camelcaseToHyphen } from '../../../utils/assist';
Notification.newInstance = properties => {
const _props = properties || {};
let props = '';
Object.keys(_props).forEach(prop => {
props += ' :' + camelcaseToHyphen(prop) + '=' + prop;
});
const div = document.createElement('div');
div.innerHTML = `<notification${props}></notification>`;
document.body.appendChild(div);
const notification = new Vue({
el: div,
data: _props,
components: { Notification }
}).$children[0];
return {
notice (noticeProps) {
notification.add(noticeProps);
},
remove (key) {
notification.close(key);
},
component: notification,
destroy () {
document.body.removeChild(div);
}
}
};
export default Notification;
與上文介紹的 $compile() 不同的是,這種方法是在全局(body)直接使用 new Vue 創建一個 Vue 實例,我們只需要在入口處對外暴露幾個 API 即可:
import Notification from '../base/notification';
const prefixCls = 'ivu-message';
const iconPrefixCls = 'ivu-icon';
const prefixKey = 'ivu_message_key_';
let defaultDuration = 1.5;
let top;
let messageInstance;
let key = 1;
const iconTypes = {
'info': 'information-circled',
'success': 'checkmark-circled',
'warning': 'android-alert',
'error': 'close-circled',
'loading': 'load-c'
};
function getMessageInstance () {
messageInstance = messageInstance || Notification.newInstance({
prefixCls: prefixCls,
style: {
top: `${top}px`
}
});
return messageInstance;
}
function notice (content, duration = defaultDuration, type, onClose) {
if (!onClose) {
onClose = function () {
}
}
const iconType = iconTypes[type];
// if loading
const loadCls = type === 'loading' ? ' ivu-load-loop' : '';
let instance = getMessageInstance();
instance.notice({
key: `${prefixKey}${key}`,
duration: duration,
style: {},
transitionName: 'move-up',
content: `
<div class="${prefixCls}-custom-content ${prefixCls}-${type}">
<i class="${iconPrefixCls} ${iconPrefixCls}-${iconType}${loadCls}"></i>
<span>${content}</span>
</div>
`,
onClose: onClose
});
// 用于手動消除
return (function () {
let target = key++;
return function () {
instance.remove(`${prefixKey}${target}`);
}
})();
}
export default {
info (content, duration, onClose) {
return notice(content, duration, 'info', onClose);
},
success (content, duration, onClose) {
return notice(content, duration, 'success', onClose);
},
warning (content, duration, onClose) {
return notice(content, duration, 'warning', onClose);
},
error (content, duration, onClose) {
return notice(content, duration, 'error', onClose);
},
loading (content, duration, onClose) {
return notice(content, duration, 'loading', onClose);
},
config (options) {
if (options.top) {
top = options.top;
}
if (options.duration) {
defaultDuration = options.duration;
}
},
destroy () {
let instance = getMessageInstance();
messageInstance = null;
instance.destroy();
}
}
到這里組件已經可以通過 Message.info() 直接調用了,不過我們還可以在 Vue 上進行擴展: Vue.prototype.$Message = Message;
這樣我們可以直接用 this.$Message.info() 來調用,就不用 import Message 了。
后記
Vue 組件開發中有很多有意思的技巧,用好了會減少很多不必要的邏輯,用不好反而還弄巧成拙。在開發一個較復雜的組件時,一定要先對技術方案進行調研和設計,然后再編碼。
來自:http://div.io/topic/1880