微信小程序高频面试题

微信小程序高频面试题

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

💡 面试加分点onLoadonShow 的核心区别 ------ onLoad 只在页面第一次加载时调用一次(navigateTo 过去再 navigateBack 回来不会重新触发),而 onShow 每次页面出现都会调用。navigateTo 会触发上一个页面的 onHideredirectTo 会触发 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-ifv-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),页面样式不会影响组件内部;propertiesdatathis.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 会占满整个页面,且小程序原生组件(如 cameramap)层级高于 web-viewweb-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> 组件的 bindrefresherrefreshbindscrolltolower 实现自定义下拉刷新 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 包支持);另一个选择是微信官方的 westorestoreBindings 会自动在数据变化时调用 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 → 渲染首页 不需要重新加载,直接恢复页面状态
生命周期 触发 onLaunchonShow 仅触发 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 调试面板,但正式版中应关闭。

相关推荐
郭邯1 小时前
手写一个Cron表达式解析器:从需求分析到AI辅助实现
前端
阿黎梨梨1 小时前
Docker 容器化实战:从零搭建 Web 服务与反向代理
前端·后端·docker
王中阳Go1 小时前
做了 3 年 Spring Boot,老板让我一周搭个 AI Agent,上线当天被安全叫停:Java 团队落地 AI 的三道坎,我替你踩完了
后端·面试
Liora_Yvonne1 小时前
前端项目为什么需要一个配置单一真相源?
前端
禁止摆烂_才浅1 小时前
Vite 面试题
前端·面试·vite
GISer_Jing1 小时前
全栈AI实战:基于 TypeScript + LangChain + MCP 的企业级智能研发助手
前端·后端·ai·langchain·前端框架
禁止摆烂_才浅1 小时前
Vue2 高频面试题
前端·vue.js·面试
禁止摆烂_才浅1 小时前
前端性能优化面试题
前端·面试·性能优化
ClouGence1 小时前
文件上传也能录制回放了:CueCast UI 自动化测试更新
前端·测试