vue2 组件库开发记录-开发技巧
Posted 在厕所喝茶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue2 组件库开发记录-开发技巧相关的知识,希望对你有一定的参考价值。
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,在default
和validator
字段中是不可用的。
provide 和 inject 属性
这两个属性我相信很多同学都是没有使用过的,甚至有的人可能都没听过。这两个属性可以用来做跨组件层级通讯,实现父子或者祖孙组件之间的通信。可能会有人说,我使用props
属性将数据一层一层的传递下去不也可以实现 provide
和 inject
的效果吗,这个说的也没错,但是有些场景却实现不了。我们看一下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
组件中的,从而实现了父子关系的组件。此时,provide
和 inject
属性就可以解决这种组合组件的写法之间的通讯问题。provide
和 inject
的特点如下:
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)
}
};
provide
和 inject
的使用场景有两个,分别如下:
- 具有组合关系的组件。比方说上面的
checkbox-group
组件和checkbox
组件,他们就是组合关系。checkbox
组件可以单个进行使用,也可以和checkbox-group
组件在一起组合使用。checkbox
组件通过判断this.CheckboxGroup
是否存在来判断开发者是单个使用还是结合checkbox-group
组件一起使用,从而实现不同的逻辑。 - 跨层级组件传递数据。当你的组件层级很深的时候,比如
A->B->C->D
。如果A
想要跟D
进行通讯,就必须通过B
和C
的props
属性一层一层的传递下去,这样会造成数据的混乱的,而且如果传递的数据非常多,写起来也很麻烦。所以这个时候可以使用provide
和inject
来进行通信,数据的流向就不用经过B
和C
,你只需要专注于A
和D
之间的数据处理即可。
$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 组件的广播的
广播与派发的应用场景可用于Form
和FormItem
组件的表单校验:
input
,checkbox
等表单组件值发生变化的时候,通过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 在组件库开发中使用的场景比较少,但也是一种跨组件的通信方式,对比于props
,provide 和 inject
,$children 和 $parent
这些通信方式(只能在具有上下级关系的组件中进行通信,不能再兄弟组件之间进行通信),优点在于可以在任意组件之间进行通信,缺点就是一旦事件多了,就变得很难管理。
$attrs
$attrs
包含哪些没有在组件的props
字段中声明的属性(class 和 style 除外)。
例子
const A = {
props:['name']
}
<A name='张三' age='17' sex='男' />
从上面的例子中,我们可以看见props
属性中只声明了name
字段,所以age
和sex
字段包含在了$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
组件只负责收集数据,并不会渲染任何东西。比如prop
,label
这些数据,然后将这些数据存放在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-for
将columns
字段的 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-ui
的message
,message-box
等组件都通过 js 的方式进行调用。在实际项目开发中有时候也需要根据需求实现一个 js 调用的组件,比如,我点击一个按钮,用户没有权限的时候需要弹框显示暂无权限,游客则需要弹出登录框。所以,下面以message
组件为例,讲解一下怎么通过 Vue.extends 实现 js 调用组件。
首先新建一个message.vue
,并实现你需要的功能
<template&g以上是关于vue2 组件库开发记录-开发技巧的主要内容,如果未能解决你的问题,请参考以下文章