mta-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +818 -0
- package/agents/_TEMPLATE.md +153 -0
- package/agents/flutter.agent.md +222 -0
- package/agents/i18n.agent.md +78 -0
- package/agents/logicflow.agent.md +97 -0
- package/agents/vue3.agent.md +176 -0
- package/agents/wechat-miniprogram.agent.md +89 -0
- package/bin/mta.cjs +132 -0
- package/common/i18n.md +385 -0
- package/common/typescript-strict.md +186 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +6493 -0
- package/dist/index.js.map +1 -0
- package/package.json +81 -0
- package/standards/README.md +194 -0
- package/standards/core/code-generation.md +421 -0
- package/standards/core/code-style.md +308 -0
- package/standards/core/dart-base.md +572 -0
- package/standards/core/mandatory-rules.md +103 -0
- package/standards/core/typescript-base.md +179 -0
- package/standards/frameworks/flutter-ui-system.md +497 -0
- package/standards/frameworks/flutter.md +1268 -0
- package/standards/frameworks/pinia.md +172 -0
- package/standards/frameworks/vue3-composition.md +779 -0
- package/standards/frameworks/wechat-miniprogram.md +2177 -0
- package/standards/libraries/element-plus.md +1128 -0
- package/standards/libraries/i18n.md +360 -0
- package/standards/libraries/logicflow.md +1007 -0
- package/standards/patterns/api-layer.md +187 -0
- package/standards/patterns/component-design.md +200 -0
- package/standards/patterns/design-system-restoration.md +570 -0
- package/standards/patterns/vue-api-mock-layer.md +958 -0
- package/standards/patterns/vue-css-nesting.md +604 -0
- package/standards/troubleshooting-cases/flutter/textfield-vertical-centering.md +107 -0
- package/standards/workflows/design-restoration-guide.md +164 -0
- package/standards/workflows/large-project-split.md +359 -0
- package/standards/workflows/problem-diagnosis.md +280 -0
- package/standards/workflows/textfield-centering-guide.md +157 -0
- package/templates/README.md +144 -0
- package/templates/common/types/_CONFIG.md +12 -0
- package/templates/common/types/api.ts +39 -0
- package/templates/common/types/common.ts +70 -0
- package/templates/config-templates/agents-section.md +9 -0
- package/templates/config-templates/custom-section.md +6 -0
- package/templates/config-templates/header.md +29 -0
- package/templates/config-templates/workflow-minimal.md +44 -0
- package/templates/copilot-instructions-mcp-optimized.md +158 -0
- package/templates/vue/api-layer/_CONFIG.md +145 -0
- package/templates/vue/api-layer/index.ts +58 -0
- package/templates/vue/api-layer/mock/index.ts +122 -0
- package/templates/vue/api-layer/modules/_template.ts +109 -0
- package/templates/vue/api-layer/modules/index.ts +16 -0
- package/templates/vue/api-layer/request.ts +279 -0
- package/templates/vue/api-layer/types.ts +80 -0
- package/troubleshooting/README.md +368 -0
- package/troubleshooting/USAGE_GUIDE.md +289 -0
- package/troubleshooting/flutter/clip-/351/230/264/345/275/261/350/243/201/345/211/252.md +244 -0
- package/troubleshooting/flutter/component-/351/200/232/347/224/250/345/214/226/346/217/220/345/217/226.md +269 -0
- package/troubleshooting/flutter/input-/345/255/227/346/256/265/347/274/272/345/244/261.md +240 -0
- package/troubleshooting/flutter/input-/350/276/271/346/241/206/351/227/256/351/242/230.md +236 -0
- package/troubleshooting/flutter/layout-/345/260/272/345/257/270/344/270/215/345/214/271/351/205/215.md +214 -0
- package/troubleshooting/flutter/shadow-/351/200/217/345/207/272/351/227/256/351/242/230.md +172 -0
- package/troubleshooting/flutter/sketch-/345/210/227/350/241/250item/345/214/272/345/237/237.md +212 -0
- package/troubleshooting/flutter/sketch-/345/233/276/346/240/207/345/260/272/345/257/270.md +135 -0
- package/troubleshooting/flutter/sketch-/345/256/214/346/225/264/346/217/220/345/217/226.md +201 -0
- package/troubleshooting/flutter/sketch-/345/261/236/346/200/247/346/234/252/344/275/277/347/224/250.md +139 -0
- package/troubleshooting/flutter/sketch-/350/203/214/346/231/257/345/261/202/351/253/230/345/272/246.md +264 -0
- package/troubleshooting/flutter/svg-/346/234/252/345/261/205/344/270/255.md +120 -0
- package/troubleshooting/flutter/svg-/351/242/234/350/211/262/345/274/202/345/270/270.md +117 -0
- package/troubleshooting/flutter/tabbar-/345/212/250/347/224/273/345/220/214/346/255/245.md +107 -0
- package/troubleshooting/flutter/withopacity-/345/274/203/347/224/250.md +81 -0
- package/troubleshooting/vue3/cascader-/350/257/257/346/233/277/346/215/242.md +130 -0
- package/troubleshooting/vue3/drawer-input-/346/240/267/345/274/217.md +181 -0
- package/troubleshooting/vue3/table-/347/274/226/350/276/221/345/217/226/346/266/210.md +148 -0
- package/troubleshooting/vue3/table-/350/276/271/346/241/206/351/227/256/351/242/230.md +178 -0
|
@@ -0,0 +1,2177 @@
|
|
|
1
|
+
# 微信小程序开发规范
|
|
2
|
+
|
|
3
|
+
> 基于微信官方开发文档与优质开源项目最佳实践
|
|
4
|
+
> 版本:适用于基础库 3.0+
|
|
5
|
+
|
|
6
|
+
## 🎯 核心原则
|
|
7
|
+
|
|
8
|
+
1. **组件化开发** - 充分利用自定义组件,提高代码复用性
|
|
9
|
+
2. **数据驱动视图** - 使用 setData 更新视图,避免直接操作 DOM
|
|
10
|
+
3. **性能优先** - 优化渲染性能,控制 setData 频率和数据大小
|
|
11
|
+
4. **用户体验** - 完善的加载状态、错误处理和交互反馈
|
|
12
|
+
5. **安全规范** - 敏感信息加密,接口鉴权,防止 XSS 攻击
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 📁 项目结构
|
|
17
|
+
|
|
18
|
+
### 推荐目录结构
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
miniprogram/
|
|
22
|
+
├── app.js # 小程序逻辑
|
|
23
|
+
├── app.json # 小程序公共配置
|
|
24
|
+
├── app.wxss # 小程序公共样式
|
|
25
|
+
├── sitemap.json # 搜索配置
|
|
26
|
+
├── project.config.json # 项目配置
|
|
27
|
+
├── project.private.config.json # 私有配置(不提交)
|
|
28
|
+
│
|
|
29
|
+
├── pages/ # 页面目录
|
|
30
|
+
│ ├── index/
|
|
31
|
+
│ │ ├── index.js # 页面逻辑
|
|
32
|
+
│ │ ├── index.json # 页面配置
|
|
33
|
+
│ │ ├── index.wxml # 页面结构
|
|
34
|
+
│ │ └── index.wxss # 页面样式
|
|
35
|
+
│ └── ...
|
|
36
|
+
│
|
|
37
|
+
├── components/ # 自定义组件
|
|
38
|
+
│ ├── user-card/
|
|
39
|
+
│ │ ├── index.js
|
|
40
|
+
│ │ ├── index.json
|
|
41
|
+
│ │ ├── index.wxml
|
|
42
|
+
│ │ └── index.wxss
|
|
43
|
+
│ └── ...
|
|
44
|
+
│
|
|
45
|
+
├── utils/ # 工具函数
|
|
46
|
+
│ ├── request.js # 网络请求封装
|
|
47
|
+
│ ├── storage.js # 本地存储封装
|
|
48
|
+
│ ├── auth.js # 鉴权工具
|
|
49
|
+
│ └── util.js # 通用工具
|
|
50
|
+
│
|
|
51
|
+
├── api/ # API 接口管理
|
|
52
|
+
│ ├── user.js
|
|
53
|
+
│ ├── product.js
|
|
54
|
+
│ └── order.js
|
|
55
|
+
│
|
|
56
|
+
├── config/ # 配置文件
|
|
57
|
+
│ ├── env.js # 环境配置
|
|
58
|
+
│ └── constants.js # 常量定义
|
|
59
|
+
│
|
|
60
|
+
├── models/ # 数据模型
|
|
61
|
+
│ └── user.js
|
|
62
|
+
│
|
|
63
|
+
├── store/ # 全局状态管理(可选)
|
|
64
|
+
│ └── index.js
|
|
65
|
+
│
|
|
66
|
+
├── styles/ # 公共样式
|
|
67
|
+
│ ├── variables.wxss # CSS 变量
|
|
68
|
+
│ └── common.wxss # 公共样式
|
|
69
|
+
│
|
|
70
|
+
└── images/ # 图片资源
|
|
71
|
+
├── icons/
|
|
72
|
+
└── ...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 文件命名规范
|
|
76
|
+
|
|
77
|
+
| 类型 | 规范 | 示例 |
|
|
78
|
+
|-----|------|-----|
|
|
79
|
+
| 页面 | kebab-case | `user-profile/` |
|
|
80
|
+
| 组件 | kebab-case | `product-card/` |
|
|
81
|
+
| JS 文件 | camelCase | `userService.js` |
|
|
82
|
+
| 常量文件 | UPPER_CASE | `API_CONFIG.js` |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 📄 页面开发规范
|
|
87
|
+
|
|
88
|
+
### Page 生命周期
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// pages/user/user.js
|
|
92
|
+
Page({
|
|
93
|
+
/**
|
|
94
|
+
* 页面的初始数据
|
|
95
|
+
*/
|
|
96
|
+
data: {
|
|
97
|
+
userInfo: null,
|
|
98
|
+
list: [],
|
|
99
|
+
loading: false,
|
|
100
|
+
page: 1,
|
|
101
|
+
hasMore: true
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 生命周期函数--监听页面加载
|
|
106
|
+
* @param {Object} options - 页面参数
|
|
107
|
+
*/
|
|
108
|
+
onLoad(options) {
|
|
109
|
+
// ✅ 获取路由参数
|
|
110
|
+
const { id } = options
|
|
111
|
+
|
|
112
|
+
// ✅ 初始化数据
|
|
113
|
+
this.fetchUserInfo(id)
|
|
114
|
+
this.fetchList()
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 生命周期函数--监听页面初次渲染完成
|
|
119
|
+
*/
|
|
120
|
+
onReady() {
|
|
121
|
+
// ✅ 可以进行页面节点操作
|
|
122
|
+
wx.setNavigationBarTitle({
|
|
123
|
+
title: '用户中心'
|
|
124
|
+
})
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 生命周期函数--监听页面显示
|
|
129
|
+
*/
|
|
130
|
+
onShow() {
|
|
131
|
+
// ✅ 页面每次显示时执行
|
|
132
|
+
// 适合刷新页面数据
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 生命周期函数--监听页面隐藏
|
|
137
|
+
*/
|
|
138
|
+
onHide() {
|
|
139
|
+
// ✅ 页面隐藏时执行
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 生命周期函数--监听页面卸载
|
|
144
|
+
*/
|
|
145
|
+
onUnload() {
|
|
146
|
+
// ✅ 清理定时器、取消请求等
|
|
147
|
+
if (this.timer) {
|
|
148
|
+
clearInterval(this.timer)
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 页面相关事件处理函数--监听用户下拉动作
|
|
154
|
+
*/
|
|
155
|
+
onPullDownRefresh() {
|
|
156
|
+
// ✅ 刷新数据
|
|
157
|
+
this.setData({
|
|
158
|
+
page: 1,
|
|
159
|
+
list: [],
|
|
160
|
+
hasMore: true
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
this.fetchList().then(() => {
|
|
164
|
+
wx.stopPullDownRefresh()
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 页面上拉触底事件的处理函数
|
|
170
|
+
*/
|
|
171
|
+
onReachBottom() {
|
|
172
|
+
// ✅ 加载更多
|
|
173
|
+
if (!this.data.hasMore || this.data.loading) return
|
|
174
|
+
|
|
175
|
+
this.setData({
|
|
176
|
+
page: this.data.page + 1
|
|
177
|
+
})
|
|
178
|
+
this.fetchList()
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 用户点击右上角分享
|
|
183
|
+
*/
|
|
184
|
+
onShareAppMessage() {
|
|
185
|
+
return {
|
|
186
|
+
title: '分享标题',
|
|
187
|
+
path: `/pages/user/user?id=${this.data.userInfo.id}`,
|
|
188
|
+
imageUrl: this.data.userInfo.avatar
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 获取用户信息
|
|
194
|
+
*/
|
|
195
|
+
async fetchUserInfo(id) {
|
|
196
|
+
try {
|
|
197
|
+
this.setData({ loading: true })
|
|
198
|
+
|
|
199
|
+
const res = await userApi.getUserInfo({ id })
|
|
200
|
+
|
|
201
|
+
this.setData({
|
|
202
|
+
userInfo: res.data
|
|
203
|
+
})
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error('获取用户信息失败:', error)
|
|
206
|
+
wx.showToast({
|
|
207
|
+
title: '获取信息失败',
|
|
208
|
+
icon: 'none'
|
|
209
|
+
})
|
|
210
|
+
} finally {
|
|
211
|
+
this.setData({ loading: false })
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 获取列表数据
|
|
217
|
+
*/
|
|
218
|
+
async fetchList() {
|
|
219
|
+
try {
|
|
220
|
+
this.setData({ loading: true })
|
|
221
|
+
|
|
222
|
+
const { page } = this.data
|
|
223
|
+
const res = await userApi.getList({ page, pageSize: 10 })
|
|
224
|
+
|
|
225
|
+
this.setData({
|
|
226
|
+
list: page === 1 ? res.data.list : [...this.data.list, ...res.data.list],
|
|
227
|
+
hasMore: res.data.hasMore
|
|
228
|
+
})
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('获取列表失败:', error)
|
|
231
|
+
wx.showToast({
|
|
232
|
+
title: '加载失败',
|
|
233
|
+
icon: 'none'
|
|
234
|
+
})
|
|
235
|
+
} finally {
|
|
236
|
+
this.setData({ loading: false })
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 跳转到详情页
|
|
242
|
+
*/
|
|
243
|
+
handleNavigateToDetail(e) {
|
|
244
|
+
const { id } = e.currentTarget.dataset
|
|
245
|
+
wx.navigateTo({
|
|
246
|
+
url: `/pages/detail/detail?id=${id}`
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### WXML 模板规范
|
|
253
|
+
|
|
254
|
+
```xml
|
|
255
|
+
<!-- ✅ 正确示例 -->
|
|
256
|
+
<view class="container">
|
|
257
|
+
<!-- 加载状态 -->
|
|
258
|
+
<view wx:if="{{loading}}" class="loading">
|
|
259
|
+
<text>加载中...</text>
|
|
260
|
+
</view>
|
|
261
|
+
|
|
262
|
+
<!-- 数据展示 -->
|
|
263
|
+
<block wx:else>
|
|
264
|
+
<!-- 条件渲染 -->
|
|
265
|
+
<view wx:if="{{userInfo}}" class="user-info">
|
|
266
|
+
<image class="avatar" src="{{userInfo.avatar}}" mode="aspectFill" />
|
|
267
|
+
<text class="name">{{userInfo.name}}</text>
|
|
268
|
+
</view>
|
|
269
|
+
|
|
270
|
+
<!-- 列表渲染 - 必须添加 key -->
|
|
271
|
+
<view
|
|
272
|
+
wx:for="{{list}}"
|
|
273
|
+
wx:key="id"
|
|
274
|
+
class="item"
|
|
275
|
+
data-id="{{item.id}}"
|
|
276
|
+
bindtap="handleNavigateToDetail"
|
|
277
|
+
>
|
|
278
|
+
<text>{{item.title}}</text>
|
|
279
|
+
<text class="time">{{item.createTime}}</text>
|
|
280
|
+
</view>
|
|
281
|
+
|
|
282
|
+
<!-- 空状态 -->
|
|
283
|
+
<view wx:if="{{list.length === 0 && !loading}}" class="empty">
|
|
284
|
+
<text>暂无数据</text>
|
|
285
|
+
</view>
|
|
286
|
+
</block>
|
|
287
|
+
</view>
|
|
288
|
+
|
|
289
|
+
<!-- ❌ 错误示例 -->
|
|
290
|
+
<!-- 1. 缺少 wx:key -->
|
|
291
|
+
<view wx:for="{{list}}">
|
|
292
|
+
{{item.name}}
|
|
293
|
+
</view>
|
|
294
|
+
|
|
295
|
+
<!-- 2. 复杂的模板表达式 -->
|
|
296
|
+
<text>{{list.filter(item => item.active).map(i => i.name).join(', ')}}</text>
|
|
297
|
+
<!-- 应该在 JS 中处理 -->
|
|
298
|
+
|
|
299
|
+
<!-- 3. 内联样式过多 -->
|
|
300
|
+
<view style="width: 100px; height: 100px; background: red; margin: 10px;">
|
|
301
|
+
内容
|
|
302
|
+
</view>
|
|
303
|
+
<!-- 应该使用 class -->
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### WXSS 样式规范
|
|
307
|
+
|
|
308
|
+
```css
|
|
309
|
+
/* ✅ 使用 CSS 变量 */
|
|
310
|
+
page {
|
|
311
|
+
--primary-color: #1aad19;
|
|
312
|
+
--text-color: #333;
|
|
313
|
+
--border-color: #e5e5e5;
|
|
314
|
+
--bg-color: #f5f5f5;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ✅ BEM 命名规范 */
|
|
318
|
+
.user-card {
|
|
319
|
+
padding: 20rpx;
|
|
320
|
+
background: #fff;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.user-card__avatar {
|
|
324
|
+
width: 100rpx;
|
|
325
|
+
height: 100rpx;
|
|
326
|
+
border-radius: 50%;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.user-card__name {
|
|
330
|
+
font-size: 32rpx;
|
|
331
|
+
color: var(--text-color);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.user-card__name--vip {
|
|
335
|
+
color: #ff9500;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* ✅ 响应式单位 rpx */
|
|
339
|
+
.container {
|
|
340
|
+
width: 750rpx;
|
|
341
|
+
padding: 30rpx;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ✅ Flex 布局 */
|
|
345
|
+
.flex-row {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-direction: row;
|
|
348
|
+
align-items: center;
|
|
349
|
+
justify-content: space-between;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ❌ 避免使用固定像素 px(特殊情况除外) */
|
|
353
|
+
.bad-width {
|
|
354
|
+
width: 375px; /* 不推荐 */
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ❌ 避免过深的选择器嵌套 */
|
|
358
|
+
.page .container .content .item .title .text {
|
|
359
|
+
/* 太深了! */
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 🧩 组件开发规范
|
|
366
|
+
|
|
367
|
+
### 自定义组件
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
// components/user-card/index.js
|
|
371
|
+
Component({
|
|
372
|
+
/**
|
|
373
|
+
* 组件的属性列表
|
|
374
|
+
*/
|
|
375
|
+
properties: {
|
|
376
|
+
// ✅ 完整的属性定义
|
|
377
|
+
user: {
|
|
378
|
+
type: Object,
|
|
379
|
+
value: null,
|
|
380
|
+
observer(newVal, oldVal) {
|
|
381
|
+
// 属性变化时的处理
|
|
382
|
+
if (newVal) {
|
|
383
|
+
this._processUserData(newVal)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
size: {
|
|
389
|
+
type: String,
|
|
390
|
+
value: 'medium', // small | medium | large
|
|
391
|
+
validator(value) {
|
|
392
|
+
return ['small', 'medium', 'large'].includes(value)
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
showActions: {
|
|
397
|
+
type: Boolean,
|
|
398
|
+
value: true
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* 组件的初始数据
|
|
404
|
+
*/
|
|
405
|
+
data: {
|
|
406
|
+
processedUser: null,
|
|
407
|
+
isFollowing: false
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 组件的方法列表
|
|
412
|
+
*/
|
|
413
|
+
methods: {
|
|
414
|
+
/**
|
|
415
|
+
* 处理用户数据
|
|
416
|
+
* @private
|
|
417
|
+
*/
|
|
418
|
+
_processUserData(user) {
|
|
419
|
+
// ✅ 私有方法使用 _ 前缀
|
|
420
|
+
this.setData({
|
|
421
|
+
processedUser: {
|
|
422
|
+
...user,
|
|
423
|
+
displayName: user.nickname || user.name
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* 关注/取消关注
|
|
430
|
+
*/
|
|
431
|
+
async handleFollow() {
|
|
432
|
+
const { user } = this.properties
|
|
433
|
+
const { isFollowing } = this.data
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
if (isFollowing) {
|
|
437
|
+
await userApi.unfollow({ userId: user.id })
|
|
438
|
+
} else {
|
|
439
|
+
await userApi.follow({ userId: user.id })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.setData({
|
|
443
|
+
isFollowing: !isFollowing
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
// ✅ 触发自定义事件
|
|
447
|
+
this.triggerEvent('followchange', {
|
|
448
|
+
userId: user.id,
|
|
449
|
+
isFollowing: !isFollowing
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
wx.showToast({
|
|
453
|
+
title: isFollowing ? '已取消关注' : '关注成功',
|
|
454
|
+
icon: 'success'
|
|
455
|
+
})
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error('操作失败:', error)
|
|
458
|
+
wx.showToast({
|
|
459
|
+
title: '操作失败',
|
|
460
|
+
icon: 'none'
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 跳转到用户主页
|
|
467
|
+
*/
|
|
468
|
+
handleNavigateToProfile() {
|
|
469
|
+
const { user } = this.properties
|
|
470
|
+
|
|
471
|
+
// ✅ 触发导航事件,由父组件处理
|
|
472
|
+
this.triggerEvent('navigate', {
|
|
473
|
+
userId: user.id
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* 组件生命周期
|
|
480
|
+
*/
|
|
481
|
+
lifetimes: {
|
|
482
|
+
attached() {
|
|
483
|
+
// ✅ 组件被挂载到页面时执行
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
detached() {
|
|
487
|
+
// ✅ 组件从页面移除时执行
|
|
488
|
+
// 清理定时器等
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 组件所在页面的生命周期
|
|
494
|
+
*/
|
|
495
|
+
pageLifetimes: {
|
|
496
|
+
show() {
|
|
497
|
+
// ✅ 页面显示时执行
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
hide() {
|
|
501
|
+
// ✅ 页面隐藏时执行
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
})
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### 组件 WXML
|
|
508
|
+
|
|
509
|
+
```xml
|
|
510
|
+
<!-- components/user-card/index.wxml -->
|
|
511
|
+
<view class="user-card user-card--{{size}}">
|
|
512
|
+
<!-- 用户信息 -->
|
|
513
|
+
<view class="user-card__info" bindtap="handleNavigateToProfile">
|
|
514
|
+
<image
|
|
515
|
+
class="user-card__avatar"
|
|
516
|
+
src="{{user.avatar}}"
|
|
517
|
+
mode="aspectFill"
|
|
518
|
+
/>
|
|
519
|
+
<view class="user-card__detail">
|
|
520
|
+
<text class="user-card__name">{{processedUser.displayName}}</text>
|
|
521
|
+
<text class="user-card__desc">{{user.bio}}</text>
|
|
522
|
+
</view>
|
|
523
|
+
</view>
|
|
524
|
+
|
|
525
|
+
<!-- 操作按钮 -->
|
|
526
|
+
<view wx:if="{{showActions}}" class="user-card__actions">
|
|
527
|
+
<button
|
|
528
|
+
class="user-card__btn {{isFollowing ? 'user-card__btn--following' : ''}}"
|
|
529
|
+
bindtap="handleFollow"
|
|
530
|
+
>
|
|
531
|
+
{{isFollowing ? '已关注' : '关注'}}
|
|
532
|
+
</button>
|
|
533
|
+
</view>
|
|
534
|
+
</view>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### 组件配置
|
|
538
|
+
|
|
539
|
+
```json
|
|
540
|
+
{
|
|
541
|
+
"component": true,
|
|
542
|
+
"usingComponents": {}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## 🌐 网络请求规范
|
|
549
|
+
|
|
550
|
+
### 请求封装
|
|
551
|
+
|
|
552
|
+
```javascript
|
|
553
|
+
// utils/request.js
|
|
554
|
+
|
|
555
|
+
const BASE_URL = 'https://api.example.com'
|
|
556
|
+
const TOKEN_KEY = 'auth_token'
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* 网络请求封装
|
|
560
|
+
* @param {Object} options - 请求配置
|
|
561
|
+
* @param {string} options.url - 请求路径
|
|
562
|
+
* @param {string} options.method - 请求方法
|
|
563
|
+
* @param {Object} options.data - 请求数据
|
|
564
|
+
* @param {Object} options.header - 请求头
|
|
565
|
+
* @param {boolean} options.needAuth - 是否需要鉴权
|
|
566
|
+
* @param {boolean} options.showLoading - 是否显示加载提示
|
|
567
|
+
* @returns {Promise}
|
|
568
|
+
*/
|
|
569
|
+
function request(options) {
|
|
570
|
+
const {
|
|
571
|
+
url,
|
|
572
|
+
method = 'GET',
|
|
573
|
+
data = {},
|
|
574
|
+
header = {},
|
|
575
|
+
needAuth = true,
|
|
576
|
+
showLoading = true
|
|
577
|
+
} = options
|
|
578
|
+
|
|
579
|
+
// ✅ 显示加载提示
|
|
580
|
+
if (showLoading) {
|
|
581
|
+
wx.showLoading({
|
|
582
|
+
title: '加载中...',
|
|
583
|
+
mask: true
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return new Promise((resolve, reject) => {
|
|
588
|
+
// ✅ 构建请求头
|
|
589
|
+
const requestHeader = {
|
|
590
|
+
'content-type': 'application/json',
|
|
591
|
+
...header
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ✅ 添加 Token
|
|
595
|
+
if (needAuth) {
|
|
596
|
+
const token = wx.getStorageSync(TOKEN_KEY)
|
|
597
|
+
if (token) {
|
|
598
|
+
requestHeader['Authorization'] = `Bearer ${token}`
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
wx.request({
|
|
603
|
+
url: `${BASE_URL}${url}`,
|
|
604
|
+
method,
|
|
605
|
+
data,
|
|
606
|
+
header: requestHeader,
|
|
607
|
+
success: (res) => {
|
|
608
|
+
if (showLoading) {
|
|
609
|
+
wx.hideLoading()
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ✅ 统一处理响应
|
|
613
|
+
const { statusCode, data } = res
|
|
614
|
+
|
|
615
|
+
if (statusCode === 200) {
|
|
616
|
+
// ✅ 业务成功
|
|
617
|
+
if (data.code === 0) {
|
|
618
|
+
resolve(data)
|
|
619
|
+
} else {
|
|
620
|
+
// ✅ 业务失败
|
|
621
|
+
const errorMsg = data.message || '请求失败'
|
|
622
|
+
wx.showToast({
|
|
623
|
+
title: errorMsg,
|
|
624
|
+
icon: 'none'
|
|
625
|
+
})
|
|
626
|
+
reject(new Error(errorMsg))
|
|
627
|
+
}
|
|
628
|
+
} else if (statusCode === 401) {
|
|
629
|
+
// ✅ 未授权,跳转登录
|
|
630
|
+
wx.removeStorageSync(TOKEN_KEY)
|
|
631
|
+
wx.redirectTo({
|
|
632
|
+
url: '/pages/login/login'
|
|
633
|
+
})
|
|
634
|
+
reject(new Error('未授权'))
|
|
635
|
+
} else {
|
|
636
|
+
// ✅ 其他错误
|
|
637
|
+
const errorMsg = '网络请求失败'
|
|
638
|
+
wx.showToast({
|
|
639
|
+
title: errorMsg,
|
|
640
|
+
icon: 'none'
|
|
641
|
+
})
|
|
642
|
+
reject(new Error(errorMsg))
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
fail: (error) => {
|
|
646
|
+
if (showLoading) {
|
|
647
|
+
wx.hideLoading()
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// ✅ 网络错误处理
|
|
651
|
+
console.error('网络请求失败:', error)
|
|
652
|
+
wx.showToast({
|
|
653
|
+
title: '网络连接失败',
|
|
654
|
+
icon: 'none'
|
|
655
|
+
})
|
|
656
|
+
reject(error)
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
module.exports = {
|
|
663
|
+
request,
|
|
664
|
+
|
|
665
|
+
// ✅ 快捷方法
|
|
666
|
+
get(url, data, options = {}) {
|
|
667
|
+
return request({ url, method: 'GET', data, ...options })
|
|
668
|
+
},
|
|
669
|
+
|
|
670
|
+
post(url, data, options = {}) {
|
|
671
|
+
return request({ url, method: 'POST', data, ...options })
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
put(url, data, options = {}) {
|
|
675
|
+
return request({ url, method: 'PUT', data, ...options })
|
|
676
|
+
},
|
|
677
|
+
|
|
678
|
+
delete(url, data, options = {}) {
|
|
679
|
+
return request({ url, method: 'DELETE', data, ...options })
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### API 管理
|
|
685
|
+
|
|
686
|
+
```javascript
|
|
687
|
+
// api/user.js
|
|
688
|
+
const { get, post } = require('../utils/request')
|
|
689
|
+
|
|
690
|
+
module.exports = {
|
|
691
|
+
/**
|
|
692
|
+
* 获取用户信息
|
|
693
|
+
*/
|
|
694
|
+
getUserInfo(data) {
|
|
695
|
+
return get('/user/info', data)
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* 更新用户信息
|
|
700
|
+
*/
|
|
701
|
+
updateUserInfo(data) {
|
|
702
|
+
return post('/user/update', data)
|
|
703
|
+
},
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* 获取用户列表
|
|
707
|
+
*/
|
|
708
|
+
getUserList(data) {
|
|
709
|
+
return get('/user/list', data)
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* 关注用户
|
|
714
|
+
*/
|
|
715
|
+
followUser(data) {
|
|
716
|
+
return post('/user/follow', data)
|
|
717
|
+
},
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* 取消关注
|
|
721
|
+
*/
|
|
722
|
+
unfollowUser(data) {
|
|
723
|
+
return post('/user/unfollow', data)
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## ☁️ 云开发规范
|
|
731
|
+
|
|
732
|
+
> 微信小程序云开发提供云函数、云数据库、云存储、云调用等能力
|
|
733
|
+
|
|
734
|
+
### 云开发初始化
|
|
735
|
+
|
|
736
|
+
```javascript
|
|
737
|
+
// app.js
|
|
738
|
+
App({
|
|
739
|
+
onLaunch() {
|
|
740
|
+
// ✅ 初始化云开发环境
|
|
741
|
+
if (!wx.cloud) {
|
|
742
|
+
console.error('请使用 2.2.3 或以上的基础库以使用云能力')
|
|
743
|
+
} else {
|
|
744
|
+
wx.cloud.init({
|
|
745
|
+
env: 'your-env-id', // 云开发环境 ID
|
|
746
|
+
traceUser: true // 记录用户访问记录
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### 环境配置管理
|
|
754
|
+
|
|
755
|
+
```javascript
|
|
756
|
+
// config/cloud.js
|
|
757
|
+
|
|
758
|
+
// ✅ 多环境配置
|
|
759
|
+
const ENV_CONFIG = {
|
|
760
|
+
development: {
|
|
761
|
+
envId: 'dev-xxxxx',
|
|
762
|
+
functionRoot: 'cloudfunctions'
|
|
763
|
+
},
|
|
764
|
+
production: {
|
|
765
|
+
envId: 'prod-xxxxx',
|
|
766
|
+
functionRoot: 'cloudfunctions'
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const currentEnv = process.env.NODE_ENV || 'development'
|
|
771
|
+
|
|
772
|
+
module.exports = {
|
|
773
|
+
...ENV_CONFIG[currentEnv],
|
|
774
|
+
// 云函数超时时间(毫秒)
|
|
775
|
+
timeout: 10000
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
### 云函数开发规范
|
|
782
|
+
|
|
783
|
+
#### 云函数结构
|
|
784
|
+
|
|
785
|
+
```javascript
|
|
786
|
+
// cloudfunctions/getUserInfo/index.js
|
|
787
|
+
|
|
788
|
+
const cloud = require('wx-server-sdk')
|
|
789
|
+
|
|
790
|
+
// ✅ 初始化云环境
|
|
791
|
+
cloud.init({
|
|
792
|
+
env: cloud.DYNAMIC_CURRENT_ENV
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
const db = cloud.database()
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* 获取用户信息云函数
|
|
799
|
+
* @param {Object} event - 云函数调用参数
|
|
800
|
+
* @param {string} event.userId - 用户 ID
|
|
801
|
+
* @returns {Object} 用户信息
|
|
802
|
+
*/
|
|
803
|
+
exports.main = async (event, context) => {
|
|
804
|
+
// ✅ 获取调用者信息
|
|
805
|
+
const { OPENID, APPID, UNIONID } = cloud.getWXContext()
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
// ✅ 参数验证
|
|
809
|
+
if (!event.userId) {
|
|
810
|
+
return {
|
|
811
|
+
success: false,
|
|
812
|
+
message: '缺少必要参数 userId'
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ✅ 数据库查询
|
|
817
|
+
const result = await db.collection('users')
|
|
818
|
+
.doc(event.userId)
|
|
819
|
+
.get()
|
|
820
|
+
|
|
821
|
+
// ✅ 返回统一格式
|
|
822
|
+
return {
|
|
823
|
+
success: true,
|
|
824
|
+
data: result.data,
|
|
825
|
+
openid: OPENID
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
// ✅ 错误处理
|
|
829
|
+
console.error('获取用户信息失败:', error)
|
|
830
|
+
return {
|
|
831
|
+
success: false,
|
|
832
|
+
message: error.message
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
#### 小程序端调用云函数
|
|
839
|
+
|
|
840
|
+
```javascript
|
|
841
|
+
// utils/cloudFunctions.js
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* 调用云函数封装
|
|
845
|
+
* @param {string} name - 云函数名称
|
|
846
|
+
* @param {Object} data - 传递参数
|
|
847
|
+
* @returns {Promise}
|
|
848
|
+
*/
|
|
849
|
+
async function callFunction(name, data = {}) {
|
|
850
|
+
try {
|
|
851
|
+
wx.showLoading({ title: '处理中...', mask: true })
|
|
852
|
+
|
|
853
|
+
const res = await wx.cloud.callFunction({
|
|
854
|
+
name,
|
|
855
|
+
data
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
wx.hideLoading()
|
|
859
|
+
|
|
860
|
+
// ✅ 统一处理响应
|
|
861
|
+
if (res.result.success) {
|
|
862
|
+
return res.result
|
|
863
|
+
} else {
|
|
864
|
+
throw new Error(res.result.message || '操作失败')
|
|
865
|
+
}
|
|
866
|
+
} catch (error) {
|
|
867
|
+
wx.hideLoading()
|
|
868
|
+
console.error(`调用云函数 ${name} 失败:`, error)
|
|
869
|
+
|
|
870
|
+
wx.showToast({
|
|
871
|
+
title: error.message || '操作失败',
|
|
872
|
+
icon: 'none'
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
throw error
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
module.exports = {
|
|
880
|
+
callFunction,
|
|
881
|
+
|
|
882
|
+
// ✅ 具体业务方法
|
|
883
|
+
getUserInfo(userId) {
|
|
884
|
+
return callFunction('getUserInfo', { userId })
|
|
885
|
+
},
|
|
886
|
+
|
|
887
|
+
createOrder(orderData) {
|
|
888
|
+
return callFunction('createOrder', orderData)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
---
|
|
894
|
+
|
|
895
|
+
### 云数据库规范
|
|
896
|
+
|
|
897
|
+
#### 数据库集合设计
|
|
898
|
+
|
|
899
|
+
```javascript
|
|
900
|
+
// ✅ 用户集合 (users)
|
|
901
|
+
{
|
|
902
|
+
_id: 'user_xxx',
|
|
903
|
+
_openid: 'oXXXX', // 用户 openid(自动生成)
|
|
904
|
+
nickname: '张三',
|
|
905
|
+
avatar: 'https://...',
|
|
906
|
+
phone: '13800138000',
|
|
907
|
+
createTime: new Date(),
|
|
908
|
+
updateTime: new Date()
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ✅ 订单集合 (orders)
|
|
912
|
+
{
|
|
913
|
+
_id: 'order_xxx',
|
|
914
|
+
_openid: 'oXXXX', // 下单用户
|
|
915
|
+
userId: 'user_xxx',
|
|
916
|
+
products: [
|
|
917
|
+
{ id: 'prod_1', name: '商品1', price: 100, count: 2 }
|
|
918
|
+
],
|
|
919
|
+
totalPrice: 200,
|
|
920
|
+
status: 'pending', // pending | paid | shipped | completed
|
|
921
|
+
createTime: new Date()
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
#### 数据库操作封装
|
|
926
|
+
|
|
927
|
+
```javascript
|
|
928
|
+
// utils/cloudDB.js
|
|
929
|
+
|
|
930
|
+
const db = wx.cloud.database()
|
|
931
|
+
const _ = db.command
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* 数据库操作工具类
|
|
935
|
+
*/
|
|
936
|
+
class CloudDB {
|
|
937
|
+
constructor(collectionName) {
|
|
938
|
+
this.collection = db.collection(collectionName)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* 添加记录
|
|
943
|
+
*/
|
|
944
|
+
async add(data) {
|
|
945
|
+
try {
|
|
946
|
+
const res = await this.collection.add({
|
|
947
|
+
data: {
|
|
948
|
+
...data,
|
|
949
|
+
createTime: new Date(),
|
|
950
|
+
updateTime: new Date()
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
return { success: true, id: res._id }
|
|
954
|
+
} catch (error) {
|
|
955
|
+
console.error('添加记录失败:', error)
|
|
956
|
+
throw error
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* 查询记录(分页)
|
|
962
|
+
*/
|
|
963
|
+
async getList({ page = 1, pageSize = 10, where = {}, orderBy = 'createTime', order = 'desc' }) {
|
|
964
|
+
try {
|
|
965
|
+
const skip = (page - 1) * pageSize
|
|
966
|
+
|
|
967
|
+
const countRes = await this.collection.where(where).count()
|
|
968
|
+
const total = countRes.total
|
|
969
|
+
|
|
970
|
+
const res = await this.collection
|
|
971
|
+
.where(where)
|
|
972
|
+
.orderBy(orderBy, order)
|
|
973
|
+
.skip(skip)
|
|
974
|
+
.limit(pageSize)
|
|
975
|
+
.get()
|
|
976
|
+
|
|
977
|
+
return {
|
|
978
|
+
success: true,
|
|
979
|
+
data: res.data,
|
|
980
|
+
total,
|
|
981
|
+
page,
|
|
982
|
+
pageSize,
|
|
983
|
+
hasMore: skip + res.data.length < total
|
|
984
|
+
}
|
|
985
|
+
} catch (error) {
|
|
986
|
+
console.error('查询列表失败:', error)
|
|
987
|
+
throw error
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* 获取单条记录
|
|
993
|
+
*/
|
|
994
|
+
async getById(id) {
|
|
995
|
+
try {
|
|
996
|
+
const res = await this.collection.doc(id).get()
|
|
997
|
+
return { success: true, data: res.data }
|
|
998
|
+
} catch (error) {
|
|
999
|
+
console.error('获取记录失败:', error)
|
|
1000
|
+
throw error
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* 更新记录
|
|
1006
|
+
*/
|
|
1007
|
+
async update(id, data) {
|
|
1008
|
+
try {
|
|
1009
|
+
await this.collection.doc(id).update({
|
|
1010
|
+
data: {
|
|
1011
|
+
...data,
|
|
1012
|
+
updateTime: new Date()
|
|
1013
|
+
}
|
|
1014
|
+
})
|
|
1015
|
+
return { success: true }
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
console.error('更新记录失败:', error)
|
|
1018
|
+
throw error
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* 删除记录
|
|
1024
|
+
*/
|
|
1025
|
+
async remove(id) {
|
|
1026
|
+
try {
|
|
1027
|
+
await this.collection.doc(id).remove()
|
|
1028
|
+
return { success: true }
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
console.error('删除记录失败:', error)
|
|
1031
|
+
throw error
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ✅ 导出实例
|
|
1037
|
+
module.exports = {
|
|
1038
|
+
CloudDB,
|
|
1039
|
+
usersDB: new CloudDB('users'),
|
|
1040
|
+
ordersDB: new CloudDB('orders'),
|
|
1041
|
+
productsDB: new CloudDB('products')
|
|
1042
|
+
}
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
#### 使用示例
|
|
1046
|
+
|
|
1047
|
+
```javascript
|
|
1048
|
+
// pages/user/user.js
|
|
1049
|
+
const { usersDB } = require('../../utils/cloudDB')
|
|
1050
|
+
|
|
1051
|
+
Page({
|
|
1052
|
+
data: {
|
|
1053
|
+
users: [],
|
|
1054
|
+
page: 1
|
|
1055
|
+
},
|
|
1056
|
+
|
|
1057
|
+
async onLoad() {
|
|
1058
|
+
await this.fetchUsers()
|
|
1059
|
+
},
|
|
1060
|
+
|
|
1061
|
+
async fetchUsers() {
|
|
1062
|
+
try {
|
|
1063
|
+
const res = await usersDB.getList({
|
|
1064
|
+
page: this.data.page,
|
|
1065
|
+
pageSize: 10,
|
|
1066
|
+
where: {
|
|
1067
|
+
// ✅ 查询条件
|
|
1068
|
+
status: 'active'
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
this.setData({
|
|
1073
|
+
users: res.data,
|
|
1074
|
+
hasMore: res.hasMore
|
|
1075
|
+
})
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
wx.showToast({
|
|
1078
|
+
title: '加载失败',
|
|
1079
|
+
icon: 'none'
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
})
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
---
|
|
1087
|
+
|
|
1088
|
+
### 云存储规范
|
|
1089
|
+
|
|
1090
|
+
#### 文件上传
|
|
1091
|
+
|
|
1092
|
+
```javascript
|
|
1093
|
+
// utils/cloudStorage.js
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* 上传文件到云存储
|
|
1097
|
+
* @param {string} filePath - 本地文件路径
|
|
1098
|
+
* @param {string} cloudPath - 云存储路径
|
|
1099
|
+
* @returns {Promise}
|
|
1100
|
+
*/
|
|
1101
|
+
async function uploadFile(filePath, cloudPath) {
|
|
1102
|
+
try {
|
|
1103
|
+
wx.showLoading({ title: '上传中...', mask: true })
|
|
1104
|
+
|
|
1105
|
+
const res = await wx.cloud.uploadFile({
|
|
1106
|
+
cloudPath,
|
|
1107
|
+
filePath
|
|
1108
|
+
})
|
|
1109
|
+
|
|
1110
|
+
wx.hideLoading()
|
|
1111
|
+
|
|
1112
|
+
return {
|
|
1113
|
+
success: true,
|
|
1114
|
+
fileID: res.fileID
|
|
1115
|
+
}
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
wx.hideLoading()
|
|
1118
|
+
console.error('上传文件失败:', error)
|
|
1119
|
+
|
|
1120
|
+
wx.showToast({
|
|
1121
|
+
title: '上传失败',
|
|
1122
|
+
icon: 'none'
|
|
1123
|
+
})
|
|
1124
|
+
|
|
1125
|
+
throw error
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* 选择并上传图片
|
|
1131
|
+
* @param {Object} options - 配置选项
|
|
1132
|
+
* @returns {Promise}
|
|
1133
|
+
*/
|
|
1134
|
+
async function chooseAndUploadImage(options = {}) {
|
|
1135
|
+
const {
|
|
1136
|
+
count = 1,
|
|
1137
|
+
sizeType = ['compressed'],
|
|
1138
|
+
sourceType = ['album', 'camera'],
|
|
1139
|
+
folder = 'images'
|
|
1140
|
+
} = options
|
|
1141
|
+
|
|
1142
|
+
try {
|
|
1143
|
+
// ✅ 选择图片
|
|
1144
|
+
const chooseRes = await wx.chooseImage({
|
|
1145
|
+
count,
|
|
1146
|
+
sizeType,
|
|
1147
|
+
sourceType
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
const uploadTasks = chooseRes.tempFilePaths.map((filePath, index) => {
|
|
1151
|
+
// ✅ 生成云存储路径
|
|
1152
|
+
const ext = filePath.split('.').pop()
|
|
1153
|
+
const cloudPath = `${folder}/${Date.now()}_${index}.${ext}`
|
|
1154
|
+
|
|
1155
|
+
return uploadFile(filePath, cloudPath)
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
const results = await Promise.all(uploadTasks)
|
|
1159
|
+
|
|
1160
|
+
return {
|
|
1161
|
+
success: true,
|
|
1162
|
+
fileIDs: results.map(r => r.fileID)
|
|
1163
|
+
}
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
console.error('选择上传图片失败:', error)
|
|
1166
|
+
throw error
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* 下载文件
|
|
1172
|
+
* @param {string} fileID - 云文件 ID
|
|
1173
|
+
* @returns {Promise}
|
|
1174
|
+
*/
|
|
1175
|
+
async function downloadFile(fileID) {
|
|
1176
|
+
try {
|
|
1177
|
+
const res = await wx.cloud.downloadFile({
|
|
1178
|
+
fileID
|
|
1179
|
+
})
|
|
1180
|
+
|
|
1181
|
+
return {
|
|
1182
|
+
success: true,
|
|
1183
|
+
tempFilePath: res.tempFilePath
|
|
1184
|
+
}
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
console.error('下载文件失败:', error)
|
|
1187
|
+
throw error
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* 删除文件
|
|
1193
|
+
* @param {Array<string>} fileIDs - 云文件 ID 数组
|
|
1194
|
+
* @returns {Promise}
|
|
1195
|
+
*/
|
|
1196
|
+
async function deleteFiles(fileIDs) {
|
|
1197
|
+
try {
|
|
1198
|
+
const res = await wx.cloud.deleteFile({
|
|
1199
|
+
fileList: fileIDs
|
|
1200
|
+
})
|
|
1201
|
+
|
|
1202
|
+
return {
|
|
1203
|
+
success: true,
|
|
1204
|
+
fileList: res.fileList
|
|
1205
|
+
}
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
console.error('删除文件失败:', error)
|
|
1208
|
+
throw error
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
module.exports = {
|
|
1213
|
+
uploadFile,
|
|
1214
|
+
chooseAndUploadImage,
|
|
1215
|
+
downloadFile,
|
|
1216
|
+
deleteFiles
|
|
1217
|
+
}
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
#### 使用示例
|
|
1221
|
+
|
|
1222
|
+
```javascript
|
|
1223
|
+
// pages/upload/upload.js
|
|
1224
|
+
const { chooseAndUploadImage } = require('../../utils/cloudStorage')
|
|
1225
|
+
|
|
1226
|
+
Page({
|
|
1227
|
+
data: {
|
|
1228
|
+
imageUrls: []
|
|
1229
|
+
},
|
|
1230
|
+
|
|
1231
|
+
async handleUploadImage() {
|
|
1232
|
+
try {
|
|
1233
|
+
const res = await chooseAndUploadImage({
|
|
1234
|
+
count: 3,
|
|
1235
|
+
folder: 'user-uploads'
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
this.setData({
|
|
1239
|
+
imageUrls: [...this.data.imageUrls, ...res.fileIDs]
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
wx.showToast({
|
|
1243
|
+
title: '上传成功',
|
|
1244
|
+
icon: 'success'
|
|
1245
|
+
})
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
// 错误已在封装函数中处理
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
})
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
### 云调用规范
|
|
1256
|
+
|
|
1257
|
+
#### 发送订阅消息
|
|
1258
|
+
|
|
1259
|
+
```javascript
|
|
1260
|
+
// cloudfunctions/sendMessage/index.js
|
|
1261
|
+
const cloud = require('wx-server-sdk')
|
|
1262
|
+
|
|
1263
|
+
cloud.init({
|
|
1264
|
+
env: cloud.DYNAMIC_CURRENT_ENV
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* 发送订阅消息
|
|
1269
|
+
*/
|
|
1270
|
+
exports.main = async (event, context) => {
|
|
1271
|
+
const { touser, templateId, page, data } = event
|
|
1272
|
+
|
|
1273
|
+
try {
|
|
1274
|
+
const result = await cloud.openapi.subscribeMessage.send({
|
|
1275
|
+
touser,
|
|
1276
|
+
page,
|
|
1277
|
+
data,
|
|
1278
|
+
templateId,
|
|
1279
|
+
miniprogramState: 'formal' // formal | trial | developer
|
|
1280
|
+
})
|
|
1281
|
+
|
|
1282
|
+
return {
|
|
1283
|
+
success: true,
|
|
1284
|
+
data: result
|
|
1285
|
+
}
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
console.error('发送订阅消息失败:', error)
|
|
1288
|
+
return {
|
|
1289
|
+
success: false,
|
|
1290
|
+
message: error.message
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
#### 生成小程序码
|
|
1297
|
+
|
|
1298
|
+
```javascript
|
|
1299
|
+
// cloudfunctions/generateQRCode/index.js
|
|
1300
|
+
const cloud = require('wx-server-sdk')
|
|
1301
|
+
|
|
1302
|
+
cloud.init({
|
|
1303
|
+
env: cloud.DYNAMIC_CURRENT_ENV
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* 生成小程序码
|
|
1308
|
+
*/
|
|
1309
|
+
exports.main = async (event, context) => {
|
|
1310
|
+
const { scene, page, width = 430 } = event
|
|
1311
|
+
|
|
1312
|
+
try {
|
|
1313
|
+
const result = await cloud.openapi.wxacode.getUnlimited({
|
|
1314
|
+
scene,
|
|
1315
|
+
page,
|
|
1316
|
+
width,
|
|
1317
|
+
autoColor: false,
|
|
1318
|
+
lineColor: { r: 0, g: 0, b: 0 }
|
|
1319
|
+
})
|
|
1320
|
+
|
|
1321
|
+
// ✅ 上传到云存储
|
|
1322
|
+
const upload = await cloud.uploadFile({
|
|
1323
|
+
cloudPath: `qrcodes/${Date.now()}.png`,
|
|
1324
|
+
fileContent: result.buffer
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
success: true,
|
|
1329
|
+
fileID: upload.fileID
|
|
1330
|
+
}
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
console.error('生成小程序码失败:', error)
|
|
1333
|
+
return {
|
|
1334
|
+
success: false,
|
|
1335
|
+
message: error.message
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
---
|
|
1342
|
+
|
|
1343
|
+
### 云开发最佳实践
|
|
1344
|
+
|
|
1345
|
+
#### 1. 数据库权限配置
|
|
1346
|
+
|
|
1347
|
+
```json
|
|
1348
|
+
// 云数据库权限配置(在云开发控制台设置)
|
|
1349
|
+
{
|
|
1350
|
+
"read": "doc._openid == auth.openid", // 只能读取自己的数据
|
|
1351
|
+
"write": "doc._openid == auth.openid" // 只能写入自己的数据
|
|
1352
|
+
}
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
#### 2. 云函数并发控制
|
|
1356
|
+
|
|
1357
|
+
```javascript
|
|
1358
|
+
// cloudfunctions/batchProcess/index.js
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* 批量处理数据(控制并发)
|
|
1362
|
+
*/
|
|
1363
|
+
exports.main = async (event, context) => {
|
|
1364
|
+
const { items } = event
|
|
1365
|
+
const BATCH_SIZE = 5 // 每批处理 5 个
|
|
1366
|
+
|
|
1367
|
+
try {
|
|
1368
|
+
const results = []
|
|
1369
|
+
|
|
1370
|
+
// ✅ 分批处理,避免超时
|
|
1371
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
1372
|
+
const batch = items.slice(i, i + BATCH_SIZE)
|
|
1373
|
+
const batchResults = await Promise.all(
|
|
1374
|
+
batch.map(item => processItem(item))
|
|
1375
|
+
)
|
|
1376
|
+
results.push(...batchResults)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
return {
|
|
1380
|
+
success: true,
|
|
1381
|
+
data: results
|
|
1382
|
+
}
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
console.error('批量处理失败:', error)
|
|
1385
|
+
return {
|
|
1386
|
+
success: false,
|
|
1387
|
+
message: error.message
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
#### 3. 云函数错误监控
|
|
1394
|
+
|
|
1395
|
+
```javascript
|
|
1396
|
+
// cloudfunctions/common/errorHandler.js
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* 统一错误处理
|
|
1400
|
+
*/
|
|
1401
|
+
function handleError(error, functionName) {
|
|
1402
|
+
// ✅ 记录错误日志
|
|
1403
|
+
console.error(`[${functionName}] Error:`, {
|
|
1404
|
+
message: error.message,
|
|
1405
|
+
stack: error.stack,
|
|
1406
|
+
timestamp: new Date()
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
// ✅ 错误上报(可接入第三方监控)
|
|
1410
|
+
// reportError(functionName, error)
|
|
1411
|
+
|
|
1412
|
+
// ✅ 返回统一错误格式
|
|
1413
|
+
return {
|
|
1414
|
+
success: false,
|
|
1415
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
1416
|
+
message: error.message || '服务器错误'
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
module.exports = {
|
|
1421
|
+
handleError
|
|
1422
|
+
}
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
#### 4. 云存储安全规则
|
|
1426
|
+
|
|
1427
|
+
```javascript
|
|
1428
|
+
// ✅ 限制文件大小和类型
|
|
1429
|
+
async function uploadWithValidation(filePath, options = {}) {
|
|
1430
|
+
const {
|
|
1431
|
+
maxSize = 5 * 1024 * 1024, // 最大 5MB
|
|
1432
|
+
allowedTypes = ['image/jpeg', 'image/png']
|
|
1433
|
+
} = options
|
|
1434
|
+
|
|
1435
|
+
try {
|
|
1436
|
+
// ✅ 获取文件信息
|
|
1437
|
+
const fileInfo = await wx.getFileInfo({ filePath })
|
|
1438
|
+
|
|
1439
|
+
// ✅ 验证文件大小
|
|
1440
|
+
if (fileInfo.size > maxSize) {
|
|
1441
|
+
throw new Error('文件大小超过限制')
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ✅ 验证文件类型(需要额外检查)
|
|
1445
|
+
// 实际项目中应该检查文件扩展名或 MIME 类型
|
|
1446
|
+
|
|
1447
|
+
// ✅ 上传文件
|
|
1448
|
+
const cloudPath = `uploads/${Date.now()}_${Math.random()}.jpg`
|
|
1449
|
+
return await uploadFile(filePath, cloudPath)
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
console.error('上传验证失败:', error)
|
|
1452
|
+
throw error
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
---
|
|
1458
|
+
|
|
1459
|
+
### 云开发安全规范
|
|
1460
|
+
|
|
1461
|
+
#### 1. 敏感操作必须在云函数中执行
|
|
1462
|
+
|
|
1463
|
+
```javascript
|
|
1464
|
+
// ❌ 错误:在小程序端直接操作敏感数据
|
|
1465
|
+
// pages/order/order.js
|
|
1466
|
+
await db.collection('orders').add({
|
|
1467
|
+
data: {
|
|
1468
|
+
userId: 'xxx',
|
|
1469
|
+
totalPrice: 100, // 价格可被篡改!
|
|
1470
|
+
status: 'paid' // 状态可被篡改!
|
|
1471
|
+
}
|
|
1472
|
+
})
|
|
1473
|
+
|
|
1474
|
+
// ✅ 正确:通过云函数处理
|
|
1475
|
+
// cloudfunctions/createOrder/index.js
|
|
1476
|
+
exports.main = async (event, context) => {
|
|
1477
|
+
const { OPENID } = cloud.getWXContext()
|
|
1478
|
+
const { productId, count } = event
|
|
1479
|
+
|
|
1480
|
+
// ✅ 在服务端计算价格
|
|
1481
|
+
const product = await db.collection('products').doc(productId).get()
|
|
1482
|
+
const totalPrice = product.data.price * count
|
|
1483
|
+
|
|
1484
|
+
// ✅ 创建订单
|
|
1485
|
+
await db.collection('orders').add({
|
|
1486
|
+
data: {
|
|
1487
|
+
_openid: OPENID,
|
|
1488
|
+
productId,
|
|
1489
|
+
count,
|
|
1490
|
+
totalPrice, // 服务端计算,安全
|
|
1491
|
+
status: 'pending',
|
|
1492
|
+
createTime: new Date()
|
|
1493
|
+
}
|
|
1494
|
+
})
|
|
1495
|
+
|
|
1496
|
+
return { success: true }
|
|
1497
|
+
}
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
#### 2. 数据库查询优化
|
|
1501
|
+
|
|
1502
|
+
```javascript
|
|
1503
|
+
// ✅ 使用索引
|
|
1504
|
+
// 在云开发控制台为常用查询字段创建索引
|
|
1505
|
+
|
|
1506
|
+
// ✅ 避免全表扫描
|
|
1507
|
+
// ❌ 错误
|
|
1508
|
+
const res = await db.collection('orders').get() // 可能超出限制
|
|
1509
|
+
|
|
1510
|
+
// ✅ 正确:添加条件和限制
|
|
1511
|
+
const res = await db.collection('orders')
|
|
1512
|
+
.where({
|
|
1513
|
+
_openid: OPENID,
|
|
1514
|
+
status: 'pending'
|
|
1515
|
+
})
|
|
1516
|
+
.limit(20)
|
|
1517
|
+
.get()
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
#### 3. 云函数冷启动优化
|
|
1521
|
+
|
|
1522
|
+
```javascript
|
|
1523
|
+
// ✅ 复用全局变量
|
|
1524
|
+
const cloud = require('wx-server-sdk')
|
|
1525
|
+
cloud.init()
|
|
1526
|
+
|
|
1527
|
+
const db = cloud.database() // 在函数外初始化
|
|
1528
|
+
|
|
1529
|
+
exports.main = async (event, context) => {
|
|
1530
|
+
// 直接使用已初始化的 db
|
|
1531
|
+
const res = await db.collection('users').get()
|
|
1532
|
+
return res
|
|
1533
|
+
}
|
|
1534
|
+
```
|
|
1535
|
+
|
|
1536
|
+
---
|
|
1537
|
+
|
|
1538
|
+
## 💾 本地存储规范
|
|
1539
|
+
|
|
1540
|
+
### 存储封装
|
|
1541
|
+
|
|
1542
|
+
```javascript
|
|
1543
|
+
// utils/storage.js
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* 设置存储
|
|
1547
|
+
* @param {string} key - 键名
|
|
1548
|
+
* @param {any} value - 值
|
|
1549
|
+
* @returns {Promise}
|
|
1550
|
+
*/
|
|
1551
|
+
function setStorage(key, value) {
|
|
1552
|
+
return new Promise((resolve, reject) => {
|
|
1553
|
+
wx.setStorage({
|
|
1554
|
+
key,
|
|
1555
|
+
data: value,
|
|
1556
|
+
success: resolve,
|
|
1557
|
+
fail: reject
|
|
1558
|
+
})
|
|
1559
|
+
})
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* 获取存储
|
|
1564
|
+
* @param {string} key - 键名
|
|
1565
|
+
* @returns {Promise}
|
|
1566
|
+
*/
|
|
1567
|
+
function getStorage(key) {
|
|
1568
|
+
return new Promise((resolve, reject) => {
|
|
1569
|
+
wx.getStorage({
|
|
1570
|
+
key,
|
|
1571
|
+
success: (res) => resolve(res.data),
|
|
1572
|
+
fail: reject
|
|
1573
|
+
})
|
|
1574
|
+
})
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* 同步设置存储
|
|
1579
|
+
*/
|
|
1580
|
+
function setStorageSync(key, value) {
|
|
1581
|
+
try {
|
|
1582
|
+
wx.setStorageSync(key, value)
|
|
1583
|
+
return true
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
console.error('存储失败:', error)
|
|
1586
|
+
return false
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* 同步获取存储
|
|
1592
|
+
*/
|
|
1593
|
+
function getStorageSync(key, defaultValue = null) {
|
|
1594
|
+
try {
|
|
1595
|
+
const value = wx.getStorageSync(key)
|
|
1596
|
+
return value !== '' ? value : defaultValue
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.error('读取存储失败:', error)
|
|
1599
|
+
return defaultValue
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* 移除存储
|
|
1605
|
+
*/
|
|
1606
|
+
function removeStorage(key) {
|
|
1607
|
+
return new Promise((resolve, reject) => {
|
|
1608
|
+
wx.removeStorage({
|
|
1609
|
+
key,
|
|
1610
|
+
success: resolve,
|
|
1611
|
+
fail: reject
|
|
1612
|
+
})
|
|
1613
|
+
})
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* 清空存储
|
|
1618
|
+
*/
|
|
1619
|
+
function clearStorage() {
|
|
1620
|
+
return new Promise((resolve, reject) => {
|
|
1621
|
+
wx.clearStorage({
|
|
1622
|
+
success: resolve,
|
|
1623
|
+
fail: reject
|
|
1624
|
+
})
|
|
1625
|
+
})
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
module.exports = {
|
|
1629
|
+
setStorage,
|
|
1630
|
+
getStorage,
|
|
1631
|
+
setStorageSync,
|
|
1632
|
+
getStorageSync,
|
|
1633
|
+
removeStorage,
|
|
1634
|
+
clearStorage
|
|
1635
|
+
}
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
### 存储命名规范
|
|
1639
|
+
|
|
1640
|
+
```javascript
|
|
1641
|
+
// config/constants.js
|
|
1642
|
+
|
|
1643
|
+
// ✅ 统一管理存储 key
|
|
1644
|
+
const STORAGE_KEYS = {
|
|
1645
|
+
USER_INFO: 'user_info',
|
|
1646
|
+
AUTH_TOKEN: 'auth_token',
|
|
1647
|
+
SETTINGS: 'app_settings',
|
|
1648
|
+
CACHE_DATA: 'cache_data',
|
|
1649
|
+
SEARCH_HISTORY: 'search_history'
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
module.exports = {
|
|
1653
|
+
STORAGE_KEYS
|
|
1654
|
+
}
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
---
|
|
1658
|
+
|
|
1659
|
+
## 🎯 性能优化
|
|
1660
|
+
|
|
1661
|
+
### setData 优化
|
|
1662
|
+
|
|
1663
|
+
```javascript
|
|
1664
|
+
// ❌ 错误:频繁调用 setData
|
|
1665
|
+
for (let i = 0; i < 100; i++) {
|
|
1666
|
+
this.setData({
|
|
1667
|
+
count: i
|
|
1668
|
+
})
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// ✅ 正确:合并多次 setData
|
|
1672
|
+
const updates = {}
|
|
1673
|
+
for (let i = 0; i < 100; i++) {
|
|
1674
|
+
updates.count = i
|
|
1675
|
+
}
|
|
1676
|
+
this.setData(updates)
|
|
1677
|
+
|
|
1678
|
+
// ❌ 错误:setData 数据过大
|
|
1679
|
+
this.setData({
|
|
1680
|
+
hugeList: [...Array(1000).keys()] // 一次传输大量数据
|
|
1681
|
+
})
|
|
1682
|
+
|
|
1683
|
+
// ✅ 正确:只更新需要的字段
|
|
1684
|
+
this.setData({
|
|
1685
|
+
[`list[${index}].name`]: newName // 局部更新
|
|
1686
|
+
})
|
|
1687
|
+
|
|
1688
|
+
// ❌ 错误:不必要的数据
|
|
1689
|
+
this.setData({
|
|
1690
|
+
userInfo: {
|
|
1691
|
+
...user,
|
|
1692
|
+
_rawData: rawData, // 不需要在视图中使用的数据
|
|
1693
|
+
_cache: cache
|
|
1694
|
+
}
|
|
1695
|
+
})
|
|
1696
|
+
|
|
1697
|
+
// ✅ 正确:只传必要数据
|
|
1698
|
+
this.setData({
|
|
1699
|
+
userInfo: {
|
|
1700
|
+
id: user.id,
|
|
1701
|
+
name: user.name,
|
|
1702
|
+
avatar: user.avatar
|
|
1703
|
+
}
|
|
1704
|
+
})
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
### 列表渲染优化
|
|
1708
|
+
|
|
1709
|
+
```xml
|
|
1710
|
+
<!-- ✅ 使用虚拟列表(长列表) -->
|
|
1711
|
+
<recycle-view
|
|
1712
|
+
batch="{{batchSetRecycleData}}"
|
|
1713
|
+
height="{{height}}"
|
|
1714
|
+
>
|
|
1715
|
+
<recycle-item wx:for="{{list}}" wx:key="id">
|
|
1716
|
+
<view>{{item.name}}</view>
|
|
1717
|
+
</recycle-item>
|
|
1718
|
+
</recycle-view>
|
|
1719
|
+
|
|
1720
|
+
<!-- ✅ 使用分页加载 -->
|
|
1721
|
+
<scroll-view
|
|
1722
|
+
scroll-y
|
|
1723
|
+
bindscrolltolower="onReachBottom"
|
|
1724
|
+
lower-threshold="100"
|
|
1725
|
+
>
|
|
1726
|
+
<view wx:for="{{list}}" wx:key="id">
|
|
1727
|
+
{{item.name}}
|
|
1728
|
+
</view>
|
|
1729
|
+
</scroll-view>
|
|
1730
|
+
|
|
1731
|
+
<!-- ✅ 图片懒加载 -->
|
|
1732
|
+
<image
|
|
1733
|
+
src="{{item.image}}"
|
|
1734
|
+
lazy-load
|
|
1735
|
+
mode="aspectFill"
|
|
1736
|
+
/>
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
### 代码分包
|
|
1740
|
+
|
|
1741
|
+
```json
|
|
1742
|
+
// app.json
|
|
1743
|
+
{
|
|
1744
|
+
"pages": [
|
|
1745
|
+
"pages/index/index",
|
|
1746
|
+
"pages/user/user"
|
|
1747
|
+
],
|
|
1748
|
+
"subpackages": [
|
|
1749
|
+
{
|
|
1750
|
+
"root": "packageA",
|
|
1751
|
+
"pages": [
|
|
1752
|
+
"pages/detail/detail",
|
|
1753
|
+
"pages/list/list"
|
|
1754
|
+
]
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
"root": "packageB",
|
|
1758
|
+
"name": "vip",
|
|
1759
|
+
"pages": [
|
|
1760
|
+
"pages/vip/vip"
|
|
1761
|
+
],
|
|
1762
|
+
"independent": true // 独立分包
|
|
1763
|
+
}
|
|
1764
|
+
],
|
|
1765
|
+
"preloadRule": {
|
|
1766
|
+
"pages/index/index": {
|
|
1767
|
+
"network": "all",
|
|
1768
|
+
"packages": ["packageA"]
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
---
|
|
1775
|
+
|
|
1776
|
+
## 🔐 安全规范
|
|
1777
|
+
|
|
1778
|
+
### 敏感信息处理
|
|
1779
|
+
|
|
1780
|
+
```javascript
|
|
1781
|
+
// ❌ 错误:直接存储敏感信息
|
|
1782
|
+
wx.setStorageSync('password', '123456')
|
|
1783
|
+
|
|
1784
|
+
// ✅ 正确:加密后存储
|
|
1785
|
+
const CryptoJS = require('crypto-js')
|
|
1786
|
+
|
|
1787
|
+
function encryptData(data, key) {
|
|
1788
|
+
return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString()
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function decryptData(ciphertext, key) {
|
|
1792
|
+
const bytes = CryptoJS.AES.decrypt(ciphertext, key)
|
|
1793
|
+
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8))
|
|
1794
|
+
}
|
|
1795
|
+
```
|
|
1796
|
+
|
|
1797
|
+
### XSS 防护
|
|
1798
|
+
|
|
1799
|
+
```javascript
|
|
1800
|
+
// ✅ 转义用户输入
|
|
1801
|
+
function escapeHtml(text) {
|
|
1802
|
+
const map = {
|
|
1803
|
+
'&': '&',
|
|
1804
|
+
'<': '<',
|
|
1805
|
+
'>': '>',
|
|
1806
|
+
'"': '"',
|
|
1807
|
+
"'": '''
|
|
1808
|
+
}
|
|
1809
|
+
return text.replace(/[&<>"']/g, (m) => map[m])
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
// 使用
|
|
1813
|
+
this.setData({
|
|
1814
|
+
safeContent: escapeHtml(userInput)
|
|
1815
|
+
})
|
|
1816
|
+
```
|
|
1817
|
+
|
|
1818
|
+
### 接口鉴权
|
|
1819
|
+
|
|
1820
|
+
```javascript
|
|
1821
|
+
// utils/auth.js
|
|
1822
|
+
|
|
1823
|
+
const TOKEN_KEY = 'auth_token'
|
|
1824
|
+
const REFRESH_TOKEN_KEY = 'refresh_token'
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* 保存 Token
|
|
1828
|
+
*/
|
|
1829
|
+
function saveToken(token, refreshToken) {
|
|
1830
|
+
wx.setStorageSync(TOKEN_KEY, token)
|
|
1831
|
+
if (refreshToken) {
|
|
1832
|
+
wx.setStorageSync(REFRESH_TOKEN_KEY, refreshToken)
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* 获取 Token
|
|
1838
|
+
*/
|
|
1839
|
+
function getToken() {
|
|
1840
|
+
return wx.getStorageSync(TOKEN_KEY)
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* 清除 Token
|
|
1845
|
+
*/
|
|
1846
|
+
function clearToken() {
|
|
1847
|
+
wx.removeStorageSync(TOKEN_KEY)
|
|
1848
|
+
wx.removeStorageSync(REFRESH_TOKEN_KEY)
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/**
|
|
1852
|
+
* 检查登录状态
|
|
1853
|
+
*/
|
|
1854
|
+
function checkLogin() {
|
|
1855
|
+
return !!getToken()
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* 刷新 Token
|
|
1860
|
+
*/
|
|
1861
|
+
async function refreshToken() {
|
|
1862
|
+
const refreshToken = wx.getStorageSync(REFRESH_TOKEN_KEY)
|
|
1863
|
+
|
|
1864
|
+
if (!refreshToken) {
|
|
1865
|
+
throw new Error('No refresh token')
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
try {
|
|
1869
|
+
const res = await authApi.refreshToken({ refreshToken })
|
|
1870
|
+
saveToken(res.data.token, res.data.refreshToken)
|
|
1871
|
+
return res.data.token
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
clearToken()
|
|
1874
|
+
throw error
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
module.exports = {
|
|
1879
|
+
saveToken,
|
|
1880
|
+
getToken,
|
|
1881
|
+
clearToken,
|
|
1882
|
+
checkLogin,
|
|
1883
|
+
refreshToken
|
|
1884
|
+
}
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
---
|
|
1888
|
+
|
|
1889
|
+
## 📱 用户体验
|
|
1890
|
+
|
|
1891
|
+
### 加载状态
|
|
1892
|
+
|
|
1893
|
+
```javascript
|
|
1894
|
+
// ✅ 统一的 loading 管理
|
|
1895
|
+
class LoadingManager {
|
|
1896
|
+
constructor() {
|
|
1897
|
+
this.loadingCount = 0
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
show(title = '加载中...') {
|
|
1901
|
+
this.loadingCount++
|
|
1902
|
+
if (this.loadingCount === 1) {
|
|
1903
|
+
wx.showLoading({
|
|
1904
|
+
title,
|
|
1905
|
+
mask: true
|
|
1906
|
+
})
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
hide() {
|
|
1911
|
+
this.loadingCount--
|
|
1912
|
+
if (this.loadingCount === 0) {
|
|
1913
|
+
wx.hideLoading()
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
clear() {
|
|
1918
|
+
this.loadingCount = 0
|
|
1919
|
+
wx.hideLoading()
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
const loadingManager = new LoadingManager()
|
|
1924
|
+
|
|
1925
|
+
module.exports = loadingManager
|
|
1926
|
+
```
|
|
1927
|
+
|
|
1928
|
+
### 错误处理
|
|
1929
|
+
|
|
1930
|
+
```javascript
|
|
1931
|
+
// ✅ 统一错误处理
|
|
1932
|
+
function handleError(error, showToast = true) {
|
|
1933
|
+
console.error('Error:', error)
|
|
1934
|
+
|
|
1935
|
+
if (showToast) {
|
|
1936
|
+
const message = error.message || '操作失败,请重试'
|
|
1937
|
+
wx.showToast({
|
|
1938
|
+
title: message,
|
|
1939
|
+
icon: 'none',
|
|
1940
|
+
duration: 2000
|
|
1941
|
+
})
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// ✅ 上报错误到监控平台
|
|
1945
|
+
if (typeof wx.reportMonitor === 'function') {
|
|
1946
|
+
wx.reportMonitor('error', {
|
|
1947
|
+
message: error.message,
|
|
1948
|
+
stack: error.stack,
|
|
1949
|
+
timestamp: Date.now()
|
|
1950
|
+
})
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
module.exports = {
|
|
1955
|
+
handleError
|
|
1956
|
+
}
|
|
1957
|
+
```
|
|
1958
|
+
|
|
1959
|
+
### 交互反馈
|
|
1960
|
+
|
|
1961
|
+
```javascript
|
|
1962
|
+
// ✅ 完善的用户反馈
|
|
1963
|
+
|
|
1964
|
+
// 成功提示
|
|
1965
|
+
wx.showToast({
|
|
1966
|
+
title: '操作成功',
|
|
1967
|
+
icon: 'success',
|
|
1968
|
+
duration: 2000
|
|
1969
|
+
})
|
|
1970
|
+
|
|
1971
|
+
// 失败提示
|
|
1972
|
+
wx.showToast({
|
|
1973
|
+
title: '操作失败',
|
|
1974
|
+
icon: 'none',
|
|
1975
|
+
duration: 2000
|
|
1976
|
+
})
|
|
1977
|
+
|
|
1978
|
+
// 确认对话框
|
|
1979
|
+
wx.showModal({
|
|
1980
|
+
title: '提示',
|
|
1981
|
+
content: '确认删除这条记录吗?',
|
|
1982
|
+
confirmText: '删除',
|
|
1983
|
+
confirmColor: '#ff4444',
|
|
1984
|
+
success: (res) => {
|
|
1985
|
+
if (res.confirm) {
|
|
1986
|
+
// 用户确认
|
|
1987
|
+
this.handleDelete()
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
})
|
|
1991
|
+
|
|
1992
|
+
// 操作菜单
|
|
1993
|
+
wx.showActionSheet({
|
|
1994
|
+
itemList: ['拍照', '从相册选择'],
|
|
1995
|
+
success: (res) => {
|
|
1996
|
+
if (res.tapIndex === 0) {
|
|
1997
|
+
// 拍照
|
|
1998
|
+
} else if (res.tapIndex === 1) {
|
|
1999
|
+
// 选择照片
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
})
|
|
2003
|
+
```
|
|
2004
|
+
|
|
2005
|
+
---
|
|
2006
|
+
|
|
2007
|
+
## 🧪 调试与测试
|
|
2008
|
+
|
|
2009
|
+
### 调试技巧
|
|
2010
|
+
|
|
2011
|
+
```javascript
|
|
2012
|
+
// ✅ 环境判断
|
|
2013
|
+
const isDev = process.env.NODE_ENV === 'development'
|
|
2014
|
+
|
|
2015
|
+
if (isDev) {
|
|
2016
|
+
console.log('Debug info:', data)
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// ✅ 性能监控
|
|
2020
|
+
const startTime = Date.now()
|
|
2021
|
+
// ... 执行操作
|
|
2022
|
+
const endTime = Date.now()
|
|
2023
|
+
console.log(`操作耗时: ${endTime - startTime}ms`)
|
|
2024
|
+
|
|
2025
|
+
// ✅ 使用 vConsole
|
|
2026
|
+
if (isDev) {
|
|
2027
|
+
const VConsole = require('vconsole')
|
|
2028
|
+
new VConsole()
|
|
2029
|
+
}
|
|
2030
|
+
```
|
|
2031
|
+
|
|
2032
|
+
### 单元测试
|
|
2033
|
+
|
|
2034
|
+
```javascript
|
|
2035
|
+
// test/utils/format.test.js
|
|
2036
|
+
const { formatDate, formatNumber } = require('../../utils/format')
|
|
2037
|
+
|
|
2038
|
+
describe('format utils', () => {
|
|
2039
|
+
test('formatDate should format timestamp correctly', () => {
|
|
2040
|
+
const timestamp = 1609459200000 // 2021-01-01 00:00:00
|
|
2041
|
+
expect(formatDate(timestamp)).toBe('2021-01-01')
|
|
2042
|
+
})
|
|
2043
|
+
|
|
2044
|
+
test('formatNumber should format number with comma', () => {
|
|
2045
|
+
expect(formatNumber(1234567)).toBe('1,234,567')
|
|
2046
|
+
})
|
|
2047
|
+
})
|
|
2048
|
+
```
|
|
2049
|
+
|
|
2050
|
+
---
|
|
2051
|
+
|
|
2052
|
+
## ❌ 禁止模式
|
|
2053
|
+
|
|
2054
|
+
### 代码层面
|
|
2055
|
+
|
|
2056
|
+
```javascript
|
|
2057
|
+
// ❌ 直接修改 data
|
|
2058
|
+
this.data.count = 10 // 不会触发视图更新
|
|
2059
|
+
|
|
2060
|
+
// ✅ 使用 setData
|
|
2061
|
+
this.setData({
|
|
2062
|
+
count: 10
|
|
2063
|
+
})
|
|
2064
|
+
|
|
2065
|
+
// ❌ 在 WXML 中写复杂逻辑
|
|
2066
|
+
<view>{{list.filter(i => i.active).length}}</view>
|
|
2067
|
+
|
|
2068
|
+
// ✅ 在 JS 中计算
|
|
2069
|
+
computed() {
|
|
2070
|
+
return {
|
|
2071
|
+
activeCount: this.data.list.filter(i => i.active).length
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// ❌ 没有错误处理
|
|
2076
|
+
async fetchData() {
|
|
2077
|
+
const res = await api.getData()
|
|
2078
|
+
this.setData({ data: res.data })
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// ✅ 完善的错误处理
|
|
2082
|
+
async fetchData() {
|
|
2083
|
+
try {
|
|
2084
|
+
const res = await api.getData()
|
|
2085
|
+
this.setData({ data: res.data })
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
console.error('获取数据失败:', error)
|
|
2088
|
+
wx.showToast({
|
|
2089
|
+
title: '加载失败',
|
|
2090
|
+
icon: 'none'
|
|
2091
|
+
})
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
```
|
|
2095
|
+
|
|
2096
|
+
### 性能陷阱
|
|
2097
|
+
|
|
2098
|
+
```javascript
|
|
2099
|
+
// ❌ 在循环中频繁调用 setData
|
|
2100
|
+
for (let i = 0; i < items.length; i++) {
|
|
2101
|
+
this.setData({
|
|
2102
|
+
[`items[${i}]`]: items[i]
|
|
2103
|
+
})
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// ✅ 一次性更新
|
|
2107
|
+
this.setData({
|
|
2108
|
+
items: items
|
|
2109
|
+
})
|
|
2110
|
+
|
|
2111
|
+
// ❌ setData 传递大量无用数据
|
|
2112
|
+
this.setData({
|
|
2113
|
+
hugeObject: {
|
|
2114
|
+
// 包含很多视图不需要的数据
|
|
2115
|
+
_internalState: {},
|
|
2116
|
+
_cache: {},
|
|
2117
|
+
displayData: {}
|
|
2118
|
+
}
|
|
2119
|
+
})
|
|
2120
|
+
|
|
2121
|
+
// ✅ 只传递必要数据
|
|
2122
|
+
this.setData({
|
|
2123
|
+
displayData: hugeObject.displayData
|
|
2124
|
+
})
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
---
|
|
2128
|
+
|
|
2129
|
+
## ✅ 最佳实践总结
|
|
2130
|
+
|
|
2131
|
+
### 开发规范清单
|
|
2132
|
+
|
|
2133
|
+
- [ ] **文件组织**: 遵循推荐的目录结构
|
|
2134
|
+
- [ ] **命名规范**: 使用 kebab-case/camelCase
|
|
2135
|
+
- [ ] **代码注释**: 为复杂逻辑添加注释
|
|
2136
|
+
- [ ] **错误处理**: 所有异步操作都有 try-catch
|
|
2137
|
+
- [ ] **加载状态**: 异步操作显示 loading
|
|
2138
|
+
- [ ] **用户反馈**: 操作结果有明确提示
|
|
2139
|
+
|
|
2140
|
+
### 性能优化清单
|
|
2141
|
+
|
|
2142
|
+
- [ ] **setData 优化**: 减少调用频率,控制数据大小
|
|
2143
|
+
- [ ] **列表优化**: 长列表使用虚拟列表或分页
|
|
2144
|
+
- [ ] **图片优化**: 使用 lazy-load,压缩图片
|
|
2145
|
+
- [ ] **代码分包**: 合理使用分包和预加载
|
|
2146
|
+
- [ ] **避免白屏**: 骨架屏/占位图
|
|
2147
|
+
|
|
2148
|
+
### 安全规范清单
|
|
2149
|
+
|
|
2150
|
+
- [ ] **敏感信息**: 加密存储,不明文传输
|
|
2151
|
+
- [ ] **XSS 防护**: 转义用户输入
|
|
2152
|
+
- [ ] **接口鉴权**: Token 验证,刷新机制
|
|
2153
|
+
- [ ] **HTTPS**: 所有接口使用 HTTPS
|
|
2154
|
+
- [ ] **权限校验**: 敏感操作二次确认
|
|
2155
|
+
|
|
2156
|
+
### 用户体验清单
|
|
2157
|
+
|
|
2158
|
+
- [ ] **加载提示**: 所有异步操作有反馈
|
|
2159
|
+
- [ ] **错误提示**: 清晰的错误信息
|
|
2160
|
+
- [ ] **空状态**: 无数据时显示空状态
|
|
2161
|
+
- [ ] **下拉刷新**: 列表支持下拉刷新
|
|
2162
|
+
- [ ] **上拉加载**: 长列表支持分页加载
|
|
2163
|
+
|
|
2164
|
+
---
|
|
2165
|
+
|
|
2166
|
+
## 📚 参考资源
|
|
2167
|
+
|
|
2168
|
+
- [微信小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/)
|
|
2169
|
+
- [微信小程序开发指南](https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0008aeea9a8978b00086a685851c0a)
|
|
2170
|
+
- [小程序性能优化指南](https://developers.weixin.qq.com/miniprogram/dev/framework/performance/)
|
|
2171
|
+
- [小程序安全指南](https://developers.weixin.qq.com/miniprogram/dev/framework/security.html)
|
|
2172
|
+
|
|
2173
|
+
---
|
|
2174
|
+
|
|
2175
|
+
**维护团队**: MTA工作室
|
|
2176
|
+
**版本**: 1.0.0
|
|
2177
|
+
**更新日期**: 2025-12-17
|