uniapp开发跨端小程序重点

UniApp 开发小程序端

本文档为纯小程序端专属内容,完整覆盖从项目搭建、核心配置、全量生命周期、条件编译、组件API规范、性能优化到发布上线的全流程,适配UniApp开发小程序的生产级要求。


项目实践

code/all/mp-yswx 微信小程序、钉钉小程序
code/all/dd-v5 企业微信、h5
code/all/yt-digital-resource h5
code/all/yt-straight-a-project h5


一、小程序端项目初始化与核心配置(全量专属详解)

小程序端的所有平台规则、包体积限制、路由权限、原生能力均围绕 manifest.jsonpages.json 两大配置文件展开,是小程序开发的核心根基,所有配置项直接影响功能可用性、启动性能与审核通过率。

1.1 项目初始化

  1. 使用HBuilderX创建项目,选择「uni-app项目」,根据技术栈选择Vue2/Vue3模板,推荐使用官方uni-ui模板,小程序端原生兼容性最优。
  2. 项目创建后,优先完成 manifest.json 小程序平台配置与 pages.json 路由规则配置,否则无法正常运行到微信开发者工具。

1.2 manifest.json 小程序端全量核心配置

该文件为小程序应用级配置,直接关联微信公众平台的AppID、编译规则、权限配置,核心配置项全量详解如下:

1.2.1 基础配置

配置项 核心说明 硬性规则
应用名称 小程序对外显示名称,需与微信公众平台注册名称一致 控制在4-16个字符,避免特殊字符、违规词汇
应用版本名称 对外显示的版本号,如1.0.0 遵循语义化版本规范,每次上传代码必须递增,与微信公众平台版本管理对应
应用版本号 对内整数版本号,如1、2、3 每次上传代码必须严格递增,用于版本迭代区分
微信小程序AppID 小程序唯一身份标识 必须从微信公众平台「开发管理-开发设置」中获取,未填写无法真机调试、上传代码

1.2.2 微信小程序专属编译配置

配置项 核心作用 最佳实践
ES6转ES5 开启后将ES6+语法转为ES5,兼容低版本微信客户端 强制开启,否则低版本基础库会出现语法报错
增强编译 支持async/await、可选链等新语法,扩展Polyfill 强制开启,解决新语法兼容问题
上传代码时自动压缩 自动压缩混淆代码、图片、JSON文件,减小包体积 强制开启,是包体积瘦身的基础配置
上传时自动清理console.log 剔除调试日志,减小包体积,避免泄露敏感信息 正式发布时强制开启,开发环境可关闭
忽略未使用的资源 自动剔除未引用的静态资源、组件、代码,减小包体积 强制开启,解决冗余资源占用包体积问题
代码按需注入 对应微信小程序lazyCodeLoading配置 开启并设置为requiredComponents,大幅提升小程序启动速度
调试时不校验合法域名、web-view业务域名、TLS版本 开发环境关闭域名校验 开发时开启,正式发布必须关闭,且所有域名必须在微信公众平台配置白名单

1.2.3 多小程序平台兼容配置

如需兼容支付宝、抖音、百度等小程序平台,需在对应平台配置栏填写对应平台的AppID,开启对应平台的编译配置,后续通过条件编译处理平台差异。

1.3 pages.json 小程序端全量核心配置

pages.json 是小程序的全局路由与窗口配置文件,控制页面路由、分包规则、导航栏、tabBar、下拉刷新等核心能力,所有页面必须在此注册,否则无法访问,小程序端专属规则全量详解如下:

1.3.1 全局配置 globalStyle

控制所有页面的默认窗口表现,页面级配置优先级高于全局配置,核心配置项:

配置项 核心作用 小程序端专属规则
navigationBarBackgroundColor 导航栏背景色 仅支持十六进制颜色值,不支持rgba、渐变、英文颜色名
navigationBarTextStyle 导航栏标题文字颜色 仅支持black/white两个值,无其他选项
navigationBarTitleText 全局默认导航栏标题 页面级配置会覆盖该值
navigationStyle 导航栏样式 设置为custom可隐藏原生导航栏,实现自定义导航栏,仅微信基础库2.4.3+支持
enablePullDownRefresh 全局开启下拉刷新 默认关闭,开启后所有页面均可触发下拉刷新,推荐页面级单独配置
backgroundColor 窗口背景色(下拉露底区域) 仅支持十六进制颜色值
backgroundTextStyle 下拉刷新loading样式 仅支持dark/light两个值
onReachBottomDistance 页面上拉触底事件触发距离 单位px,默认50px,数值越小触发越早

1.3.2 页面路由配置 pages

数组格式,每个元素对应一个页面,数组第一项为小程序启动首页,单页面配置结构如下:

1
2
3
4
5
6
7
8
9
10
{
"path": "pages/detail/index",
"style": {
"navigationBarTitleText": "商品详情",
"enablePullDownRefresh": true,
"onReachBottomDistance": 100,
"disableScroll": false,
"backgroundColor": "#f5f5f5"
}
}

小程序端硬性规则:页面路径无需写.vue后缀,必须放在pages目录下(分包页面除外),路径大小写敏感,错误路径会导致页面无法访问。

1.3.3 分包加载配置 subPackages(小程序核心硬性要求)

微信小程序官方硬性限制:主包(含tabBar页面)体积≤2MB,主包+所有分包总大小≤20MB,超过限制无法上传代码、提交审核,必须通过分包拆分非核心页面,是小程序开发的重中之重。

核心配置结构
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
{
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/entry/entry",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"disableScroll": true,
"app-plus": {
"bounce": "none"
},
"mp-alipay": {
"allowsBounceVertical": "NO"
}
}
},
{
"path": "pages/tabBar/home/home",
"style": {
"navigationBarTitleText": "亿童幼师云",
// #ifndef MP-WEIXIN
"enablePullDownRefresh": true,
// #endif
"navigationStyle": "custom"
}

}, {
"path": "pages/tabBar/product/product",
"style": {
"navigationBarTitleText": "产品",
// #ifndef MP-WEIXIN
"enablePullDownRefresh": true,
// #endif
"navigationStyle": "custom"
}

}, {
"path": "pages/tabBar/mine/mine",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}

}, {
"path": "pages/web/web",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}

}, {
"path": "pages/live/live",
"style": {
"navigationBarTitleText": "直播预告",
"enablePullDownRefresh": true,
"onReachBottomDistance": 100

}

}, {
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索",
"enablePullDownRefresh": false
}

},
{
"path": "pages/resource/resource",
"style": {
"navigationBarTitleText": "精品资源",
"enablePullDownRefresh": false
}
},
{
"path": "pages/transition/transition",
"style": {
"navigationBarTitleText": "咨询客服",
"enablePullDownRefresh": false
}
},
{
"path": "pages/poster/poster",
"style": {
"navigationBarTitleText": "加入服务群",
"enablePullDownRefresh": false
}
}
],
"subPackages": [{
"root": "pages-course",
"pages": [{
"path": "course_detail/course_detail",
"style": {
"navigationBarTitleText": "",
"disableScroll": true,
"mp-alipay": {
"allowsBounceVertical": "NO",
"enableScrollBar": "NO"
}
}
}, {
"path": "course_detail/new_product_video_detail",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"mp-alipay": {
"allowsBounceVertical": "NO",
"enableScrollBar": "NO"
}
}
}, {
"path": "course_detail/product_video_player",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"mp-alipay": {
"allowsBounceVertical": "NO",
"enableScrollBar": "NO"
}
}
}, {
"path": "course_detail/file_detail",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
}, {
"path": "course_detail/media_player",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false,
"mp-alipay": {
"allowsBounceVertical": "NO",
"enableScrollBar": "NO"
}
}
},
{
"path": "voice_love/voice_love",
"style": {
"navigationBarTitleText": "声爱幼教",
"enablePullDownRefresh": false
}

},
{
"path": "group_course/group_course",
"style": {
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}

}

]
},
{
"root": "pages-login",
"pages": [{
"path": "login/login",
"style": {
"navigationBarTitleText": "登录",
"enablePullDownRefresh": false
}
}, {
"path": "login/loginByPhone",
"style": {
"navigationBarTitleText": "手机号登录",
"enablePullDownRefresh": false,
"mp-weixin": {
"usingComponents": {
"t-captcha": "plugin://myPlugin/t-captcha"
}
}
}
}]
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "亿童幼师云",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#ffffff",
"mp-alipay": {
"allowsBounceVertical": "YES"
}
},
"uniIdRouter": {},
"tabBar": {
"color": "#666666",
"selectedColor": "#E72728",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"list": [{
"pagePath": "pages/tabBar/home/home",
"iconPath": "static/images/tabBar/tab_icon_home_nor.png",
"selectedIconPath": "static/images/tabBar/tab_icon_home_pes.png",
"text": "首页"
},
{
"pagePath": "pages/tabBar/product/product",
"iconPath": "static/images/tabBar/tab_icon_product_nor.png",
"selectedIconPath": "static/images/tabBar/tab_icon_product_pes.png",
"text": "产品"
}, {
"pagePath": "pages/tabBar/mine/mine",
"iconPath": "static/images/tabBar/tab_icon_mine_nor.png",
"selectedIconPath": "static/images/tabBar/tab_icon_mine_pes.png",
"text": "我的"
}
]
}
}
核心规则详解
  1. root:分包根目录名称,不能与其他分包重名,不能嵌套分包;分包内的所有页面、资源必须放在该根目录下。
  2. pages:分包内的页面路径,规则与主包pages一致。
  3. independent:是否为独立分包,设置为true时,该分包不依赖主包,可独立启动,无需下载主包即可打开,适合活动页、推广页、H5跳转落地页;独立分包不能使用主包的公共组件、公共资源、全局变量,需单独封装。
  4. 硬性规则:tabBar页面必须放在主包内,不能放入分包;主包只能引用主包内的资源,分包可引用主包和自身分包内的资源,分包之间不能互相引用资源。

