vue2 组件库开发记录-开发技巧

Posted 在厕所喝茶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue2 组件库开发记录-开发技巧相关的知识,希望对你有一定的参考价值。

前言

本文主要是记录我在开发组件库时的一些开发技巧。并且会讲解一些比较特殊的组件。

install

我们在使用element-ui的时候,可以通过Vue.use(Button)注册组件。use函数内部实际上就是调用了传入的对象的install函数,同时install函数会接受到一个vue参数。

import Alert from "./src/alert.vue";

Alert.install = Vue => Vue.component(Alert.name, Alert);

export default Alert;

props 属性

props 是用来做父子组件之间的通信,并且 props 的写法有很多种,相信做 vue 开发的同学应该都知道的。所以我这里主要说 2 点

  • 当默认值是一个数组或者是对象时,必须从一个工厂函数中返回,否则所有组件实例都会共用一个值
export default {
  props: {
    obj: {
      type: Object,
      // 简写的时候需要注意返回的{}外层需要套一个(),否则就是一个空函数,没有返回值
      default: () => ({})
    }
  }
};
  • 自定义校验。这个在项目开发中可能会很少会用到。但是在组件开发中却经常用到。比如一个Button组件的type属性是String类型,但是只接受default, primary, success,开发者可能传入的字符串不符合我们的预期,所以需要用到自定义校验,给开发者一个适当的提示。
export default {
  props: {
    type: {
      type: String,
      default: 'default',
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['default', 'primary', 'success'].indexOf(value) !== -1;
      }
    }
  }
};

注意:组件的 this,在defaultvalidator字段中是不可用的。

provide 和 inject 属性

这两个属性我相信很多同学都是没有使用过的,甚至有的人可能都没听过。这两个属性可以用来做跨组件层级通讯,实现父子或者祖孙组件之间的通信。可能会有人说,我使用props属性将数据一层一层的传递下去不也可以实现 provideinject 的效果吗,这个说的也没错,但是有些场景却实现不了。我们看一下checkbox组件的使用方式

<checkbox-group v-model="value">
  <checkbox label="抽烟"></checkbox>
  <checkbox label="喝酒"></checkbox>
  <checkbox label="探头"></checkbox>
</checkbox-group>

上面可以看见checkbox-group组件无法通过props属性给checkbox 组件传递数据,因为checkbox组件并不是直接写在checkbox-group组件内部的,而是通过slot插槽放置到checkbox-group组件中的,从而实现了父子关系的组件。此时,provideinject 属性就可以解决这种组合组件的写法之间的通讯问题。provideinject的特点如下:

  • provide可以是一个对象或者是返回一个对象的函数(推荐使用这种写法)。
  • inject可以是一个数组或者是一个对象(推荐这种写法)。
  • 如果provide传入的是一个响应式的对象(组件开发中一般直接传入this),那么inject接收的值也是个响应式数据

代码示例:

// checkbox-group组件
export default {
  props: {
    // 绑定值
    value: {
      type: Array
    },
  },
  provide() {
    return {
      // 直接把组件实例传递给所有子孙组件
      CheckboxGroup: this
    };
  }
};

// checkbox组件
export default {
  inject: {
    // 接收checkbox-group组件通过provide传递过来的数据
    CheckboxGroup: {
      default: ""
    }
  },
  mounted(){
    // checkbox-group组件的value值
    console.log(this.CheckboxGroup?.value)
  }
};

provideinject的使用场景有两个,分别如下:

  • 具有组合关系的组件。比方说上面的checkbox-group组件和checkbox组件,他们就是组合关系。checkbox组件可以单个进行使用,也可以和checkbox-group组件在一起组合使用。checkbox组件通过判断this.CheckboxGroup是否存在来判断开发者是单个使用还是结合checkbox-group组件一起使用,从而实现不同的逻辑。
  • 跨层级组件传递数据。当你的组件层级很深的时候,比如A->B->C->D。如果A想要跟D进行通讯,就必须通过BCprops属性一层一层的传递下去,这样会造成数据的混乱的,而且如果传递的数据非常多,写起来也很麻烦。所以这个时候可以使用provideinject来进行通信,数据的流向就不用经过BC,你只需要专注于AD之间的数据处理即可。

$children 和 $parent

$children是用来获取当前组件的所有子组件,$parent是用来获取当前组件的父组件。组件的子组件可能会有多个,但是父组件只能有一个(根组件没有父组件)。所以我们可以通过递归的方式获取该组件的子孙组件和父级组件,并实现广播和派发的功能,实现具有上下级组件关系的通讯。

注意:一般查找父级组件或者子孙组件都是通过组件的name字段进行查找的,所以每个组件内部最好有一个name字段,这样才能有效过滤出你想要查找的组件。

代码示例:

// 查找所有子孙组件
function findChildren(context, componentName) {
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) {
      components.push(child);
    }
    const children = findChildren(child, componentName);
    return components.concat(children);
  }, []);
}

