跟着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();
}
相关文章
相关标签/搜索