1.3.4 分包预加载配置 preloadRule

用于配置进入某个页面时,自动预下载指定分包,解决分包页面打开时的加载延迟,提升用户体验,配置结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"preloadRule": {
"pages/entry/entry": {
"network": "all",
"packages": ["pages-login"]
},
"pages/tabBar/home/home": {
"network": "wifi",
"packages": ["pages-course"]
}
}
}
配置项 核心说明
key值 触发预加载的页面路径,必须是主包内已注册的页面
network 预加载的网络环境,可选wifi(仅wifi环境预加载)/all(所有网络环境均预加载)
packages 要预加载的分包root名称数组,可同时预加载多个分包

1.3.5 tabBar底部导航配置

小程序端tabBar硬性规则:最少2个、最多5个tab;tab对应的页面必须在主包pages数组中,不能放在分包;tabBar页面切换时,仅触发onShow/onHide,不会触发onLoad/onUnload

pages.json中完整配置结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"tabBar": {
"color": "#666666",
"selectedColor": "#E72728",
"borderStyle": "black",
"backgroundColor": "#FFFFFF",
"list": [{
"pagePath": "pages/tabBar/home/home",
"iconPath": "static/images/tabBar/tab_icon_home_nor.png",
"selectedIconPath": "static/images/tabBar/tab_icon_home_pes.png",
"text": "首页"
},
{
"pagePath": "pages/tabBar/product/product",
"iconPath": "static/images/tabBar/tab_icon_product_nor.png",
"selectedIconPath": "static/images/tabBar/tab_icon_product_pes.png",
"text": "产品"
}, {
"pagePath": "pages/tabBar/mine/mine",
"iconPath": "static/images/tabBar/tab_icon_mine_nor.png",
"selectedIconPath": "static/images/tabBar/tab_icon_mine_pes.png",
"text": "我的"
}
]
}
配置项 小程序端专属规则
color tab默认文字颜色,仅支持十六进制颜色值
selectedColor tab选中时的文字颜色,仅支持十六进制颜色值
borderStyle tabBar上边框颜色,仅支持black/white两个值
position tabBar位置,仅支持bottom/top,设置为top时不显示图标
iconPath/selectedIconPath 图标路径,仅支持本地图片,不支持网络图片;图片大小限制40KB,尺寸建议81px*81px

1.3.6 小程序端专属权限与能力配置

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
{
// 权限声明,申请系统权限必须在此配置用途描述,否则无法调用对应API
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于附近门店展示与导航服务"
},
"scope.userLocationBackground": {
"desc": "需要持续获取你的位置信息,用于后台导航服务"
},
"scope.record": {
"desc": "需要使用你的麦克风,用于语音录制与识别"
},
"scope.camera": {
"desc": "需要使用你的相机,用于扫码、拍照与视频拍摄"
}
},
// 后台运行能力配置,如音频后台播放
"requiredBackgroundModes": ["audio"],
// 多线程worker配置,用于处理高耗时逻辑,避免阻塞主线程
"workers": "workers",
// 站点地图配置,影响微信搜索收录
"sitemapLocation": "sitemap.json",
// 用于配置小程序可跳转的其他小程序白名单
"navigateToMiniProgramAppIdList": ["wx1234567890abcdef"]
}

二、小程序端全量生命周期体系(100%全覆盖)

小程序端生命周期分为应用生命周期页面生命周期组件生命周期三大类,每个钩子的触发时机、接收参数、使用场景、小程序专属注意事项全量详解如下,完全适配UniApp开发规范。

2.1 应用生命周期(仅在App.vue中生效,全局唯一)

控制整个小程序的启动、前后台切换、全局错误捕获,全局仅在App.vue中生效,是小程序全局初始化的唯一入口。

生命周期钩子 触发时机 接收参数 核心使用场景 小程序端专属注意事项
onLaunch 小程序冷启动初始化完成时触发,全局仅触发1次 options对象,核心字段:
- scene:微信小程序场景值(如1001=主界面下拉进入、1007=分享卡片进入、1011=扫码进入)
- path:启动页面路径
- query:启动页面参数
- shareTicket:群分享票据,用于获取群分享信息
- referrerInfo:来源小程序/公众号的AppID
1. 全局初始化:检查用户登录状态、token有效性
2. 全局配置挂载:接口域名、全局常量、公共方法
3. 处理扫码、分享、外部链接进入的启动逻辑
4. 注册全局错误监听、小程序更新监听
5. 统计小程序启动次数、渠道来源
1. 仅冷启动触发,热启动(后台切回)不触发
2. 此时页面未渲染,不能操作DOM、不能调用页面级方法
3. 小程序被销毁后重新打开才会再次触发
4. 禁止写入高耗时同步逻辑,否则会导致小程序启动白屏、卡顿
onShow 小程序启动,或从后台切入前台显示时触发,每次切前台都会触发 onLaunchoptions参数 1. 刷新用户登录状态、token续期
2. 重启暂停的音频/视频播放
3. 同步全局用户信息、刷新实时数据
4. 检查小程序版本更新
5. 统计小程序前台停留时长
1. 冷启动时,在onLaunch之后触发
2. 热启动仅触发onShow,不触发onLaunch
3. 锁屏后解锁、从其他App/公众号切回小程序都会触发,触发频率高,禁止写入高耗时同步逻辑
onHide 小程序从前台切入后台时触发(按Home键、切换App/小程序、关闭小程序) 无参数 1. 暂停音频/视频播放,避免后台耗电
2. 保存用户临时操作数据、页面状态
3. 断开非必要的websocket、定位服务
4. 统计小程序后台停留时长
1. 微信对小程序后台运行时间有限制,禁止执行耗时过长操作
2. 页面切换不会触发,仅小程序整体进入后台触发
onError 小程序发生脚本错误、API调用失败时触发 error:错误信息完整字符串 1. 全局错误捕获与上报
2. 友好错误提示,避免小程序白屏
3. 记录错误日志,用于线上问题排查
1. 同步代码错误会触发,异步Promise reject需额外监听onUnhandledRejection
2. 禁止在此钩子内抛出新错误,导致死循环
onPageNotFound 小程序打开的页面路径不存在时触发 对象参数,核心字段:
- path:无效页面路径
- query:页面参数
- isEntryPage:是否为启动页
1. 页面不存在的兜底跳转,如跳转到首页/404页
2. 无效路径上报,用于修复下线页面的跳转问题
3. 兼容旧版本下线页面的重定向
1. 重定向必须使用uni.reLaunch/uni.redirectTo,禁止用uni.navigateTo
2. 扫码、外部链接、分享卡片打开无效页面均会触发
onUnhandledRejection 小程序出现未处理的Promise reject时触发 对象参数,核心字段:
- reason:拒绝原因
- promise:对应Promise对象
全局捕获异步接口、异步操作的错误,统一上报和友好提示 仅微信基础库2.10.0以上版本支持,低版本需做兼容降级
onThemeChange 小程序系统深色/浅色主题切换时触发 对象参数,包含theme(dark/light) 监听系统主题变化,动态切换小程序深色/浅色模式 仅微信基础库2.11.0以上版本支持