// 查找所有父级组件
function findParents(context, componentName) {
  const parents = [];
  const parent = context.$parent;
  if (parent) {
    if (parent.$options.name === componentName) {
      parents.push(parent);
    }
    return parents.concat(findParents(parent, componentName));
  } else {
    return [];
  }
}

派发与广播

// 向下通知
function broadcast(options) {
  const { eventName, params, componentName } = options;
  // 获取当前组件下的所有的孩子
  const broad = (children) => {
    children.forEach((child) => {
      if (componentName) {
        if (child.$options.name === componentName) {
          child.$emit(eventName, params);
        }
      } else {
        child.$emit(eventName, params);
      }

      if (child.$children) {
        broad(child.$children);
      }
    });
  };
  broad(this.$children);
}

// 向上通知
function dispatch(options) {
  const { eventName, params, componentName } = options;

  let parent = this.$parent || this.$root;
  let name = parent.$options.name;

  while (parent) {
    if (componentName) {
      if (name === componentName) {
        parent.$emit(eventName, params);
      }
    } else {
      parent.$emit(eventName, params);
    }
    parent = parent.$parent;
    name = parent?.$options.name;
  }
}

广播通信例子

const parent = {
  name:'parent'
  template:`
  <div>
    <div @click='onBroadcast'>broadcast</div>
    <child/>
  </div>
  `,
  methods:{
    onBroadcast(){
      broadcast.call(this,{
        name:'custom',
        params:'hello world',
        componentName:'child'
      })
    }
  }
}

const child = {
  name:'child',
  created(){
    this.$on('custom',event=>{
      console.log(event) // hello world
    })
  }
}

在上面例子中,child 组件需要在 parent 组件进行广播前使用$on注册事件,否则是接收不到 parent 组件的广播的

广播与派发的应用场景可用于FormFormItem组件的表单校验:

  • inputcheckbox等表单组件值发生变化的时候,通过dispatch向上通知FormItem组件进行校验
  • Form组件需要校验整个表单的时候,通过findChildren查找到所有FormItem组件,调用FormItem组件内部的方法进行校验,获得校验结果,从而反馈给用户

EventBus 事件总线

EventBus 可实现任意组件之间的通信,借助 EventBus 的$emit派发事件,$on监听事件就可以实现任意组件之间的通信。 EventBus 实际上是通过发布/订阅方法来实现的,通过导出一个new Vue()实例(单例),所有组件都是用该实例进行事件的派发和监听。

代码示例:

// event-bus.js
import Vue from 'vue';
const EventBus = new Vue();

// 发送事件
EventBus.$emit('custom', { age: 1 });
// 接收事件
EventBus.$on('custom', (event) => {});
// 监听一次事件
EventBus.$once('custom', (event) => {});
// 移除事件
EventBus.$off('custom');

EventBus 在组件库开发中使用的场景比较少,但也是一种跨组件的通信方式,对比于propsprovide 和 inject$children 和 $parent这些通信方式(只能在具有上下级关系的组件中进行通信,不能再兄弟组件之间进行通信),优点在于可以在任意组件之间进行通信,缺点就是一旦事件多了,就变得很难管理。

$attrs

$attrs包含哪些没有在组件的props字段中声明的属性(class 和 style 除外)。

例子

const A = {
  props:['name']
}

<A name='张三' age='17' sex='男' />

从上面的例子中,我们可以看见props属性中只声明了name字段,所以agesex字段包含在了$attrs中,name并不在$attrs

常用于那些有许多原生属性的组件中,比如input组件,原生的input标签包含了很多字段,如果我们将input标签的所有原生属性都在props中都声明一边,那就会非常麻烦。我们一般会将一些非原生属性或者需要在组件内部使用到的原生属性声明在props中,其余的字段通过v-bind="$attrs"挂在到input标签上面

代码示例:

const LinInput = {
  template:'<input :disabled="disabled" :value="value" v-bind="$attrs" />',
  props:['disabled','value']
}


<lin-input disabled  value='1' name='age' placeholder='请输入'  />

$scopedSlots 作用域插槽

$scopedSlots作用域插槽。这个东西我相信很多同学都没接触过。我也是在开发Table组件时第一次使用到它。不得不说这个东西真的很强大很巧妙。文字说明可能不够透彻,所以下面我们通过Table组件的代码来讲解。

首先看一下使用方式:

<template>
  <lin-table value-key="id" :dataSource="tableData">
    <lin-table-column prop="date" label="日期">
      <template slot-scope="scope">
        <div>{{ scope.row.date }}</div>
      </template>
    </lin-table-column>
    <lin-table-column prop="name" label="姓名"></lin-table-column>
    <lin-table-column prop="address" label="地址"></lin-table-column>
  </lin-table>
</template>

export default {
  data() {
    return {
      tableData: [
        {
          id: 1,
          date: "2016-05-02",
          name: "王小虎",
          address: "上海市普陀区金沙江路 1518 弄",
        }
      ],
    };
  },
};

lin-table-column组件只负责收集数据,并不会渲染任何东西。比如proplabel这些数据,然后将这些数据存放在table组件中。然后通过table组件来渲染这些东西。

lin-table-column.jsx

