uni-app组件开发实战:从基础到进阶的最佳实践 0 次阅读

组件化开发是现代前端的重要特征,本文将详细介绍如何在uni-app中开发高质量的UI组件,助你打造自己的组件库。

1. 组件基础开发

1.1 组件目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
components
├── base # 基础组件
│ ├── button
│ ├── input
│ └── icon
├── business # 业务组件
│ ├── product-card
│ ├── order-item
│ └── user-info
└── common # 公共组件
├── loading
├── empty
└── error

1.2 基础组件示例

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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
<!-- components/base/button/index.vue -->
<template>
<view
class="custom-button"
:class="[
`custom-button--${type}`,
`custom-button--${size}`,
{
'custom-button--plain': plain,
'custom-button--disabled': disabled,
'custom-button--loading': loading
}
]"
:hover-class="disabled ? '' : 'custom-button--hover'"
@click="handleClick"
>
<view class="custom-button__content">
<text v-if="loading" class="custom-button__loading"></text>
<text v-if="icon" class="custom-button__icon" :class="icon"></text>
<text class="custom-button__text"><slot></slot></text>
</view>
</view>
</template>

<script>
export default {
name: 'CustomButton',
props: {
// 按钮类型
type: {
type: String,
default: 'default',
validator: value => {
return ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
}
},
// 按钮尺寸
size: {
type: String,
default: 'normal',
validator: value => {
return ['small', 'normal', 'large'].includes(value)
}
},
// 是否朴素按钮
plain: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 图标类名
icon: {
type: String,
default: ''
}
},
methods: {
handleClick(event) {
if (this.disabled || this.loading) return
this.$emit('click', event)
}
}
}
</script>

<style lang="scss">
.custom-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 30rpx;
font-size: 28rpx;
height: 80rpx;
line-height: 1;
text-align: center;
border-radius: 8rpx;
background: #fff;
border: 2rpx solid #dcdfe6;
box-sizing: border-box;
transition: all 0.3s;

&--hover {
opacity: 0.8;
}

&--primary {
color: #fff;
background: #409eff;
border-color: #409eff;
}

&--success {
color: #fff;
background: #67c23a;
border-color: #67c23a;
}

&--warning {
color: #fff;
background: #e6a23c;
border-color: #e6a23c;
}

&--danger {
color: #fff;
background: #f56c6c;
border-color: #f56c6c;
}

&--plain {
background: transparent;

&.custom-button--primary {
color: #409eff;
}

&.custom-button--success {
color: #67c23a;
}

&.custom-button--warning {
color: #e6a23c;
}

&.custom-button--danger {
color: #f56c6c;
}
}

&--disabled {
opacity: 0.5;
cursor: not-allowed;
}

&--small {
height: 60rpx;
padding: 0 20rpx;
font-size: 24rpx;
}

&--large {
height: 100rpx;
padding: 0 40rpx;
font-size: 32rpx;
}

&__content {
display: flex;
align-items: center;
justify-content: center;
}

&__loading {
width: 28rpx;
height: 28rpx;
margin-right: 10rpx;
border: 2rpx solid currentColor;
border-radius: 50%;
border-right-color: transparent;
animation: button-loading 0.8s linear infinite;
}

&__icon {
margin-right: 10rpx;
}
}

@keyframes button-loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
</style>

2. 组件通信方式

2.1 Props/Events

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
<!-- 父组件 -->
<template>
<custom-form
:model="formData"
:rules="formRules"
@submit="handleSubmit"
>
<custom-form-item label="用户名" prop="username">
<custom-input v-model="formData.username" />
</custom-form-item>
</custom-form>
</template>

<script>
export default {
data() {
return {
formData: {
username: ''
},
formRules: {
username: [
{ required: true, message: '请输入用户名' }
]
}
}
},
methods: {
handleSubmit(values) {
console.log('表单提交:', values)
}
}
}
</script>

2.2 Provide/Inject

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
<!-- components/form/index.vue -->
<script>
export default {
name: 'CustomForm',
provide() {
return {
form: this
}
},
props: {
model: {
type: Object,
required: true
},
rules: {
type: Object,
default: () => ({})
}
},
methods: {
validate(callback) {
// 表单验证逻辑
}
}
}
</script>

<!-- components/form-item/index.vue -->
<script>
export default {
name: 'CustomFormItem',
inject: ['form'],
props: {
label: String,
prop: String
},
mounted() {
if (this.prop) {
this.form.fields.push(this)
}
}
}
</script>