App.vue 完整代码示例(小程序端专属)

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
<script>
export default {
// 小程序冷启动初始化,仅触发1次
onLaunch(options) {
console.log('小程序冷启动,启动参数:', options)
// 1. 登录状态校验
const token = uni.getStorageSync('token')
if (!token) {
// 未登录,跳转到登录页,禁止使用navigateTo
uni.reLaunch({ url: '/pages/login/login' })
return
}
// 2. 记录启动场景值,用于渠道统计
uni.setStorageSync('launch_scene', options.scene)
// 3. 检查小程序版本更新
this.checkMiniProgramUpdate()
},

// 小程序切前台触发,每次都会执行
onShow(options) {
console.log('小程序进入前台')
// token续期逻辑
const tokenExpire = uni.getStorageSync('token_expire')
if (tokenExpire && Date.now() > tokenExpire - 3600 * 1000) {
this.refreshToken()
}
},

// 小程序切后台触发
onHide() {
console.log('小程序进入后台')
// 暂停全局背景音频播放
const backgroundAudioManager = wx.getBackgroundAudioManager()
if (!backgroundAudioManager.paused) {
backgroundAudioManager.pause()
}
// 保存用户最后操作时间
uni.setStorageSync('last_operation_time', Date.now())
},

// 全局错误捕获
onError(error) {
console.error('小程序全局错误:', error)
// 错误信息上报到服务端
uni.request({
url: 'https://api.example.com/error/report',
method: 'POST',
data: {
error_msg: error,
app_version: '1.0.0',
platform: 'mp-weixin',
scene: uni.getStorageSync('launch_scene')
}
})
},

// 页面不存在兜底处理
onPageNotFound(res) {
console.error('访问的页面不存在:', res.path)
uni.showToast({ title: '页面不存在,即将返回首页', icon: 'none' })
setTimeout(() => {
uni.reLaunch({ url: '/pages/index/index' })
}, 1500)
},

// 未处理的Promise错误捕获
onUnhandledRejection(res) {
console.error('未处理的异步错误:', res.reason)
// 错误上报逻辑
},

methods: {
// 检查小程序版本更新
checkMiniProgramUpdate() {
// 微信小程序更新管理器,仅小程序端支持
const updateManager = wx.getUpdateManager()

// 检查更新结果
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('发现小程序新版本')
}
})

// 新版本下载完成
updateManager.onUpdateReady(() => {
uni.showModal({
title: '版本更新',
content: '新版本已准备好,是否重启小程序?',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate() // 重启小程序应用新版本
}
}
})
})

// 新版本下载失败
updateManager.onUpdateFailed(() => {
console.error('小程序新版本下载失败')
})
},

// 刷新token
async refreshToken() {
// token续期接口请求逻辑
}
}
}
</script>

<style>
/* 小程序全局样式 */
page {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'PingFang SC', sans-serif;
box-sizing: border-box;
}
/* 通用样式重置 */
view, text, image {
box-sizing: border-box;
}
</style>

2.2 页面生命周期(每个页面.vue文件中生效,页面级专属)

控制单个页面的加载、渲染、显示、隐藏、卸载、用户交互,是小程序开发中最常用的生命周期,包含微信小程序专属社交、交互钩子,全量详解如下:

生命周期钩子 触发时机 接收参数 核心使用场景 小程序端专属注意事项
onLoad 页面加载时触发,每个页面仅触发1次 options:接收上一页通过URL传递的参数(如/pages/detail?id=123,可通过options.id获取) 1. 获取页面入参,初始化核心数据
2. 发起页面初始数据接口请求
3. 初始化定时器、事件监听
1. 仅触发1次,页面卸载前不会重复触发
2. 页面返回再进入,不会触发onLoad,仅触发onShow
3. 超长参数禁止通过URL传递,改用全局存储/事件总线
4. 此时DOM未渲染,不能获取节点尺寸、操作DOM
onShow 页面显示时触发,每次切入前台都会触发(首次加载、返回当前页、小程序切前台) 无参数 1. 刷新页面数据,如详情页返回后刷新列表状态
2. 重启暂停的动画、音频
3. 更新实时数据(倒计时、用户信息)
1. tabBar页面切换时,仅触发onShow/onHide,不触发onLoad/onUnload
2. 触发频率高,禁止写入高耗时同步逻辑
onReady 页面初次渲染完成时触发,每个页面仅触发1次 无参数 1. 获取DOM节点尺寸、位置信息
2. 初始化canvas、地图、视频等原生组件
3. 执行依赖DOM渲染完成的动画
4. 自定义导航栏场景下,获取微信胶囊按钮位置
1. 仅触发1次,在onLoad/onShow之后触发
2. 必须配合this.$nextTick使用,确保节点100%渲染完成
3. 小程序端仅在此钩子触发后,才能通过uni.createSelectorQuery()获取节点信息
onHide 页面隐藏时触发(跳转到下一页、tab切换、小程序切后台) 无参数 1. 暂停动画、定时器、音频播放
2. 保存临时输入内容、页面滚动位置
3. 解绑事件监听,避免内存泄漏
页面跳转时,先触发当前页onHide,再触发下一页onLoad
onUnload 页面卸载时触发,每个页面仅触发1次 无参数 1. 销毁所有定时器、延时器
2. 解绑所有事件监听(事件总线、websocket)
3. 销毁canvas、地图、视频等占用内存的组件
4. 取消未完成的接口请求
1. 触发场景:uni.redirectTouni.navigateBack关闭当前页、uni.reLaunch关闭所有页面
2. uni.navigateTo不会触发当前页onUnload,仅触发onHide
3. 必须在此清理所有副作用,否则会导致小程序内存泄漏、卡顿、闪退
4. 小程序页面栈最多10层,超出后navigateTo无法打开新页面
onPullDownRefresh 页面下拉刷新时触发 无参数 1. 重新请求页面数据,刷新列表
2. 重置页面状态、分页参数
1. 必须在pages.jsonglobalStyle或页面style中配置enablePullDownRefresh: true才能触发
2. 刷新完成后必须调用uni.stopPullDownRefresh()停止刷新动画,否则会一直处于刷新状态
3. 自定义导航栏需额外处理下拉刷新的兼容
onReachBottom 页面滚动到底部时触发 无参数 列表分页加载,触发下一页数据请求 1. 触发距离可通过onReachBottomDistance配置,默认50px
2. 页面高度不足1屏时不会触发
3. scroll-view的滚动不会触发此钩子,需用scrolltolower事件
4. 必须做节流和加载状态锁,避免重复请求
onShareAppMessage 页面右上角「分享给朋友」按钮点击时触发,必须return分享配置对象 对象参数,核心字段:
- from:触发来源,button=页面内分享按钮触发、menu=右上角菜单触发
- target:触发分享的button组件实例
- webViewUrl:web-view组件的当前链接
1. 自定义分享标题、路径、封面图
2. 统计分享次数、分享渠道
3. 配置分享参数,实现分享裂变
1. 只有定义此钩子,右上角菜单才会显示「分享给朋友」按钮
2. 微信已回收分享成功/失败回调,无法监听用户是否真的分享成功
3. 分享路径必须是小程序已注册的页面路径,可携带query参数
4. 分享图片大小限制2MB,比例推荐5:4
onShareTimeline 页面右上角「分享到朋友圈」按钮点击时触发,必须return分享配置对象 无参数 自定义分享到朋友圈的标题、封面图、查询参数 1. 仅微信基础库2.11.0以上版本支持
2. 只有定义此钩子,右上角菜单才会显示「分享到朋友圈」按钮
3. 无法自定义分享路径,仅能通过query传递参数,打开后会进入单页模式
onAddToFavorites 页面右上角「收藏」按钮点击时触发,必须return收藏配置对象 对象参数,包含webViewUrl:当前页面链接 自定义收藏的标题、封面图、收藏内容、query参数 仅微信基础库2.10.0以上版本支持
onPageScroll 页面滚动时实时触发 对象参数,包含scrollTop:页面垂直滚动距离,单位px 1. 滚动时动态修改导航栏透明度、标题样式
2. 控制回到顶部按钮的显示/隐藏
3. 实现滚动动画效果
1. 触发频率极高,必须做节流处理(推荐100ms以上)
2. 禁止在此钩子里做频繁的setData操作,会严重占用小程序主线程,导致卡顿、掉帧
onTabItemTap tabBar页面点击tab按钮时触发 对象参数,核心字段:
- index:tab序号,从0开始
- pagePath:tab对应的页面路径
- text:tab的文字
1. 点击tab时刷新页面数据
2. 拦截tab点击,如未登录时跳转到登录页
3. 统计tab点击次数
1. 仅在tabBar页面中生效,非tabBar页面不触发
2. 点击当前已选中的tab也会触发
onResize 小程序窗口尺寸变化时触发(如横竖屏切换) 对象参数,包含size:窗口尺寸信息,windowWidth/windowHeight 适配横竖屏切换,动态修改页面布局 仅支持横竖屏切换的页面触发

