微信小程序高频面试题

18 阅读18分钟

微信小程序高频面试题

1. 小程序的目录结构和文件类型?

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页面/组件配置无直接对应
.wxsWXS 脚本(在视图层运行)类似 JS 子集
// 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组件从页面节点树移除
// 应用生命周期(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 模板语法

<!-- 数据绑定(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捕获阶段触发
<!-- 事件绑定 -->
<button bindtap="handleTap" data-id="{{ item.id }}" data-name="{{ item.name }}">
  点击
</button>
<button catchtap="handleTap">阻止冒泡</button>
<input bindinput="handleInput" value="{{ inputValue }}" />
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:ifhidden
原理条件渲染,不满足条件时组件不渲染始终渲染,通过 display:none 控制显隐
初始开销低(不满足条件不创建)高(始终创建)
切换开销高(每次切换会销毁/重建组件)低(仅切换 CSS)
适用场景运行时条件不太可能改变需要频繁切换显隐
子组件生命周期切换时触发 attached/detached不触发
<!-- ✅ 不频繁切换 → 用 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返回上一页出栈❌
// 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. 小程序的网络请求和封装?

// ✅ 完整的网络请求封装
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/catchsuccess/fail 回调
// ✅ 同步操作(简单场景推荐)
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. 小程序的登录流程?

小程序登录流程(官方推荐):

┌──────────┐     ┌──────────┐     ┌──────────────┐
│  小程序    │     │  开发者服务器  │     │  微信服务器    │
└────┬─────┘     └────┬─────┘     └─────┬────────┘
     │                 │                  │
     │ 1. wx.login()   │                  │
     │ ───────────────>│                  │
     │    返回 code     │                  │
     │ <───────────────│                  │
     │                 │                  │
     │ 2. 发送 code     │                  │
     │ ─────────────────────────────────>│
     │                 │                  │
     │                 │ 3. code + appid + secret
     │                 │ ──────────────────>│
     │                 │                  │
     │                 │ 4. 返回 openid + session_key
     │                 │ <──────────────────│
     │                 │                  │
     │ 5. 返回自定义 token                  │
     │ <─────────────────────────────────│
     │                 │                  │
// ✅ 完整登录实现
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() 已废弃
// 新方式:使用头像昵称填写能力
<!-- 新版获取头像昵称(button + open-type) -->
<button open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
  <image src="{{ avatarUrl }}" />
</button>
<input type="nickname" placeholder="请输入昵称" bindinput="onNicknameInput" />
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. 小程序自定义组件?

// ✅ 组件 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: /^_/,        // 以 _ 开头的为纯数据字段(不参与渲染)
  },
})
<!-- 组件 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. 小程序的双线程架构是什么?

小程序双线程模型:

┌─────────────────┐          ┌─────────────────┐
│    渲染层          │          │    逻辑层         │
│  (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 跨线程传输)
// ✅ 双线程的性能影响: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进入指定页面时提前下载
// 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 分包
    }
  }
}
// 分包跳转
wx.navigateTo({ url: '/packageA/pages/goods/goods?id=123' })

// ✅ 分包异步化:跨分包引用组件
// 在主包页面的 json 中
{
  "usingComponents": {
    "shop-card": "/packageA/components/card/card"  // 组件所在分包下载后才渲染
  },
  "componentPlaceholder": {
    "shop-card": "view"  // 分包未下载时的占位组件
  }
}

💡 面试加分点:独立分包不能引用主包的资源(JS/组件/样式),适合做活动页、广告页等可以独立运行的页面。分包预下载可以提升用户体验,但预下载大小限制为 2MB。


12. 小程序的性能优化?

启动性能优化

// ✅ 1. 控制包体积
// - 分包加载(核心页面放主包,非核心放分包)
// - 移除无用代码和资源
// - 图片上 CDN,不放本地
// - 开启「按需注入」和「用时注入」

// app.json
{
  "lazyCodeLoading": "requiredComponents"  // ✅ 用时注入
}

渲染性能优化

// ✅ 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)
// - 懒加载
<image lazy-load src="{{ item.imageUrl }}" mode="aspectFill" />
// ✅ 5. 使用 WXS 处理数据格式化(在渲染层执行,不走 setData)
<!-- 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>
// ✅ 6. 合理使用 wx:if 和 hidden
// 频繁切换 → hidden
// 不常显示 → wx:if

// ✅ 7. 避免不必要的节点数(WXML 节点数建议 < 1000)
// 使用 scroll-view 代替 page 级滚动
// 移除不可见元素而非隐藏

