微信小程序
一、小程序架构与原理
1. 小程序是什么?
定义: 微信小程序是一种不需要下载安装即可使用的应用,用户扫一扫或搜一下即可打开应用,实现了应用"触手可及"的梦想,用户扫一扫或者搜一下即可打开应用。
原理:
- 小程序运行在微信客户端内,基于微信提供的运行环境
- 采用类似 Web 技术(HTML5/CSS3/JavaScript)进行开发
- 小程序代码经过微信编译处理后,在微信内部运行
- 不需要安装,用完即走
示例场景:
- 扫码点餐:餐厅提供小程序,顾客扫码即可点餐支付
- 共享充电宝:扫描充电宝二维码,直接使用小程序租借
- 公交乘车码:打开微信小程序,展示乘车码扫码乘车
核心代码示例:
// app.js - 小程序入口文件
App({
onLaunch() {
console.log('小程序启动')
},
globalData: {
userInfo: null
}
})
常见误区:
- 误区一:小程序就是 H5 页面。实际上小程序有自己的运行环境和 API
- 误区二:小程序完全替代原生 App。实际上小程序适合轻量级场景,复杂功能仍需 App
- 误区三:小程序不需要审核。实际上小程序发布前需要经过微信审核
2. 小程序架构 / 双线程模型 / 渲染层与逻辑层
定义: 小程序采用双线程模型,将渲染层和逻辑层分离,分别运行在不同的线程中,以提高安全性和性能。
架构组成:
┌─────────────────────────────────────────────────────────┐
│ 微信客户端 (Native) │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────────────┐ │
│ │ 渲染层线程 │◄─────►│ 逻辑层线程 │ │
│ │ (WebView) │ │ (JsCore / V8) │ │
│ │ │ │ │ │
│ │ - WXML 渲染 │ │ - JavaScript 执行 │ │
│ │ - WXSS 样式 │ │ - 业务逻辑处理 │ │
│ │ - 事件收集 │ │ - API 调用 │ │
│ │ │ │ - 数据管理 │ │
│ └─────────────────┘ └──────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 微信 Native 层 │ │
│ │ - 网络请求 - 支付 - 登录 - 定位 - 扫码 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
渲染层:
- 使用 WebView 进行渲染
- 负责 WXML 和 WXSS 的解析和渲染
- 收集用户交互事件,传递给逻辑层
逻辑层:
- 使用 JsCore(iOS)或 V8(Android)运行 JavaScript
- 处理业务逻辑、数据管理
- 调用微信 Native API
线程通信:
- 通过微信客户端的 JSBridge 进行通信
- 数据传递采用 JSON 格式序列化
- 异步通信机制,避免阻塞
对比表格:渲染层 vs 逻辑层
| 维度 | 渲染层 | 逻辑层 |
|---|---|---|
| 运行环境 | WebView | JsCore / V8 |
| 主要职责 | 页面渲染、事件收集 | 业务逻辑、数据处理 |
| 使用技术 | WXML、WXSS | JavaScript |
| 是否能调用 Native API | 否 | 是 |
| 是否能直接操作 DOM | 否 | 否 |
| 数据更新方式 | 通过 setData 接收数据 | 调用 setData 发送数据 |
选择策略: 开发者无需选择,小程序框架自动管理双线程。开发者只需关注:
- 不要在逻辑层直接操作 DOM
- 使用 setData 进行数据更新
- 合理使用事件系统
代码示例:
// 逻辑层 - 通过 setData 更新数据,通知渲染层重新渲染
Page({
data: {
message: 'Hello'
},
updateMessage() {
this.setData({
message: 'World'
})
// 渲染层接收数据更新,自动重新渲染页面
}
})
常见误区:
- 误区一:可以直接操作 DOM。小程序不允许直接操作 DOM
- 误区二:setData 是同步的。setData 是异步通信过程
- 误区三:双线程意味着性能更好。双线程通信有开销,频繁 setData 会影响性能
3. 小程序原理
定义: 小程序基于 Web 技术栈,但在微信客户端内运行,通过双线程模型和微信 Native 能力,实现接近原生 App 的体验。
工作流程:
- 下载代码包:用户打开小程序时,微信客户端从服务器下载小程序代码包
- 解析编译:微信客户端解析代码包,将 WXML 转换为虚拟 DOM,WXSS 转换为样式
- 初始化运行:执行 app.js 初始化全局对象,然后加载页面逻辑层代码
- 渲染页面:渲染层根据虚拟 DOM 渲染页面,展示给用户
- 交互处理:用户操作触发事件,渲染层传递给逻辑层,逻辑层处理后通过 setData 更新数据
核心原理:
- 双线程模型:渲染层和逻辑层分离
- 虚拟 DOM:提高渲染效率
- 数据驱动:通过 setData 实现数据驱动视图更新
- Native 能力桥接:通过 JSBridge 调用微信原生能力
代码示例:
// 小程序初始化流程
// 1. App 实例化
App({
onLaunch(options) {
// 小程序启动时触发,全局只触发一次
console.log('场景值:', options.scene)
}
})
// 2. Page 实例化
Page({
data: { text: 'Hello' },
onLoad(options) {
// 页面加载时触发
this.setData({ text: 'World' })
}
})
常见误区:
- 误区一:小程序代码在本地运行。实际上首次需要下载代码包
- 误区二:小程序能实现所有原生功能。实际上小程序只能使用微信提供的 API
4. 小程序运行机制
定义: 小程序运行机制包括启动机制、页面管理、后台运行机制等。
启动机制:
- 冷启动:用户首次打开或小程序被销毁后重新打开,需要重新初始化
- 热启动:小程序已在后台运行,用户再次打开,直接恢复到前台
后台运行机制:
- 小程序进入后台后,会保持一段时间的运行状态(最长 5 分钟)
- 超过时间或内存占用过大,会被微信主动销毁
- 可使用
wx.onAppShow和wx.onAppHide监听前后台切换
内存管理:
- iOS:占用内存超过一定阈值(约 1GB)会被销毁
- Android:占用内存超过一定阈值(约 500MB)会被销毁
- 页面栈最大 10 层
代码示例:
// 监听前后台切换
App({
onLaunch() {
wx.onAppShow(() => {
console.log('小程序进入前台')
})
wx.onAppHide(() => {
console.log('小程序进入后台')
})
}
})
// 监听内存不足警告
wx.onMemoryWarning(function () {
console.log('内存不足警告')
// 应主动释放内存,如清理缓存数据
})
常见误区:
- 误区一:小程序在后台一直运行。实际上会在一定条件下被销毁
- 误区二:页面栈没有上限。实际上最大支持 10 层
5. 小程序与 H5 的区别
双方定义:
- 小程序:运行在微信客户端内的轻量级应用,使用 WXML/WXSS/JS 开发,有独立的运行环境和 API
- H5:基于 HTML5 技术的移动网页,运行在浏览器中,通过 URL 访问
对比表格:
| 维度 | 小程序 | H5 |
|---|---|---|
| 运行环境 | 微信客户端内 | 浏览器 |
| 技术栈 | WXML、WXSS、JS | HTML、CSS、JS |
| 开发工具 | 微信开发者工具 | 任意编辑器 |
| 审核发布 | 需微信审核 | 自行发布 |
| 入口方式 | 扫码、搜索、分享等 | URL、二维码 |
| 系统能力 | 丰富的微信 Native API | 受浏览器限制 |
| 性能 | 接近原生体验 | 受网络影响大 |
| 更新方式 | 发版审核 | 即时更新 |
| 缓存机制 | 代码包缓存、本地存储 | 浏览器缓存 |
| 分享能力 | 原生分享支持 | 需借助分享组件 |
| 支付能力 | 原生微信支付 | 需跳转或 JSAPI |
| 用户体系 | 微信账号体系 | 需自建用户体系 |
选择策略:
| 场景 | 选择 |
|---|---|
| 需要快速迭代、频繁更新 | H5 |
| 需要调用系统能力(蓝牙、NFC等) | 小程序/原生 App |
| 轻量级服务、即用即走 | 小程序 |
| 复杂图形、游戏 | 原生 App 或 H5 + Canvas |
| 需要微信平台流量 | 小程序 |
| 多平台兼容需求 | H5 |
6. 小程序的优势
- 无需安装:用户无需下载安装,节省手机存储空间
- 即用即走:打开速度快,使用完即可关闭
- 开发成本低:一套代码可在微信生态内运行
- 流量入口多:扫码、搜索、公众号、分享等多种入口
- 微信生态:天然接入微信支付、用户体系、社交分享
- 审核机制:保证应用质量和用户体验
- 离线可用:代码包缓存后可离线使用部分功能
7. 小程序的劣势
- 体积限制:主包不超过 2MB,所有分包不超过 20MB
- 能力受限:只能使用微信提供的 API,无法完全调用系统能力
- 审核周期:需要微信审核,更新不如 H5 灵活
- 平台依赖:强依赖微信生态,无法脱离微信独立运行
- 性能瓶颈:复杂场景下性能不如原生 App
- 双线程限制:无法直接操作 DOM,部分 Web 技术无法使用
二、小程序生命周期
8. 小程序生命周期概览
小程序生命周期分为三个层次:App(应用)、Page(页面)、Component(组件)。
生命周期流程图:
App 生命周期:
onLaunch → onShow → onHide → (后台) → onShow → onDestroy
Page 生命周期:
onLoad → onShow → onReady → (用户交互) → onHide → onUnload
Component 生命周期:
created → attached → ready → moved → detached
9. App 生命周期
定义: App 生命周期是整个小程序应用的生命周期,在 app.js 中定义,全局唯一。
生命周期函数:
| 函数 | 触发时机 | 使用场景 |
|---|---|---|
| onLaunch | 小程序初始化完成时(全局只触发一次) | 获取启动参数、初始化全局数据 |
| onShow | 小程序启动或从后台进入前台时 | 数据刷新、状态恢复 |
| onHide | 小程序从前台进入后台时 | 数据保存、清理定时器 |
| onError | 小程序发生脚本错误或 API 调用失败时 | 错误上报 |
| onPageNotFound | 小程序要打开的页面不存在时 | 跳转兜底页面 |
代码示例:
// app.js
App({
onLaunch(options) {
console.log('小程序启动,场景值:', options.scene)
// 获取用户信息
const userInfo = wx.getStorageSync('userInfo')
if (userInfo) {
this.globalData.userInfo = userInfo
}
},
onShow(options) {
console.log('小程序进入前台,场景值:', options.scene)
// 检查版本更新
this.checkUpdate()
},
onHide() {
console.log('小程序进入后台')
// 保存用户数据
},
onError(error) {
console.error('小程序发生错误:', error)
// 上报错误到服务器
this.reportError(error)
},
onPageNotFound() {
wx.redirectTo({
url: '/pages/404/404'
})
},
checkUpdate() {
if (wx.canIUse('getUpdateManager')) {
const updateManager = wx.getUpdateManager()
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,是否重启应用?',
success(res) {
if (res.confirm) {
updateManager.applyUpdate()
}
}
})
})
}
},
globalData: {
userInfo: null,
baseUrl: 'https://api.example.com'
}
})
常见误区:
- 误区一:onLaunch 每次打开都会触发。实际上只在首次启动或销毁后重新打开时触发
- 误区二:onShow 和 onLaunch 功能相同。onShow 每次从小程序从前台到后台再到前台都会触发
10. Page 生命周期
定义: Page 生命周期是小程序页面的生命周期,在页面的 .js 文件中定义。
生命周期函数:
| 函数 | 触发时机 | 使用场景 |
|---|---|---|
| onLoad | 页面加载时触发(只触发一次) | 获取页面参数、初始化数据 |
| onShow | 页面显示时触发(每次显示都触发) | 数据刷新 |
| onReady | 页面初次渲染完成时触发(只触发一次) | 操作 DOM(如创建地图、视频等) |
| onHide | 页面隐藏时触发 | 暂停视频、保存状态 |
| onUnload | 页面卸载时触发 | 清理定时器、取消监听 |
| onPullDownRefresh | 用户下拉刷新时触发 | 刷新数据 |
| onReachBottom | 页面上拉触底时触发 | 加载更多数据 |
| onShareAppMessage | 用户点击右上角分享时触发 | 设置分享内容 |
| onResize | 屏幕旋转时触发 | 适配横竖屏 |
| onTabItemTap | 点击 tab 栏时触发 | 自定义 tab 行为 |
代码示例:
// pages/detail/detail.js
Page({
data: {
id: '',
detail: null,
loading: false
},
onLoad(options) {
console.log('页面加载,参数:', options)
this.setData({ id: options.id })
this.fetchDetail(options.id)
},
onShow() {
console.log('页面显示')
// 每次显示页面时刷新数据
if (this.data.id) {
this.fetchDetail(this.data.id)
}
},
onReady() {
console.log('页面初次渲染完成')
// 可以创建 map 上下文、video 上下文等
},
onHide() {
console.log('页面隐藏')
// 暂停动画、视频等
},
onUnload() {
console.log('页面卸载')
// 清理工作
if (this.timer) {
clearInterval(this.timer)
}
},
onPullDownRefresh() {
console.log('用户下拉刷新')
this.fetchDetail(this.data.id).then(() => {
wx.stopPullDownRefresh()
})
},
onReachBottom() {
console.log('用户上拉触底')
this.loadMore()
},
onShareAppMessage() {
return {
title: '分享标题',
path: `/pages/detail/detail?id=${this.data.id}`,
imageUrl: '/images/share.png'
}
},
fetchDetail(id) {
this.setData({ loading: true })
return wx.request({
url: `https://api.example.com/detail/${id}`
}).then(res => {
this.setData({ detail: res.data })
}).finally(() => {
this.setData({ loading: false })
})
},
loadMore() {
// 加载更多数据逻辑
}
})
生命周期执行顺序:
页面首次打开: onLoad → onShow → onReady
页面切换到其他页面再返回: onShow
页面关闭: onHide (navigateTo) / onUnload (redirectTo)
常见误区:
- 误区一:onLoad 每次都触发。实际上页面缓存后再返回只触发 onShow
- 误区二:onReady 可以多次触发。实际上只触发一次
- 误区三:下拉刷新默认开启。需要在 page.json 中设置
"enablePullDownRefresh": true
11. Component 生命周期
定义: Component 生命周期是自定义组件的生命周期,在组件的 .js 文件中定义。
生命周期函数:
| 函数 | 触发时机 | 使用场景 |
|---|---|---|
| created | 组件实例刚刚被创建时 | 初始化数据,此时不能访问 this.data |
| attached | 组件被添加到页面节点树时 | 初始化数据,可访问 this.data 和父组件 |
| ready | 组件布局完成后 | 获取节点信息、创建上下文 |
| moved | 组件被移动到节点树另一个位置时 | 处理节点位置变化 |
| detached | 组件被从页面节点树移除时 | 清理工作 |
| error | 组件方法抛出错误时 | 错误处理 |
代码示例:
// components/my-component/my-component.js
Component({
lifetimes: {
created() {
console.log('组件实例创建')
// 此时 this.data 还未初始化
},
attached() {
console.log('组件被添加到节点树')
// 可以访问 this.data 和 this.properties
this.initData()
},
ready() {
console.log('组件布局完成')
// 可以获取节点信息
this.createSelectorQuery().select('.my-class').boundingClientRect()
},
moved() {
console.log('组件位置被移动')
},
detached() {
console.log('组件被移除')
// 清理定时器、取消监听等
if (this.timer) {
clearInterval(this.timer)
}
},
error(err) {
console.error('组件方法抛出错误:', err)
}
},
pageLifetimes: {
show() {
// 页面被展示时触发
},
hide() {
// 页面被隐藏时触发
},
resize(size) {
// 页面尺寸变化时触发
}
},
data: {
name: ''
},
methods: {
initData() {
this.setData({ name: 'My Component' })
}
}
})
生命周期对比:Page vs Component
| 维度 | Page | Component |
|---|---|---|
| 初始化 | onLoad | created → attached |
| 渲染完成 | onReady | ready |
| 显示 | onShow | pageLifetimes.show |
| 隐藏 | onHide | pageLifetimes.hide |
| 卸载 | onUnload | detached |
| 获取参数 | onLoad(options) | properties |
12. 关键生命周期函数详解
onLaunch vs onShow
| 维度 | onLaunch | onShow |
|---|---|---|
| 触发次数 | 全局仅一次 | 每次进入前台 |
| 触发时机 | 小程序初始化完成 | 启动或从后台进入前台 |
| 参数 | 启动参数 | 启动参数/场景值 |
| 典型用途 | 初始化全局数据、登录 | 刷新数据、检查更新 |
onLoad vs onReady
| 维度 | onLoad | onReady |
|---|---|---|
| 触发次数 | 一次 | 一次 |
| 触发时机 | 页面加载完成 | 页面初次渲染完成 |
| 典型用途 | 获取参数、请求数据 | 操作 DOM、创建上下文 |
| 能否获取节点 | 否 | 是 |
onPullDownRefresh(下拉刷新)
// page.json
{
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
// page.js
Page({
onPullDownRefresh() {
this.refreshData().then(() => {
wx.stopPullDownRefresh() // 停止下拉刷新动画
})
}
})
onReachBottom(上拉触底)
// page.json
{
"onReachBottomDistance": 50 // 距底部距离触发,默认 50px
}
// page.js
Page({
data: {
list: [],
page: 1,
hasMore: true
},
onReachBottom() {
if (this.data.hasMore) {
this.loadMore()
}
}
})
三、页面与组件
13. 小程序页面
定义: 小程序页面是构成小程序的基本单元,每个页面由四个文件组成:.wxml(结构)、.wxss(样式)、.js(逻辑)、.json(配置)。
页面注册:
// app.json
{
"pages": [
"pages/index/index",
"pages/detail/detail",
"pages/user/user"
]
}
页面结构:
pages/
├── index/
│ ├── index.wxml // 页面结构
│ ├── index.wxss // 页面样式
│ ├── index.js // 页面逻辑
│ └── index.json // 页面配置
页面配置(index.json):
{
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true,
"navigationBarBackgroundColor": "#ffffff"
}
14. 小程序组件 / 自定义组件
定义: 自定义组件是可以复用的代码单元,具有独立的 wxml、wxss、js、json 文件,可以在多个页面中引用使用。
创建组件:
// components/my-button/my-button.json
{
"component": true,
"usingComponents": {}
}
<!-- components/my-button/my-button.wxml -->
<view class="my-button" bindtap="onTap">
<slot></slot>
</view>
/* components/my-button/my-button.wxss */
.my-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12rpx 24rpx;
background-color: #07c160;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
}
// components/my-button/my-button.js
Component({
properties: {
disabled: {
type: Boolean,
value: false
},
type: {
type: String,
value: 'default'
}
},
data: {},
methods: {
onTap() {
if (!this.data.disabled) {
this.triggerEvent('click', { time: Date.now() })
}
}
}
})
使用组件:
// pages/index/index.json
{
"usingComponents": {
"my-button": "/components/my-button/my-button"
}
}
<!-- pages/index/index.wxml -->
<my-button type="primary" bind:click="handleClick">
点击我
</my-button>
15. 组件通信
定义: 组件通信是指父组件、子组件、兄弟组件之间传递数据和事件的方式。
通信方式:
| 方式 | 方向 | 说明 |
|---|---|---|
| properties | 父 → 子 | 父组件通过属性传递数据给子组件 |
| triggerEvent | 子 → 父 | 子组件通过事件传递数据给父组件 |
| parent | 子 → 父 | 子组件访问父组件实例 |
| selectComponent | 父 → 子 | 父组件获取子组件实例 |
| EventChannel | 跨页面 | 页面间事件通信通道 |
| globalData | 全局 | 通过 getApp().globalData 全局共享 |
| Storage | 全局 | 通过本地存储共享 |
父传子(properties):
<!-- 父组件 wxml -->
<child-component title="{{pageTitle}}" count="{{itemCount}}"></child-component>
// 子组件 js
Component({
properties: {
title: String,
count: {
type: Number,
value: 0,
observer(newVal, oldVal) {
console.log('count 变化:', oldVal, '→', newVal)
}
}
}
})
子传父(triggerEvent):
<!-- 子组件 wxml -->
<button bindtap="handleClick">提交</button>
// 子组件 js
Component({
methods: {
handleClick() {
this.triggerEvent('submit', {
data: this.data.formData,
timestamp: Date.now()
}, { bubbles: true })
}
}
})
<!-- 父组件 wxml -->
<child-component bind:submit="onChildSubmit"></child-component>
// 父组件 js
Page({
onChildSubmit(event) {
console.log('子组件提交的数据:', event.detail)
}
})
父获取子实例(selectComponent):
<!-- 父组件 wxml -->
<child-component id="myChild"></child-component>
<button bindtap="callChildMethod">调用子组件方法</button>
// 父组件 js
Page({
callChildMethod() {
const child = this.selectComponent('#myChild')
if (child) {
child.reset() // 调用子组件方法
}
}
})
兄弟组件通信:
// 方式一:通过父组件中转
// 子组件A → 父组件 → 子组件B
// 方式二:通过 globalData
const app = getApp()
app.globalData.sharedData = value
// 方式三:通过 EventBus
// 实现简单的事件发布订阅
const eventBus = {
events: {},
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
},
emit(event, data) {
const callbacks = this.events[event] || []
callbacks.forEach(cb => cb(data))
}
}
常见误区:
- 误区一:子组件可以直接修改 properties。应该通过事件通知父组件修改
- 误区二:triggerEvent 可以直接传给兄弟组件。需要通过父组件中转
16. 组件属性
定义: 组件属性是父组件向子组件传递数据的方式,在 Component 的 properties 中定义。
属性定义:
Component({
properties: {
// 简写形式
name: String,
// 完整形式
config: {
type: Object,
value: { show: true },
observer: function(newVal, oldVal, changedPath) {
console.log('属性变化:', newVal)
}
},
// 多类型
value: {
type: [String, Number],
value: ''
}
}
})
属性类型:
- String
- Number
- Boolean
- Object
- Array
- null(任意类型)
17. 组件方法
定义: 组件方法是组件内部定义的函数,包括事件处理函数和自定义方法。
Component({
methods: {
// 事件处理函数
onTap(event) {
console.log('点击事件:', event)
},
// 自定义方法
reset() {
this.setData({ count: 0 })
},
// 获取数据
getData() {
return this.data.list
}
}
})
18. 组件事件
定义: 组件事件是组件对外暴露的通信方式,子组件通过 triggerEvent 触发,父组件通过 bind 或 catch 绑定。
// 子组件触发事件
Component({
methods: {
handleConfirm() {
this.triggerEvent('confirm', {
selected: this.data.selected
}, {
bubbles: true, // 是否冒泡
composed: true, // 是否可以穿越组件边界
capturePhase: false // 是否有捕获阶段
})
}
}
})
<!-- 父组件绑定事件 -->
<child bind:confirm="onConfirm" />
<child catch:confirm="onConfirm" /> <!-- 阻止冒泡 -->
19. 组件插槽
定义: 组件插槽允许父组件向子组件内插入内容。
单个插槽:
<!-- 子组件 wxml -->
<view class="wrapper">
<slot></slot>
</view>
<!-- 父组件使用 -->
<my-component>
<text>插入的内容</text>
</my-component>
多个插槽:
// 子组件 js - 启用多插槽
Component({
options: {
multipleSlots: true
}
})
<!-- 子组件 wxml -->
<view class="header">
<slot name="header"></slot>
</view>
<view class="content">
<slot name="content"></slot>
</view>
<view class="footer">
<slot name="footer"></slot>
</view>
<!-- 父组件使用 -->
<my-component>
<text slot="header">头部</text>
<text slot="content">内容</text>
<text slot="footer">底部</text>
</my-component>
20. Behavior
定义: Behavior 是用于组件间代码共享的特性,类似于"mixins"或"traits",可以定义一组共用的属性、数据、方法等。
创建 Behavior:
// behaviors/my-behavior.js
export const pageRefreshBehavior = Behavior({
data: {
loading: false,
hasError: false
},
methods: {
showLoading() {
this.setData({ loading: true, hasError: false })
},
hideLoading() {
this.setData({ loading: false })
},
showError(msg) {
this.setData({ hasError: true })
wx.showToast({ title: msg, icon: 'none' })
}
}
})
使用 Behavior:
// components/my-component/my-component.js
import { pageRefreshBehavior } from '../../behaviors/my-behavior'
Component({
behaviors: [pageRefreshBehavior],
data: {
name: ''
},
methods: {
fetchData() {
this.showLoading()
wx.request({
url: '/api/data'
}).then(res => {
this.setData({ list: res.data })
}).catch(err => {
this.showError('加载失败')
}).finally(() => {
this.hideLoading()
})
}
}
})
21. 组件复用
复用方式:
| 方式 | 适用场景 |
|---|---|
| 自定义组件 | UI 组件复用 |
| Behavior | 逻辑代码复用 |
| 模板(template) | 纯视图复用 |
| WXS | 纯逻辑复用(过滤、计算) |
模板复用示例:
<!-- templates/card.wxml -->
<template name="card">
<view class="card">
<text class="title">{{title}}</text>
<text class="desc">{{desc}}</text>
</view>
</template>
<!-- 使用模板 -->
<import src="/templates/card.wxml" />
<template is="card" data="{{title: '标题', desc: '描述'}}" />
WXS 复用示例:
<!-- filters.wxs -->
var filters = {
formatPrice: function(price) {
return '¥' + price.toFixed(2)
},
formatDate: function(date) {
var d = getDate(date)
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
}
}
module.exports = {
formatPrice: filters.formatPrice,
formatDate: filters.formatDate
}
<!-- 使用 WXS -->
<wxs src="/filters.wxs" module="filters" />
<text>{{filters.formatPrice(price)}}</text>
四、数据绑定与事件处理
22. 数据绑定
定义: 数据绑定是将逻辑层(JS)的数据传递到渲染层(WXML),实现视图动态更新。
绑定方式:
| 方式 | 语法 | 说明 |
|---|---|---|
| 内容绑定 | \{\{value\}\} | 在文本中插入变量 |
| 属性绑定 | attr="\{\{value\}\}" | 在属性中使用变量 |
| 控制属性 | wx:if="\{\{condition\}\}" | 条件判断中使用 |
| 关键字 | \{\{true\}\} / \{\{false\}\} | 布尔值绑定 |
| 运算 | \{\{a + b\}\} | 支持简单运算 |
代码示例:
<!-- WXML -->
<view>
<!-- 内容绑定 -->
<text>{{message}}</text>
<!-- 属性绑定 -->
<image src="{{imageUrl}}" />
<view id="item-{{id}}">动态ID</view>
<!-- 三元运算 -->
<text class="{{isActive ? 'active' : ''}}">状态</text>
<!-- 数学运算 -->
<text>价格: {{price * quantity}}</text>
<!-- 字符串操作 -->
<text>{{str.substring(0, 10)}}</text>
<!-- 数组操作 -->
<text>长度: {{array.length}}</text>
</view>
// JS
Page({
data: {
message: 'Hello World',
imageUrl: '/images/logo.png',
id: 123,
isActive: true,
price: 19.9,
quantity: 2,
str: 'Hello World',
array: [1, 2, 3, 4, 5]
}
})
常见误区:
- 误区一:可以在 WXML 中调用 JS 函数。WXML 不支持直接调用函数,应使用 WXS
- 误区二:数据绑定是双向的。小程序是单向数据绑定,需要通过事件手动更新数据
23. setData
定义: setData 是小程序中更新数据的唯一方式,将数据从逻辑层发送到渲染层,实现异步视图更新。
原理:
- 逻辑层调用 setData,将数据序列化
- 数据通过 JSBridge 传递给渲染层
- 渲染层对比虚拟 DOM,计算最小更新
- 渲染层更新真实 DOM,触发视图重绘
基础用法:
Page({
data: {
name: '初始值',
count: 0
},
updateData() {
this.setData({
name: '新值',
count: this.data.count + 1
})
}
})
路径更新:
Page({
data: {
user: { name: 'Tom', age: 20 },
list: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
},
updatePath() {
// 更新对象属性
this.setData({
'user.name': 'Jerry'
})
// 更新数组元素
this.setData({
'list[0].name': 'A-Updated',
'list[1]': { id: 3, name: 'C' }
})
}
})
回调函数:
Page({
data: { count: 0 },
updateWithCallback() {
this.setData({
count: this.data.count + 1
}, () => {
// setData 完成后的回调
console.log('更新完成后的值:', this.data.count)
})
}
})
性能优化:
| 优化策略 | 说明 | 示例 |
|---|---|---|
| 减少 setData 频率 | 合并多次更新为一次 | this.setData({ a: 1, b: 2 }) 而非两次调用 |
| 减少 setData 数据量 | 只传递变化的数据 | 使用路径更新,而非整个对象 |
| 避免频繁 setData | 列表数据使用局部更新 | 使用虚拟列表 |
| 后台页面不调用 | 页面隐藏时停止更新 | 在 onHide 中暂停定时器 |
| 避免 setData 大数据 | 不传递超大对象/数组 | 只传递必要字段 |
最佳实践:
Page({
data: {
list: [],
total: 0
},
// 局部更新数组元素
updateItem(index, newData) {
this.setData({
[`list[${index}]`]: Object.assign({}, this.data.list[index], newData)
})
},
// 追加数组元素(避免全量更新)
appendItems(newItems) {
const index = this.data.list.length
const updateObj = {}
newItems.forEach((item, i) => {
updateObj[`list[${index + i}]`] = item
})
updateObj.total = index + newItems.length
this.setData(updateObj)
},
// 后台停止更新
onHide() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
})
常见误区:
- 误区一:直接修改 this.data。必须使用 setData,否则视图不会更新
- 误区二:频繁调用 setData。会导致性能问题
- 误区三:setData 传递整个大对象。应使用路径更新减少数据传输
24. 数据更新
定义: 数据更新指在小程序中修改和同步数据到视图的过程。
更新方式对比:
| 方式 | 是否更新视图 | 说明 |
|---|---|---|
this.setData() | 是 | 推荐方式,同步到视图 |
this.data.xxx = value | 否 | 仅修改数据,不触发视图更新 |
数据更新策略:
Page({
data: {
counter: 0,
form: { name: '', email: '' },
items: []
},
// 方式一:全量更新(适合少量数据)
fullUpdate() {
this.setData({
form: { name: 'Tom', email: 'tom@example.com' }
})
},
// 方式二:路径更新(适合部分更新)
partialUpdate() {
this.setData({
'form.name': 'Tom'
})
},
// 方式三:批量更新(合并多次更新)
batchUpdate() {
this.setData({
counter: 1,
'form.name': 'Tom',
'form.email': 'tom@example.com'
})
}
})
25. 列表渲染
定义:
列表渲染使用 wx:for 指令遍历数组,重复渲染模板块。
基础用法:
<!-- wx:for 渲染列表 -->
<view wx:for="{{users}}" wx:key="id">
<text>{{index}}: {{item.name}} - {{item.age}}岁</text>
</view>
<!-- 指定 item 和 index 名称 -->
<view wx:for="{{users}}" wx:for-item="user" wx:for-index="idx" wx:key="id">
<text>{{idx}}: {{user.name}}</text>
</view>
wx:key 的作用:
- 提高列表渲染性能
- 保持组件状态
- 避免不必要的重新渲染
<!-- 使用唯一 id 作为 key -->
<view wx:for="{{list}}" wx:key="id">{{item.name}}</view>
<!-- 使用 *this 表示数组元素为字符串等基础类型 -->
<view wx:for="{{['a', 'b', 'c']}}" wx:key="*this">{{item}}</view>
<!-- 使用保留字 *this -->
<block wx:for="{{12345}}" wx:key="*this">
<view>数字: {{item}}</view>
</block>
嵌套列表:
<view wx:for="{{categories}}" wx:key="id">
<text>{{item.name}}</text>
<view wx:for="{{item.children}}" wx:for-item="child" wx:key="id">
<text>-- {{child.name}}</text>
</view>
</view>
最佳实践:
// JS - 使用唯一 ID 作为 key
Page({
data: {
list: [
{ id: 'uuid-1', name: 'Item 1', checked: false },
{ id: 'uuid-2', name: 'Item 2', checked: true }
]
}
})
常见误区:
- 误区一:不使用 wx:key。会导致性能问题和状态混乱
- 误区二:使用 index 作为 key。在列表动态变化时会导致问题
26. 条件渲染
定义: 条件渲染根据条件决定是否渲染元素。
指令:
| 指令 | 说明 |
|---|---|
wx:if | 条件为 true 时渲染 |
wx:elif | 额外条件判断 |
wx:else | 条件为 false 时渲染 |
hidden | 通过 CSS display 控制显示隐藏 |
wx:if vs hidden 对比:
| 维度 | wx:if | hidden |
|---|---|---|
| 渲染方式 | 条件为 false 时不渲染(销毁) | 始终渲染,通过 CSS 控制 |
| 切换开销 | 较高(需要创建/销毁) | 较低 |
| 初始渲染 | 较低(条件为 false 时不渲染) | 较高(始终渲染) |
| 适用场景 | 条件很少切换 | 频繁切换显示状态 |
代码示例:
<!-- wx:if -->
<view wx:if="{{score >= 90}}">优秀</view>
<view wx:elif="{{score >= 60}}">及格</view>
<view wx:else>不及格</view>
<!-- block wx:if(不渲染自身节点) -->
<block wx:if="{{isLoggedIn}}">
<view>欢迎回来,{{userName}}</view>
<button>退出登录</button>
</block>
<!-- hidden -->
<view hidden="{{!isLoading}}">加载中...</view>
27. 事件绑定
定义: 事件绑定是将用户操作(点击、滑动等)与处理函数关联。
绑定方式:
| 语法 | 说明 | 是否冒泡 |
|---|---|---|
bind:eventName | 绑定事件 | 是 |
bind:eventName / catch:eventName | catch 阻止冒泡 | catch 阻止冒泡 |
mut-bind:eventName | 互斥事件绑定 | 是,但互斥 |
常见事件:
| 事件 | 说明 |
|---|---|
| tap | 点击 |
| longpress | 长按(超过 350ms) |
| touchstart | 触摸开始 |
| touchmove | 触摸移动 |
| touchend | 触摸结束 |
| touchcancel | 触摸取消 |
| input | 输入框输入 |
| change | 值变化 |
代码示例:
<view bind:tap="onTap" catch:longpress="onLongPress">
点击或长按
</view>
<input bind:input="onInput" />
<view bind:touchstart="onTouchStart"
bind:touchmove="onTouchMove"
bind:touchend="onTouchEnd">
触摸区域
</view>
Page({
onTap(event) {
console.log('点击', event)
},
onLongPress(event) {
console.log('长按', event)
},
onInput(event) {
this.setData({ value: event.detail.value })
},
onTouchStart(event) {
console.log('触摸开始', event.touches)
}
})
28. 事件对象
定义: 事件对象包含事件的详细信息,通过事件处理函数的参数传递。
事件对象属性:
| 属性 | 说明 |
|---|---|
| type | 事件类型 |
| timeStamp | 事件生成时的时间戳 |
| target | 触发事件的组件 |
| currentTarget | 当前绑定事件的组件 |
| touches | 触摸点信息数组 |
| changedTouches | 变化的触摸点信息数组 |
| detail | 自定义事件携带的额外信息 |
代码示例:
Page({
onTap(event) {
console.log('事件类型:', event.type)
console.log('触发事件的目标组件:', event.target)
console.log('当前绑定事件的组件:', event.currentTarget)
console.log('自定义数据:', event.detail)
console.log('dataset:', event.currentTarget.dataset)
}
})
<!-- 通过 data- 传递自定义数据 -->
<view data-id="{{item.id}}" data-name="{{item.name}}" bind:tap="onTap">
{{item.name}}
</view>
// 获取自定义数据
Page({
onTap(event) {
const { id, name } = event.currentTarget.dataset
console.log('id:', id, 'name:', name)
}
})
29. 事件传参
定义: 小程序事件传参不能直接在绑定中传参,需要通过 data- 属性或自定义事件。
方式一:data- 属性
<view data-id="{{item.id}}" data-index="{{index}}" bind:tap="handleTap">
{{item.name}}
</view>
Page({
handleTap(event) {
const { id, index } = event.currentTarget.dataset
console.log('id:', id, 'index:', index)
}
})
方式二:自定义组件 triggerEvent
// 子组件
Component({
methods: {
handleClick() {
this.triggerEvent('custom-event', {
id: this.data.id,
data: this.data.data
})
}
}
})
<!-- 父组件 -->
<child bind:custom-event="onCustomEvent" />
// 父组件
Page({
onCustomEvent(event) {
const { id, data } = event.detail
}
})
30. 事件冒泡 / 事件捕获 / 阻止事件冒泡
定义:
- 事件冒泡:事件从触发节点向上传递到父节点
- 事件捕获:事件从外层向内部传递(微信小程序不直接支持)
- 阻止冒泡:使用 catch 阻止事件继续向上传递
事件冒泡:
<view bind:tap="onOuterTap" style="padding: 30px; background: #f0f0f0;">
外层
<view bind:tap="onInnerTap" style="padding: 20px; background: #ccc;">
内层
<view bind:tap="onCenterTap" style="padding: 10px; background: #999;">
中心
</view>
</view>
</view>
Page({
onOuterTap() { console.log('外层触发') },
onInnerTap() { console.log('内层触发') },
onCenterTap() { console.log('中心触发') }
})
// 点击"中心"时,依次输出:中心触发 → 内层触发 → 外层触发
阻止冒泡:
<!-- 使用 catch 代替 bind 阻止冒泡 -->
<view catch:tap="onInnerTap">
<view catch:tap="onCenterTap">中心</view>
</view>
// 点击"中心"时,仅输出:中心触发
mut-bind(互斥事件):
<!-- mut-bind 只触发第一个被触发的事件,其他 mut-bind 失效 -->
<view mut-bind:tap="onTap1">
<view mut-bind:tap="onTap2">子元素</view>
</view>
五、API 调用
31. 小程序 API 概览
微信小程序提供了丰富的原生能力 API,涵盖网络、媒体、位置、设备、开放接口等多个方面。
API 分类:
| 分类 | 常用 API |
|---|---|
| 网络 | wx.request, wx.uploadFile, wx.downloadFile |
| 媒体 | wx.chooseImage, wx.previewImage, wx.chooseVideo |
| 位置 | wx.getLocation, wx.openLocation, wx.chooseLocation |
| 设备 | wx.getSystemInfo, wx.scanCode, wx.setClipboardData |
| 界面 | wx.showToast, wx.showModal, wx.navigateTo |
| 开放接口 | wx.login, wx.getUserProfile, wx.requestPayment |
| 数据缓存 | wx.setStorage, wx.getStorage, wx.clearStorage |
| 文件 | wx.getFileSystemManager |
API 调用规范:
// Promise 风格调用(基础库 2.10.2+)
wx.request({
url: 'https://api.example.com/data'
}).then(res => {
console.log('成功', res.data)
}).catch(err => {
console.error('失败', err)
})
// 回调风格调用
wx.request({
url: 'https://api.example.com/data',
success(res) {
console.log('成功', res.data)
},
fail(err) {
console.error('失败', err)
},
complete() {
console.log('完成')
}
})
32. 网络请求 / wx.request
定义: wx.request 用于发起 HTTPS 网络请求。
配置要求:
- 必须在小程序管理后台配置请求域名
- 仅支持 HTTPS 协议
- 默认超时 60 秒
基础用法:
wx.request({
url: 'https://api.example.com/data',
method: 'GET',
data: {
page: 1,
size: 10
},
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
success(res) {
if (res.statusCode === 200) {
console.log('请求成功', res.data)
}
},
fail(err) {
console.error('请求失败', err)
}
})
请求封装最佳实践:
// utils/request.js
const BASE_URL = 'https://api.example.com'
function request(options) {
const { url, method = 'GET', data = {}, header = {} } = options
// 获取 token
const token = wx.getStorageSync('token')
return new Promise((resolve, reject) => {
wx.request({
url: `${BASE_URL}${url}`,
method,
data,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...header
},
timeout: 10000,
success(res) {
if (res.statusCode === 200) {
if (res.data.code === 0) {
resolve(res.data.data)
} else {
wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
reject(res.data)
}
} else if (res.statusCode === 401) {
// token 过期,跳转登录
wx.removeStorageSync('token')
wx.redirectTo({ url: '/pages/login/login' })
reject(new Error('未授权'))
} else {
reject(new Error(`HTTP ${res.statusCode}`))
}
},
fail(err) {
wx.showToast({ title: '网络异常', icon: 'none' })
reject(err)
}
})
})
}
module.exports = {
get: (url, data, header) => request({ url, method: 'GET', data, header }),
post: (url, data, header) => request({ url, method: 'POST', data, header }),
put: (url, data, header) => request({ url, method: 'PUT', data, header }),
delete: (url, data, header) => request({ url, method: 'DELETE', data, header })
}
// 使用
const request = require('../../utils/request')
Page({
onLoad() {
request.get('/api/users', { page: 1 }).then(users => {
this.setData({ users })
})
}
})
33. 文件上传 / wx.uploadFile
定义: wx.uploadFile 用于将本地资源上传到服务器。
代码示例:
// 选择图片并上传
Page({
data: { uploadedUrl: '' },
uploadImage() {
wx.chooseMedia({
count: 1,
mediaType: ['image'],
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath
wx.uploadFile({
url: 'https://api.example.com/upload',
filePath: tempFilePath,
name: 'file',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
formData: {
type: 'avatar'
},
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data)
this.setData({ uploadedUrl: data.url })
wx.showToast({ title: '上传成功', icon: 'success' })
},
fail: (err) => {
wx.showToast({ title: '上传失败', icon: 'none' })
}
})
}
})
}
})
34. 文件下载 / wx.downloadFile
定义: wx.downloadFile 用于下载文件资源到本地。
代码示例:
Page({
downloadFile() {
wx.downloadFile({
url: 'https://example.com/file.pdf',
success: (res) => {
if (res.statusCode === 200) {
// 打开文档
wx.openDocument({
filePath: res.tempFilePath,
showMenu: true,
success() {
console.log('打开文档成功')
}
})
}
},
fail: (err) => {
wx.showToast({ title: '下载失败', icon: 'none' })
}
})
}
})
35. 图片选择 / wx.chooseImage
定义: wx.chooseImage 用于从本地相册选择图片或使用相机拍照。
代码示例:
Page({
data: { images: [] },
chooseImages() {
wx.chooseImage({
count: 9, // 最多选择数量
sizeType: ['compressed'], // original 原图, compressed 压缩图
sourceType: ['album', 'camera'], // 来源
success: (res) => {
this.setData({
images: [...this.data.images, ...res.tempFilePaths]
})
}
})
}
})
注意:wx.chooseImage 已被 wx.chooseMedia 替代,建议使用新版 API。
36. 图片预览 / wx.previewImage
定义: wx.previewImage 用于全屏预览图片,支持缩放和滑动切换。
代码示例:
Page({
data: { images: ['/img/1.jpg', '/img/2.jpg', '/img/3.jpg'] },
previewImage(event) {
const index = event.currentTarget.dataset.index
wx.previewImage({
current: this.data.images[index], // 当前显示图片
urls: this.data.images // 所有图片列表
})
}
})
<view wx:for="{{images}}" wx:key="*this">
<image src="{{item}}" bind:tap="previewImage" data-index="{{index}}" />
</view>
37. 扫码 / wx.scanCode
定义: wx.scanCode 用于调起客户端扫码界面进行扫码。
代码示例:
Page({
scanCode() {
wx.scanCode({
onlyFromCamera: false, // 是否只能从相机扫码
scanType: ['qrCode', 'barCode'],
success: (res) => {
console.log('扫码结果:', res.result)
console.log('码类型:', res.scanType)
// 根据扫码结果处理
if (res.result.startsWith('http')) {
wx.navigateToMiniProgram({ appId: 'xxx' })
} else {
this.setData({ scanResult: res.result })
}
}
})
}
})
38. 定位 / wx.getLocation
定义: wx.getLocation 用于获取当前的地理位置信息。
配置要求:
- 需要在 app.json 中声明 permission
- 需要用户授权
代码示例:
// app.json
{
"permission": {
"scope.userLocation": {
"desc": "您的位置信息将用于展示附近内容"
}
}
}
Page({
getLocation() {
wx.getLocation({
type: 'wgs84', // wgs84 返回 gps 坐标, gcj02 返回可用于 wx.openLocation 的坐标
altitude: false, // 是否返回高度
isHighAccuracy: true, // 是否高精度
highAccuracyExpireTime: 3000, // 高精度有效期
success: (res) => {
console.log('经度:', res.longitude)
console.log('纬度:', res.latitude)
console.log('速度:', res.speed)
console.log('精确度:', res.accuracy)
this.setData({
location: {
latitude: res.latitude,
longitude: res.longitude
}
})
},
fail: (err) => {
console.error('获取位置失败', err)
// 引导用户授权
wx.openSetting()
}
})
}
})
39. 地图 / wx.openLocation
定义: wx.openLocation 用于打开微信内置地图查看位置。
代码示例:
Page({
openLocation() {
wx.openLocation({
latitude: 39.9042,
longitude: 116.4074,
name: '天安门',
address: '北京市东城区长安街',
scale: 15, // 缩放比例 5-18
success() {
console.log('打开地图成功')
}
})
}
})
使用 map 组件:
<map
id="myMap"
latitude="{{latitude}}"
longitude="{{longitude}}"
scale="14"
markers="{{markers}}"
show-location
bindmarkertap="onMarkerTap"
style="width: 100%; height: 400px;"
/>
Page({
data: {
latitude: 39.9042,
longitude: 116.4074,
markers: [{
id: 1,
latitude: 39.9042,
longitude: 116.4074,
title: '天安门',
iconPath: '/images/marker.png',
width: 30,
height: 30
}]
},
onMarkerTap(event) {
console.log('点击标记', event.markerId)
}
})
40. 支付 / wx.requestPayment
定义: wx.requestPayment 用于调起微信支付。
支付流程:
用户下单 → 后端创建订单 → 返回支付参数 → 调起微信支付 → 支付结果回调 → 后端确认支付
代码示例:
Page({
async makePayment(orderId) {
try {
// 1. 调用后端获取支付参数
const payParams = await request.post('/api/pay', {
orderId: orderId
})
// 2. 调起微信支付
await wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType,
paySign: payParams.paySign
})
// 3. 支付成功
wx.showToast({ title: '支付成功', icon: 'success' })
this.setData({ payStatus: 'success' })
// 4. 通知后端
await request.post('/api/pay/success', { orderId })
} catch (err) {
if (err.errMsg === 'requestPayment:fail cancel') {
wx.showToast({ title: '取消支付', icon: 'none' })
} else {
wx.showToast({ title: '支付失败', icon: 'none' })
}
this.setData({ payStatus: 'failed' })
}
}
})
41. 登录 / wx.login
定义: wx.login 用于调用接口获取临时登录凭证(code),用于后端换取用户登录态。
登录流程:
小程序 wx.login() → 获取 code → 发送 code 到后端 → 后端用 code + appid + secret 换取 openid 和 session_key → 后端生成自定义登录态返回 token → 小程序存储 token
代码示例:
// app.js
App({
onLaunch() {
this.login()
},
login() {
return new Promise((resolve, reject) => {
wx.login({
success: (res) => {
if (res.code) {
// 将 code 发送到后端
wx.request({
url: 'https://api.example.com/login',
method: 'POST',
data: { code: res.code },
success: (response) => {
const { token, userInfo } = response.data
wx.setStorageSync('token', token)
this.globalData.token = token
this.globalData.userInfo = userInfo
resolve(userInfo)
},
fail: reject
})
} else {
reject(new Error('登录失败'))
}
}
})
})
},
globalData: {
token: '',
userInfo: null
}
})
42. 获取用户信息 / wx.getUserProfile
定义: wx.getUserProfile 用于获取用户信息(昵称、头像等),需要用户主动触发。
注意: wx.getUserInfo 已废弃,现在使用 wx.getUserProfile 或头像昵称填写组件。
代码示例:
Page({
getUserInfo() {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
console.log('用户信息:', res.userInfo)
this.setData({
userInfo: res.userInfo
})
// 保存到本地
wx.setStorageSync('userInfo', res.userInfo)
},
fail: (err) => {
console.log('用户拒绝授权')
}
})
}
})
使用 button 授权:
<button bind:tap="getUserProfile">获取用户信息</button>
<!-- 或使用新的头像昵称填写组件 -->
<button open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">选择头像</button>
<input type="nickname" placeholder="请输入昵称" />
43. 存储 / wx.setStorage / wx.getStorage / wx.removeStorage
定义: 小程序提供本地存储能力,用于持久化保存数据。
代码示例:
// 异步存储
wx.setStorage({
key: 'userInfo',
data: { name: 'Tom', age: 20 },
success() {
console.log('存储成功')
}
})
// 异步获取
wx.getStorage({
key: 'userInfo',
success(res) {
console.log('获取到数据:', res.data)
},
fail() {
console.log('数据不存在')
}
})
// 异步删除
wx.removeStorage({
key: 'userInfo',
success() {
console.log('删除成功')
}
})
// 同步操作
wx.setStorageSync('key', 'value')
const value = wx.getStorageSync('key')
wx.removeStorageSync('key')
六、路由与导航
44. 小程序路由概览
定义: 小程序路由用于管理页面之间的跳转和导航,由微信客户端统一管理。
路由方式对比:
| API | 功能 | 页面栈变化 | 使用场景 |
|---|---|---|---|
| wx.navigateTo | 保留当前页面,跳转到新页面 | 页面栈 +1 | 普通页面跳转 |
| wx.redirectTo | 关闭当前页面,跳转到新页面 | 页面栈不变 | 不需要返回的场景 |
| wx.switchTab | 跳转到 tabBar 页面 | 清空非 tabBar 页面栈 | 切换 tab |
| wx.reLaunch | 关闭所有页面,打开新页面 | 页面栈重置为1 | 重新登录、退出 |
| wx.navigateBack | 返回上一级或多级页面 | 页面栈 -N | 返回上一页 |
45. wx.navigateTo
// 跳转到新页面,保留当前页面
wx.navigateTo({
url: '/pages/detail/detail?id=123&name=test',
success() {
console.log('跳转成功')
},
fail(err) {
console.log('跳转失败', err)
}
})
限制: 页面栈最多 10 层,超过后 navigateTo 会失败。
46. wx.redirectTo
// 关闭当前页面,跳转到新页面
wx.redirectTo({
url: '/pages/login/login'
})
与 navigateTo 的区别:
- navigateTo 保留当前页面,可以返回
- redirectTo 关闭当前页面,不能返回到当前页面
47. wx.switchTab
// 跳转到 tabBar 页面,关闭其他非 tabBar 页面
wx.switchTab({
url: '/pages/index/index'
})
注意: switchTab 只能跳转到 tabBar 页面,且不能带参数。
48. wx.reLaunch
// 关闭所有页面,打开新页面
wx.reLaunch({
url: '/pages/index/index'
})
典型场景: 退出登录、强制重新登录。
49. wx.navigateBack
// 返回上一页
wx.navigateBack({
delta: 1 // 返回的页面数,默认 1
})
// 返回多级
wx.navigateBack({
delta: 2 // 返回上两级页面
})
50. 路由传参 / 获取路由参数
传参方式:
// 通过 URL 参数传递
wx.navigateTo({
url: `/pages/detail/detail?id=${id}&type=${type}`
})
// 通过事件通道传递(基础库 2.7.3+)
wx.navigateTo({
url: '/pages/detail/detail',
events: {
// 监听被打开页面发送到当前页面的数据
acceptDataFromOpenedPage(data) {
console.log(data)
}
},
success(res) {
// 通过 eventChannel 向被打开页面传送数据
res.eventChannel.emit('acceptDataFromOpenerPage', {
source: 'index',
data: { id: 123 }
})
}
})
获取参数:
// pages/detail/detail.js
Page({
onLoad(options) {
// 方式一:从 options 获取 URL 参数
const { id, type } = options
console.log('URL参数:', id, type)
// 方式二:通过 eventChannel 获取事件通道数据
const eventChannel = this.getOpenerEventChannel()
eventChannel.on('acceptDataFromOpenerPage', (data) => {
console.log('事件通道数据:', data)
})
// 向上一页发送数据
eventChannel.emit('acceptDataFromOpenedPage', {
result: 'success'
})
}
})
51. 页面栈
定义: 小程序维护一个页面栈,记录当前所有已打开的页面。
获取页面栈:
const pages = getCurrentPages()
console.log('页面栈长度:', pages.length)
console.log('当前页面:', pages[pages.length - 1])
console.log('上一个页面:', pages[pages.length - 2])
页面栈变化规则:
| 操作 | 页面栈变化 |
|---|---|
| navigateTo | 新页面入栈 |
| redirectTo | 当前页面出栈,新页面入栈 |
| navigateBack | 页面连续出栈 |
| switchTab | 清空栈,新页面入栈 |
| reLaunch | 清空栈,新页面入栈 |
实战场景:修改上一页数据
Page({
goBackAndUpdate() {
const pages = getCurrentPages()
if (pages.length > 1) {
const prevPage = pages[pages.length - 2]
prevPage.setData({
needRefresh: true
})
}
wx.navigateBack()
}
})
七、本地存储
52. 本地存储概览
定义: 小程序提供本地存储能力,以键值对形式存储数据,类似于浏览器的 localStorage。
存储方式对比:
| API | 说明 | 是否异步 |
|---|---|---|
| wx.setStorage | 设置存储 | 异步 |
| wx.getStorage | 获取存储 | 异步 |
| wx.removeStorage | 删除存储 | 异步 |
| wx.clearStorage | 清空所有存储 | 异步 |
| wx.setStorageSync | 同步设置存储 | 同步 |
| wx.getStorageSync | 同步获取存储 | 同步 |
| wx.removeStorageSync | 同步删除存储 | 同步 |
| wx.clearStorageSync | 同步清空存储 | 同步 |
| wx.getStorageInfo | 获取存储信息 | 异步 |
| wx.getStorageInfoSync | 同步获取存储信息 | 同步 |
53. 同步存储
// 同步写入
try {
wx.setStorageSync('token', 'abc123')
wx.setStorageSync('userInfo', { name: 'Tom', age: 20 })
} catch (e) {
console.error('写入失败', e)
}
// 同步读取
try {
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
} catch (e) {
console.error('读取失败', e)
}
// 同步删除
wx.removeStorageSync('token')
// 同步清空
wx.clearStorageSync()
54. 异步存储
// 异步写入
wx.setStorage({
key: 'token',
data: 'abc123',
success() { console.log('写入成功') },
fail(err) { console.error('写入失败', err) }
})
// 异步读取
wx.getStorage({
key: 'token',
success(res) { console.log('读取成功', res.data) },
fail(err) { console.error('读取失败', err) }
})
// 获取存储信息
wx.getStorageInfo({
success(res) {
console.log('所有 keys:', res.keys)
console.log('当前占用空间:', res.currentSize) // KB
console.log('限制空间:', res.limitSize) // KB
}
})
55. 存储限制
限制说明:
- 单个 key 限制:1MB
- 总容量限制:10MB
- key 最大长度:1024 字节
- 支持的数据类型:字符串、数字、布尔值、对象、数组
最佳实践:
// 封装存储工具类
const Storage = {
// 设置带过期时间的存储
set(key, value, expireTime) {
const data = {
value,
expireTime: expireTime ? Date.now() + expireTime : null
}
wx.setStorageSync(key, JSON.stringify(data))
},
// 获取并检查过期
get(key) {
try {
const raw = wx.getStorageSync(key)
if (!raw) return null
const data = JSON.parse(raw)
if (data.expireTime && Date.now() > data.expireTime) {
// 已过期,删除
wx.removeStorageSync(key)
return null
}
return data.value
} catch (e) {
return null
}
},
// 删除
remove(key) {
wx.removeStorageSync(key)
},
// 清空
clear() {
wx.clearStorageSync()
}
}
// 使用
Storage.set('token', 'abc123', 7 * 24 * 60 * 60 * 1000) // 7天过期
const token = Storage.get('token')
56. 缓存策略
策略分类:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 永久缓存 | 数据持久化存储 | 用户信息、偏好设置 |
| 时间缓存 | 设置过期时间 | Token、接口数据 |
| 版本缓存 | 加入版本号控制 | 需要强制更新的数据 |
| 条件缓存 | 根据条件决定是否缓存 | 需要实时性的数据 |
实现示例:
// 带版本号的缓存
const Cache = {
VERSION: '1.0.0',
set(key, value) {
const data = {
value,
version: this.VERSION,
timestamp: Date.now()
}
wx.setStorageSync(key, JSON.stringify(data))
},
get(key) {
try {
const raw = wx.getStorageSync(key)
if (!raw) return null
const data = JSON.parse(raw)
if (data.version !== this.VERSION) {
// 版本不匹配,删除旧缓存
wx.removeStorageSync(key)
return null
}
return data.value
} catch (e) {
return null
}
},
clear() {
wx.clearStorageSync()
}
}
八、网络请求
57. 网络请求配置
域名配置:
- 在小程序管理后台配置 request 合法域名
- 域名必须使用 HTTPS 协议
- 域名每月修改次数有限制
开发环境设置:
// project.config.json
{
"setting": {
"urlCheck": false, // 开发时关闭域名校验
"es6": true,
"minified": true
}
}
58. 请求域名配置
配置要求:
- 域名必须已备案
- 必须使用 HTTPS 协议
- 域名不支持 IP 地址和 localhost
- 端口必须为 443(HTTPS)
开发时绕过: 在开发者工具中勾选"不校验合法域名",仅开发环境有效。
59. 请求封装 / 请求拦截 / 响应拦截
完整封装示例:
// utils/http.js
const BASE_URL = 'https://api.example.com'
class HttpRequest {
constructor() {
this.interceptors = {
request: [],
response: []
}
}
// 请求拦截器
useRequestInterceptor(fn) {
this.interceptors.request.push(fn)
}
// 响应拦截器
useResponseInterceptor(fn) {
this.interceptors.response.push(fn)
}
// 发起请求
request(options) {
// 执行请求拦截器
let config = { ...options }
this.interceptors.request.forEach(fn => {
config = fn(config) || config
})
// 添加公共配置
config.url = `${BASE_URL}${config.url}`
config.header = {
'Content-Type': 'application/json',
'Authorization': wx.getStorageSync('token') || '',
...config.header
}
config.timeout = config.timeout || 10000
return new Promise((resolve, reject) => {
wx.request({
...config,
success: (res) => {
// 执行响应拦截器
let result = res
this.interceptors.response.forEach(fn => {
result = fn(result) || result
})
if (res.statusCode === 200) {
if (res.data.code === 0) {
resolve(res.data.data)
} else {
wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
reject(res.data)
}
} else if (res.statusCode === 401) {
wx.removeStorageSync('token')
wx.redirectTo({ url: '/pages/login/login' })
reject(new Error('未授权'))
} else {
reject(new Error(`HTTP ${res.statusCode}`))
}
},
fail: (err) => {
wx.showToast({ title: '网络异常,请检查网络', icon: 'none' })
reject(err)
}
})
})
}
get(url, data, options = {}) {
return this.request({ url, method: 'GET', data, ...options })
}
post(url, data, options = {}) {
return this.request({ url, method: 'POST', data, ...options })
}
put(url, data, options = {}) {
return this.request({ url, method: 'PUT', data, ...options })
}
delete(url, data, options = {}) {
return this.request({ url, method: 'DELETE', data, ...options })
}
}
export const http = new HttpRequest()
// 使用示例
http.useRequestInterceptor((config) => {
console.log('发起请求:', config)
return config
})
http.useResponseInterceptor((response) => {
console.log('收到响应:', response)
return response
})
60. 错误处理
错误类型:
Page({
fetchData() {
wx.request({
url: 'https://api.example.com/data',
success(res) {
if (res.statusCode >= 500) {
// 服务器错误
this.handleServerError(res)
} else if (res.statusCode === 404) {
// 资源不存在
wx.showToast({ title: '资源不存在', icon: 'none' })
} else if (res.statusCode === 401) {
// 未授权
wx.redirectTo({ url: '/pages/login/login' })
}
},
fail(err) {
// 网络错误
switch (err.errMsg) {
case 'request:fail timeout':
wx.showToast({ title: '请求超时', icon: 'none' })
break
case 'request:fail url not in domain list':
wx.showToast({ title: '域名未配置', icon: 'none' })
break
default:
wx.showToast({ title: '网络异常', icon: 'none' })
}
}
})
}
})
61. 请求超时
设置超时时间:
wx.request({
url: 'https://api.example.com/data',
timeout: 10000, // 10秒超时
success(res) {
console.log('请求成功', res.data)
},
fail(err) {
if (err.errMsg.includes('timeout')) {
wx.showToast({ title: '请求超时,请重试', icon: 'none' })
}
}
})
全局配置:
// app.js
App({
onLaunch() {
// 设置全局请求超时时间(仅影响后续创建的 request)
// 注意:此 API 仅部分平台支持
}
})
九、小程序开发工具
62. 微信开发者工具
定义: 微信官方提供的小程序开发 IDE,集成了编辑、调试、预览、发布等功能。
主要功能:
- 代码编辑:语法高亮、智能提示、代码格式化
- 实时预览:代码修改后自动刷新预览
- 调试工具:Wxml 面板、Console 面板、Network 面板、Storage 面板
- 性能分析:Performance 面板、Audits 面板
- 云开发:支持云开发环境管理
63. 小程序调试
调试方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
| 模拟器调试 | 在开发者工具内调试 | 日常开发 |
| 真机调试 | 连接手机调试 | 测试真机表现 |
| vConsole | 移动端调试面板 | 线上问题排查 |
调试技巧:
// 1. 使用 console 调试
console.log('普通日志')
console.info('信息日志')
console.warn('警告日志')
console.error('错误日志')
console.debug('调试日志')
// 2. 使用 debugger 断点
function test() {
debugger // 代码执行到此会暂停
console.log('断点之后')
}
// 3. 使用 vConsole
// 在 app.js 中引入
import vConsole from './miniprogram_npm/vconsole'
const vConsole = new vConsole()
开发者工具调试面板:
- Console:查看日志和执行 JS
- Sources:断点调试源码
- Network:查看网络请求
- Storage:查看本地存储
- Wxml:查看和编辑页面结构
- Sensor:模拟地理位置
64. 真机调试
使用方式:
- 在开发者工具点击"真机调试"
- 使用微信扫码
- 在手机端打开小程序进行调试
特点:
- 可以在真实设备上测试
- 可以在开发者工具查看真机日志
- 支持断点调试
- 可以看到真机的网络请求和存储
注意事项:
- 真机调试和线上环境有差异
- 部分 API 在调试模式下行为不同
- 需要关注真机性能表现
65. 代码编译
编译流程:
- WXML 编译为虚拟 DOM
- WXSS 编译为 CSS
- JS 编译为可执行代码
- 打包为小程序代码包
编译配置:
// project.config.json
{
"compileType": "miniprogram",
"setting": {
"es6": true, // 是否将 ES6 转 ES5
"minified": true, // 是否压缩代码
"uglifyFileName": false, // 是否压缩文件名
"autoPushProxy": true,
"packNpmManually": false, // npm 构建配置
"packNpmRelationList": []
}
}
66. 代码上传 / 版本管理
上传流程:
- 在开发者工具点击"上传"
- 填写版本号和项目备注
- 代码上传到微信服务器
- 在小程序管理后台查看提交的版本
版本号管理:
- 版本号格式:major.minor.patch(如 1.0.0)
- 每次上传版本号必须递增
- 体验版和正式版共用版本号体系
十、小程序发布与审核
67. 小程序发布
发布流程:
开发完成 → 开发者工具上传 → 管理后台设置体验版 → 提交审核 → 审核通过 → 发布上线
版本类型:
| 版本 | 说明 | 使用场景 |
|---|---|---|
| 开发版 | 开发者工具直接预览 | 开发阶段测试 |
| 体验版 | 上传代码后设置 | 内部测试、产品验收 |
| 审核版 | 提交审核后的版本 | 等待微信审核 |
| 正式版 | 审核通过后发布 | 线上用户访问 |
68. 小程序审核
审核流程:
- 提交代码到管理后台
- 填写审核信息(类目、标签等)
- 微信团队进行审核
- 审核结果通知(通常 1-7 个工作日)
审核注意事项:
- 功能完整,无明显 bug
- 不涉及违法违规内容
- 用户体验良好
- 符合小程序运营规范
69. 审核规范
常见审核不通过原因:
- 存在未完成的功能
- 诱导分享、关注等行为
- 收集用户信息未说明用途
- 涉及需要资质但未提供
- 小程序内容与服务类目不符
- 存在虚拟支付(iOS 端)
70. 版本回退
操作方式: 在小程序管理后台的"版本管理"中,可以选择回退到上一个线上版本。
使用场景:
- 新版本出现严重 bug
- 新版本用户体验差
- 新版本审核通过后发现问题
71. 灰度发布
定义: 灰度发布是指将新版本先推送给部分用户,观察效果和反馈后,再逐步扩大推送范围。
操作方式: 在小程序管理后台提交发布时,可以选择"分阶段发布":
- 先推送一定比例的用户(如 10%)
- 观察数据和反馈
- 确认无问题后扩大推送比例
- 最终全量发布
72. 体验版 / 开发版
开发版:
- 通过开发者工具预览
- 无需上传代码
- 仅开发者自己可见
- 有效期较短
体验版:
- 需要上传代码后设置
- 可以添加体验成员
- 体验成员通过扫描二维码访问
- 有效期较长
十一、小程序登录详解
73. 完整登录流程
┌──────────┐ code ┌──────────┐ code + appid + secret ┌──────────┐
│ 小程序 │ ─────────────► │ 后端 │ ─────────────────────────► │ 微信 │
│ │ │ │ ◄───────────────────────── │ 服务器 │
│ │ ◄───────────── │ │ openid + session_key │ │
│ │ token │ │ │ │
└──────────┘ └──────────┘ └──────────┘
详细步骤:
- 小程序端调用 wx.login()
wx.login({
success(res) {
if (res.code) {
// 将 code 发送给后端
wx.request({
url: 'https://your-server.com/api/login',
method: 'POST',
data: { code: res.code }
})
}
}
})
- 后端接收 code,调用微信接口换取 openid
// Node.js 后端示例
const https = require('https')
function code2Session(code, appId, secret) {
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${secret}&js_code=${code}&grant_type=authorization_code`
return new Promise((resolve, reject) => {
https.get(url, res => {
let data = ''
res.on('data', chunk => data += chunk)
res.on('end', () => resolve(JSON.parse(data)))
}).on('error', reject)
})
}
// 登录接口
app.post('/api/login', async (req, res) => {
const { code } = req.body
const result = await code2Session(code, APP_ID, SECRET)
if (result.openid) {
// 生成自定义登录态(token)
const token = generateToken(result.openid)
// 存储 session_key(不要返回给前端)
storeSession(result.openid, result.session_key)
res.json({ token, success: true })
} else {
res.json({ success: false, msg: '登录失败' })
}
})
- 小程序端存储 token
wx.request({
url: 'https://your-server.com/api/login',
method: 'POST',
data: { code: res.code },
success(response) {
if (response.data.success) {
wx.setStorageSync('token', response.data.token)
}
}
})
- 后续请求携带 token
wx.request({
url: 'https://your-server.com/api/user',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
}
})
74. 登录态维护
静默登录:
// app.js
App({
async onLaunch() {
await this.silentLogin()
},
async silentLogin() {
try {
const token = wx.getStorageSync('token')
if (token) {
// 检查 token 是否有效
const valid = await this.checkToken(token)
if (valid) return
}
// 重新登录
await this.doLogin()
} catch (e) {
await this.doLogin()
}
},
doLogin() {
return new Promise((resolve, reject) => {
wx.login({
success: async (res) => {
if (res.code) {
const result = await wx.request({
url: 'https://your-server.com/api/login',
method: 'POST',
data: { code: res.code }
})
if (result.data.success) {
wx.setStorageSync('token', result.data.token)
this.globalData.token = result.data.token
resolve()
} else {
reject(new Error('登录失败'))
}
}
}
})
})
},
checkToken(token) {
return wx.request({
url: 'https://your-server.com/api/check-token',
header: { Authorization: `Bearer ${token}` }
}).then(res => res.data.valid)
},
globalData: {
token: ''
}
})
十二、小程序支付详解
75. 完整支付流程
┌──────────┐ 1.下单请求 ┌──────────┐ 2.创建订单 ┌──────────┐
│ 小程序 │ ────────────► │ 后端 │ ──────────────► │ 微信 │
│ │ ◄──────────── │ │ ◄────────────── │ 支付 │
│ │ 支付参数 │ │ 预支付结果 │ 服务器 │
│ │ ────────────► │ │ │ │
│ │ 3.调起支付 │ │ │ │
│ │ ◄──────────── │ │ │ │
│ │ │ │ 5.支付通知 │ │
│ │ 6.支付结果 │ │ ◄────────────── │ │
└──────────┘ └──────────┘ └──────────┘
代码实现:
// 小程序端
Page({
async payOrder(orderId) {
try {
wx.showLoading({ title: '正在支付...' })
// 1. 请求后端获取支付参数
const payParams = await request.post('/api/pay/create', {
orderId: orderId
})
// 2. 调起微信支付
await wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign
})
// 3. 支付成功处理
wx.hideLoading()
wx.showToast({ title: '支付成功', icon: 'success' })
// 4. 跳转到订单详情或支付结果页
wx.redirectTo({
url: `/pages/pay-result/pay-result?orderId=${orderId}&status=success`
})
} catch (err) {
wx.hideLoading()
if (err.errMsg === 'requestPayment:fail cancel') {
wx.showToast({ title: '已取消支付', icon: 'none' })
} else {
wx.showToast({ title: '支付失败', icon: 'none' })
}
}
}
})
76. 支付安全注意事项
- 不在前端处理签名:签名必须在后端完成
- 验证支付结果:支付成功后需后端确认
- 处理支付超时:设置合理的支付超时时间
- 防止重复支付:使用订单号幂等性控制
- iOS 虚拟支付限制:iOS 端小程序不支持虚拟物品支付
十三、小程序性能优化
77. 性能优化策略
优化维度:
| 维度 | 优化策略 | 说明 |
|---|---|---|
| 启动速度 | 分包加载 | 减少首屏加载体积 |
| 启动速度 | 精简 app.js | 减少启动时初始化工作 |
| 渲染性能 | 减少 setData 频次 | 合并数据更新 |
| 渲染性能 | 减少 setData 数据量 | 使用路径更新 |
| 渲染性能 | 虚拟列表 | 长列表按需渲染 |
| 网络性能 | 请求合并 | 减少请求次数 |
| 网络性能 | 数据缓存 | 减少重复请求 |
| 图片优化 | 压缩图片 | 减小图片体积 |
| 图片优化 | 懒加载 | 按需加载图片 |
| 代码优化 | 删除无用代码 | 减少包体积 |
78. 分包加载
配置分包:
// app.json
{
"pages": [
"pages/index/index",
"pages/detail/detail"
],
"subpackages": [
{
"root": "packageA",
"name": "A",
"pages": [
"pages/a1/a1",
"pages/a2/a2"
],
"independent": false
},
{
"root": "packageB",
"name": "B",
"pages": [
"pages/b1/b1"
],
"independent": true
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["A"]
}
}
}
预加载分包:
// 预加载分包
wx.loadSubpackage({
name: 'A',
success() {
console.log('分包加载成功')
},
fail(err) {
console.error('分包加载失败', err)
}
})
79. setData 优化
Page({
data: {
list: []
},
// 不推荐:全量更新大列表
badUpdate() {
this.setData({ list: newList })
},
// 推荐:局部更新
goodUpdate(newItem) {
const index = this.data.list.length
this.setData({
[`list[${index}]`]: newItem
})
},
// 推荐:使用节流
throttledUpdate: throttle(function(value) {
this.setData({ value })
}, 100)
})
// 节流函数
function throttle(fn, delay) {
let timer = null
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
}
80. 图片优化
优化方式:
<!-- 使用 lazy-load 懒加载 -->
<image src="{{item.image}}" lazy-load mode="aspectFill" />
<!-- 使用云图片裁剪 -->
<image src="{{item.image}}?x-oss-process=image/resize,w_400" />
<!-- 使用 WebP 格式 -->
<image src="{{item.image}}.webp" />
代码优化建议:
- 使用 CDN 加速
- 根据设备像素比加载不同尺寸图片
- 避免在列表中使用过大的图片
81. 网络优化
// 请求合并
Page({
async loadAllData() {
// 不推荐:串行请求
const users = await request.get('/api/users')
const orders = await request.get('/api/orders')
// 推荐:并行请求
const [users, orders] = await Promise.all([
request.get('/api/users'),
request.get('/api/orders')
])
}
})
// 数据缓存
const cache = {}
function fetchWithCache(url, ttl = 300000) {
if (cache[url] && cache[url].expire > Date.now()) {
return Promise.resolve(cache[url].data)
}
return request.get(url).then(data => {
cache[url] = {
data,
expire: Date.now() + ttl
}
return data
})
}
82. 性能检测工具
使用 Performance 面板:
- 打开开发者工具的 Performance 面板
- 点击录制按钮
- 执行操作后停止录制
- 分析性能数据
关键指标:
- 首屏渲染时间
- setData 耗时
- 页面切换耗时
- 网络请求耗时
获取性能数据:
// 获取启动性能数据
const launchOptions = wx.getLaunchOptionsSync()
// 自定义性能打点
const performance = wx.getPerformance()
const observer = performance.createObserver((entryList) => {
console.log('性能数据:', entryList.getEntries())
})
observer.observe({ entryTypes: ['render', 'script', 'navigation'] })
十四、常见面试题总结
83. 小程序双线程模型的优势是什么?
- 安全性:逻辑层运行在沙盒中,无法直接操作 DOM
- 性能:渲染和逻辑并行执行
- 稳定性:逻辑层崩溃不影响渲染层
84. 小程序和 H5 的区别?
- 运行环境不同:小程序在微信客户端内,H5 在浏览器中
- 能力不同:小程序有丰富的微信 Native API
- 审核机制:小程序需审核,H5 不需要
- 性能:小程序首次加载后缓存,性能更好
85. setData 的原理和优化?
- 原理:逻辑层序列化数据,通过 JSBridge 传递给渲染层
- 优化:减少频次、减少数据量、使用路径更新、避免大数据
86. 小程序登录流程?
- wx.login 获取 code → 发送 code 到后端 → 后端用 code 换取 openid → 后端生成 token 返回 → 小程序存储 token
87. 小程序支付流程?
- 下单 → 后端创建预支付订单 → 返回支付参数 → 调起微信支付 → 支付结果回调
88. 小程序性能优化方案?
- 分包加载、减少 setData 频次和数据量、图片优化、网络优化、代码优化
89. 小程序页面间通信方式?
- URL 参数、EventChannel、globalData、Storage、页面栈操作
90. 小程序本地存储的限制?
- 单个 key 限制 1MB,总容量 10MB,支持字符串、对象、数组等