页面生命周期完整代码示例(小程序端专属)

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
<template>
<view class="list-container">
<!-- 分享按钮,必须设置open-type="share" -->
<button open-type="share" class="share-btn">分享给好友</button>
<!-- 列表内容 -->
<view class="list-item" v-for="item in list" :key="item.id">
<text>{{ item.title }}</text>
</view>
<!-- 加载状态 -->
<view class="loading-text" v-if="isLoading">加载中...</view>
<view class="no-more" v-if="!hasMore && list.length > 0">没有更多数据了</view>
</view>
</template>

<script>
export default {
data() {
return {
id: '',
list: [],
page: 1,
pageSize: 10,
isLoading: false,
hasMore: true,
scrollTimer: null
}
},
// 页面加载,仅触发1次
onLoad(options) {
console.log('页面加载,入参:', options)
this.id = options.id
this.getListData()
},
// 页面显示,每次切前台都触发
onShow() {
console.log('页面显示')
},
// 页面渲染完成,仅触发1次
onReady() {
console.log('页面渲染完成')
// 获取DOM节点信息,必须在onReady+this.$nextTick中执行
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this)
query.select('.list-container').boundingClientRect((rect) => {
console.log('容器尺寸:', rect)
}).exec()
})
},
// 页面隐藏
onHide() {
console.log('页面隐藏')
},
// 页面卸载
onUnload() {
console.log('页面卸载')
// 清理所有副作用,避免内存泄漏
clearTimeout(this.scrollTimer)
uni.$off('refreshList')
},
// 下拉刷新
onPullDownRefresh() {
console.log('下拉刷新触发')
this.page = 1
this.hasMore = true
this.getListData().finally(() => {
// 必须调用,停止刷新动画
uni.stopPullDownRefresh()
})
},
// 滚动到底部,加载更多
onReachBottom() {
console.log('滚动到底部触发')
// 加锁,避免重复请求
if (!this.isLoading && this.hasMore) {
this.page++
this.getListData()
}
},
// 页面滚动
onPageScroll(e) {
// 节流处理,避免频繁触发
if (this.scrollTimer) clearTimeout(this.scrollTimer)
this.scrollTimer = setTimeout(() => {
console.log('当前滚动距离:', e.scrollTop)
}, 100)
},
// 分享给朋友,必须return配置对象
onShareAppMessage(res) {
console.log('分享触发来源:', res.from)
return {
title: '这是分享标题',
path: `/pages/list?id=${this.id}`,
imageUrl: 'https://cdn.example.com/share-cover.jpg'
}
},
// 分享到朋友圈,必须return配置对象
onShareTimeline() {
return {
title: '这是分享到朋友圈的标题',
imageUrl: 'https://cdn.example.com/share-cover.jpg',
query: `id=${this.id}`
}
},
// 收藏功能,必须return配置对象
onAddToFavorites() {
return {
title: '这是收藏标题',
imageUrl: 'https://cdn.example.com/favorite-cover.jpg',
query: `id=${this.id}`
}
},
// tab点击触发,仅tabBar页面生效
onTabItemTap(e) {
console.log('tab点击:', e)
// 点击当前tab时刷新列表
if (e.index === 0) {
this.page = 1
this.getListData()
}
},
methods: {
async getListData() {
this.isLoading = true
try {
const res = await uni.request({
url: 'https://api.example.com/goods/list',
method: 'GET',
data: {
id: this.id,
page: this.page,
pageSize: this.pageSize
}
})
const { code, data } = res.data
if (code === 200) {
if (this.page === 1) {
this.list = data.list
} else {
this.list = [...this.list, ...data.list]
}
// 判断是否还有更多数据
this.hasMore = this.list.length < data.total
} else {
uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
}
} catch (err) {
console.error('列表请求失败:', err)
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' })
} finally {
this.isLoading = false
}
}
}
}
</script>

<style scoped>
.list-container {
padding: 30rpx;
min-height: 100vh;
}
.share-btn {
margin-bottom: 30rpx;
}
.list-item {
padding: 20rpx;
margin-bottom: 20rpx;
background: #fff;
border-radius: 12rpx;
}
.loading-text, .no-more {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 30rpx 0;
}
</style>

2.3 组件生命周期(自定义组件中生效,Vue标准+小程序专属)

小程序端组件生命周期优先遵循Vue标准生命周期,同时兼容微信小程序原生组件的lifetimes生命周期,全量钩子详解如下:

生命周期钩子 类型 触发时机 核心使用场景 小程序端注意事项
beforeCreate Vue标准 组件实例刚创建,数据观测未初始化 极少使用,仅能获取this,无法访问data/methods 触发时机极早,无法访问组件数据、props
created Vue标准 组件实例创建完成,data/methods/props已初始化,DOM未生成 1. 发起组件所需数据的接口请求
2. 初始化组件内部状态
3. 注册事件监听
此时组件未挂载,不能操作DOM节点、获取组件尺寸
beforeMount Vue标准 组件挂载开始前,模板编译完成 极少使用 触发在created之后,mounted之前
mounted Vue标准 组件挂载到DOM完成 1. 获取组件内DOM节点信息
2. 初始化canvas、动画等依赖DOM的逻辑
1. 小程序端mounted触发时,未必能100%获取节点,必须配合this.$nextTick
2. v-if控制的组件,每次显示都会重新触发mounted
3. 小程序端必须使用uni.createSelectorQuery().in(this)才能获取组件内的节点
beforeUpdate Vue标准 组件数据更新,DOM重渲染前触发 监听数据变化,获取更新前的DOM状态 频繁触发,慎用,避免影响小程序渲染性能
updated Vue标准 组件数据更新,DOM重渲染完成后触发 数据更新后,操作更新后的DOM节点 禁止在此钩子里修改组件data数据,否则会触发无限更新循环
beforeDestroy Vue标准 组件实例销毁前触发,实例完全可用 1. 清理定时器、延时器
2. 解绑事件监听
3. 取消未完成的接口请求
必须在此清理所有副作用,避免小程序内存泄漏
destroyed Vue标准 组件实例销毁完成后触发 最终清理逻辑,销毁占用内存的对象 仅触发1次
attached 小程序专属 组件实例进入页面节点树时触发 小程序原生组件初始化逻辑,类似created 1. 必须用#ifdef MP-WEIXIN条件编译包裹
2. 必须写在lifetimes节点内,不能直接写在组件根级别
detached 小程序专属 组件实例被移出页面节点树时触发 小程序原生组件销毁逻辑,类似beforeDestroy 同上,必须条件编译包裹
ready 小程序专属 组件在视图层布局完成后触发 类似mounted,获取DOM节点信息 同上,需基础库2.2.3+支持

