最近數月一直投身於 iView 的開源工做中,完成了大大小小 30 多個 UI 組件,在 Vue 組件化開發中積累了很多經驗。其中也有不少帶有技巧性和黑科技的組件,這些特性有的是 Vue 文檔中提到但卻容易被忽略的,有的更是沒有寫在文檔裏,今天就說說 Vue 組件的高級玩法。html
本文所講內容大多在 iView 項目中使用,你們能夠前往關注,並結合源代碼來研究其中的奧妙。項目地址:
https://github.com/iview/iviewvue
遞歸組件android
自定義組件使用 v-model
webpack
使用$compile()
在指定上下文中手動編譯組件git
內聯模板inline-template
github
隱式建立 Vue 實例web
遞歸組件在文檔中有介紹,只要給組件指定一個 name
字段,就能夠在該組件遞歸地調用本身,例如:app
var iview = Vue.extend({ name: 'iview', template: '<div>' + // 遞歸地調用它本身 '<iview></iview>' + '</div>' })
這種用法在業務中並不常見,在 iView 的級聯選擇組件中使用了該特性
(https://github.com/iview/iview/tree/master/src/components/cascader)
效果以下圖所示:
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 中有使用:
https://github.com/iview/iview/tree/master/src/components/table/cell.vue
因爲表格的列配置是經過一個 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 所在的上下文,是子組件的,並非父組件的,因此,在使用內聯模板時,最容易產生的誤區就是混淆做用域。
在 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 來管理。相關代碼地址:
https://github.com/iview/iview/tree/master/src/components/base/notification
下面咱們來看一下具體實現:
上圖是最終效果圖,這部分 .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 組件開發中有不少有意思的技巧,用好了會減小不少沒必要要的邏輯,用很差反而還弄巧成拙。在開發一個較複雜的組件時,必定要先對技術方案進行調研和設計,而後再編碼。
iView 還有不少開發技巧和有意思的代碼,後面有時間咱們再繼續探討吧,最近發佈的幾個版本都有較大的更新,但願你們能夠關注和推廣 iView ?: