惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News | PayPal Newsroom
云风的 BLOG
云风的 BLOG
GbyAI
GbyAI
Engineering at Meta
Engineering at Meta
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Register - Security
The Register - Security
L
LangChain Blog
A
About on SuperTechFans
S
Schneier on Security
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
博客园 - 司徒正美
Scott Helme
Scott Helme
K
Kaspersky official blog
Cyberwarzone
Cyberwarzone
T
Tenable Blog
腾讯CDC
Recorded Future
Recorded Future
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
G
GRAHAM CLULEY
Security Latest
Security Latest
S
Securelist
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
Google DeepMind News
Google DeepMind News
V
Vulnerabilities – Threatpost
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
T
The Blog of Author Tim Ferriss
D
Docker
S
Security Affairs
F
Full Disclosure
Know Your Adversary
Know Your Adversary
N
News and Events Feed by Topic
N
News and Events Feed by Topic
T
Tor Project blog
Hugging Face - Blog
Hugging Face - Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Microsoft Security Blog
Microsoft Security Blog
Simon Willison's Weblog
Simon Willison's Weblog
Recent Announcements
Recent Announcements
博客园_首页
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Security @ Cisco Blogs

吖远zzyの博客

解决华为开发者工具 DevEco Studio 登录跳转 localhost:10101 端口被阻止问题 | 吖远zzyの博客 微信小程序个人资质审核血泪史:踩坑,从反复被拒到神奇过审 | 吖远zzyの博客 如何实现公众号自动回复或做一个短视频无水印解析机器人? | 吖远zzyの博客 以后在手机上用 Termux 和 GitHub Actions 更新 Hexo 博客 | 吖远zzyの博客 必备!这款万能小程序能给抖音/豆包视频图集去除水印、还有有趣的测反应、玩成语模拟工具。 自制的一些免费API接口第二弹(获取QQ昵称、头像、抖音/豆包视频去水印等...) 告别原生导航栏:微信小程序自定义导航栏完美适配方案 在安卓手机上运行OpenClaw?当然可以,用Termux折腾OpenClaw安装QQ机器人的踩坑记录... | 吖远zzyの博客 个人开发工具之抖音直播录制工具:一款功能强大的Android直播录制应用 UniApp中Canvas绘图的易错点与踩坑指南 QQ频道机器人与UniApp开发:常见踩坑点与解决方案 UniApp中Canvas绘图不显示的常见问题与解决方案 Hexo博客实现随机文章功能的完整教程 QQ频道机器人Android客户端使用指南 | 吖远zzyの博客 安卓版QQ频道机器人APP客户端插件开发指南 | 吖远zzyの博客 2024年AI编程助手深度评测:哪款最适合你? uni-app地图定位踩坑记:地图功能和定位的那些坑 uni-app文件上传踩坑记:图片处理和上传全攻略 uni-app表单验证踩坑记:这些坑我替你踩过了 uni-app开发踩坑记录:新手必看的常见问题与解决方案 uni-app安全防护指南:构建可靠的跨端应用 uni-app性能优化指南:从加载到渲染的全方位提升 uni-app动画效果实战:从基础到高级的动画实现指南 uni-app网络请求与缓存策略:构建高效的数据层 uni-app状态管理进阶:Vuex最佳实践与性能优化 uni-app路由与页面跳转:最佳实践与踩坑指南
uni-app组件开发实战:从基础到进阶的最佳实践
吖远zzy · 2024-03-30 · via 吖远zzyの博客
  1. 1. 1. 组件基础开发
    1. 1.1. 1.1 组件目录结构
    2. 1.2. 1.2 基础组件示例
  2. 2. 2. 组件通信方式
    1. 2.1. 2.1 Props/Events
    2. 2.2. 2.2 Provide/Inject
  3. 3. 3. 跨端适配
    1. 3.1. 3.1 条件编译
    2. 3.2. 3.2 样式适配
  4. 4. 4. 组件优化
    1. 4.1. 4.1 性能优化
    2. 4.2. 4.2 体验优化
  5. 5. 5. 最佳实践建议
  6. 6. 6. 总结

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. 提升用户体验

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