组件生命周期完整代码示例(小程序端专属)

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
<template>
<view class="custom-card">
<text class="title">{{ title }}</text>
<view class="content">{{ content }}</view>
</view>
</template>

<script>
export default {
// 父组件传值
props: {
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
}
},
data() {
return {
list: []
}
},
// Vue标准生命周期
beforeCreate() {
console.log('组件实例创建前')
},
created() {
console.log('组件实例创建完成')
this.getComponentData()
},
beforeMount() {
console.log('组件挂载前')
},
mounted() {
console.log('组件挂载完成')
// 获取组件内DOM节点,必须配合in(this)
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this)
query.select('.custom-card').boundingClientRect((rect) => {
console.log('组件尺寸:', rect)
}).exec()
})
},
beforeUpdate() {
console.log('组件更新前')
},
updated() {
console.log('组件更新完成')
},
beforeDestroy() {
console.log('组件销毁前')
// 清理副作用
clearInterval(this.timer)
uni.$off('refreshCard')
},
destroyed() {
console.log('组件销毁完成')
},
// 小程序专属生命周期,必须条件编译包裹
// #ifdef MP-WEIXIN
lifetimes: {
attached() {
console.log('小程序组件进入页面节点树')
},
detached() {
console.log('小程序组件移出页面节点树')
},
ready() {
console.log('小程序组件布局完成')
}
}
// #endif
,
methods: {
async getComponentData() {
const res = await uni.request({
url: 'https://api.example.com/card/data'
})
this.list = res.data.data || []
}
}
}
</script>

<style scoped>
.custom-card {
padding: 30rpx;
border-radius: 16rpx;
background: #fff;
margin-bottom: 20rpx;
}
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.content {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
</style>

三、小程序端条件编译 全场景实战详解

条件编译是UniApp解决多小程序平台差异的核心方案,通过特殊注释语法,让代码在不同平台编译时仅包含对应端的代码,避免冗余代码、兼容报错,小程序端全场景用法全量详解如下。

3.1 基础语法与平台标识

核心语法

  • #ifdef 平台标识:如果当前编译平台匹配该标识,编译这段代码
  • #ifndef 平台标识:如果当前编译平台不匹配该标识,编译这段代码
  • #endif:条件编译结束标记

小程序端专属平台标识

平台标识 对应平台
MP-WEIXIN 微信小程序
MP-ALIPAY 支付宝小程序
MP-TOUTIAO 抖音/头条小程序
MP-BAIDU 百度小程序
MP-QQ QQ小程序
MP-KUAISHOU 快手小程序
MP 所有小程序平台(不含H5、App端)

3.2 模板(template)中的条件编译

用于控制不同小程序平台显示不同的组件、内容、功能入口,是最常用的场景,完整示例:

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
<template>
<view class="container">
<!-- 所有端通用内容 -->
<view class="common-content">跨平台通用内容</view>

<!-- 仅微信小程序显示的广告组件 -->
<!-- #ifdef MP-WEIXIN -->
<ad unit-id="adunit-xxxxxxxxx" class="ad-box"></ad>
<!-- #endif -->

<!-- 仅支付宝小程序显示的芝麻信用组件 -->
<!-- #ifdef MP-ALIPAY -->
<view class="zhima-box">
<text>支付宝芝麻信用专属内容</text>
</view>
<!-- #endif -->

<!-- 仅抖音小程序显示的抖音授权组件 -->
<!-- #ifdef MP-TOUTIAO -->
<button open-type="getUserInfo" class="douyin-auth-btn">抖音一键登录</button>
<!-- #endif -->

<!-- 所有小程序平台都显示,H5和App端不显示 -->
<!-- #ifdef MP -->
<view class="mp-common">所有小程序平台通用内容</view>
<!-- #endif -->

<!-- 非微信小程序平台显示 -->
<!-- #ifndef MP-WEIXIN -->
<view class="no-wechat">非微信小程序平台显示内容</view>
<!-- #endif -->
</view>
</template>

3.3 脚本(script)中的条件编译

用于处理不同小程序平台的API差异、授权逻辑、业务处理,避免不兼容API导致的报错,全场景示例:

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
export default {
data() {
return {
userInfo: null,
code: ''
}
},
onLoad() {
// 所有小程序平台通用:获取平台code
this.getPlatformCode()
},
methods: {
// 获取各平台登录code
getPlatformCode() {
// 微信小程序专属逻辑
// #ifdef MP-WEIXIN
wx.login({
success: (res) => {
this.code = res.code
console.log('微信code:', res.code)
}
})
// #endif

// 支付宝小程序专属逻辑
// #ifdef MP-ALIPAY
my.getAuthCode({
scopes: 'auth_base',
success: (res) => {
this.code = res.authCode
console.log('支付宝code:', res.authCode)
}
})
// #endif

// 抖音小程序专属逻辑
// #ifdef MP-TOUTIAO
tt.login({
success: (res) => {
this.code = res.code
console.log('抖音code:', res.code)
}
})
// #endif
},

// 获取用户信息
getUserInfo() {
// 微信小程序:必须用户主动点击按钮触发,使用getUserProfile
// #ifdef MP-WEIXIN
wx.getUserProfile({
desc: '用于完善会员资料',
success: (res) => {
this.userInfo = res.userInfo
uni.setStorageSync('userInfo', res.userInfo)
},
fail: () => {
uni.showToast({ title: '用户拒绝授权', icon: 'none' })
}
})
// #endif

// 支付宝小程序
// #ifdef MP-ALIPAY
my.getOpenUserInfo({
success: (res) => {
const userInfo = JSON.parse(res.response).response
this.userInfo = userInfo
}
})
// #endif

// 非微信小程序通用降级逻辑
// #ifndef MP-WEIXIN
uni.showToast({ title: '请手动完善用户信息', icon: 'none' })
// #endif
},

// 支付逻辑
handlePay(orderId) {
// 微信小程序支付
// #ifdef MP-WEIXIN
uni.request({
url: 'https://api.example.com/pay/wechat',
method: 'POST',
data: { order_id: orderId }
}).then((res) => {
const { data } = res
uni.requestPayment({
provider: 'wxpay',
timeStamp: data.timeStamp,
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign,
success: () => {
uni.showToast({ title: '支付成功' })
uni.redirectTo({ url: '/pages/order/success' })
},
fail: () => {
uni.showToast({ title: '支付失败', icon: 'none' })
}
})
})
// #endif

// 支付宝小程序支付
// #ifdef MP-ALIPAY
uni.request({
url: 'https://api.example.com/pay/alipay',
method: 'POST',
data: { order_id: orderId }
}).then((res) => {
uni.requestPayment({
provider: 'alipay',
orderInfo: res.data.orderStr,
success: () => {
uni.showToast({ title: '支付成功' })
}
})
})
// #endif
}
}
}

3.4 样式(style)中的条件编译

用于处理不同小程序平台的样式适配、导航栏适配、UI差异,示例:

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
.container {
width: 100%;
padding: 30rpx;
box-sizing: border-box;
}

/* 微信小程序专属样式:适配右上角胶囊按钮 */
/* #ifdef MP-WEIXIN */
.container {
padding-top: 120rpx;
}
/* #endif */

/* 支付宝小程序专属样式 */
/* #ifdef MP-ALIPAY */
.container {
padding-top: 100rpx;
background-color: #f6f6f6;
}
/* #endif */

/* 抖音小程序专属样式 */
/* #ifdef MP-TOUTIAO */
.container {
padding-top: 88rpx;
}
/* #endif */

/* 所有小程序通用样式 */
/* #ifdef MP */
.container {
font-size: 28rpx;
line-height: 1.5;
}
/* #endif */

3.5 配置文件(pages.json)中的条件编译

用于处理不同小程序平台的路由、tabBar、导航栏、权限配置差异,示例:

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
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
// #ifdef MP-WEIXIN
"navigationBarBackgroundColor": "#07c160",
"navigationBarTextStyle": "white",
// #endif
// #ifdef MP-ALIPAY
"navigationBarBackgroundColor": "#1677ff",
"navigationBarTextStyle": "white",
// #endif
// #ifdef MP-TOUTIAO
"navigationBarBackgroundColor": "#FE2C55",
"navigationBarTextStyle": "white",
// #endif
"enablePullDownRefresh": true
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"tabBar": {
"color": "#999999",
// #ifdef MP-WEIXIN
"selectedColor": "#07c160",
"backgroundColor": "#ffffff",
// #endif
// #ifdef MP-ALIPAY
"selectedColor": "#1677ff",
"backgroundColor": "#fafafa",
// #endif
// #ifdef MP-TOUTIAO
"selectedColor": "#FE2C55",
"backgroundColor": "#ffffff",
// #endif
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tab/home.png",
"selectedIconPath": "static/tab/home-active.png"
},
{
"pagePath": "pages/mine/index",
"text": "我的",
"iconPath": "static/tab/mine.png",
"selectedIconPath": "static/tab/mine-active.png"
}
]
},
// 分包配置,仅小程序端生效
// #ifdef MP
"subPackages": [
{
"root": "pages/goods",
"pages": [
{ "path": "detail", "style": { "navigationBarTitleText": "商品详情" } },
{ "path": "list", "style": { "navigationBarTitleText": "商品列表" } }
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "wifi",
"packages": ["pages/goods"]
}
},
// #endif
// 微信小程序权限配置
// #ifdef MP-WEIXIN
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于附近门店展示"
}
},
"requiredBackgroundModes": ["audio"],
"sitemapLocation": "sitemap.json"
// #endif
}