💡 面试加分点:可以用小程序开发者工具的「Audits」面板检测性能问题;setData 单次传输数据不超过 256KB;开启「初始渲染缓存」可以让页面秒开(首次渲染使用缓存的数据)。


13. 小程序的授权和用户信息获取?

// ✅ 授权流程(以获取位置信息为例)

// 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) })
    }
  }
})

// ✅ 获取手机号(需要企业认证小程序)
<!-- 手机号快速验证(推荐) -->
<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhone">
  手机号快捷登录
</button>
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)是什么?

对比项WXSJS(逻辑层)
运行环境渲染层(WebView)逻辑层(JSCore)
性能无需跨线程通信,快setData 跨线程,慢
能力不能调用小程序 API可以调用所有 API
语法ES5 子集支持 ES6+
用途数据格式化、简单逻辑业务逻辑、网络请求
<!-- 内联写法 -->
<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>
// ✅ 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,
}
<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 + JSHTML + CSS + JS
入口微信扫码/搜索/分享URL 链接
安装无需安装无需安装
体验接近原生(流畅)依赖浏览器性能
能力微信原生 API(支付、蓝牙等)Web API(受限)
审核需要微信审核无需审核
包大小主包 2MB / 总包 20MB无限制
DOM 操作❌ 不能直接操作 DOM✅ 可以操作 DOM
生态微信生态(搜索/朋友圈/公众号)开放 Web 生态
热更新❌ 需重新提审✅ 随时更新
// 小程序内嵌 H5(web-view)
// ✅ 可以打开 H5 页面
<web-view src="https://example.com/h5page"></web-view>
// 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
// ✅ 父 → 子: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 单位是什么?

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      │
└──────────────┴──────────┴─────────────┘
/* ✅ 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 配置
{
  "enablePullDownRefresh": true,
  "backgroundTextStyle": "dark",
  "backgroundColor": "#f5f5f5"
}
// ✅ 完整的下拉刷新 + 上拉加载实现
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()
    }
  },
})
<!-- 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. 小程序如何实现全局状态管理?

// ✅ 方案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()
  }
})
// ✅ 方案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:小程序码 / 分享

// ✅ 分享给好友
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:支付流程

// ✅ 微信支付流程
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:图片上传和预览

// ✅ 选择图片 + 上传
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:骨架屏

<!-- ✅ 骨架屏实现 -->
<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>
.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 → 恢复页面(无需重新加载)
// 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. 小程序的更新机制是怎样的?如何强制更新?

小程序的更新机制分为同步更新和异步更新两种。

更新方式触发条件用户体验
同步更新小程序版本距上次使用超过一定时间 / 后台长期未使用阻塞加载,用户等待更新完成后再进入
异步更新大多数正常冷启动场景先用旧版本,后台静默下载新版本,下次启动生效
异步更新流程:
用户打开小程序 → 使用旧版本启动 → 后台静默下载新版本 → 下载完成触发回调
                                                            ↓
                                          开发者可以提示用户重启应用新版本
// 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. 微信开发者工具 → 工具 → 清除全部缓存(模拟冷启动)

版本管理最佳实践:

// 配合后端版本控制
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 数据对象递归合并—
// ========== 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)
      }
    }
  }
})
<!-- 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>

同名字段的覆盖规则:

优先级:组件本身 > 后引入的 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 函数,注入拦截逻辑全局拦截

方案一:封装统一路由函数(推荐 ✅)

// 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
// 页面中使用封装的路由
const router = require('../../utils/router')

Page({
  goToOrder() {
    // 自动检查登录态,未登录会跳到登录页
    router.navigateTo('/pages/order/order?id=123')
  },

  goToProfile() {
    router.switchTab('/pages/profile/profile')
  }
})

方案二:自定义 Page 包装(全局拦截)

// 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 个)。

// 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
// 页面中使用
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公众号自定义菜单公众号引流
1036App 分享消息卡片App 引流
1043公众号模板消息消息触达
1047扫描小程序码线下推广
1048长按识别小程序码图片分享
1089微信聊天主界面下拉最近使用

获取方式

// 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)
  }
})

实际应用:根据场景值做不同处理

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 没有 .catchApp.onUnhandledRejection
页面未找到跳转不存在的页面App.onPageNotFound
内存不足告警系统内存紧张wx.onMemoryWarning
网络请求失败接口超时、服务端错误请求拦截器统一处理

全局错误捕获

// 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,
          // 错误上报本身失败不再重试,避免死循环
        })
      }
    })
  }
})

网络请求错误的统一处理

// 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 最佳实践

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 调试面板,但正式版中应关闭。