組件以插件的形式引入使用,固然,也能夠直接在頁面引入組件文件,二者按需使用。html
安裝插件:vue
import Button from './oyButton'; Button.install = function (Vue) { Vue.component(Button.name, Button); } export default Button;
vue.install源碼:element-ui
export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { # /*檢測該插件是否已經被安裝*/ if (plugin.installed) { return } const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { # /*install執行插件安裝*/ plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } plugin.installed = true return this } }
經過源碼可知,vue不會重複安裝同一個插件。以第一次安裝爲準api
如今,能夠在代碼中使用組件啦~數組
<oy-button>我是按鈕按鈕</oy-button>
以上,是一個很是簡單的組件庫實現。
如今來看看element組件庫是如何實現的。瀏覽器
這裏重點說下packages目錄和src目錄bash
|-- packages # 組件源碼目錄 |-- button # button組件目錄,一個組件一個文件,方便管理 |-- src # 組件實現代碼 |-- button-group.vue |-- button.vue |-- index.js # 組件入口文件 |-- src |--directives # 實現滾輪優化,鼠標點擊優化 |--locale # 國際化 |--mixins # 公用邏輯代碼 |--transitions # 樣式過分效果 |--utils # 工具類包 |--index.js # 源碼入口文件
整個目錄結構很是清晰。app
button模塊目錄,有一個index.js做爲模塊入口異步
import ElButton from './src/button'; ElButton.install = function(Vue) { Vue.component(ElButton.name, ElButton); }; export default ElButton;
在index.js文件中,對組件進行拓展,添加Install方法。ide
import Button from '../packages/button/index.js'; const components = [Button] # 定義一個install方法 const install = function(Vue, opts = {}) { locale.use(opts.locale); locale.i18n(opts.i18n); # 將全部的功能模塊進行註冊。 components.map(component => { Vue.component(component.name, component); }); # 註冊插件 Vue.use(Loading.directive); const ELEMENT = {}; ELEMENT.size = opts.size || ''; # 綁定Vue實例方法 Vue.prototype.$message = Message; }; if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } # 最後,將全部功能模塊和install方法一塊兒導出。 # 這樣當引入element-ui時,即可以使用vue.use(element-ui)進行註冊,即將全部的功能組件進行全局註冊。 module.exports = { version: '2.3.8', locale: locale.use, i18n: locale.i18n, install, Button, } module.exports.default = module.exports;
element組件實現時,html基本實現了語義化標籤。
標記組件。
Badge 標記組件部分源碼:
<!-- sup標籤語義:上標文本 --> <transition name="el-zoom-in-center"> <sup v-show="!hidden && (content || content === 0 || isDot)" v-text="content" class="el-badge__content" :class="{ 'is-fixed': $slots.default, 'is-dot': isDot }"> </sup> </transition>
ps: 本身寫代碼都是div span
element組件基本都兼容了v-model綁定值,組件使用起來更加溫馨~
兼容v-model須要作一下幾點:
(如text元素使用input事件來改變value屬性 和 checkbox使用的change事件來改變check屬性)
input組件源碼:
export default { props: { # 定義value value: [String, Number], }, methods: { handleInput(event) { if (this.isOnComposition) return; const value = event.target.value; # 變動數據之後經過input去更新父組件數據 this.$emit('input', value); this.setCurrentValue(value); }, } }
vue中,存在幾種組件之間數據傳遞的方案:
在平常開發中,父子組件之間數據傳遞用到比較多的方案是props。當組件層次比較深,就使用attrs來透傳數據:
<el-select v-model="selectValue" v-bind="$attrs" v-on="$listeners"> <template v-if="label && keyValue"> <el-option v-for="(item, index) in selectList" :key="index" :label="item[label]" :value="item[keyValue]"></el-option> </template> </el-select>
element組件,在父子組件傳遞數據也是使用props,可是當組件層次比較深,或者不清楚組件層次時,使用的是:provide / inject
inject: { elForm: { default: '' }, elFormItem: { default: '' } },
關於provide / inject:
「這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效」 --vue文檔簡單來講,就是父組件經過provide來提供變量,子組件經過inject來引用變量。
vue的inject源碼:
# src/core/instance/inject.js export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
provide是向下傳遞數據,先獲取provide內容,而後傳遞給vm._provided設置成全局數據。inject會根據選項的 key 數組一層層向上遍歷,拿到結果。
provide 相對於props,實現了跨層級提供數據。須要注意的是provide不是響應式的。
方法 | 解釋 | 適用場景 |
---|---|---|
props | 用於接收來自父組件的數據 | 父子組件之間傳遞數據 |
provide | 以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效 | 替代嵌套過深的props,能夠理解爲一個bus,但只作父組件通知子組件的單向傳遞的一個屬性 |
attrs | 包含了父做用域中不做爲 prop 被識別 (且獲取) 的特性綁定 (class 和 style 除外) | 父組件傳向子組件傳的,子組件沒有經過prop接受的數據都會放在$attrs中 |
parent/child | 獲取父/子組件實例 |
二者都是通知父組件執行事件的方法,可是有必定的區別:
對於組件嵌套過深,element本身實現了一個簡易版的發佈訂閱方式:
function broadcast(componentName, eventName, params) { # 組件名稱,事件名稱,參數 # 當前組件下的子組件循環 this.$children.forEach(child => { # 獲取組件名稱 var name = child.$options.componentName; # 若是組件名稱和要觸發的事件組件名稱相同 if (name === componentName) { # 當前子組件,調用$emit方法 child.$emit.apply(child, [eventName].concat(params)); } else { # 若是沒有相等,那就繼續查找當前子組件的子組件 broadcast.apply(child, [componentName, eventName].concat([params])); } }); }
export default { name: 'ElButton', props: { type: { type: String, default: 'default' }, }, };
組件在使用過程當中,會不斷的優化添加功能,可是組件的內部變動不能影響組件的使用,這就須要組件有很好的擴展性,在一開始,可以提供足夠比較友好的接口。
在組件中預留一些「插槽」,使用組件的時候,能夠再「插槽」中注入自定義的內容,從而改變組件渲染結果。element組件庫在這方面作得很好。
input組件部分源碼:
<div> <template v-if="type !== 'textarea'"> <!-- 前置元素 --> <div class="el-input-group__prepend" v-if="$slots.prepend"> <slot name="prepend"></slot> </div> <input> <!-- 前置內容 --> <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon" :style="prefixOffset"> <slot name="prefix"></slot> </span> <!-- 後置內容 --> <span> <span class="el-input__suffix-inner"> <template v-if="!showClear"> <slot name="suffix"></slot> </template> </span> </span> <!-- 後置元素 --> <div class="el-input-group__append" v-if="$slots.append"> <slot name="append"></slot> </div> </template> </div>
Input組件預留了四個「插槽」,容許使用者在先後位置均可以插入內容。
element組件提供了豐富的鉤子函數:
focus() { (this.$refs.input || this.$refs.textarea).focus(); }, blur() { (this.$refs.input || this.$refs.textarea).blur(); },
組件要能接受必定的錯誤使用,能針對可預知的錯誤使用進行處理。
focus() { # 先判斷this.$refs.input是否存在,才進行接下來操做,避免數據爲空報錯狀況。 (this.$refs.input || this.$refs.textarea).focus(); }