3.6 manifest.json 中的条件编译

用于处理不同小程序平台的AppID、编译配置、权限配置,示例:

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
{
"name": "uni-app小程序",
"appid": "__UNI__XXXXXX",
"versionName": "1.0.0",
"versionCode": "1",
"description": "",
"transformPx": false,
"app-plus": {},
"quickapp": {},
"mp-weixin": {
"appid": "wx1234567890abcdef",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于附近门店展示"
}
}
},
"mp-alipay": {
"appid": "2024000000000000",
"privateKey": ""
},
"mp-toutiao": {
"appid": "tt1234567890abcdef"
}
}

四、小程序端组件与API 全量规范与兼容处理

4.1 组件使用规范(小程序端专属)

  1. 优先使用UniApp内置组件:必须使用<view><text><image><button>等跨端内置组件,底层自动编译为小程序原生组件,禁止直接使用div、span等HTML标签,小程序端无法识别。
  2. <image>组件核心规则(高频踩坑)
    • 必须设置mode属性(常用widthFixaspectFitaspectFill),否则图片会按默认320px*240px尺寸变形
    • 支持lazy-load懒加载属性,长列表图片必须开启,提升滚动性能
    • 本地图片必须放在static目录下,否则打包后无法识别
    • 网络图片必须在微信公众平台配置request合法域名,否则无法加载
    • 图片大小限制:本地图片≤2MB,网络图片无大小限制,但建议≤500KB,避免占用内存
  3. 小程序原生组件层级规则(核心踩坑点)
    小程序端<video><map><canvas><camera><input>等原生组件为客户端原生渲染,层级最高,无法通过CSS的z-index覆盖,必须使用<cover-view><cover-image>组件才能覆盖在原生组件之上。

    注意:<cover-view>仅支持嵌套<cover-view><cover-image><button>组件,不支持嵌套其他组件,样式支持有限,避免复杂布局。

  4. 自定义组件规范
    • 遵循UniApp的easycom规范:组件路径符合components/组件名/组件名.vue结构,无需import和注册,直接在页面中使用,小程序端默认开启。
    • 小程序端自定义组件命名禁止使用微信原生组件名(如viewbuttonad),避免冲突。
    • 自定义组件中获取节点信息,必须使用uni.createSelectorQuery().in(this),否则无法获取到组件内的节点。
  5. 小程序原生组件使用规则
    如需使用微信专属原生组件(如<ad>广告、<official-account>公众号关注组件、<live-player>直播组件),必须配合#ifdef MP-WEIXIN条件编译使用,避免其他平台编译报错。

4.2 API使用规范与封装(小程序端专属)

  1. 优先使用uni.前缀API:必须使用uni.requestuni.showToastuni.setStorageSync等跨端API,底层自动编译为小程序wx.前缀API,禁止直接使用wx.前缀API,否则无法跨端兼容,仅在微信专属API场景下配合条件编译使用。
  2. 网络请求统一封装(核心必备)
    小程序端强制要求:所有网络请求必须使用HTTPS协议,不支持HTTP、IP地址、localhost请求;必须在微信公众平台「开发管理-开发设置-服务器域名」中配置request合法域名,域名必须完成ICP备案,支持TLS 1.2+协议。
    必须统一封装请求,处理token、错误码、loading、权限过期等逻辑,完整封装示例:
    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
    // utils/request.js
    // 接口基础域名
    const BASE_URL = 'https://api.example.com'
    // 无需token的白名单接口
    const WHITE_LIST = ['/auth/login', '/auth/register', '/home/banner']

    const request = (options) => {
    return new Promise((resolve, reject) => {
    // 显示loading,可通过options.loading=false关闭
    if (options.loading !== false) {
    uni.showLoading({
    title: options.loadingText || '加载中...',
    mask: true // 防止穿透点击
    })
    }

    // 拼接完整url
    const url = BASE_URL + options.url
    // 获取token
    const token = uni.getStorageSync('token')
    // 构造请求头
    const header = {
    'Content-Type': options.contentType || 'application/json',
    'Authorization': token ? `Bearer ${token}` : ''
    }

    uni.request({
    url,
    method: options.method || 'GET',
    data: options.data || {},
    header,
    timeout: options.timeout || 10000,
    success: (res) => {
    uni.hideLoading()
    const { statusCode, data } = res

    // HTTP状态码200处理
    if (statusCode === 200) {
    const { code, msg, data: result } = data
    // 业务成功
    if (code === 200) {
    resolve(data)
    }
    // token过期,权限不足
    else if (code === 401) {
    uni.clearStorageSync()
    uni.showModal({
    title: '登录过期',
    content: '您的登录已过期,请重新登录',
    showCancel: false,
    success: () => {
    uni.reLaunch({ url: '/pages/login/login' })
    }
    })
    reject(data)
    }
    // 其他业务错误
    else {
    uni.showToast({
    title: msg || '请求失败',
    icon: 'none',
    duration: 2000
    })
    reject(data)
    }
    }
    // HTTP状态码非200处理
    else {
    uni.showToast({
    title: `网络错误:${statusCode}`,
    icon: 'none'
    })
    reject(res)
    }
    },
    fail: (err) => {
    uni.hideLoading()
    // 网络错误处理
    uni.showToast({
    title: '网络异常,请检查网络设置',
    icon: 'none'
    })
    reject(err)
    }
    })
    })
    }

    // 封装常用请求方法
    export const get = (url, data = {}, options = {}) => {
    return request({ url, method: 'GET', data, ...options })
    }
    export const post = (url, data = {}, options = {}) => {
    return request({ url, method: 'POST', data, ...options })
    }
    export const put = (url, data = {}, options = {}) => {
    return request({ url, method: 'PUT', data, ...options })
    }
    export const del = (url, data = {}, options = {}) => {
    return request({ url, method: 'DELETE', data, ...options })
    }

    export default request
  3. 小程序端专属核心API场景与规范
    API场景 核心用法与小程序端硬性规则
    登录授权 1. 调用uni.login()获取code,传给后端换取openid、unionid、session_key,code有效期5分钟,只能使用一次
    2. 微信已回收静默获取用户信息能力,获取用户昵称、头像必须通过<button open-type="chooseAvatar">(新版)或wx.getUserProfile触发,必须用户主动点击
    微信支付 调用uni.requestPayment(),支付参数(timeStamp、nonceStr、package、signType、paySign)必须由后端生成,前端禁止参与签名计算,避免密钥泄露
    获取手机号 必须通过<button open-type="getPhoneNumber">,用户主动点击触发,获取encryptedDataiv,传给后端解密,前端无法直接解密获取手机号;个人主体小程序无此权限
    订阅消息 调用uni.requestSubscribeMessage(),获取用户订阅授权,仅能下发已在微信公众平台配置的模板消息,用户拒绝后无法再次弹窗申请
    定位 调用uni.getLocation()获取用户位置,必须在pages.json中配置permission权限声明,必须在微信公众平台开通「地理位置接口权限」,否则无法使用
    扫码 调用uni.scanCode(),调用微信扫码能力,识别二维码/条形码,无需额外申请权限
    小程序跳转 调用uni.navigateToMiniProgram()跳转到其他小程序,必须在pages.json中配置navigateToMiniProgramAppIdList白名单,否则无法跳转
    本地存储 调用uni.setStorageSync()/uni.getStorageSync(),单个key存储数据上限1MB,所有数据存储上限10MB,禁止存储大量数据