3. 跨端适配

3.1 条件编译

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
<template>
<view class="custom-input">
<!-- #ifdef MP -->
<input
:value="value"
:type="type"
:password="password"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
/>
<!-- #endif -->

<!-- #ifdef H5 -->
<input
v-model="inputValue"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
@focus="handleFocus"
@blur="handleBlur"
/>
<!-- #endif -->
</view>
</template>

<script>
export default {
name: 'CustomInput',
props: {
value: String,
type: {
type: String,
default: 'text'
},
password: Boolean,
placeholder: String,
disabled: Boolean,
maxlength: {
type: Number,
default: -1
}
},
data() {
return {
inputValue: this.value
}
},
watch: {
value(val) {
this.inputValue = val
},
inputValue(val) {
this.$emit('input', val)
this.$emit('change', val)
}
},
methods: {
handleInput(e) {
// #ifdef MP
const value = e.detail.value
// #endif

// #ifdef H5
const value = e.target.value
// #endif

this.inputValue = value
},
handleFocus(e) {
this.$emit('focus', e)
},
handleBlur(e) {
this.$emit('blur', e)
}
}
}
</script>

3.2 样式适配

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
/* styles/mixins.scss */
// 1px边框
@mixin hairline($position: bottom, $color: #dcdfe6) {
position: relative;

&::after {
content: '';
position: absolute;
#{$position}: 0;
left: 0;
width: 100%;
height: 1px;
background: $color;
transform: scaleY(0.5);

// #ifdef H5
@media (-webkit-min-device-pixel-ratio: 2) {
transform: scaleY(0.5);
}
// #endif
}
}

// 安全区域
@mixin safe-area($position: bottom) {
// #ifdef H5
padding-#{$position}: constant(safe-area-inset-#{$position});
padding-#{$position}: env(safe-area-inset-#{$position});
// #endif

// #ifdef MP
padding-#{$position}: 0;
// #endif
}

4. 组件优化

4.1 性能优化

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
<script>
export default {
name: 'CustomList',
props: {
list: Array
},
// 避免不必要的更新
data() {
return {
renderList: this.list.map(item => ({
...item,
_id: item.id
}))
}
},
watch: {
list: {
handler(val) {
this.renderList = val.map(item => ({
...item,
_id: item.id
}))
},
deep: true
}
}
}
</script>

4.2 体验优化

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
<template>
<view class="custom-image" :style="imageStyle">
<image
:src="src"
:mode="mode"
:lazy-load="lazyLoad"
@load="handleLoad"
@error="handleError"
/>
<view v-if="loading" class="custom-image__loading">
<text class="custom-image__loading-text">加载中...</text>
</view>
<view v-if="error" class="custom-image__error">
<text class="custom-image__error-text">加载失败</text>
</view>
</view>
</template>

<script>
export default {
name: 'CustomImage',
props: {
src: String,
mode: {
type: String,
default: 'aspectFill'
},
width: {
type: [String, Number],
default: '100%'
},
height: {
type: [String, Number],
default: '100%'
},
lazyLoad: {
type: Boolean,
default: true
}
},
data() {
return {
loading: true,
error: false
}
},
computed: {
imageStyle() {
return {
width: typeof this.width === 'number' ? `${this.width}rpx` : this.width,
height: typeof this.height === 'number' ? `${this.height}rpx` : this.height
}
}
},
methods: {
handleLoad(e) {
this.loading = false
this.$emit('load', e)
},
handleError(e) {
this.loading = false
this.error = true
this.$emit('error', e)
}
}
}
</script>

5. 最佳实践建议

  1. 组件命名规范
  2. 合理的目录结构
  3. 完善的文档说明
  4. 统一的样式规范
  5. 做好跨端适配

6. 总结

  1. 掌握组件开发基础
  2. 实现组件通信
  3. 处理跨端适配
  4. 优化组件性能
  5. 提升用户体验

如果觉得文章对你有帮助,欢迎点赞、评论、分享,你的支持是我继续创作的动力!

上一篇 uni-app动画效果实战:从基础到高级的动画实现指南
下一篇 uni-app网络请求与缓存策略:构建高效的数据层
感谢您的支持!
微信赞赏码 微信赞赏
支付宝赞赏码 支付宝赞赏