let columnId = 0;
export default {
  name: 'LinTableColumn',
  props: {
    prop: String,
    label: String
  },
  inject: {
    // table组件的实例
    table: {
      default: null
    }
  },
  watch: {
    prop(val) {
      this.column.prop = val;
    },
    label(val) {
      this.column.label = val;
    }
  },
  created() {
    // 把该组件的props属性都存储起来
    const column = {
      ...this.$props,
      id: `col-${columnId++}`
    };
    // 默认提供一个渲染单元格的render函数,核心内容
    // h是渲染函数,rowData是每一行的数据
    column.renderCell = (h, rowData) => {
      let render = (h, data) => {
        return data.row[column.prop];
      };
      // 判断是不是使用了插槽
      if (this.$scopedSlots.default) {
        // 通过this.$scopedSlots.default获取默认插槽的VNode,也就是这个东西
        // <template slot-scope="scope">
        //   <div>{{ scope.row.date }}</div>
        // </template>
        render = (h, data) => this.$scopedSlots.default(data);
      }
      return render(h, rowData);
    };
    this.column = column;
  },
  mounted() {
    if (this.table) {
      // 把该组件收集到的数据存储在table组件中。
      this.table.columns.push(this.column);
    }
  },
  destroyed() {
    if (this.table) {
      // 销毁的时候需要把对应的列移除掉
      const index = this.table.columns.findIndex(
        (column) => column.id === this.column.id
      );
      if (index > -1) {
        this.table.columns.splice(index, 1);
      }
    }
  },
  render() {
    // 不做实际的渲染
    return null;
  }
};

table.vue

<template>
  <div>
    <div class="lin-table-slot">
      <!-- lin-table-column组件 -->
      <slot></slot>
    </div>
    <table class="lin-table">
      <!-- 渲染头部相关的东西 -->
      <lin-table-header ref="linTableHeaderComp"></lin-table-header>

      <!-- 渲染表格内容 -->
      <lin-table-body ref="linTableBodyComp"></lin-table-body>
    </table>
  </div>
</template>

<script>
import LinTableHeader from './TableHeader.jsx';
import LinTableBody from './TableBody.jsx';
export default {
  name: 'LinTable',
  components: {
    LinTableHeader,
    LinTableBody
  },
  props: {
    // 数据源
    dataSource: {
      type: Array,
      default: () => [],
      require: true
    },
    // 每一行数据的唯一标识key
    valueKey: {
      type: String,
      require: true
    }
  },
  provide() {
    return {
      // 往子组件中注入table实例,以便子组件可以访问到table组件的数据
      table: this
    };
  },
  data() {
    return {
      // 存储lin-table-column组件收集到的信息
      columns: []
    };
  }
};
</script>

TableHeader 组件的内容还是很简单的,就是根据使用v-forcolumns字段的 label 字段渲染出来。所以这里不讲解 TableHeader 组件,直接讲解TableBody组件

TableBody.jsx

export default {
  name: 'LinTableBody',
  computed: {
    // 数据源
    dataSource() {
      if (this.table) {
        return this.table.dataSource;
      }
      return [];
    },
    // 列数组
    columns() {
      if (this.table) {
        return this.table.columns;
      }
      return [];
    },
    // 每一行数据的唯一标识 key
    valueKey() {
      if (this.table) {
        return this.table.valueKey;
      }
      return '';
    }
  },
  inject: {
    table: {
      default: null
    }
  },
  render(h) {
    const { dataSource, columns, valueKey } = this;
    return (
      <tbody class="lin-table-tbody">
        {dataSource.map((row, rowIndex) => {
          const rowKey = row[valueKey] || rowIndex;
          return (
            <tr key={rowKey}>
              {columns.map((column, idx) => (
                <td key={`${rowKey}-${idx}`}>
                  // lin-table-column中的renderCell渲染函数
                  {column.renderCell(h, {
                    row,
                    column,
                    rowIndex
                  })}
                </td>
              ))}
            </tr>
          );
        })}
      </tbody>
    );
  }
};

从上面可以看出TableBody组件的核心就是调用renderCell函数,而renderCell这个函数就是在lin-table-column组件收集到的每个单元格的渲染函数。

Vue.extends 实现 js 调用组件

element-uimessagemessage-box等组件都通过 js 的方式进行调用。在实际项目开发中有时候也需要根据需求实现一个 js 调用的组件,比如,我点击一个按钮,用户没有权限的时候需要弹框显示暂无权限,游客则需要弹出登录框。所以,下面以message组件为例,讲解一下怎么通过 Vue.extends 实现 js 调用组件。

首先新建一个message.vue,并实现你需要的功能

<template&g

以上是关于vue2 组件库开发记录-开发技巧的主要内容,如果未能解决你的问题,请参考以下文章

饿了么基于Vue2.0的通用组件开发之路(分享会记录)

vue2.0开发自己的UI组件库

微信小程序组件库开发记录

记录--九个超级好用的 Javascript 技巧

ElementUI获取表格选择的行数据小技巧

ElementUI获取表格选择的行数据小技巧