五、小程序端数据通信与状态管理

5.1 全局状态管理

方案 适用场景 用法示例 小程序端注意事项
getApp().globalData 小型项目、少量全局数据 - 1. 仅能在App.vue中定义,全局唯一
2. 数据修改后无法自动响应到页面,需配合watch或事件总线实现响应式
Vuex Vue2项目、中大型项目、复杂全局状态 标准Vuex用法,分模块管理用户、购物车、全局配置等状态 小程序端完全兼容,需注意分包中无法直接访问主包的Vuex模块,需统一挂载
Pinia Vue3项目、全量项目,推荐使用 标准Pinia用法,比Vuex更简洁,支持TypeScript、模块化 小程序端完全兼容,无分包访问限制,Vue3项目优先选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// App.vue
export default {
globalData: {
token: '',
userInfo: null,
baseUrl: 'https://api.example.com'
},
onLaunch() {
this.globalData.token = uni.getStorageSync('token')
}
}
// 页面中使用
const app = getApp()
console.log(app.globalData.token)

5.2 页面间通信全方案

  1. URL参数传递(最常用)
    最基础、最稳定的页面通信方式,通过uni.navigateTo的url拼接参数,目标页面onLoad中接收,适合少量简单参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 页面A:跳转传参
    uni.navigateTo({
    url: `/pages/detail?id=123&name=测试商品`
    })

    // 页面B:接收参数
    onLoad(options) {
    console.log('页面入参:', options.id, options.name)
    }

    注意:参数长度限制1024字节,超长参数、特殊字符、复杂对象禁止通过URL传递,改用全局存储/事件总线。

  2. 事件总线(EventBus)
    通过uni.$emit触发事件,uni.$on监听事件,实现跨页面、跨组件通信,适合非父子页面的实时通信。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 页面A:触发事件,传递数据
    uni.$emit('refreshOrderList', { status: 1, page: 1 })

    // 页面B:监听事件,接收数据
    onLoad() {
    uni.$on('refreshOrderList', (data) => {
    console.log('收到事件:', data)
    this.getOrderList(data.status, data.page)
    })
    },
    // 必须在页面卸载时解绑,避免内存泄漏、多次触发
    onUnload() {
    uni.$off('refreshOrderList')
    }
  3. 全局本地存储
    通过uni.setStorageSync()/uni.getStorageSync()存储和读取数据,适合大量数据、复杂对象、持久化数据传递。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 页面A:存储数据
    uni.setStorageSync('goods_detail', {
    id: 123,
    name: '测试商品',
    price: 99.9,
    spec: ['红色', 'XL']
    })

    // 页面B:读取数据
    onLoad() {
    const goodsDetail = uni.getStorageSync('goods_detail')
    console.log('商品详情:', goodsDetail)
    }
  4. 页面栈获取与操作
    通过getCurrentPages()获取当前页面栈实例,直接操作上一个页面的数据和方法,适合父子页面通信。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 页面B:返回前操作上一个页面A的数据和方法
    goBack() {
    const pages = getCurrentPages()
    // 获取上一个页面实例
    const prevPage = pages[pages.length - 2]
    // 调用上一个页面的方法
    prevPage.getList()
    // 修改上一个页面的数据
    prevPage.$vm.status = 2
    // 返回上一页
    uni.navigateBack()
    }

    注意:小程序页面栈最多10层,超出后navigateTo无法打开新页面,需使用redirectTo替换。

5.3 组件间通信

  1. 父子组件通信:父组件通过props向子组件传值,子组件通过$emit触发事件向父组件传值,最基础的组件通信方式。
  2. 兄弟组件通信:通过共同的父组件中转,或使用事件总线、全局状态管理。
  3. 跨层级组件通信:Vue2/Vue3均使用provide/inject依赖注入,或全局状态管理。

六、小程序端专属性能优化全方案

小程序的性能限制比App、H5端更严格,性能优化直接影响小程序的启动速度、流畅度、用户留存,甚至影响微信搜索排名,核心优化方案全量详解如下。

6.1 包体积优化(最核心,影响启动速度与上架)

微信小程序硬性限制主包≤2MB,总大小≤20MB,包体积越小,启动速度越快,是优化的第一优先级。

  1. 严格执行分包加载(核心中的核心)
    • 主包仅保留tabBar页面、公共组件、公共资源,所有非核心页面全部分包,严格控制主包体积≤1.5MB,预留冗余空间。
    • 独立分包用于活动页、推广页、H5跳转落地页,不依赖主包,可独立启动,不占用主包体积。
    • 合理配置分包预加载,在用户进入首页时,预加载高频访问的分包,平衡启动速度和页面切换体验。
    • 分包之间的公共资源,提取到主包内,避免重复打包,占用体积。
  2. 资源极致优化
    • 所有图片、音频、视频等静态资源,尽量上传CDN,仅保留极小的tab图标放在static目录,这是包体积瘦身最有效的手段。
    • 图片必须压缩,推荐使用webp格式,比png/jpg体积小50%以上,小程序端全支持。
    • 字体文件使用线上地址,仅保留需要的字体子集,禁止全量字体包打包到项目中。
    • 剔除未使用的静态资源,开启manifest.json中的「忽略未使用的资源」配置。
  3. 代码瘦身
    • 开启manifest.json中的代码压缩、摇树优化,剔除未使用的代码、组件、依赖。
      摇树优化(Tree Shaking)是一种死代码消除(Dead Code Elimination, DCE)技术,核心作用是在打包构建时,精准剔除项目中从未被引用、从未执行的 “无用代码”,从而大幅减少最终生成的代码包体积。
    • 避免引入过大的第三方库,如完整的lodash,按需引入工具函数,或自行封装轻量工具方法。
    • 开启上传时自动清除console.log,剔除调试代码、注释。
    • 合理使用条件编译,剔除其他平台的冗余代码,只保留当前小程序平台的代码。
    • 避免在代码中内嵌大段base64图片,改用CDN地址。

6.2 渲染性能优化(解决卡顿、掉帧、白屏)

小程序的渲染层和逻辑层分离,所有数据更新都需要通过setData从逻辑层传递到渲染层,频繁、大量的setData是卡顿的核心原因。

  1. setData极致优化(小程序渲染核心)
    • 避免频繁setData,合并多次数据更新,一次性提交,禁止在循环中执行setData。
    • 减少setData的数据量,仅更新需要变化的字段,禁止全量更新大对象、长列表,支持路径式更新:this.setData({ 'list[0].title': '新标题' })
    • 单条setData数据量不超过100KB,过大会占用主线程传输时间,导致渲染延迟。
    • 页面隐藏、小程序进入后台后,停止所有setData操作,避免占用后台资源。
    • 与渲染无关的数据,不要放在data中,避免触发不必要的setData和重渲染。
  2. 长列表优化
    • 短列表(<100条):配合onReachBottom分页加载,每次加载10-20条数据。
    • 长列表(>100条):必须使用虚拟滚动,仅渲染可视区域的节点,销毁离开可视区域的节点,推荐使用uni-listmescroll-uniuview的长列表组件。
    • 列表item结构精简,减少DOM嵌套层级,图片开启懒加载,设置固定宽高,避免渲染时重排。
    • 列表item设置唯一key值,禁止使用index作为key,提升重渲染性能。
  3. 通用渲染优化
    • 频繁切换显示/隐藏的组件,使用v-show;仅渲染一次的组件,使用v-if,避免频繁销毁重建。
    • 页面DOM节点数控制在1000个以内,嵌套层级不超过10层,减少渲染压力。
    • 禁止在watchercomputedonPageScroll中做复杂计算和频繁的数据更新。
    • 自定义组件按需渲染,避免全局注册大量未使用的组件。
    • 减少CSS动画的使用,优先使用CSS3硬件加速属性(transform、opacity),避免修改width、height、top等属性导致重排。

