微信小程序高频面试题
1. 小程序的目录结构和文件类型?
csharp
miniprogram/
├── app.js # 小程序入口逻辑(全局)
├── app.json # 小程序全局配置
├── app.wxss # 全局样式
├── project.config.json # 项目配置(编译选项、AppID 等)
├── sitemap.json # 页面索引配置(搜索引擎)
├── pages/ # 页面目录
│ ├── index/
│ │ ├── index.js # 页面逻辑
│ │ ├── index.json # 页面配置(可覆盖全局配置)
│ │ ├── index.wxml # 页面结构(类似 HTML)
│ │ └── index.wxss # 页面样式(类似 CSS)
│ └── user/
├── components/ # 自定义组件
├── utils/ # 工具函数
├── services/ # 接口请求
└── static/ # 静态资源
| 文件类型 | 作用 | 类比 Web |
|---|---|---|
.wxml |
页面结构模板 | HTML |
.wxss |
页面样式 | CSS(支持 rpx 单位) |
.js |
页面/组件逻辑 | JavaScript |
.json |
页面/组件配置 | 无直接对应 |
.wxs |
WXS 脚本(在视图层运行) | 类似 JS 子集 |
json
// app.json 全局配置示例
{
"pages": [
"pages/index/index",
"pages/user/user"
],
"window": {
"navigationBarTitleText": "我的小程序",
"navigationBarBackgroundColor": "#007bff",
"navigationBarTextStyle": "white",
"backgroundColor": "#f5f5f5",
"enablePullDownRefresh": false
},
"tabBar": {
"color": "#999",
"selectedColor": "#007bff",
"list": [
{ "pagePath": "pages/index/index", "text": "首页", "iconPath": "images/home.png", "selectedIconPath": "images/home-active.png" },
{ "pagePath": "pages/user/user", "text": "我的", "iconPath": "images/user.png", "selectedIconPath": "images/user-active.png" }
]
},
"subpackages": [
{ "root": "packageA", "pages": ["pages/detail/detail"] }
],
"permission": {
"scope.userLocation": { "desc": "你的位置信息将用于附近门店推荐" }
}
}
💡 面试加分点:page.json 中的配置会覆盖 app.json 中 window 的同名配置;每个页面必须在 app.json 的 pages 数组中注册;tabBar 最少 2 个最多 5 个 tab。
2. 小程序的生命周期?
三类生命周期对比
| 类型 | 钩子 | 触发时机 |
|---|---|---|
应用 App() |
onLaunch |
小程序初始化(仅一次) |
onShow |
小程序从后台进入前台 | |
onHide |
小程序从前台进入后台 | |
onError |
脚本错误或 API 调用失败 | |
页面 Page() |
onLoad(options) |
页面加载(仅一次),可获取路由参数 |
onShow |
页面显示(每次进入都触发) | |
onReady |
页面初次渲染完成(仅一次) | |
onHide |
页面隐藏(navigateTo 或 tab 切换) | |
onUnload |
页面卸载(redirectTo 或 navigateBack) | |
组件 Component() |
created |
组件实例创建(不能调用 setData) |
attached |
组件进入页面节点树 | |
ready |
组件布局完成 | |
detached |
组件从页面节点树移除 |
javascript
// 应用生命周期(app.js)
App({
onLaunch(options) {
// ✅ 适合做:获取场景值、检查更新、初始化全局数据
console.log('启动场景:', options.scene)
// 检查小程序更新
const updateManager = wx.getUpdateManager()
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,是否重启?',
success: (res) => { if (res.confirm) updateManager.applyUpdate() }
})
})
},
onShow(options) {
// ✅ 适合做:恢复数据、刷新登录态
console.log('前台显示,场景值:', options.scene)
},
onHide() {
// ✅ 适合做:暂停计时器、保存临时数据
},
onError(msg) {
// ✅ 适合做:错误上报
console.error('全局错误:', msg)
// reportError(msg) // 上报到监控平台
},
globalData: {
userInfo: null,
token: '',
}
})
// 页面生命周期(page.js)
Page({
data: { list: [], loading: false },
onLoad(options) {
// ✅ 接收页面参数(仅一次)
const { id } = options
this.fetchData(id)
},
onShow() {
// ✅ 每次进入页面都执行,适合刷新数据
},
onReady() {
// ✅ DOM 已就绪,可操作 createSelectorQuery
},
onHide() { /* 页面隐藏 */ },
onUnload() { /* 页面卸载,清理定时器等 */ },
// 页面事件
onPullDownRefresh() {
this.fetchData().then(() => wx.stopPullDownRefresh())
},
onReachBottom() {
this.loadMore()
},
onShareAppMessage() {
return { title: '分享标题', path: '/pages/index/index', imageUrl: '/images/share.png' }
},
onShareTimeline() {
// 分享到朋友圈
return { title: '朋友圈标题' }
},
onPageScroll(e) {
console.log('滚动距离:', e.scrollTop)
},
})
💡 面试加分点 :
onLoad和onShow的核心区别 ------onLoad只在页面第一次加载时调用一次(navigateTo过去再navigateBack回来不会重新触发),而onShow每次页面出现都会调用。navigateTo会触发上一个页面的onHide,redirectTo会触发onUnload。
3. 小程序的数据绑定和事件处理?
WXML 模板语法
html
<!-- 数据绑定(Mustache 语法) -->
<view>{{ message }}</view>
<view class="{{ isActive ? 'active' : '' }}">条件类名</view>
<view hidden="{{ !show }}">hidden 控制显隐(节点始终存在)</view>
<!-- 条件渲染 -->
<view wx:if="{{ status === 'loading' }}">加载中...</view>
<view wx:elif="{{ status === 'empty' }}">暂无数据</view>
<view wx:else>{{ content }}</view>
<!-- ✅ block 标签:不会渲染成 DOM 节点 -->
<block wx:if="{{ showHeader }}">
<view>标题</view>
<view>副标题</view>
</block>
<!-- 列表渲染 -->
<view wx:for="{{ list }}" wx:key="id" wx:for-item="item" wx:for-index="idx">
<text>{{ idx + 1 }}. {{ item.name }}</text>
</view>
<!-- ⚠️ wx:key 的重要性:提高列表渲染效率 -->
<!-- wx:key="id" → 使用 item.id 作为 key -->
<!-- wx:key="*this" → 使用 item 本身作为 key(适合基本类型数组) -->
事件系统
| 事件类型 | 写法 | 说明 |
|---|---|---|
| 冒泡事件 | bindtap / bind:tap |
事件向上冒泡 |
| 非冒泡事件 | catchtap / catch:tap |
阻止冒泡 |
| 互斥事件 | mut-bind:tap |
同一层级只触发一个 |
| 捕获阶段 | capture-bind:tap |
捕获阶段触发 |
html
<!-- 事件绑定 -->
<button bindtap="handleTap" data-id="{{ item.id }}" data-name="{{ item.name }}">
点击
</button>
<button catchtap="handleTap">阻止冒泡</button>
<input bindinput="handleInput" value="{{ inputValue }}" />
javascript
Page({
data: { message: 'Hello', inputValue: '' },
handleTap(e) {
// ✅ 通过 dataset 传递数据(data-xxx 属性)
const { id, name } = e.currentTarget.dataset
console.log(id, name)
// e.target:触发事件的源组件
// e.currentTarget:绑定事件的当前组件
// ⚠️ 冒泡时 target 和 currentTarget 可能不同
},
handleInput(e) {
// ⚠️ 小程序没有双向绑定,需要手动 setData
this.setData({ inputValue: e.detail.value })
},
// ✅ setData 最佳实践
updateData() {
// 路径写法:精确更新(减少数据传输量)
this.setData({
'list[0].name': '新名字', // 更新数组中某项的属性
'user.address.city': '深圳', // 更新嵌套属性
}, () => {
// 回调:视图渲染完成后执行
console.log('更新完成')
})
},
})
💡 面试加分点 :
setData的工作原理 ------ 小程序采用双线程架构(逻辑层 JSCore + 渲染层 WebView),setData会将数据从逻辑层传到渲染层(通过evaluateJavascript进行序列化传输),因此传输大量数据或频繁调用会导致性能问题。
4. wx:if 和 hidden 的区别?
| 对比项 | wx:if |
hidden |
|---|---|---|
| 原理 | 条件渲染,不满足条件时组件不渲染 | 始终渲染,通过 display:none 控制显隐 |
| 初始开销 | 低(不满足条件不创建) | 高(始终创建) |
| 切换开销 | 高(每次切换会销毁/重建组件) | 低(仅切换 CSS) |
| 适用场景 | 运行时条件不太可能改变 | 需要频繁切换显隐 |
| 子组件生命周期 | 切换时触发 attached/detached |
不触发 |
html
<!-- ✅ 不频繁切换 → 用 wx:if(减少初始渲染量) -->
<view wx:if="{{ isVip }}">
<vip-content /> <!-- 不是 VIP 时完全不渲染 -->
</view>
<!-- ✅ 频繁切换 → 用 hidden(避免反复创建/销毁) -->
<view hidden="{{ activeTab !== 'list' }}">
<long-list /> <!-- Tab 切换时只是隐藏,不会丢失滚动位置 -->
</view>
<!-- ❌ 错误:在 <block> 上使用 hidden(block 不是真实 DOM) -->
<block hidden="{{ !show }}">
<view>这样无效!</view>
</block>
💡 面试加分点 :类比 Vue 的
v-if和v-show,原理完全一致。hidden不能用在自定义组件上(除非组件内部处理了hidden属性),而wx:if对任何元素都有效。
5. 小程序的页面跳转方式?
| 方法 | 说明 | 页面栈变化 | 能否跳 tabBar |
|---|---|---|---|
navigateTo |
保留当前页,跳转新页 | 入栈(最多 10 层) | ❌ |
redirectTo |
关闭当前页,跳转新页 | 替换栈顶 | ❌ |
switchTab |
跳转到 tabBar 页面 | 清空非 tab 页面栈 | ✅ |
reLaunch |
关闭所有页面,打开新页 | 清空后入栈 | ✅ |
navigateBack |
返回上一页 | 出栈 | ❌ |
javascript
// 1. navigateTo:保留当前页面,跳转(最多10层栈)
wx.navigateTo({
url: '/pages/detail/detail?id=123&name=%E5%BC%A0%E4%B8%89',
success: () => console.log('跳转成功'),
fail: (err) => console.error('跳转失败:', err),
})
// 2. redirectTo:关闭当前页面,打开新页面(不能返回)
wx.redirectTo({ url: '/pages/login/login' })
// 3. switchTab:跳转到 tabBar 页面(url 不能带参数)
wx.switchTab({ url: '/pages/index/index' })
// 4. reLaunch:关闭所有页面,打开新页面
wx.reLaunch({ url: '/pages/index/index?from=reset' })
// 5. navigateBack:返回上一页
wx.navigateBack({ delta: 1 }) // delta 为返回层数
// ========================
// 页面间通信方式
// ========================
// 方式1:URL 参数(最常用)
// 在目标页面 onLoad 中接收
Page({
onLoad(options) {
const { id, name } = options
console.log(id, decodeURIComponent(name))
}
})
// 方式2:EventChannel(navigateTo 专用,推荐)
wx.navigateTo({
url: '/pages/select/select',
events: {
// 监听被打开页面发回的数据
onSelect: (data) => {
this.setData({ selected: data })
}
},
success: (res) => {
// 也可以通过 eventChannel 向新页面发送数据
res.eventChannel.emit('initData', { fromPage: 'index' })
}
})
// 在被打开的页面中
Page({
onLoad() {
const eventChannel = this.getOpenerEventChannel()
// 接收上个页面传来的数据
eventChannel.on('initData', (data) => {
console.log('收到:', data)
})
// 发送数据回上个页面
eventChannel.emit('onSelect', { id: 1, name: '选中项' })
wx.navigateBack()
}
})
// 方式3:全局数据(getApp().globalData)
const app = getApp()
app.globalData.selectedCity = '深圳'
// 方式4:本地存储(适合持久化数据)
wx.setStorageSync('tempData', { id: 1 })
💡 面试加分点 :
switchTab跳转时 URL 不能带参数;页面栈最多 10 层,超出会报错;可以用getCurrentPages()获取当前页面栈实例数组,直接操作上一个页面的方法(如prevPage.setData())。
6. 小程序的网络请求和封装?
javascript
// ✅ 完整的网络请求封装
const BASE_URL = 'https://api.example.com'
// 请求队列(管理并发)
let requestCount = 0
const request = (options) => {
return new Promise((resolve, reject) => {
// 显示 loading
if (requestCount === 0) {
wx.showLoading({ title: '加载中', mask: true })
}
requestCount++
wx.request({
url: `${BASE_URL}${options.url}`,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${wx.getStorageSync('token')}`,
...options.header,
},
timeout: options.timeout || 10000,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
if (res.data.code === 0) {
resolve(res.data)
} else if (res.data.code === 401) {
// token 过期,重新登录
wx.removeStorageSync('token')
wx.reLaunch({ url: '/pages/login/login' })
reject(new Error('登录过期'))
} else {
wx.showToast({ title: res.data.message || '请求失败', icon: 'none' })
reject(res.data)
}
} else {
reject(new Error(`HTTP ${res.statusCode}`))
}
},
fail: (err) => {
wx.showToast({ title: '网络异常', icon: 'none' })
reject(err)
},
complete: () => {
requestCount--
if (requestCount === 0) wx.hideLoading()
}
})
})
}
// 快捷方法
const get = (url, data) => request({ url, method: 'GET', data })
const post = (url, data) => request({ url, method: 'POST', data })
const put = (url, data) => request({ url, method: 'PUT', data })
const del = (url, data) => request({ url, method: 'DELETE', data })
module.exports = { request, get, post, put, del }
// 使用示例
const { get, post } = require('../../services/request')
Page({
async onLoad() {
try {
const res = await get('/api/users', { page: 1 })
this.setData({ list: res.data.list })
} catch (err) {
console.error(err)
}
}
})
💡 面试加分点 :小程序
wx.request默认并发限制为 10 个;域名必须在小程序后台「服务器域名」中配置白名单;开发阶段可在「详情 → 本地设置」中勾选「不校验合法域名」。
7. 小程序的本地存储?
| 对比项 | 同步 API | 异步 API |
|---|---|---|
| 方法 | wx.setStorageSync(key, data) |
wx.setStorage({ key, data }) |
| 阻塞 | ✅ 阻塞当前线程 | ❌ 不阻塞 |
| 适用 | 简单数据、启动时读取 | 大量数据、非关键操作 |
| 错误处理 | try/catch |
success/fail 回调 |
javascript
// ✅ 同步操作(简单场景推荐)
try {
wx.setStorageSync('token', 'abc123')
wx.setStorageSync('userInfo', { name: '张三', age: 25 })
const token = wx.getStorageSync('token')
wx.removeStorageSync('token')
// wx.clearStorageSync() // 清空所有
} catch (err) {
console.error('存储操作失败:', err)
}
// ✅ 异步操作(大数据推荐)
wx.setStorage({
key: 'cacheData',
data: largeObject,
success: () => console.log('存储成功'),
fail: (err) => console.error('存储失败:', err)
})
// 获取存储信息
wx.getStorageInfo({
success: (res) => {
console.log(res.keys) // 所有 key 列表
console.log(res.currentSize) // 当前占用大小(KB)
console.log(res.limitSize) // 上限大小(约 10MB)
}
})
// ✅ 封装带过期时间的存储
const storage = {
set(key, value, expireMinutes) {
const data = {
value,
expire: expireMinutes ? Date.now() + expireMinutes * 60 * 1000 : null
}
wx.setStorageSync(key, data)
},
get(key) {
try {
const data = wx.getStorageSync(key)
if (!data) return null
if (data.expire && Date.now() > data.expire) {
wx.removeStorageSync(key)
return null // 已过期
}
return data.value
} catch { return null }
}
}
// 缓存接口数据 30 分钟
storage.set('userList', list, 30)
const cached = storage.get('userList')
💡 面试加分点 :小程序本地存储上限约 10MB ,单个 key 允许存储的最大数据长度为 1MB;存储的数据支持原生类型(不需要手动 JSON 序列化);隔离策略是按小程序维度隔离(同一用户不同小程序互不可见)。
8. 小程序的登录流程?
css
小程序登录流程(官方推荐):
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ 小程序 │ │ 开发者服务器 │ │ 微信服务器 │
└────┬─────┘ └────┬─────┘ └─────┬────────┘
│ │ │
│ 1. wx.login() │ │
│ ───────────────>│ │
│ 返回 code │ │
│ <───────────────│ │
│ │ │
│ 2. 发送 code │ │
│ ─────────────────────────────────>│
│ │ │
│ │ 3. code + appid + secret
│ │ ──────────────────>│
│ │ │
│ │ 4. 返回 openid + session_key
│ │ <──────────────────│
│ │ │
│ 5. 返回自定义 token │
│ <─────────────────────────────────│
│ │ │
javascript
// ✅ 完整登录实现
const login = async () => {
try {
// 1. 获取临时登录凭证 code
const { code } = await wx.login()
// 2. 将 code 发送到自己的服务器
const res = await post('/api/auth/wx-login', { code })
// 3. 存储 token 和用户信息
wx.setStorageSync('token', res.data.token)
wx.setStorageSync('userInfo', res.data.userInfo)
return res.data
} catch (err) {
console.error('登录失败:', err)
throw err
}
}
// ✅ 检查登录态是否有效
const checkLogin = () => {
return new Promise((resolve) => {
const token = wx.getStorageSync('token')
if (!token) return resolve(false)
// 检查微信 session 是否过期
wx.checkSession({
success: () => resolve(true), // session 有效
fail: () => {
// session 过期,清除 token,需重新登录
wx.removeStorageSync('token')
resolve(false)
},
})
})
}
// ✅ 获取用户头像和昵称(2022 年后新方式)
// 旧方式 wx.getUserInfo() 已废弃
// 新方式:使用头像昵称填写能力
html
<!-- 新版获取头像昵称(button + open-type) -->
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image src="{{ avatarUrl }}" />
</button>
<input type="nickname" placeholder="请输入昵称" bindinput="onNicknameInput" />
javascript
Page({
data: {
avatarUrl: '/images/default-avatar.png',
nickname: ''
},
onChooseAvatar(e) {
// 返回临时头像路径,需上传到自己的服务器
this.setData({ avatarUrl: e.detail.avatarUrl })
},
onNicknameInput(e) {
this.setData({ nickname: e.detail.value })
}
})
💡 面试加分点 :
openid是用户在当前小程序下的唯一标识;unionid是用户在同一开放平台下所有应用的唯一标识(需绑定开放平台);session_key用于解密加密数据(如手机号),绝对不能传给前端 。2022 年后wx.getUserProfile()也被废弃,只能使用头像昵称填写组件。
9. 小程序自定义组件?
javascript
// ✅ 组件 JS(Component 构造器)
Component({
// 外部属性(父 → 子)
properties: {
title: { type: String, value: '默认标题' },
count: { type: Number, value: 0 },
list: { type: Array, value: [] },
// 简写形式
visible: Boolean,
},
// 内部数据
data: {
isExpanded: false,
},
// 组件生命周期
lifetimes: {
created() {
// ⚠️ 此时还不能调用 setData
},
attached() {
// ✅ 组件挂载到页面,可以执行初始化逻辑
console.log('组件挂载,title:', this.properties.title)
},
ready() {
// ✅ 组件布局完成,可以获取节点信息
},
detached() {
// ✅ 组件卸载,清理定时器等
},
},
// 所在页面的生命周期
pageLifetimes: {
show() { console.log('页面显示') },
hide() { console.log('页面隐藏') },
resize(size) { console.log('页面尺寸变化:', size) },
},
// 方法
methods: {
handleToggle() {
const expanded = !this.data.isExpanded
this.setData({ isExpanded: expanded })
// ✅ 触发自定义事件(子 → 父)
this.triggerEvent('toggle', { expanded }, {
bubbles: true, // 是否冒泡
composed: true, // 是否穿越组件边界
})
},
},
// ✅ 数据监听器(类似 Vue 的 watch)
observers: {
'count': function(newCount) {
console.log('count 变化:', newCount)
},
'list.**': function(newList) {
// 深度监听(数组/对象内部变化也能触发)
console.log('list 深度变化:', newList.length)
},
'propA, propB': function(a, b) {
// 同时监听多个属性
this.setData({ sum: a + b })
},
},
// 组件外部样式类
externalClasses: ['custom-class'],
// 组件选项
options: {
multipleSlots: true, // 启用多插槽
styleIsolation: 'isolated', // 样式隔离(默认)
pureDataPattern: /^_/, // 以 _ 开头的为纯数据字段(不参与渲染)
},
})
html
<!-- 组件 WXML(支持多插槽) -->
<view class="card custom-class">
<slot name="header"></slot>
<view class="title" bindtap="handleToggle">{{ title }}</view>
<view wx:if="{{ isExpanded }}">
<slot></slot> <!-- 默认插槽 -->
</view>
<slot name="footer"></slot>
</view>
<!-- 组件 JSON -->
<!-- { "component": true } -->
<!-- ======================== -->
<!-- 使用组件 -->
<!-- ======================== -->
<!-- 页面 JSON 注册 -->
<!-- { "usingComponents": { "my-card": "/components/card/card" } } -->
<!-- 页面 WXML 使用 -->
<my-card title="标题" count="{{ 5 }}" custom-class="red" bindtoggle="onCardToggle">
<view slot="header">头部内容</view>
<text>默认插槽内容</text>
<view slot="footer">底部内容</view>
</my-card>
💡 面试加分点 :组件样式默认隔离(
isolated),页面样式不会影响组件内部;properties和data在this.data中统一访问;可以使用behaviors(类似 Vue mixins)实现代码复用;pureDataPattern正则匹配的字段不参与渲染,适合存内部状态。
10. 小程序的双线程架构是什么?
scss
小程序双线程模型:
┌─────────────────┐ ┌─────────────────┐
│ 渲染层 │ │ 逻辑层 │
│ (WebView 线程) │ Native │ (JSCore 线程) │
│ │ <======> │ │
│ WXML + WXSS │ 桥接通信 │ JS (App/Page │
│ 渲染 DOM │ │ /Component) │
│ │ │ │
│ ⚠️ 不能执行 JS │ │ ⚠️ 不能操作 DOM │
└─────────────────┘ └─────────────────┘
│ │
└───────────┬───────────────────┘
│
┌──────┴──────┐
│ Native │
│ (微信客户端) │
│ 系统能力调用 │
└─────────────┘
| 对比项 | Web 浏览器 | 微信小程序 |
|---|---|---|
| 线程模型 | JS 和渲染在同一线程(互斥) | JS 和渲染在不同线程(并行) |
| JS 执行 | 可操作 DOM | 不能操作 DOM |
| 安全性 | 可执行任意 JS(eval/new Function) |
沙箱环境,禁止动态执行代码 |
| 性能 | JS 阻塞渲染 | JS 不阻塞渲染 |
| 通信 | 同步 | 异步(setData 跨线程传输) |
javascript
// ✅ 双线程的性能影响:setData 数据传输
// 逻辑层 → (序列化) → Native → (反序列化) → 渲染层
// ❌ 传输大量无关数据
this.setData({
entireList: hugeList, // 整个大列表
userInfo: allUserData, // 不需要渲染的数据
})
// ✅ 只传输视图需要的最小数据
this.setData({
[`list[${index}].liked`]: true, // 精确更新
})
// ✅ 纯数据字段:不需要渲染的数据用 _ 前缀
Component({
options: { pureDataPattern: /^_/ },
data: {
_timer: null, // 不传输到渲染层
_rawData: [], // 不传输到渲染层
displayList: [], // 传输到渲染层
}
})
💡 面试加分点 :双线程是小程序的核心设计,优势是安全(JS 无法操作 DOM,防 XSS)和流畅(JS 不阻塞渲染),但劣势是
setData需要跨线程通信,频繁或大量数据传输会导致性能问题。WXS 脚本运行在渲染层,可以直接操作数据避免跨线程通信,适合做数据格式化。
11. 小程序的分包加载?
为什么要分包?
- 小程序主包大小限制 2MB ,总包限制 20MB
- 用户首次打开只需下载主包,访问分包页面时再按需下载
| 分包类型 | 说明 | 触发时机 |
|---|---|---|
| 普通分包 | 配置 subpackages |
访问分包页面时下载 |
| 独立分包 | independent: true |
可独立运行,不依赖主包 |
| 分包预下载 | 配置 preloadRule |
进入指定页面时提前下载 |
json
// app.json 分包配置
{
"pages": [
"pages/index/index",
"pages/user/user"
],
"subpackages": [
{
"root": "packageA",
"name": "shop",
"pages": [
"pages/goods/goods",
"pages/order/order"
]
},
{
"root": "packageB",
"name": "activity",
"pages": [
"pages/lottery/lottery"
],
"independent": true // ✅ 独立分包(可独立运行)
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["shop"] // ✅ 进入首页时预下载 shop 分包
}
}
}
javascript
// 分包跳转
wx.navigateTo({ url: '/packageA/pages/goods/goods?id=123' })
// ✅ 分包异步化:跨分包引用组件
// 在主包页面的 json 中
{
"usingComponents": {
"shop-card": "/packageA/components/card/card" // 组件所在分包下载后才渲染
},
"componentPlaceholder": {
"shop-card": "view" // 分包未下载时的占位组件
}
}
💡 面试加分点 :独立分包不能引用主包的资源(JS/组件/样式),适合做活动页、广告页等可以独立运行的页面。分包预下载可以提升用户体验,但预下载大小限制为 2MB。
12. 小程序的性能优化?
启动性能优化
javascript
// ✅ 1. 控制包体积
// - 分包加载(核心页面放主包,非核心放分包)
// - 移除无用代码和资源
// - 图片上 CDN,不放本地
// - 开启「按需注入」和「用时注入」
// app.json
{
"lazyCodeLoading": "requiredComponents" // ✅ 用时注入
}
渲染性能优化
javascript
// ✅ 2. setData 优化(最重要!)
// ❌ 频繁调用
this.setData({ a: 1 })
this.setData({ b: 2 })
this.setData({ c: 3 })
// ✅ 合并调用
this.setData({ a: 1, b: 2, c: 3 })
// ❌ 传输大量数据
this.setData({ list: this.data.list }) // 整个列表
// ✅ 精确更新(路径语法)
this.setData({
[`list[${index}].selected`]: true,
[`list[${index}].count`]: newCount,
})
// ❌ 后台页面 setData(页面不可见时仍在更新)
onHide() {
// 定时器没有停止,继续 setData
}
// ✅ 页面不可见时停止不必要的 setData
onHide() {
clearInterval(this._timer)
},
onShow() {
this._timer = setInterval(() => this.refresh(), 5000)
}
// ✅ 3. 长列表优化
// 使用 recycle-view(官方虚拟列表组件)
// 或按分页加载 + 只渲染可视区域的数据
// ✅ 4. 图片优化
// - CDN + WebP 格式
// - 合适的 mode(aspectFill / aspectFit)
// - 懒加载
html
<image lazy-load src="{{ item.imageUrl }}" mode="aspectFill" />
javascript
// ✅ 5. 使用 WXS 处理数据格式化(在渲染层执行,不走 setData)
xml
<!-- filter.wxs -->
<wxs module="filter">
module.exports = {
formatPrice: function(price) {
return '¥' + (price / 100).toFixed(2)
},
formatTime: function(ts) {
var d = getDate(ts)
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
}
}
</wxs>
<text>{{ filter.formatPrice(item.price) }}</text>
<text>{{ filter.formatTime(item.createTime) }}</text>
javascript
// ✅ 6. 合理使用 wx:if 和 hidden
// 频繁切换 → hidden
// 不常显示 → wx:if
// ✅ 7. 避免不必要的节点数(WXML 节点数建议 < 1000)
// 使用 scroll-view 代替 page 级滚动
// 移除不可见元素而非隐藏
💡 面试加分点 :可以用小程序开发者工具的「Audits」面板检测性能问题;
setData单次传输数据不超过 256KB;开启「初始渲染缓存」可以让页面秒开(首次渲染使用缓存的数据)。
13. 小程序的授权和用户信息获取?
javascript
// ✅ 授权流程(以获取位置信息为例)
// 1. 先查询授权状态
wx.getSetting({
success: (res) => {
if (res.authSetting['scope.userLocation']) {
// 已授权,直接获取
wx.getLocation({ type: 'gcj02', success: (loc) => console.log(loc) })
} else if (res.authSetting['scope.userLocation'] === false) {
// 用户曾拒绝过,引导去设置页
wx.showModal({
title: '提示',
content: '需要位置权限,请到设置中开启',
success: (res) => {
if (res.confirm) wx.openSetting()
}
})
} else {
// 未请求过,调用 API 会自动弹授权框
wx.getLocation({ type: 'gcj02', success: (loc) => console.log(loc) })
}
}
})
// ✅ 获取手机号(需要企业认证小程序)
html
<!-- 手机号快速验证(推荐) -->
<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhone">
手机号快捷登录
</button>
javascript
Page({
onGetPhone(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok') {
// 将 code 发送到后端,后端调用微信 API 换取手机号
const { code } = e.detail
post('/api/auth/phone', { code }).then(res => {
console.log('手机号:', res.data.phoneNumber)
})
} else {
console.log('用户拒绝授权手机号')
}
}
})
常见授权 scope
| scope | 说明 | 触发 API |
|---|---|---|
scope.userLocation |
位置 | wx.getLocation |
scope.userLocationBackground |
后台定位 | 配置 + wx.startLocationUpdateBackground |
scope.record |
录音 | wx.startRecord |
scope.camera |
摄像头 | <camera> 组件 |
scope.writePhotosAlbum |
保存到相册 | wx.saveImageToPhotosAlbum |
💡 面试加分点 :小程序基础库 2.21.2 起,
wx.getUserProfile不再返回真实昵称和头像,改为匿名数据。获取手机号需要付费(每次约 0.03 元)。用户拒绝授权后再次调用不会弹窗,必须引导用户到wx.openSetting手动开启。
14. 小程序的 WXS(WeiXin Script)是什么?
| 对比项 | WXS | JS(逻辑层) |
|---|---|---|
| 运行环境 | 渲染层(WebView) | 逻辑层(JSCore) |
| 性能 | 无需跨线程通信,快 | setData 跨线程,慢 |
| 能力 | 不能调用小程序 API | 可以调用所有 API |
| 语法 | ES5 子集 | 支持 ES6+ |
| 用途 | 数据格式化、简单逻辑 | 业务逻辑、网络请求 |
xml
<!-- 内联写法 -->
<wxs module="utils">
var formatPhone = function(phone) {
if (!phone || phone.length !== 11) return phone
return phone.substr(0, 3) + '****' + phone.substr(7)
}
var classByStatus = function(status) {
var map = { 0: 'pending', 1: 'active', 2: 'done' }
return 'status-' + (map[status] || 'unknown')
}
module.exports = {
formatPhone: formatPhone,
classByStatus: classByStatus,
}
</wxs>
<text>{{ utils.formatPhone(user.phone) }}</text>
<view class="{{ utils.classByStatus(item.status) }}">{{ item.name }}</view>
<!-- 外部文件引用 -->
<wxs src="../../wxs/filter.wxs" module="filter" />
<text>{{ filter.formatPrice(item.price) }}</text>
javascript
// ✅ WXS 响应事件(高性能手势处理)
// wxs/bindingx.wxs
function touchStart(event, ownerInstance) {
ownerInstance.callMethod('onTouchStart', { x: event.touches[0].pageX })
}
function touchMove(event, ownerInstance) {
var instance = ownerInstance.selectComponent('.slider')
instance.setStyle({ 'transform': 'translateX(' + event.touches[0].pageX + 'px)' })
}
module.exports = {
touchStart: touchStart,
touchMove: touchMove,
}
html
<view
bindtouchstart="{{ bindingx.touchStart }}"
bindtouchmove="{{ bindingx.touchMove }}"
>
<view class="slider">滑块</view>
</view>
<wxs src="./wxs/bindingx.wxs" module="bindingx" />
💡 面试加分点 :WXS 直接在渲染层运行,避免了
setData的跨线程通信,在做数据格式化和手势响应时性能优势明显(2~20 倍提升)。但 WXS 只支持 ES5 语法,不能使用let/const/箭头函数等。
15. 小程序和 H5(Web)的区别?
| 对比项 | 小程序 | H5(Web) |
|---|---|---|
| 运行环境 | 微信客户端(双线程) | 浏览器(单线程) |
| 开发语言 | WXML + WXSS + JS | HTML + CSS + JS |
| 入口 | 微信扫码/搜索/分享 | URL 链接 |
| 安装 | 无需安装 | 无需安装 |
| 体验 | 接近原生(流畅) | 依赖浏览器性能 |
| 能力 | 微信原生 API(支付、蓝牙等) | Web API(受限) |
| 审核 | 需要微信审核 | 无需审核 |
| 包大小 | 主包 2MB / 总包 20MB | 无限制 |
| DOM 操作 | ❌ 不能直接操作 DOM | ✅ 可以操作 DOM |
| 生态 | 微信生态(搜索/朋友圈/公众号) | 开放 Web 生态 |
| 热更新 | ❌ 需重新提审 | ✅ 随时更新 |
javascript
// 小程序内嵌 H5(web-view)
// ✅ 可以打开 H5 页面
html
<web-view src="https://example.com/h5page"></web-view>
javascript
// H5 向小程序发消息
// 在 H5 页面引入 JSSDK
wx.miniProgram.postMessage({ data: { action: 'pay', amount: 100 } })
wx.miniProgram.navigateTo({ url: '/pages/result/result' })
// 小程序接收 H5 消息
Page({
onMessage(e) {
console.log('H5 消息:', e.detail.data)
}
})
💡 面试加分点 :
web-view会占满整个页面,且小程序原生组件(如camera、map)层级高于web-view;web-view中的 H5 域名必须在小程序后台配置「业务域名」白名单。
16. 小程序如何实现组件间通信?
| 通信方式 | 场景 | 说明 |
|---|---|---|
properties |
父 → 子 | 传递属性(单向数据流) |
triggerEvent |
子 → 父 | 触发自定义事件 |
selectComponent |
父操作子 | 获取子组件实例,直接调用方法 |
relations |
关联组件 | 定义组件间的关联关系 |
EventBus |
跨组件/跨页面 | 发布/订阅模式 |
globalData |
全局共享 | App 实例上的全局数据 |
store(如 MobX) |
全局状态管理 | 类 Vuex/Redux |
javascript
// ✅ 父 → 子:properties
// 父页面
// <child-comp title="Hello" count="{{ 10 }}" />
// 子组件
Component({
properties: {
title: String,
count: { type: Number, value: 0 }
}
})
// ✅ 子 → 父:triggerEvent
// 子组件
this.triggerEvent('change', { value: 'new' })
// 父页面
// <child-comp bindchange="onChildChange" />
// ✅ 父调用子方法:selectComponent
Page({
onReady() {
const child = this.selectComponent('#myChild')
child.doSomething() // 直接调用子组件方法
console.log(child.data.someData) // 读取子组件数据
}
})
// <child-comp id="myChild" />
// ✅ EventBus(跨组件/跨页面通信)
class EventBus {
constructor() { this._events = {} }
on(event, fn) {
(this._events[event] || (this._events[event] = [])).push(fn)
}
emit(event, ...args) {
(this._events[event] || []).forEach(fn => fn(...args))
}
off(event, fn) {
if (!fn) { delete this._events[event]; return }
const fns = this._events[event]
if (fns) this._events[event] = fns.filter(f => f !== fn)
}
}
// 挂载到全局
const app = getApp()
app.eventBus = new EventBus()
// 页面 A 订阅
app.eventBus.on('cartUpdate', (count) => {
this.setData({ cartCount: count })
})
// 页面 B 发布
app.eventBus.emit('cartUpdate', 5)
// ⚠️ 页面卸载时取消订阅(防止内存泄漏)
onUnload() {
app.eventBus.off('cartUpdate')
}
💡 面试加分点 :
selectComponent返回的是组件实例,可以直接调用其methods中的方法;在复杂场景下推荐使用官方的 MobX 扩展库(mobx-miniprogram+mobx-miniprogram-bindings)做全局状态管理。
17. 小程序的 rpx 单位是什么?
ini
rpx(responsive pixel):小程序响应式单位
原理:以 iPhone 6 (375px) 为基准,规定屏幕宽度 = 750rpx
换算公式:1rpx = 屏幕宽度 / 750
不同设备下的换算:
┌──────────────┬──────────┬─────────────┐
│ 设备 │ 屏幕宽度 │ 1rpx = │
├──────────────┼──────────┼─────────────┤
│ iPhone 5 │ 320px │ 0.427px │
│ iPhone 6/7/8 │ 375px │ 0.5px │
│ iPhone X │ 375px │ 0.5px │
│ iPhone 12 Pro│ 390px │ 0.52px │
│ iPad │ 768px │ 1.024px │
└──────────────┴──────────┴─────────────┘
css
/* ✅ rpx 使用最佳实践 */
.container {
width: 750rpx; /* 满屏宽 */
padding: 20rpx 30rpx;
}
.card {
width: 690rpx; /* 750 - 30*2 = 690rpx 两侧留边距 */
border-radius: 16rpx;
font-size: 28rpx; /* 约等于 14px @iPhone6 */
}
/* ⚠️ 边框建议使用 px,1rpx 在某些设备上可能不显示 */
.divider {
height: 1px; /* 不用 rpx */
background: #eee;
}
/* ✅ 文字大小可以混用 */
.title {
font-size: 32rpx; /* 随屏幕缩放 */
}
.caption {
font-size: 12px; /* 固定大小 */
}
💡 面试加分点:设计稿通常以 750px 宽度出图,这样设计稿上的 px 数值可以直接作为 rpx 值使用(1:1);不建议对字体大小全用 rpx,因为大屏设备上字体会过大,小字号建议使用 px。
18. 小程序如何实现下拉刷新和上拉加载?
json
// 页面 json 配置
{
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark",
"backgroundColor": "#f5f5f5"
}
javascript
// ✅ 完整的下拉刷新 + 上拉加载实现
Page({
data: {
list: [],
page: 1,
pageSize: 10,
hasMore: true,
loading: false,
refreshing: false,
},
onLoad() {
this.fetchList(true)
},
// 下拉刷新
onPullDownRefresh() {
this.fetchList(true)
},
// 上拉触底加载更多
onReachBottom() {
if (this.data.hasMore && !this.data.loading) {
this.fetchList(false)
}
},
async fetchList(isRefresh) {
if (this.data.loading) return
const page = isRefresh ? 1 : this.data.page + 1
this.setData({ loading: true, refreshing: isRefresh })
try {
const res = await get('/api/list', { page, pageSize: this.data.pageSize })
const { list: newList, total } = res.data
this.setData({
list: isRefresh ? newList : [...this.data.list, ...newList],
page,
hasMore: page * this.data.pageSize < total,
})
} catch (err) {
wx.showToast({ title: '加载失败', icon: 'none' })
} finally {
this.setData({ loading: false, refreshing: false })
if (isRefresh) wx.stopPullDownRefresh()
}
},
})
html
<!-- WXML -->
<view class="list">
<view wx:for="{{ list }}" wx:key="id" class="item">
<text>{{ item.name }}</text>
</view>
<!-- 底部状态 -->
<view class="load-status">
<text wx:if="{{ loading }}">加载中...</text>
<text wx:elif="{{ !hasMore }}">没有更多了</text>
</view>
</view>
💡 面试加分点 :也可以使用
<scroll-view>组件的bindrefresherrefresh和bindscrolltolower实现自定义下拉刷新 UI;注意在上拉加载时要做防抖处理(通过loading标志位),防止重复请求。
19. 小程序如何实现全局状态管理?
javascript
// ✅ 方案1:简单方案 --- globalData + 事件通知
// app.js
App({
globalData: {
userInfo: null,
cartCount: 0,
},
// 全局状态更新方法
setGlobalData(key, value) {
this.globalData[key] = value
// 通知所有监听者
if (this._watchers && this._watchers[key]) {
this._watchers[key].forEach(fn => fn(value))
}
},
watchGlobalData(key, fn) {
if (!this._watchers) this._watchers = {}
if (!this._watchers[key]) this._watchers[key] = []
this._watchers[key].push(fn)
// 返回取消监听函数
return () => {
this._watchers[key] = this._watchers[key].filter(f => f !== fn)
}
}
})
// 在页面中使用
const app = getApp()
Page({
data: { cartCount: 0 },
onShow() {
this.setData({ cartCount: app.globalData.cartCount })
this._unwatch = app.watchGlobalData('cartCount', (count) => {
this.setData({ cartCount: count })
})
},
onHide() {
this._unwatch && this._unwatch()
}
})
javascript
// ✅ 方案2:MobX 状态管理(官方推荐)
// npm install --save mobx-miniprogram mobx-miniprogram-bindings
// store/index.js
import { observable, action } from 'mobx-miniprogram'
export const store = observable({
// 状态
count: 0,
userInfo: null,
// 计算属性
get doubleCount() {
return this.count * 2
},
// 修改状态的方法
increment: action(function () {
this.count++
}),
setUserInfo: action(function (info) {
this.userInfo = info
}),
})
// 页面中使用
import { createStoreBindings } from 'mobx-miniprogram-bindings'
import { store } from '../../store/index'
Page({
onLoad() {
this.storeBindings = createStoreBindings(this, {
store,
fields: ['count', 'doubleCount', 'userInfo'],
actions: ['increment', 'setUserInfo'],
})
},
onUnload() {
this.storeBindings.destroyStoreBindings()
},
handleAdd() {
this.increment() // 直接调用 action
},
})
// 组件中使用
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings'
import { store } from '../../store/index'
Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store,
fields: { count: () => store.count },
actions: { increment: 'increment' },
},
})
💡 面试加分点 :小型项目用
globalData+ 事件通知即可;中大型项目推荐 MobX(官方有 npm 包支持);另一个选择是微信官方的westore。storeBindings会自动在数据变化时调用setData,不需要手动同步。
20. 小程序的常见场景和解决方案?
场景1:小程序码 / 分享
javascript
// ✅ 分享给好友
Page({
onShareAppMessage() {
return {
title: '推荐你一个好物',
path: '/pages/detail/detail?id=123&inviter=user001',
imageUrl: '/images/share-cover.png',
}
},
// ✅ 分享到朋友圈
onShareTimeline() {
return {
title: '朋友圈分享标题',
query: 'id=123&inviter=user001',
imageUrl: '/images/share-cover.png',
}
},
})
// 后端生成小程序码
// 调用微信 API:POST https://api.weixin.qq.com/wxa/getwxacodeunlimit
// scene 参数有 32 字符限制,复杂参数需要自己编码解码
场景2:支付流程
javascript
// ✅ 微信支付流程
const handlePay = async (orderId) => {
try {
// 1. 后端创建订单,返回支付参数
const res = await post('/api/order/pay', { orderId })
const { timeStamp, nonceStr, package: pkg, signType, paySign } = res.data
// 2. 调起支付
await new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp,
nonceStr,
package: pkg, // 格式:prepay_id=xxx
signType, // 'MD5' 或 'RSA'
paySign,
success: resolve,
fail: reject,
})
})
// 3. 支付成功
wx.showToast({ title: '支付成功', icon: 'success' })
wx.redirectTo({ url: `/pages/order/result?id=${orderId}` })
} catch (err) {
if (err.errMsg?.includes('cancel')) {
wx.showToast({ title: '已取消支付', icon: 'none' })
} else {
wx.showToast({ title: '支付失败', icon: 'none' })
}
}
}
场景3:图片上传和预览
javascript
// ✅ 选择图片 + 上传
const chooseAndUpload = () => {
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const files = res.tempFiles
// 逐个上传
const uploadTasks = files.map(file => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: 'https://api.example.com/upload',
filePath: file.tempFilePath,
name: 'file',
header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` },
success: (res) => resolve(JSON.parse(res.data)),
fail: reject,
})
})
})
Promise.all(uploadTasks).then(results => {
console.log('上传结果:', results)
})
}
})
}
// ✅ 图片预览
wx.previewImage({
current: imageUrl, // 当前显示的图片
urls: imageList, // 所有图片列表
})
场景4:骨架屏
html
<!-- ✅ 骨架屏实现 -->
<view wx:if="{{ loading }}" class="skeleton">
<view class="skeleton-avatar"></view>
<view class="skeleton-line"></view>
<view class="skeleton-line short"></view>
</view>
<view wx:else>
<!-- 真实内容 -->
</view>
css
.skeleton-line {
height: 30rpx;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 8rpx;
margin-bottom: 20rpx;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
💡 面试加分点:小程序开发者工具可以自动生成骨架屏代码(模拟器右下角菜单 → 生成骨架屏);微信支付的签名验证必须在后端完成,前端只负责调起支付;小程序码的 scene 参数有 32 字节限制,建议用短码或 ID 映射方案。
21. 小程序的冷启动和热启动有什么区别?
小程序的启动分为冷启动 和热启动两种方式,理解它们的区别对性能优化至关重要。
| 对比项 | 冷启动 | 热启动 |
|---|---|---|
| 定义 | 用户首次打开小程序,或小程序被微信销毁后再次打开 | 小程序已在后台运行,用户切回前台 |
| 触发条件 | 小程序未在后台运行 / 被销毁后重新打开 | 小程序在后台运行时,用户重新进入 |
| 加载过程 | 需要重新下载代码包 → 初始化 → 执行 App.onLaunch → 渲染首页 |
不需要重新加载,直接恢复页面状态 |
| 生命周期 | 触发 onLaunch → onShow |
仅触发 onShow(不触发 onLaunch) |
| 启动速度 | 较慢(需要完整加载流程) | 极快(直接恢复) |
| 销毁时机 | --- | 后台超过 30 分钟(或系统资源紧张时)被销毁 |
冷启动流程:
用户点击 → 下载/更新代码包 → 初始化运行环境 → App.onLaunch → App.onShow → Page.onLoad → Page.onShow → 首屏渲染
热启动流程:
用户切回前台 → App.onShow → Page.onShow → 恢复页面(无需重新加载)
javascript
// App.js 中区分冷启动和热启动
App({
onLaunch(options) {
// 仅冷启动时执行(只执行一次)
console.log('冷启动 - 场景值:', options.scene)
console.log('冷启动 - 来源:', options.referrerInfo)
// 适合做:全局初始化、登录检查、版本更新检查
this.checkUpdate()
this.initGlobalData()
},
onShow(options) {
// 冷启动和热启动都会执行
console.log('进入前台 - 场景值:', options.scene)
// 适合做:刷新数据、检查登录态是否过期
this.checkLoginStatus()
// 区分冷启动和热启动
if (this._hasShown) {
console.log('这是热启动(从后台恢复)')
this.refreshData() // 热启动时刷新数据
} else {
this._hasShown = true
console.log('这是冷启动的 onShow')
}
},
onHide() {
// 小程序切到后台
console.log('进入后台')
this.recordExitTime() // 记录退出时间
},
checkLoginStatus() {
// 热启动时检查登录态是否过期
const loginTime = wx.getStorageSync('loginTime')
const now = Date.now()
if (now - loginTime > 30 * 60 * 1000) {
// 登录态超过 30 分钟,重新登录
this.reLogin()
}
}
})
销毁机制:
后台运行 → 超过 30 分钟 → 微信自动销毁
后台运行 → 系统内存不足 → 按 LRU(最近最少使用)策略销毁
连续 5 次未响应 → 小程序被强制关闭
💡 面试加分点: 小程序还有一个概念叫**"热启动回退"------当小程序从后台恢复时,如果页面栈中的页面被回收了(系统回收内存),微信会重新加载该页面并触发
onLoad,但不会重新触发App.onLaunch。这时候如果页面依赖onLoad中的参数,需要从 页面栈或 全局变量**中恢复。另外,wx.getLaunchOptionsSync()可以在任意时机获取冷启动时的参数。
22. 小程序的更新机制是怎样的?如何强制更新?
小程序的更新机制分为同步更新 和异步更新两种。
| 更新方式 | 触发条件 | 用户体验 |
|---|---|---|
| 同步更新 | 小程序版本距上次使用超过一定时间 / 后台长期未使用 | 阻塞加载,用户等待更新完成后再进入 |
| 异步更新 | 大多数正常冷启动场景 | 先用旧版本,后台静默下载新版本,下次启动生效 |
markdown
异步更新流程:
用户打开小程序 → 使用旧版本启动 → 后台静默下载新版本 → 下载完成触发回调
↓
开发者可以提示用户重启应用新版本
javascript
// App.js ------ 强制更新方案
App({
onLaunch() {
this.checkUpdate()
},
checkUpdate() {
// 1. 获取更新管理器(基础库 1.9.90+)
if (!wx.canIUse('getUpdateManager')) {
return // 低版本不支持
}
const updateManager = wx.getUpdateManager()
// 2. 监听:是否有新版本
updateManager.onCheckForUpdate((res) => {
console.log('是否有新版本:', res.hasUpdate)
})
// 3. 监听:新版本下载完成
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
showCancel: false, // 不给取消按钮 → 强制更新
success(res) {
if (res.confirm) {
// 4. 调用 applyUpdate 强制重启并应用新版本
updateManager.applyUpdate()
}
}
})
})
// 5. 监听:新版本下载失败
updateManager.onUpdateFailed(() => {
wx.showModal({
title: '更新提示',
content: '新版本下载失败,请删除小程序后重新搜索打开',
showCancel: false
})
})
}
})
// ========== 开发调试技巧 ==========
// 在开发者工具中测试更新:
// 1. 编译模式 → 添加编译模式 → 勾选"下次编译模拟更新"
// 2. 微信开发者工具 → 工具 → 清除全部缓存(模拟冷启动)
版本管理最佳实践:
javascript
// 配合后端版本控制
App({
onShow() {
// 方案 B:通过接口检查是否需要强制更新(更灵活)
wx.request({
url: 'https://api.example.com/check-version',
data: { version: '1.2.0' }, // 当前小程序版本
success(res) {
if (res.data.forceUpdate) {
wx.showModal({
title: '版本过低',
content: '当前版本过低,请更新到最新版本',
showCancel: false,
success() {
// 引导用户删除重进(无法通过 API 主动触发更新)
}
})
}
}
})
}
})
💡 面试加分点:
getUpdateManager只在冷启动 时检查更新,热启动不会触发。如果你的小程序发布了紧急修复版本,用户可能需要等到下次冷启动才能收到更新。可以配合后端版本接口实现"软强制更新"------在onShow中检查版本号,如果低于最低要求就弹窗提示用户删除小程序重新进入。另外,微信后台有"小程序版本回退"功能,发现线上 bug 可以快速回退到上一个版本。
23. 什么是 behaviors?如何用它实现组件复用?
behaviors 是微信小程序的组件复用机制 ,类似于 Vue 的 mixins 或 React 的 HOC(高阶组件)。它允许多个组件共享属性、数据、方法和生命周期。
| 对比 | behaviors(小程序) | mixins(Vue) | HOC(React) |
|---|---|---|---|
| 定义方式 | Behavior({}) |
普通对象 | 高阶函数 |
| 使用方式 | behaviors: [myBehavior] |
mixins: [myMixin] |
withXxx(Component) |
| 同名冲突处理 | 组件覆盖 behavior | 组件覆盖 mixin | 无直接冲突 |
| 生命周期合并 | 都会执行(behavior 先执行) | 都会执行(mixin 先执行) | 手动控制 |
| 数据合并 | 组件数据覆盖 behavior 数据 | 对象递归合并 | --- |
javascript
// ========== 1. 定义 behavior ==========
// behaviors/formValidation.js
module.exports = Behavior({
// 共享属性
properties: {
required: {
type: Boolean,
value: false
}
},
// 共享数据
data: {
errorMsg: '',
isValid: true
},
// 共享方法
methods: {
// 通用校验方法
validate(value, rules) {
if (this.data.required && !value) {
this.setData({ errorMsg: '此字段不能为空', isValid: false })
return false
}
if (rules?.minLength && value.length < rules.minLength) {
this.setData({
errorMsg: `最少输入 ${rules.minLength} 个字符`,
isValid: false
})
return false
}
this.setData({ errorMsg: '', isValid: true })
return true
},
clearError() {
this.setData({ errorMsg: '', isValid: true })
}
},
// 共享生命周期
lifetimes: {
attached() {
console.log('behavior attached - 表单校验初始化')
}
}
})
// ========== 2. 定义另一个 behavior ==========
// behaviors/loading.js
module.exports = Behavior({
data: {
loading: false,
loadError: ''
},
methods: {
showLoading(msg = '加载中...') {
this.setData({ loading: true, loadError: '' })
wx.showLoading({ title: msg })
},
hideLoading() {
this.setData({ loading: false })
wx.hideLoading()
},
showError(msg) {
this.setData({ loading: false, loadError: msg })
wx.showToast({ title: msg, icon: 'none' })
}
}
})
// ========== 3. 在组件中使用 behavior ==========
// components/input-field/input-field.js
const formValidation = require('../../behaviors/formValidation')
const loading = require('../../behaviors/loading')
Component({
// 可以引入多个 behavior,用数组
behaviors: [formValidation, loading],
properties: {
label: String,
type: {
type: String,
value: 'text'
}
},
// 组件自己的数据(同名会覆盖 behavior 中的数据)
data: {
inputValue: ''
},
methods: {
onInput(e) {
const value = e.detail.value
this.setData({ inputValue: value })
// 调用 behavior 中的 validate 方法
this.validate(value, { minLength: 2 })
},
async onSubmit() {
if (!this.validate(this.data.inputValue)) return
// 调用 behavior 中的 loading 方法
this.showLoading('提交中...')
try {
await this.submitData(this.data.inputValue)
this.hideLoading()
} catch (err) {
this.showError(err.message)
}
}
}
})
html
<!-- components/input-field/input-field.wxml -->
<view class="field {{isValid ? '' : 'field--error'}}">
<text class="field__label">{{label}}</text>
<input
type="{{type}}"
value="{{inputValue}}"
bindinput="onInput"
placeholder="请输入{{label}}"
/>
<!-- errorMsg 来自 formValidation behavior -->
<text wx:if="{{errorMsg}}" class="field__error">{{errorMsg}}</text>
<!-- loading 来自 loading behavior -->
<view wx:if="{{loading}}" class="field__loading">加载中...</view>
</view>
同名字段的覆盖规则:
ini
优先级:组件本身 > 后引入的 behavior > 先引入的 behavior
behaviors: [behaviorA, behaviorB]
同名 data:组件 > behaviorB > behaviorA
同名 methods:组件 > behaviorB > behaviorA
同名生命周期:全部执行(behaviorA → behaviorB → 组件)
💡 面试加分点:
behaviors还支持内置 behavior ,如wx://form-field(让自定义组件可以作为表单控件)和wx://component-export(自定义组件的导出值)。另外,behavior 可以嵌套引用其他 behavior,形成复用链。与 Vue 3 的 Composition API 类似的思路,小程序也可以用 behavior 实现逻辑关注点分离。
24. 小程序没有路由守卫,如何实现路由拦截?
微信小程序不像 Vue Router 有 beforeEach 路由守卫,需要自己封装来实现页面访问控制。
常见方案
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 封装路由函数 | 拦截所有跳转,统一鉴权 | ⭐ 最常用 |
| 页面 onLoad 检查 | 每个页面的 onLoad 中判断 | 简单场景 |
| 自定义 Page 包装 | 重写 Page 函数,注入拦截逻辑 | 全局拦截 |
方案一:封装统一路由函数(推荐 ✅)
javascript
// utils/router.js ------ 统一路由管理
const loginPages = ['/pages/login/login'] // 无需登录的白名单
// 需要登录才能访问的页面
const authPages = [
'/pages/order/order',
'/pages/profile/profile',
'/pages/cart/cart'
]
function isLoggedIn() {
return !!wx.getStorageSync('token')
}
function getCurrentPageUrl() {
const pages = getCurrentPages()
if (pages.length === 0) return ''
const currentPage = pages[pages.length - 1]
return '/' + currentPage.route
}
const router = {
/**
* 统一路由跳转(带拦截)
* @param {string} url - 目标页面路径
* @param {object} options - 额外配置
*/
navigateTo(url, options = {}) {
return this._navigate('navigateTo', url, options)
},
redirectTo(url, options = {}) {
return this._navigate('redirectTo', url, options)
},
switchTab(url, options = {}) {
return this._navigate('switchTab', url, options)
},
_navigate(method, url, options = {}) {
const { skipAuth = false } = options
const path = url.split('?')[0] // 去掉参数取路径
// 1. 登录拦截
if (!skipAuth && authPages.includes(path) && !isLoggedIn()) {
console.log('未登录,跳转到登录页')
// 保存目标页面,登录后回跳
wx.setStorageSync('redirectUrl', url)
return wx.navigateTo({ url: '/pages/login/login' })
}
// 2. 页面栈限制(最多 10 层)
const pages = getCurrentPages()
if (method === 'navigateTo' && pages.length >= 10) {
console.warn('页面栈已满,改用 redirectTo')
method = 'redirectTo'
}
// 3. 执行跳转
return wx[method]({ url, ...options })
},
// 登录成功后回跳
loginSuccess() {
const redirectUrl = wx.getStorageSync('redirectUrl')
wx.removeStorageSync('redirectUrl')
if (redirectUrl) {
wx.redirectTo({ url: redirectUrl })
} else {
wx.switchTab({ url: '/pages/index/index' })
}
}
}
module.exports = router
javascript
// 页面中使用封装的路由
const router = require('../../utils/router')
Page({
goToOrder() {
// 自动检查登录态,未登录会跳到登录页
router.navigateTo('/pages/order/order?id=123')
},
goToProfile() {
router.switchTab('/pages/profile/profile')
}
})
方案二:自定义 Page 包装(全局拦截)
javascript
// utils/interceptPage.js ------ 重写全局 Page
const originalPage = Page
Page = function(config) {
const originalOnLoad = config.onLoad
config.onLoad = function(options) {
const currentRoute = '/' + this.route
// 全局拦截逻辑
const needAuth = ['/pages/order/order', '/pages/profile/profile']
if (needAuth.includes(currentRoute) && !wx.getStorageSync('token')) {
wx.setStorageSync('redirectUrl', currentRoute)
wx.redirectTo({ url: '/pages/login/login' })
return // 阻止原 onLoad 执行
}
// 放行:执行原始 onLoad
if (originalOnLoad) {
originalOnLoad.call(this, options)
}
}
// 调用原始 Page 构造函数
return originalPage(config)
}
// 在 app.js 顶部引入即可全局生效
// require('./utils/interceptPage')
💡 面试加分点: 微信小程序的页面栈最多 10 层 (
getCurrentPages().length <= 10),超过会跳转失败。封装路由时需要处理这个边界情况。另外,switchTab无法传递参数,如果 tab 页需要接收数据,通常通过全局变量或事件总线传递。面试时可以提到------完善的路由拦截方案还应包含页面权限配置化 (用 JSON 配置而非硬编码)和拦截链模式(支持多个拦截器按顺序执行)。
25. 小程序中如何使用 WebSocket?
小程序提供了 wx.connectSocket 等原生 API 实现 WebSocket 长连接,适用于实时通信场景(聊天、实时数据推送、协同编辑等)。
API 概览
| API | 说明 |
|---|---|
wx.connectSocket() |
创建 WebSocket 连接 |
SocketTask.send() |
发送数据 |
SocketTask.onMessage() |
接收消息 |
SocketTask.onOpen() |
连接打开回调 |
SocketTask.onClose() |
连接关闭回调 |
SocketTask.onError() |
连接错误回调 |
SocketTask.close() |
主动关闭连接 |
限制: 同时最多存在 5 个 WebSocket 连接(基础库 1.7.0+,之前只能 1 个)。
javascript
// utils/websocket.js ------ 封装 WebSocket(含心跳 + 断线重连)
class WxSocket {
constructor(options) {
this.url = options.url
this.socketTask = null
this.isConnected = false
this.reconnectCount = 0
this.maxReconnect = options.maxReconnect || 5
this.heartbeatInterval = options.heartbeatInterval || 30000 // 30s
this.heartbeatTimer = null
this.reconnectTimer = null
this.onMessageCallback = options.onMessage || (() => {})
this.onConnectCallback = options.onConnect || (() => {})
}
// 建立连接
connect() {
if (this.isConnected) return
this.socketTask = wx.connectSocket({
url: this.url,
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
}
})
// 连接成功
this.socketTask.onOpen(() => {
console.log('WebSocket 连接成功')
this.isConnected = true
this.reconnectCount = 0
this.startHeartbeat()
this.onConnectCallback()
})
// 接收消息
this.socketTask.onMessage((res) => {
const data = JSON.parse(res.data)
if (data.type === 'pong') {
// 心跳回应,忽略
return
}
this.onMessageCallback(data)
})
// 连接关闭
this.socketTask.onClose((res) => {
console.log('WebSocket 关闭:', res.code, res.reason)
this.isConnected = false
this.stopHeartbeat()
// 非主动关闭 → 尝试重连
if (res.code !== 1000) {
this.reconnect()
}
})
// 连接错误
this.socketTask.onError((err) => {
console.error('WebSocket 错误:', err)
this.isConnected = false
this.reconnect()
})
}
// 发送消息
send(data) {
if (!this.isConnected) {
console.warn('WebSocket 未连接,消息暂存')
return
}
this.socketTask.send({
data: JSON.stringify(data)
})
}
// 心跳保活
startHeartbeat() {
this.stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.send({ type: 'ping', timestamp: Date.now() })
}
}, this.heartbeatInterval)
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
// 断线重连(指数退避)
reconnect() {
if (this.reconnectCount >= this.maxReconnect) {
console.error('WebSocket 重连次数已达上限')
wx.showToast({ title: '网络连接失败', icon: 'none' })
return
}
this.reconnectCount++
const delay = Math.min(1000 * Math.pow(2, this.reconnectCount), 30000) // 指数退避,最大 30s
console.log(`${delay / 1000}s 后第 ${this.reconnectCount} 次重连...`)
this.reconnectTimer = setTimeout(() => {
this.connect()
}, delay)
}
// 主动关闭
close() {
this.stopHeartbeat()
clearTimeout(this.reconnectTimer)
this.maxReconnect = 0 // 防止触发重连
if (this.socketTask) {
this.socketTask.close({ code: 1000, reason: '用户主动关闭' })
}
}
}
module.exports = WxSocket
javascript
// 页面中使用
const WxSocket = require('../../utils/websocket')
Page({
data: {
messages: [],
inputMsg: ''
},
onLoad() {
this.socket = new WxSocket({
url: 'wss://api.example.com/ws/chat',
heartbeatInterval: 30000,
maxReconnect: 5,
onConnect: () => {
// 连接成功后加入房间
this.socket.send({ type: 'join', roomId: '12345' })
},
onMessage: (data) => {
// 收到消息更新列表
this.setData({
messages: [...this.data.messages, data]
})
}
})
this.socket.connect()
},
sendMessage() {
const msg = this.data.inputMsg.trim()
if (!msg) return
this.socket.send({ type: 'message', content: msg })
this.setData({ inputMsg: '' })
},
onUnload() {
// 页面卸载时关闭连接
this.socket.close()
}
})
💡 面试加分点: 小程序 WebSocket 需要注意几点:① 地址必须是
wss://(不支持ws://),域名需在小程序后台白名单中配置;② 小程序进入后台后 WebSocket 连接不会自动断开 ,但超过约 5 分钟后系统可能回收网络资源;③ 最佳实践是在App.onHide时主动断开、App.onShow时重连,避免无效连接占用资源;④ 指数退避重连策略比固定间隔重连更合理,可以避免服务器在故障恢复时被大量重连请求压垮。
26. 小程序的场景值(scene)有什么用?
场景值(scene)用于标识用户进入小程序的来源渠道,是小程序运营数据分析的关键指标。
常见场景值
| 场景值 | 来源 | 典型用途 |
|---|---|---|
| 1001 | 发现栏小程序主入口 | 自然流量 |
| 1007 | 单人聊天会话 | 好友分享 |
| 1008 | 群聊会话 | 群分享 |
| 1011 | 扫描二维码 | 线下推广 |
| 1012 | 长按识别二维码 | 图片分享 |
| 1014 | 小程序模板消息 | 消息触达 |
| 1017 | 前往小程序体验版 | 内部测试 |
| 1020 | 公众号 profile 页 | 公众号引流 |
| 1035 | 公众号自定义菜单 | 公众号引流 |
| 1036 | App 分享消息卡片 | App 引流 |
| 1043 | 公众号模板消息 | 消息触达 |
| 1047 | 扫描小程序码 | 线下推广 |
| 1048 | 长按识别小程序码 | 图片分享 |
| 1089 | 微信聊天主界面下拉 | 最近使用 |
获取方式
javascript
// App.js ------ 获取场景值
App({
onLaunch(options) {
// 冷启动时获取
console.log('场景值:', options.scene)
console.log('启动参数:', options.query) // 页面参数
console.log('来源信息:', options.referrerInfo) // 来源小程序/App
this.reportScene(options.scene, options.query)
},
onShow(options) {
// 每次进入前台都能获取(含热启动)
console.log('当前场景值:', options.scene)
},
// 上报场景值用于数据分析
reportScene(scene, query) {
wx.request({
url: 'https://api.example.com/analytics/scene',
method: 'POST',
data: {
scene,
query,
timestamp: Date.now(),
platform: wx.getSystemInfoSync().platform
}
})
}
})
// 页面中获取场景值
Page({
onLoad(options) {
// 方式 1:通过页面参数(从分享卡片进入时)
console.log('页面参数:', options)
// 方式 2:通过 API 获取启动场景
const launchOptions = wx.getLaunchOptionsSync()
console.log('启动场景值:', launchOptions.scene)
// 方式 3:获取进入前台的场景
const enterOptions = wx.getEnterOptionsSync()
console.log('进入场景值:', enterOptions.scene)
}
})
实际应用:根据场景值做不同处理
javascript
App({
onShow(options) {
const { scene, query } = options
switch (scene) {
// 扫码进入 → 解析二维码/小程序码参数
case 1011: // 扫描二维码
case 1047: // 扫描小程序码
this.handleScanEntry(query.scene || query)
break
// 分享进入 → 统计分享效果
case 1007: // 好友聊天
case 1008: // 群聊
this.trackShareEffect(query)
break
// 公众号进入 → 特殊引流逻辑
case 1020:
case 1035:
this.handleOfficialAccountEntry()
break
default:
break
}
},
// 处理小程序码的 scene 参数(有 32 字节限制)
handleScanEntry(sceneValue) {
// 小程序码的 scene 是经过 encodeURIComponent 编码的
const decoded = decodeURIComponent(sceneValue)
console.log('解码后的 scene:', decoded)
// 常见方案:scene=id_123 → 解析出 id
// 由于 32 字节限制,通常用短 ID 映射到后端的完整数据
const params = {}
decoded.split('&').forEach(item => {
const [key, value] = item.split('=')
params[key] = value
})
if (params.id) {
wx.navigateTo({ url: `/pages/detail/detail?id=${params.id}` })
}
}
})
💡 面试加分点: 小程序码的
scene参数有 32 字节 限制(不是 32 个字符),中文等多字节字符会占用更多空间。解决方案:① 使用后端生成短码 ID,通过接口映射完整参数;② 使用 Base62 等短编码方案。另外,getLaunchOptionsSync()返回的是冷启动参数 (全程不变),getEnterOptionsSync()返回的是最近一次进入前台的参数(热启动可能变化),面试中要区分清楚。
27. 小程序如何做错误监控与异常处理?
小程序的错误监控是生产环境稳定性的保障,面试中考察你对工程化的理解深度。
错误类型
| 错误类型 | 说明 | 捕获方式 |
|---|---|---|
| JS 运行时错误 | 代码逻辑错误、undefined 访问等 | App.onError / try-catch |
| 未处理的 Promise 拒绝 | Promise.reject 没有 .catch |
App.onUnhandledRejection |
| 页面未找到 | 跳转不存在的页面 | App.onPageNotFound |
| 内存不足告警 | 系统内存紧张 | wx.onMemoryWarning |
| 网络请求失败 | 接口超时、服务端错误 | 请求拦截器统一处理 |
全局错误捕获
javascript
// app.js ------ 全局错误监控
App({
onLaunch() {
this.initErrorMonitor()
},
// 1. JS 运行时错误
onError(err) {
console.error('全局错误:', err)
this.reportError({
type: 'jsError',
message: err,
page: this.getCurrentPageInfo(),
timestamp: Date.now()
})
},
// 2. 未处理的 Promise 拒绝(基础库 2.10.0+)
onUnhandledRejection(res) {
console.error('未处理的 Promise 拒绝:', res.reason)
this.reportError({
type: 'unhandledRejection',
message: String(res.reason),
page: this.getCurrentPageInfo(),
timestamp: Date.now()
})
},
// 3. 页面未找到(基础库 1.9.90+)
onPageNotFound(options) {
console.warn('页面未找到:', options.path)
this.reportError({
type: 'pageNotFound',
message: `Page not found: ${options.path}`,
timestamp: Date.now()
})
// 跳转到 404 页面或首页
wx.redirectTo({ url: '/pages/404/404' })
},
initErrorMonitor() {
// 4. 内存不足告警
wx.onMemoryWarning((res) => {
// level: 5(TRIM_MEMORY_RUNNING_MODERATE)
// 10(TRIM_MEMORY_RUNNING_LOW)
// 15(TRIM_MEMORY_RUNNING_CRITICAL)
console.warn('内存告警,级别:', res.level)
this.reportError({
type: 'memoryWarning',
message: `Memory warning level: ${res.level}`,
timestamp: Date.now()
})
// 释放不必要的缓存
this.releaseCache()
})
},
// 获取当前页面信息
getCurrentPageInfo() {
const pages = getCurrentPages()
if (pages.length === 0) return { route: 'unknown' }
const current = pages[pages.length - 1]
return {
route: current.route,
options: current.options
}
},
// 释放缓存
releaseCache() {
// 清理图片缓存、临时数据等
wx.getSavedFileList({
success(res) {
res.fileList.forEach(file => {
wx.removeSavedFile({ filePath: file.filePath })
})
}
})
},
// 上报错误到监控平台
reportError(errorInfo) {
const systemInfo = wx.getSystemInfoSync()
const reportData = {
...errorInfo,
// 附加环境信息
appVersion: '1.0.0',
sdkVersion: systemInfo.SDKVersion,
platform: systemInfo.platform,
system: systemInfo.system,
model: systemInfo.model,
networkType: '',
}
// 获取网络类型
wx.getNetworkType({
success(res) {
reportData.networkType = res.networkType
},
complete() {
// 上报到自建监控平台
wx.request({
url: 'https://monitor.example.com/api/report',
method: 'POST',
data: reportData,
// 错误上报本身失败不再重试,避免死循环
})
}
})
}
})
网络请求错误的统一处理
javascript
// utils/request.js ------ 请求封装(含错误处理)
function request(options) {
return new Promise((resolve, reject) => {
const startTime = Date.now()
wx.request({
...options,
success(res) {
const duration = Date.now() - startTime
// 慢请求上报(超过 3 秒)
if (duration > 3000) {
getApp().reportError({
type: 'slowRequest',
message: `Slow request: ${options.url} (${duration}ms)`,
timestamp: Date.now()
})
}
// HTTP 状态码处理
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else if (res.statusCode === 401) {
// token 过期,重新登录
wx.navigateTo({ url: '/pages/login/login' })
reject(new Error('登录已过期'))
} else {
const error = new Error(`HTTP ${res.statusCode}: ${options.url}`)
getApp().reportError({
type: 'httpError',
message: error.message,
statusCode: res.statusCode,
timestamp: Date.now()
})
reject(error)
}
},
fail(err) {
// 网络错误(无网络、DNS 失败、超时等)
const error = new Error(`Network Error: ${err.errMsg}`)
getApp().reportError({
type: 'networkError',
message: error.message,
url: options.url,
timestamp: Date.now()
})
reject(error)
}
})
})
}
module.exports = { request }
页面级 try-catch 最佳实践
javascript
Page({
async onLoad(options) {
try {
await this.loadData(options.id)
} catch (err) {
// 页面级错误处理
this.setData({ loadError: err.message })
}
},
async loadData(id) {
this.setData({ loading: true })
try {
const data = await request({ url: `/api/detail/${id}` })
this.setData({ detail: data, loading: false })
} catch (err) {
this.setData({ loading: false })
throw err // 向上抛出,让 onLoad 处理
}
}
})
💡 面试加分点: 实际项目中通常接入第三方监控平台(如 Fundebug、Sentry、腾讯云前端性能监控 RUM)。微信后台也提供了**「开发 → 运维中心 → 错误日志」查看线上错误。
onError的参数是 字符串**(不是 Error 对象),包含错误堆栈信息但可能被压缩混淆。建议上传 SourceMap 到监控平台以便还原。另外,wx.setEnableDebug({ enableDebug: true })可以在真机上开启 vConsole 调试面板,但正式版中应关闭。