0%

如何二次封装vue组件

当我们在项目中需要多处使用开源组件,且开源组件的默认配置行为不满足需求,需要我们手动配置大量选项时,采用二次封装方式应用一些默认选项是一种比较优雅的方式。

二次封装主要是要理解 $props,$attrs,$listeners,inheritAttrs 这几个 api。在二次封装时,我们希望原有 UI 组件的配置项在封装的组件上同样有效,我们只是对一些配置项进行了默认的配置,或者进行一些功能的增强,那问题的关键就是如何实现 prop 和 listeners 的透传。对于 listeners 的透传 我们可以使用 v-on="$listeners"。对于 prop 的透传,一个可行的方式是将被封装组件的 prop 在,然后通过 v-bind="$props" 的方式透传 prop,但是这种方式要求我们必须定义与被封装组件相同的 prop,繁琐不说,组件更新增加 prop,我们也要同步添加。

以下是官方文档对于 inheritAttrs 的解释

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。

所有不被认作 props 的 attribute 可以通过 $attrs 访问。通过 $attrsinheritAttrs:false 是一种更好地方式。

对于对部分选项需要设置默认值得需求,vue2.x 中对于 v-bind 绑定的属性和单独设置的属性处理方式是默认单独设置的属性会覆盖 v-bind 绑定的属性。如果需要实现可以手动设置覆盖二次封装默认配置的需求,可以采用下面的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<componentsA v-bind="propsWithDefault" />
</template>
<script>
export default {
.......
computed:{
propsWithDefault(){
return {
defaultOption:'defaultValue'
...this.$attrs
}
}
}
}
</script>

如果采用 jsx + render 函数的方式,则可以利用 js 的完全编程能力。
可以参考我下面封装的上传组件,在上传类型是图片时,对 accept 设置了默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<script>
import { message, Upload, Button, Icon, Modal } from "ant-design-vue";

function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
}

/**
* 基于 a-upload 二次封装的文件上传组件。
* @displayName SeekerUpload
*/
export default {
inheritAttrs: false,
name: "SeekerUpload",
components: {
"a-upload": Upload,
"a-button": Button,
"a-icon": Icon,
"a-modal": Modal,
},
props: {
/**
* 上传文件类型
* @values file image
*/
uploadType: {
type: String,
default: "file",
validator: (x) => ["file", "image"].includes(x),
},
/** 上传文件数量限制 */
number: {
type: Number,
default: 1,
},
/** 文件上传按钮说明文字 */
btnText: {
type: String,
default: "选择文件",
},
/** 图片上传说明文字 */
tipText: {
type: String,
default: "选择图片",
},
alwaysShowBtn: {
type: Boolean,
default: false,
},
},
computed: {
listType() {
if (this.uploadType === "image") {
return "picture-card";
}
return "text";
},
},
data() {
return {
previewImage: "",
previewVisible: false,
};
},
methods: {
beforeUpload() {
// 使用自定义上传方法
if (typeof this.$attrs.customRequest === "function") {
return true;
}
return false;
},
handleCancel() {
this.previewVisible = false;
},
async handlePreview(file) {
if (this.uploadType === "file") {
return;
}
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
this.previewImage = file.url || file.preview;
this.previewVisible = true;
},
handleChange({ fileList }) {
const files = fileList.slice(-this.number);
// 对于上传图片 限制图片大小在 800KB 以内
if (this.uploadType === "image" && files.length) {
const limit = 800 * 1024;
// 判断最后添加的图片
const latestAdd = files[files.length - 1];
if (latestAdd.size > limit) {
message.error("图片大小超过限制,最大不得超过800KB!");
files.pop();
}
}
this.$emit("update:fileList", files);
this.$emit("listChange", files);
},
},
render() {
const {
listType,
uploadType,
$attrs,
$listeners,
handleCancel,
previewVisible,
previewImage,
handlePreview,
handleChange,
tipText,
btnText,
beforeUpload,
number,
alwaysShowBtn,
} = this;
const bind = {
props: {
accept: uploadType === "image" ? "image/*" : undefined,
...$attrs,
},
on: {
preview: handlePreview,
change: handleChange,
...$listeners,
},
};

const uploadControl =
uploadType === "image" ? (
<div>
<a-icon type="plus" />
<div class="ant-upload-text">{tipText}</div>
</div>
) : (
<a-button disabled={$attrs.disabled}>
<a-icon type="upload" />
{btnText}
</a-button>
);

return (
<div>
<a-upload list-type={listType} beforeUpload={beforeUpload} {...bind}>
{($attrs.fileList && $attrs.fileList.length < number) || alwaysShowBtn
? uploadControl
: null}
</a-upload>
<a-modal
visible={previewVisible}
footer={null}
width={600}
{...{ on: { cancel: handleCancel } }}
>
<img alt="example" style="width: 100%" src={previewImage} />
</a-modal>
</div>
);
},
};
</script>