6.3 启动速度优化(解决打开慢、白屏问题)

小程序启动速度是微信官方考核的核心指标,直接影响用户体验和搜索排名。

  1. 主包极致瘦身,主包体积越小,代码下载、注入速度越快,启动速度越快。
  2. 开启lazyCodeLoading: "requiredComponents",实现代码按需注入,仅注入当前页面需要的代码和组件,提升启动速度。
  3. 首页精简,onLoad/onShow中不做高耗时同步逻辑,先渲染骨架屏,再异步加载数据,减少白屏时间。
  4. App.vueonLaunch中,非必须的初始化逻辑延迟到首页渲染完成后执行,避免阻塞启动。
  5. 提前预请求,在onLaunch中预请求首页核心数据,减少等待时间,先拿到数据,页面渲染完成后直接渲染。
  6. 避免在首页使用过多的自定义组件,减少组件初始化时间。

6.4 内存优化(解决闪退、卡顿、被系统回收)

  1. 页面onUnload、组件beforeDestroy时,必须清理所有定时器、延时器、事件监听、websocket、canvas实例,避免内存泄漏。
  2. uni.$on监听的事件,必须在销毁时用uni.$off解绑,避免事件多次触发和内存泄漏。
  3. 避免在data中存放大量不渲染的数据,仅把需要渲染的数据放在data中,减少内存占用。
  4. 小程序页面栈最多10层,避免层级过深,使用uni.redirectTo代替uni.navigateTo关闭不需要的页面,释放内存。
  5. 避免页面内同时渲染大量大图,大图使用缩略图,点击查看原图,图片尺寸不要超过显示尺寸的2倍,减少内存占用。
  6. 音频、视频、地图组件,在页面卸载时必须销毁,停止播放,释放占用的内存。

七、小程序端调试、兼容与发布上线

7.1 全流程调试方法

  1. 开发环境调试
    HBuilderX中点击「运行」→ 「运行到小程序模拟器」→ 「微信开发者工具」,项目会自动编译并打开微信开发者工具,在开发者工具中进行调试。
  2. 微信开发者工具调试面板
    • Console面板:查看控制台日志、报错信息,执行JS代码,调试逻辑问题。
    • Network面板:查看接口请求、响应、请求头、状态码,抓包分析接口问题,支持过滤、断点调试。
    • AppData面板:查看页面data数据、全局数据,支持实时修改,调试数据渲染问题。
    • Wxml面板:查看DOM结构、样式,调试布局、样式问题,支持实时修改样式。
    • Storage面板:查看本地存储数据,支持新增、修改、删除,调试存储相关问题。
    • 真机调试面板:扫码连接真机,远程调试真机上的小程序,支持查看日志、网络、存储。
  3. 真机调试
    微信开发者工具点击「真机调试」,扫码在手机上打开小程序,可调试真机上的授权、支付、定位、蓝牙等必须真机才能使用的功能,是发布前必须的测试环节。
  4. 体验版测试
    代码上传到微信公众平台后,可设置为体验版,生成体验码,给测试人员、产品人员测试,模拟正式环境的全功能、全流程测试,是发布前的最后测试环节。

7.2 兼容性处理

  1. 基础库版本兼容
    • manifest.json中设置最低基础库版本为2.10.0以上,可覆盖99%以上的微信用户。
    • 高版本基础库新增的API,必须做兼容判断,低版本做降级处理,示例:
      1
      2
      3
      4
      5
      6
      7
      // 判断API是否可用
      if (wx.getUserProfile) {
      // 支持,使用新API
      } else {
      // 不支持,降级处理
      uni.showToast({ title: '微信版本过低,请升级后重试', icon: 'none' })
      }
  2. 机型与屏幕适配
    • 使用rpx单位(基于750px设计稿)+ flex弹性布局,适配不同屏幕尺寸、分辨率。
    • 适配iOS刘海屏、底部小黑条,使用padding-bottom: env(safe-area-inset-bottom)设置安全区域。
    • 自定义导航栏必须适配不同机型的状态栏高度、微信胶囊按钮位置,避免布局错乱。
  3. 多小程序平台兼容
    • 通过条件编译处理不同平台的API、组件、样式差异,避免兼容报错。
    • 优先使用UniApp跨端API,减少平台专属代码,提升兼容性。

7.3 发布上线全流程

  1. 代码上传
    测试完成后,在微信开发者工具中点击右上角「上传」,填写版本号、项目备注,上传代码到微信公众平台,上传前必须开启代码压缩、清除console.log。
  2. 提交审核
    登录微信公众平台,进入「管理」→「版本管理」,找到刚上传的版本,点击「提交审核」,填写审核信息:
    • 小程序类目:必须与实际功能一致,特殊类目需要提供对应资质。
    • 功能页面:填写小程序的核心页面、功能说明。
    • 测试账号:提供完整的测试账号、密码,供审核人员测试所有功能,无登录功能可不填。
    • 审核备注:说明小程序的核心功能、特殊逻辑,帮助审核人员快速了解。
  3. 审核与发布
    提交审核后,等待微信官方审核,审核时长通常1-3个工作日,加急审核可缩短到几小时。
    审核通过后,进入版本管理,点击「发布」,可选择全量发布、灰度发布(按比例发布),发布后用户即可更新到最新版本。

7.4 审核避坑核心要点

  1. 必须符合《微信小程序平台运营规范》,无违规内容、违法信息。
  2. 必须提供完整的《用户隐私保护指引》,小程序首次启动时弹窗提示,获取用户同意后,才能收集用户信息、申请权限,否则审核直接被拒。
  3. 特殊类目(金融、医疗、教育、新闻、电商等)必须提供对应的资质文件,否则无法通过审核。
  4. 禁止诱导用户分享、点赞、关注、下载,禁止分享裂变、红包诱导等违规行为。
  5. iOS端禁止虚拟支付相关内容,包括会员、充值、虚拟商品购买等,必须使用苹果内购,否则审核直接被拒。
  6. 小程序必须提供完整的功能,不能是简单的H5套壳、官网展示,否则审核被拒。
  7. 所有申请的权限,必须有对应的功能场景,不能申请无关权限,否则审核被拒。

八、小程序端安全合规要点

  1. 网络安全
    • 所有接口必须使用HTTPS协议,禁止使用HTTP协议,必须在微信公众平台配置合法域名。
    • 接口请求做签名校验,防止抓包篡改请求参数。
    • 敏感参数、用户信息加密传输,禁止明文传输。
    • 配置接口请求频率限制,防止恶意刷接口、CC攻击。
      Challenge Collapsar(挑战黑洞),是针对 Web 应用 / 接口的应用层(OSI 七层模型第 7 层,HTTP/HTTPS 层)分布式拒绝服务攻击(DDoS),也是目前最主流、最难防护的业务层攻击方式之一
  2. 数据安全
    • 用户敏感信息(手机号、身份证、银行卡)加密存储,禁止明文存储在本地。
    • token设置有效期,定期续期,退出登录时清除所有用户信息。
    • 遵循最小必要原则,仅收集功能必须的用户信息,不收集无关的个人信息。
    • 禁止泄露、出售、共享用户个人信息,符合《个人信息保护法》要求。
  3. 合规要求
    • 严格遵守《微信小程序平台运营规范》《网络安全法》《个人信息保护法》《数据安全法》等法律法规。
    • 制定完整的《用户隐私保护指引》《用户服务协议》,明确告知用户数据收集的用途、范围、存储期限,获取用户同意后才能收集。
    • 权限申请必须明确告知用途,在用户使用对应功能时才申请,禁止启动时一次性申请所有权限。
    • 小程序内容、功能必须与注册类目一致,禁止超范围经营。
    • 禁止提供违法、违规、低俗、侵权的内容和服务。

uniapp开发跨端小程序重点
https://cszy.top/20250913-uniapp开发跨端小程序重点/
作者
csorz
发布于
2025年9月13日
许可协议