跟着element學習寫組件

如何使用vue寫一個組件庫

組件以插件的形式引入使用,固然,也能夠直接在頁面引入組件文件,二者按需使用。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組件庫是如何實現的。瀏覽器

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模塊解析

button模塊目錄,有一個index.js做爲模塊入口異步

import ElButton from './src/button';

ElButton.install = function(Vue) {
  Vue.component(ElButton.name, ElButton);
};
export default ElButton;

在index.js文件中,對組件進行拓展,添加Install方法。ide

element組件入口文件解析

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;

我寫的組件與elemnet組件有什麼不一樣

代碼實現

1.html語義化

element組件實現時,html基本實現了語義化標籤。

  1. 這樣在無CSS樣子時也容易閱讀,便於閱讀維護和理解。
  2. 便於瀏覽器、搜索引擎解析。 利於爬蟲標記、利於SEO

標記組件。
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

2.兼容 v-model

element組件基本都兼容了v-model綁定值,組件使用起來更加溫馨~
兼容v-model須要作一下幾點:

  1. props中要定義value屬性。
  2. 數據變化後,經過事件觸發父組件更新數據,同時傳遞變動後的值。

(如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);
        },
    }
  }

3.組件之間傳遞數據

vue中,存在幾種組件之間數據傳遞的方案:

  1. props
  2. attrs
  3. provide / inject
  4. this.$parent/$this.$children

在平常開發中,父子組件之間數據傳遞用到比較多的方案是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 獲取父/子組件實例

4.組件通訊

emit/props傳遞函數

二者都是通知父組件執行事件的方法,可是有必定的區別:

  1. emit執行的是異步方法,props傳遞的函數在子組件中執行做爲同步函數的形式執行的。
  2. emit沒法返回函數結果,props傳遞的函數能夠返回函數結果。
發佈訂閱

對於組件嵌套過深,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]));
    }
  });
}

組件設計

1.扁平化參數

  1. 傳入的參數儘可能設計簡單點,避免複雜的對象。過於複雜的數據,在watch或者update的狀況下,影響性能
  2. 扁平化的props也能夠更好的更新數據,重置數據。其次,複雜的數據變動,外部可能會監聽不到數據變化。
  3. 若是定義傳入的傳入數據是一個對象,那組件內部就要作大量的工做,來判斷外部擦混入的對象的屬性值是否正確,並找出須要的數據內容,增長了組件工做量,也不便組件的後續維護。

2.良好的api接口設計

  1. 保持組件外部提供接口的精簡,不要過於氾濫的提供接口。
  2. 組件可定製,若是常量變爲 props 能應對更多的狀況,那麼就能夠做爲 props從父組件引入。原有的常量可做爲默認值。
    按鈕組件的樣式存在默認樣式,可是能夠經過type傳入類型,定製button組件樣式,使組件能夠適用更多場景。
export default {
    name: 'ElButton',

    props: {
      type: {
        type: String,
        default: 'default'
      },
    },
  };

3.可擴展性

組件在使用過程當中,會不斷的優化添加功能,可是組件的內部變動不能影響組件的使用,這就須要組件有很好的擴展性,在一開始,可以提供足夠比較友好的接口。

如何實現?
  1. 預留「錨點」

在組件中預留一些「插槽」,使用組件的時候,能夠再「插槽」中注入自定義的內容,從而改變組件渲染結果。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組件預留了四個「插槽」,容許使用者在先後位置均可以插入內容。

  1. 提供豐富的鉤子函數,使用者在數據變化時,能對數據進行相應處理

element組件提供了豐富的鉤子函數:

focus() {
    (this.$refs.input || this.$refs.textarea).focus();
},
blur() {
    (this.$refs.input || this.$refs.textarea).blur();
},

4.錯誤處理

組件要能接受必定的錯誤使用,能針對可預知的錯誤使用進行處理。

  1. 給props屬性設置多個數據類型,同時保證傳入和傳出的數據類型相同。
  2. 若是組件中,某個字段是父組件必定要傳入的,須要把props屬性的require設置爲true。
  3. 給重要的prop屬性設置默認數據。
  4. 兜底:數據展現或者使用父組件傳入內容以前,要先判斷數據是否存在。
focus() {
    # 先判斷this.$refs.input是否存在,才進行接下來操做,避免數據爲空報錯狀況。
    (this.$refs.input || this.$refs.textarea).focus();
}
相關文章
相關標籤/搜索