重新学习前端之微信小程序

3 阅读32分钟

微信小程序

一、小程序架构与原理

1. 小程序是什么?

定义: 微信小程序是一种不需要下载安装即可使用的应用,用户扫一扫或搜一下即可打开应用,实现了应用"触手可及"的梦想,用户扫一扫或者搜一下即可打开应用。

原理:

  • 小程序运行在微信客户端内,基于微信提供的运行环境
  • 采用类似 Web 技术(HTML5/CSS3/JavaScript)进行开发
  • 小程序代码经过微信编译处理后,在微信内部运行
  • 不需要安装,用完即走

示例场景:

  • 扫码点餐:餐厅提供小程序,顾客扫码即可点餐支付
  • 共享充电宝:扫描充电宝二维码,直接使用小程序租借
  • 公交乘车码:打开微信小程序,展示乘车码扫码乘车

核心代码示例:

// app.js - 小程序入口文件
App({
  onLaunch() {
    console.log('小程序启动')
  },
  globalData: {
    userInfo: null
  }
})

常见误区:

  • 误区一:小程序就是 H5 页面。实际上小程序有自己的运行环境和 API
  • 误区二:小程序完全替代原生 App。实际上小程序适合轻量级场景,复杂功能仍需 App
  • 误区三:小程序不需要审核。实际上小程序发布前需要经过微信审核

2. 小程序架构 / 双线程模型 / 渲染层与逻辑层

定义: 小程序采用双线程模型,将渲染层和逻辑层分离,分别运行在不同的线程中,以提高安全性和性能。

架构组成:

┌─────────────────────────────────────────────────────────┐
│                     微信客户端 (Native)                     │
├─────────────────────────────────────────────────────────┤
│                                                           │
│  ┌─────────────────┐       ┌──────────────────────────┐   │
│  │   渲染层线程     │◄─────►│      逻辑层线程           │   │
│  │  (WebView)      │       │   (JsCore / V8)          │   │
│  │                 │       │                          │   │
│  │  - WXML 渲染    │       │  - JavaScript 执行        │   │
│  │  - WXSS 样式    │       │  - 业务逻辑处理            │   │
│  │  - 事件收集     │       │  - API 调用               │   │
│  │                 │       │  - 数据管理               │   │
│  └─────────────────┘       └──────────────────────────┘   │
│         │                            │                    │
│         ▼                            ▼                    │
│  ┌──────────────────────────────────────────────────┐     │
│  │              微信 Native 层                        │     │
│  │  - 网络请求  - 支付  - 登录  - 定位  - 扫码       │     │
│  └──────────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────┘

渲染层:

  • 使用 WebView 进行渲染
  • 负责 WXML 和 WXSS 的解析和渲染
  • 收集用户交互事件,传递给逻辑层

逻辑层:

  • 使用 JsCore(iOS)或 V8(Android)运行 JavaScript
  • 处理业务逻辑、数据管理
  • 调用微信 Native API

线程通信:

  • 通过微信客户端的 JSBridge 进行通信
  • 数据传递采用 JSON 格式序列化
  • 异步通信机制,避免阻塞

对比表格:渲染层 vs 逻辑层

维度渲染层逻辑层
运行环境WebViewJsCore / V8
主要职责页面渲染、事件收集业务逻辑、数据处理
使用技术WXML、WXSSJavaScript
是否能调用 Native API
是否能直接操作 DOM
数据更新方式通过 setData 接收数据调用 setData 发送数据

选择策略: 开发者无需选择,小程序框架自动管理双线程。开发者只需关注:

  • 不要在逻辑层直接操作 DOM
  • 使用 setData 进行数据更新
  • 合理使用事件系统

代码示例:

// 逻辑层 - 通过 setData 更新数据,通知渲染层重新渲染
Page({
  data: {
    message: 'Hello'
  },
  updateMessage() {
    this.setData({
      message: 'World'
    })
    // 渲染层接收数据更新,自动重新渲染页面
  }
})

常见误区:

  • 误区一:可以直接操作 DOM。小程序不允许直接操作 DOM
  • 误区二:setData 是同步的。setData 是异步通信过程
  • 误区三:双线程意味着性能更好。双线程通信有开销,频繁 setData 会影响性能

3. 小程序原理

定义: 小程序基于 Web 技术栈,但在微信客户端内运行,通过双线程模型和微信 Native 能力,实现接近原生 App 的体验。

工作流程:

  1. 下载代码包:用户打开小程序时,微信客户端从服务器下载小程序代码包
  2. 解析编译:微信客户端解析代码包,将 WXML 转换为虚拟 DOM,WXSS 转换为样式
  3. 初始化运行:执行 app.js 初始化全局对象,然后加载页面逻辑层代码
  4. 渲染页面:渲染层根据虚拟 DOM 渲染页面,展示给用户
  5. 交互处理:用户操作触发事件,渲染层传递给逻辑层,逻辑层处理后通过 setData 更新数据

核心原理:

  • 双线程模型:渲染层和逻辑层分离
  • 虚拟 DOM:提高渲染效率
  • 数据驱动:通过 setData 实现数据驱动视图更新
  • Native 能力桥接:通过 JSBridge 调用微信原生能力

代码示例:

// 小程序初始化流程
// 1. App 实例化
App({
  onLaunch(options) {
    // 小程序启动时触发,全局只触发一次
    console.log('场景值:', options.scene)
  }
})

// 2. Page 实例化
Page({
  data: { text: 'Hello' },
  onLoad(options) {
    // 页面加载时触发
    this.setData({ text: 'World' })
  }
})

常见误区:

  • 误区一:小程序代码在本地运行。实际上首次需要下载代码包
  • 误区二:小程序能实现所有原生功能。实际上小程序只能使用微信提供的 API

4. 小程序运行机制

定义: 小程序运行机制包括启动机制、页面管理、后台运行机制等。

启动机制:

  • 冷启动:用户首次打开或小程序被销毁后重新打开,需要重新初始化
  • 热启动:小程序已在后台运行,用户再次打开,直接恢复到前台

后台运行机制:

  • 小程序进入后台后,会保持一段时间的运行状态(最长 5 分钟)
  • 超过时间或内存占用过大,会被微信主动销毁
  • 可使用 wx.onAppShowwx.onAppHide 监听前后台切换

内存管理:

  • iOS:占用内存超过一定阈值(约 1GB)会被销毁
  • Android:占用内存超过一定阈值(约 500MB)会被销毁
  • 页面栈最大 10 层

代码示例:

// 监听前后台切换
App({
  onLaunch() {
    wx.onAppShow(() => {
      console.log('小程序进入前台')
    })
    wx.onAppHide(() => {
      console.log('小程序进入后台')
    })
  }
})

// 监听内存不足警告
wx.onMemoryWarning(function () {
  console.log('内存不足警告')
  // 应主动释放内存,如清理缓存数据
})

常见误区:

  • 误区一:小程序在后台一直运行。实际上会在一定条件下被销毁
  • 误区二:页面栈没有上限。实际上最大支持 10 层

5. 小程序与 H5 的区别

双方定义:

  • 小程序:运行在微信客户端内的轻量级应用,使用 WXML/WXSS/JS 开发,有独立的运行环境和 API
  • H5:基于 HTML5 技术的移动网页,运行在浏览器中,通过 URL 访问

对比表格:

维度小程序H5
运行环境微信客户端内浏览器
技术栈WXML、WXSS、JSHTML、CSS、JS
开发工具微信开发者工具任意编辑器
审核发布需微信审核自行发布
入口方式扫码、搜索、分享等URL、二维码
系统能力丰富的微信 Native API受浏览器限制
性能接近原生体验受网络影响大
更新方式发版审核即时更新
缓存机制代码包缓存、本地存储浏览器缓存
分享能力原生分享支持需借助分享组件
支付能力原生微信支付需跳转或 JSAPI
用户体系微信账号体系需自建用户体系

选择策略:

场景选择
需要快速迭代、频繁更新H5
需要调用系统能力(蓝牙、NFC等)小程序/原生 App
轻量级服务、即用即走小程序
复杂图形、游戏原生 App 或 H5 + Canvas
需要微信平台流量小程序
多平台兼容需求H5

6. 小程序的优势

  • 无需安装:用户无需下载安装,节省手机存储空间
  • 即用即走:打开速度快,使用完即可关闭
  • 开发成本低:一套代码可在微信生态内运行
  • 流量入口多:扫码、搜索、公众号、分享等多种入口
  • 微信生态:天然接入微信支付、用户体系、社交分享
  • 审核机制:保证应用质量和用户体验
  • 离线可用:代码包缓存后可离线使用部分功能

7. 小程序的劣势

  • 体积限制:主包不超过 2MB,所有分包不超过 20MB
  • 能力受限:只能使用微信提供的 API,无法完全调用系统能力
  • 审核周期:需要微信审核,更新不如 H5 灵活
  • 平台依赖:强依赖微信生态,无法脱离微信独立运行
  • 性能瓶颈:复杂场景下性能不如原生 App
  • 双线程限制:无法直接操作 DOM,部分 Web 技术无法使用

二、小程序生命周期

8. 小程序生命周期概览

小程序生命周期分为三个层次:App(应用)、Page(页面)、Component(组件)。

生命周期流程图:

App 生命周期:
onLaunch → onShow → onHide → (后台) → onShow → onDestroy

Page 生命周期:
onLoad → onShow → onReady → (用户交互) → onHide → onUnload

Component 生命周期:
created → attached → ready → moved → detached

9. App 生命周期

定义: App 生命周期是整个小程序应用的生命周期,在 app.js 中定义,全局唯一。

生命周期函数:

函数触发时机使用场景
onLaunch小程序初始化完成时(全局只触发一次)获取启动参数、初始化全局数据
onShow小程序启动或从后台进入前台时数据刷新、状态恢复
onHide小程序从前台进入后台时数据保存、清理定时器
onError小程序发生脚本错误或 API 调用失败时错误上报
onPageNotFound小程序要打开的页面不存在时跳转兜底页面

代码示例:

// app.js
App({
  onLaunch(options) {
    console.log('小程序启动,场景值:', options.scene)
    // 获取用户信息
    const userInfo = wx.getStorageSync('userInfo')
    if (userInfo) {
      this.globalData.userInfo = userInfo
    }
  },

  onShow(options) {
    console.log('小程序进入前台,场景值:', options.scene)
    // 检查版本更新
    this.checkUpdate()
  },

  onHide() {
    console.log('小程序进入后台')
    // 保存用户数据
  },

  onError(error) {
    console.error('小程序发生错误:', error)
    // 上报错误到服务器
    this.reportError(error)
  },

  onPageNotFound() {
    wx.redirectTo({
      url: '/pages/404/404'
    })
  },

  checkUpdate() {
    if (wx.canIUse('getUpdateManager')) {
      const updateManager = wx.getUpdateManager()
      updateManager.onUpdateReady(() => {
        wx.showModal({
          title: '更新提示',
          content: '新版本已准备好,是否重启应用?',
          success(res) {
            if (res.confirm) {
              updateManager.applyUpdate()
            }
          }
        })
      })
    }
  },

  globalData: {
    userInfo: null,
    baseUrl: 'https://api.example.com'
  }
})

常见误区:

  • 误区一:onLaunch 每次打开都会触发。实际上只在首次启动或销毁后重新打开时触发
  • 误区二:onShow 和 onLaunch 功能相同。onShow 每次从小程序从前台到后台再到前台都会触发

10. Page 生命周期

定义: Page 生命周期是小程序页面的生命周期,在页面的 .js 文件中定义。

生命周期函数:

函数触发时机使用场景
onLoad页面加载时触发(只触发一次)获取页面参数、初始化数据
onShow页面显示时触发(每次显示都触发)数据刷新
onReady页面初次渲染完成时触发(只触发一次)操作 DOM(如创建地图、视频等)
onHide页面隐藏时触发暂停视频、保存状态
onUnload页面卸载时触发清理定时器、取消监听
onPullDownRefresh用户下拉刷新时触发刷新数据
onReachBottom页面上拉触底时触发加载更多数据
onShareAppMessage用户点击右上角分享时触发设置分享内容
onResize屏幕旋转时触发适配横竖屏
onTabItemTap点击 tab 栏时触发自定义 tab 行为

代码示例:

// pages/detail/detail.js
Page({
  data: {
    id: '',
    detail: null,
    loading: false
  },

  onLoad(options) {
    console.log('页面加载,参数:', options)
    this.setData({ id: options.id })
    this.fetchDetail(options.id)
  },

  onShow() {
    console.log('页面显示')
    // 每次显示页面时刷新数据
    if (this.data.id) {
      this.fetchDetail(this.data.id)
    }
  },

  onReady() {
    console.log('页面初次渲染完成')
    // 可以创建 map 上下文、video 上下文等
  },

  onHide() {
    console.log('页面隐藏')
    // 暂停动画、视频等
  },

  onUnload() {
    console.log('页面卸载')
    // 清理工作
    if (this.timer) {
      clearInterval(this.timer)
    }
  },

  onPullDownRefresh() {
    console.log('用户下拉刷新')
    this.fetchDetail(this.data.id).then(() => {
      wx.stopPullDownRefresh()
    })
  },

  onReachBottom() {
    console.log('用户上拉触底')
    this.loadMore()
  },

  onShareAppMessage() {
    return {
      title: '分享标题',
      path: `/pages/detail/detail?id=${this.data.id}`,
      imageUrl: '/images/share.png'
    }
  },

  fetchDetail(id) {
    this.setData({ loading: true })
    return wx.request({
      url: `https://api.example.com/detail/${id}`
    }).then(res => {
      this.setData({ detail: res.data })
    }).finally(() => {
      this.setData({ loading: false })
    })
  },

  loadMore() {
    // 加载更多数据逻辑
  }
})

生命周期执行顺序:

页面首次打开: onLoad → onShow → onReady
页面切换到其他页面再返回: onShow
页面关闭: onHide (navigateTo) / onUnload (redirectTo)

常见误区:

  • 误区一:onLoad 每次都触发。实际上页面缓存后再返回只触发 onShow
  • 误区二:onReady 可以多次触发。实际上只触发一次
  • 误区三:下拉刷新默认开启。需要在 page.json 中设置 "enablePullDownRefresh": true

11. Component 生命周期

定义: Component 生命周期是自定义组件的生命周期,在组件的 .js 文件中定义。

生命周期函数:

函数触发时机使用场景
created组件实例刚刚被创建时初始化数据,此时不能访问 this.data
attached组件被添加到页面节点树时初始化数据,可访问 this.data 和父组件
ready组件布局完成后获取节点信息、创建上下文
moved组件被移动到节点树另一个位置时处理节点位置变化
detached组件被从页面节点树移除时清理工作
error组件方法抛出错误时错误处理

代码示例:

// components/my-component/my-component.js
Component({
  lifetimes: {
    created() {
      console.log('组件实例创建')
      // 此时 this.data 还未初始化
    },
    attached() {
      console.log('组件被添加到节点树')
      // 可以访问 this.data 和 this.properties
      this.initData()
    },
    ready() {
      console.log('组件布局完成')
      // 可以获取节点信息
      this.createSelectorQuery().select('.my-class').boundingClientRect()
    },
    moved() {
      console.log('组件位置被移动')
    },
    detached() {
      console.log('组件被移除')
      // 清理定时器、取消监听等
      if (this.timer) {
        clearInterval(this.timer)
      }
    },
    error(err) {
      console.error('组件方法抛出错误:', err)
    }
  },

  pageLifetimes: {
    show() {
      // 页面被展示时触发
    },
    hide() {
      // 页面被隐藏时触发
    },
    resize(size) {
      // 页面尺寸变化时触发
    }
  },

  data: {
    name: ''
  },

  methods: {
    initData() {
      this.setData({ name: 'My Component' })
    }
  }
})

生命周期对比:Page vs Component

维度PageComponent
初始化onLoadcreated → attached
渲染完成onReadyready
显示onShowpageLifetimes.show
隐藏onHidepageLifetimes.hide
卸载onUnloaddetached
获取参数onLoad(options)properties

12. 关键生命周期函数详解

onLaunch vs onShow
维度onLaunchonShow
触发次数全局仅一次每次进入前台
触发时机小程序初始化完成启动或从后台进入前台
参数启动参数启动参数/场景值
典型用途初始化全局数据、登录刷新数据、检查更新
onLoad vs onReady
维度onLoadonReady
触发次数一次一次
触发时机页面加载完成页面初次渲染完成
典型用途获取参数、请求数据操作 DOM、创建上下文
能否获取节点
onPullDownRefresh(下拉刷新)
// page.json
{
  "enablePullDownRefresh": true,
  "backgroundTextStyle": "dark"
}
// page.js
Page({
  onPullDownRefresh() {
    this.refreshData().then(() => {
      wx.stopPullDownRefresh() // 停止下拉刷新动画
    })
  }
})
onReachBottom(上拉触底)
// page.json
{
  "onReachBottomDistance": 50 // 距底部距离触发,默认 50px
}
// page.js
Page({
  data: {
    list: [],
    page: 1,
    hasMore: true
  },
  onReachBottom() {
    if (this.data.hasMore) {
      this.loadMore()
    }
  }
})

三、页面与组件

13. 小程序页面

定义: 小程序页面是构成小程序的基本单元,每个页面由四个文件组成:.wxml(结构)、.wxss(样式)、.js(逻辑)、.json(配置)。

页面注册:

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/detail/detail",
    "pages/user/user"
  ]
}

页面结构:

pages/
├── index/
│   ├── index.wxml    // 页面结构
│   ├── index.wxss    // 页面样式
│   ├── index.js      // 页面逻辑
│   └── index.json    // 页面配置

页面配置(index.json):

{
  "navigationBarTitleText": "首页",
  "enablePullDownRefresh": true,
  "navigationBarBackgroundColor": "#ffffff"
}

14. 小程序组件 / 自定义组件

定义: 自定义组件是可以复用的代码单元,具有独立的 wxml、wxss、js、json 文件,可以在多个页面中引用使用。

创建组件:

// components/my-button/my-button.json
{
  "component": true,
  "usingComponents": {}
}
<!-- components/my-button/my-button.wxml -->
<view class="my-button" bindtap="onTap">
  <slot></slot>
</view>
/* components/my-button/my-button.wxss */
.my-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 12rpx 24rpx;
  background-color: #07c160;
  color: #fff;
  border-radius: 8rpx;
  font-size: 28rpx;
}
// components/my-button/my-button.js
Component({
  properties: {
    disabled: {
      type: Boolean,
      value: false
    },
    type: {
      type: String,
      value: 'default'
    }
  },
  data: {},
  methods: {
    onTap() {
      if (!this.data.disabled) {
        this.triggerEvent('click', { time: Date.now() })
      }
    }
  }
})

使用组件:

// pages/index/index.json
{
  "usingComponents": {
    "my-button": "/components/my-button/my-button"
  }
}
<!-- pages/index/index.wxml -->
<my-button type="primary" bind:click="handleClick">
  点击我
</my-button>

15. 组件通信

定义: 组件通信是指父组件、子组件、兄弟组件之间传递数据和事件的方式。

通信方式:

方式方向说明
properties父 → 子父组件通过属性传递数据给子组件
triggerEvent子 → 父子组件通过事件传递数据给父组件
parent子 → 父子组件访问父组件实例
selectComponent父 → 子父组件获取子组件实例
EventChannel跨页面页面间事件通信通道
globalData全局通过 getApp().globalData 全局共享
Storage全局通过本地存储共享

父传子(properties):

<!-- 父组件 wxml -->
<child-component title="{{pageTitle}}" count="{{itemCount}}"></child-component>
// 子组件 js
Component({
  properties: {
    title: String,
    count: {
      type: Number,
      value: 0,
      observer(newVal, oldVal) {
        console.log('count 变化:', oldVal, '→', newVal)
      }
    }
  }
})

子传父(triggerEvent):

<!-- 子组件 wxml -->
<button bindtap="handleClick">提交</button>
// 子组件 js
Component({
  methods: {
    handleClick() {
      this.triggerEvent('submit', { 
        data: this.data.formData,
        timestamp: Date.now()
      }, { bubbles: true })
    }
  }
})
<!-- 父组件 wxml -->
<child-component bind:submit="onChildSubmit"></child-component>
// 父组件 js
Page({
  onChildSubmit(event) {
    console.log('子组件提交的数据:', event.detail)
  }
})

父获取子实例(selectComponent):

<!-- 父组件 wxml -->
<child-component id="myChild"></child-component>
<button bindtap="callChildMethod">调用子组件方法</button>
// 父组件 js
Page({
  callChildMethod() {
    const child = this.selectComponent('#myChild')
    if (child) {
      child.reset() // 调用子组件方法
    }
  }
})

兄弟组件通信:

// 方式一:通过父组件中转
// 子组件A → 父组件 → 子组件B

// 方式二:通过 globalData
const app = getApp()
app.globalData.sharedData = value

// 方式三:通过 EventBus
// 实现简单的事件发布订阅
const eventBus = {
  events: {},
  on(event, callback) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(callback)
  },
  emit(event, data) {
    const callbacks = this.events[event] || []
    callbacks.forEach(cb => cb(data))
  }
}

常见误区:

  • 误区一:子组件可以直接修改 properties。应该通过事件通知父组件修改
  • 误区二:triggerEvent 可以直接传给兄弟组件。需要通过父组件中转

16. 组件属性

定义: 组件属性是父组件向子组件传递数据的方式,在 Component 的 properties 中定义。

属性定义:

Component({
  properties: {
    // 简写形式
    name: String,
    
    // 完整形式
    config: {
      type: Object,
      value: { show: true },
      observer: function(newVal, oldVal, changedPath) {
        console.log('属性变化:', newVal)
      }
    },
    
    // 多类型
    value: {
      type: [String, Number],
      value: ''
    }
  }
})

属性类型:

  • String
  • Number
  • Boolean
  • Object
  • Array
  • null(任意类型)

17. 组件方法

定义: 组件方法是组件内部定义的函数,包括事件处理函数和自定义方法。

Component({
  methods: {
    // 事件处理函数
    onTap(event) {
      console.log('点击事件:', event)
    },
    
    // 自定义方法
    reset() {
      this.setData({ count: 0 })
    },
    
    // 获取数据
    getData() {
      return this.data.list
    }
  }
})

18. 组件事件

定义: 组件事件是组件对外暴露的通信方式,子组件通过 triggerEvent 触发,父组件通过 bind 或 catch 绑定。

// 子组件触发事件
Component({
  methods: {
    handleConfirm() {
      this.triggerEvent('confirm', {
        selected: this.data.selected
      }, {
        bubbles: true,      // 是否冒泡
        composed: true,     // 是否可以穿越组件边界
        capturePhase: false // 是否有捕获阶段
      })
    }
  }
})
<!-- 父组件绑定事件 -->
<child bind:confirm="onConfirm" />
<child catch:confirm="onConfirm" /> <!-- 阻止冒泡 -->

19. 组件插槽

定义: 组件插槽允许父组件向子组件内插入内容。

单个插槽:

<!-- 子组件 wxml -->
<view class="wrapper">
  <slot></slot>
</view>
<!-- 父组件使用 -->
<my-component>
  <text>插入的内容</text>
</my-component>

多个插槽:

// 子组件 js - 启用多插槽
Component({
  options: {
    multipleSlots: true
  }
})
<!-- 子组件 wxml -->
<view class="header">
  <slot name="header"></slot>
</view>
<view class="content">
  <slot name="content"></slot>
</view>
<view class="footer">
  <slot name="footer"></slot>
</view>
<!-- 父组件使用 -->
<my-component>
  <text slot="header">头部</text>
  <text slot="content">内容</text>
  <text slot="footer">底部</text>
</my-component>

20. Behavior

定义: Behavior 是用于组件间代码共享的特性,类似于"mixins"或"traits",可以定义一组共用的属性、数据、方法等。

创建 Behavior:

// behaviors/my-behavior.js
export const pageRefreshBehavior = Behavior({
  data: {
    loading: false,
    hasError: false
  },
  methods: {
    showLoading() {
      this.setData({ loading: true, hasError: false })
    },
    hideLoading() {
      this.setData({ loading: false })
    },
    showError(msg) {
      this.setData({ hasError: true })
      wx.showToast({ title: msg, icon: 'none' })
    }
  }
})

使用 Behavior:

// components/my-component/my-component.js
import { pageRefreshBehavior } from '../../behaviors/my-behavior'

Component({
  behaviors: [pageRefreshBehavior],
  
  data: {
    name: ''
  },
  
  methods: {
    fetchData() {
      this.showLoading()
      wx.request({
        url: '/api/data'
      }).then(res => {
        this.setData({ list: res.data })
      }).catch(err => {
        this.showError('加载失败')
      }).finally(() => {
        this.hideLoading()
      })
    }
  }
})

21. 组件复用

复用方式:

方式适用场景
自定义组件UI 组件复用
Behavior逻辑代码复用
模板(template)纯视图复用
WXS纯逻辑复用(过滤、计算)

模板复用示例:

<!-- templates/card.wxml -->
<template name="card">
  <view class="card">
    <text class="title">{{title}}</text>
    <text class="desc">{{desc}}</text>
  </view>
</template>
<!-- 使用模板 -->
<import src="/templates/card.wxml" />
<template is="card" data="{{title: '标题', desc: '描述'}}" />

WXS 复用示例:

<!-- filters.wxs -->
var filters = {
  formatPrice: function(price) {
    return '¥' + price.toFixed(2)
  },
  formatDate: function(date) {
    var d = getDate(date)
    return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
  }
}
module.exports = {
  formatPrice: filters.formatPrice,
  formatDate: filters.formatDate
}
<!-- 使用 WXS -->
<wxs src="/filters.wxs" module="filters" />
<text>{{filters.formatPrice(price)}}</text>

四、数据绑定与事件处理

22. 数据绑定

定义: 数据绑定是将逻辑层(JS)的数据传递到渲染层(WXML),实现视图动态更新。

绑定方式:

方式语法说明
内容绑定\{\{value\}\}在文本中插入变量
属性绑定attr="\{\{value\}\}"在属性中使用变量
控制属性wx:if="\{\{condition\}\}"条件判断中使用
关键字\{\{true\}\} / \{\{false\}\}布尔值绑定
运算\{\{a + b\}\}支持简单运算

代码示例:

<!-- WXML -->
<view>
  <!-- 内容绑定 -->
  <text>{{message}}</text>
  
  <!-- 属性绑定 -->
  <image src="{{imageUrl}}" />
  <view id="item-{{id}}">动态ID</view>
  
  <!-- 三元运算 -->
  <text class="{{isActive ? 'active' : ''}}">状态</text>
  
  <!-- 数学运算 -->
  <text>价格: {{price * quantity}}</text>
  
  <!-- 字符串操作 -->
  <text>{{str.substring(0, 10)}}</text>
  
  <!-- 数组操作 -->
  <text>长度: {{array.length}}</text>
</view>
// JS
Page({
  data: {
    message: 'Hello World',
    imageUrl: '/images/logo.png',
    id: 123,
    isActive: true,
    price: 19.9,
    quantity: 2,
    str: 'Hello World',
    array: [1, 2, 3, 4, 5]
  }
})

常见误区:

  • 误区一:可以在 WXML 中调用 JS 函数。WXML 不支持直接调用函数,应使用 WXS
  • 误区二:数据绑定是双向的。小程序是单向数据绑定,需要通过事件手动更新数据

23. setData

定义: setData 是小程序中更新数据的唯一方式,将数据从逻辑层发送到渲染层,实现异步视图更新。

原理:

  1. 逻辑层调用 setData,将数据序列化
  2. 数据通过 JSBridge 传递给渲染层
  3. 渲染层对比虚拟 DOM,计算最小更新
  4. 渲染层更新真实 DOM,触发视图重绘

基础用法:

Page({
  data: {
    name: '初始值',
    count: 0
  },
  updateData() {
    this.setData({
      name: '新值',
      count: this.data.count + 1
    })
  }
})

路径更新:

Page({
  data: {
    user: { name: 'Tom', age: 20 },
    list: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
  },
  updatePath() {
    // 更新对象属性
    this.setData({
      'user.name': 'Jerry'
    })
    
    // 更新数组元素
    this.setData({
      'list[0].name': 'A-Updated',
      'list[1]': { id: 3, name: 'C' }
    })
  }
})

回调函数:

Page({
  data: { count: 0 },
  updateWithCallback() {
    this.setData({
      count: this.data.count + 1
    }, () => {
      // setData 完成后的回调
      console.log('更新完成后的值:', this.data.count)
    })
  }
})

性能优化:

优化策略说明示例
减少 setData 频率合并多次更新为一次this.setData({ a: 1, b: 2 }) 而非两次调用
减少 setData 数据量只传递变化的数据使用路径更新,而非整个对象
避免频繁 setData列表数据使用局部更新使用虚拟列表
后台页面不调用页面隐藏时停止更新在 onHide 中暂停定时器
避免 setData 大数据不传递超大对象/数组只传递必要字段

最佳实践:

Page({
  data: {
    list: [],
    total: 0
  },
  
  // 局部更新数组元素
  updateItem(index, newData) {
    this.setData({
      [`list[${index}]`]: Object.assign({}, this.data.list[index], newData)
    })
  },
  
  // 追加数组元素(避免全量更新)
  appendItems(newItems) {
    const index = this.data.list.length
    const updateObj = {}
    newItems.forEach((item, i) => {
      updateObj[`list[${index + i}]`] = item
    })
    updateObj.total = index + newItems.length
    this.setData(updateObj)
  },
  
  // 后台停止更新
  onHide() {
    if (this.timer) {
      clearInterval(this.timer)
      this.timer = null
    }
  }
})

常见误区:

  • 误区一:直接修改 this.data。必须使用 setData,否则视图不会更新
  • 误区二:频繁调用 setData。会导致性能问题
  • 误区三:setData 传递整个大对象。应使用路径更新减少数据传输

24. 数据更新

定义: 数据更新指在小程序中修改和同步数据到视图的过程。

更新方式对比:

方式是否更新视图说明
this.setData()推荐方式,同步到视图
this.data.xxx = value仅修改数据,不触发视图更新

数据更新策略:

Page({
  data: {
    counter: 0,
    form: { name: '', email: '' },
    items: []
  },
  
  // 方式一:全量更新(适合少量数据)
  fullUpdate() {
    this.setData({
      form: { name: 'Tom', email: 'tom@example.com' }
    })
  },
  
  // 方式二:路径更新(适合部分更新)
  partialUpdate() {
    this.setData({
      'form.name': 'Tom'
    })
  },
  
  // 方式三:批量更新(合并多次更新)
  batchUpdate() {
    this.setData({
      counter: 1,
      'form.name': 'Tom',
      'form.email': 'tom@example.com'
    })
  }
})

25. 列表渲染

定义: 列表渲染使用 wx:for 指令遍历数组,重复渲染模板块。

基础用法:

<!-- wx:for 渲染列表 -->
<view wx:for="{{users}}" wx:key="id">
  <text>{{index}}: {{item.name}} - {{item.age}}岁</text>
</view>

<!-- 指定 item 和 index 名称 -->
<view wx:for="{{users}}" wx:for-item="user" wx:for-index="idx" wx:key="id">
  <text>{{idx}}: {{user.name}}</text>
</view>

wx:key 的作用:

  • 提高列表渲染性能
  • 保持组件状态
  • 避免不必要的重新渲染
<!-- 使用唯一 id 作为 key -->
<view wx:for="{{list}}" wx:key="id">{{item.name}}</view>

<!-- 使用 *this 表示数组元素为字符串等基础类型 -->
<view wx:for="{{['a', 'b', 'c']}}" wx:key="*this">{{item}}</view>

<!-- 使用保留字 *this -->
<block wx:for="{{12345}}" wx:key="*this">
  <view>数字: {{item}}</view>
</block>

嵌套列表:

<view wx:for="{{categories}}" wx:key="id">
  <text>{{item.name}}</text>
  <view wx:for="{{item.children}}" wx:for-item="child" wx:key="id">
    <text>-- {{child.name}}</text>
  </view>
</view>

最佳实践:

// JS - 使用唯一 ID 作为 key
Page({
  data: {
    list: [
      { id: 'uuid-1', name: 'Item 1', checked: false },
      { id: 'uuid-2', name: 'Item 2', checked: true }
    ]
  }
})

常见误区:

  • 误区一:不使用 wx:key。会导致性能问题和状态混乱
  • 误区二:使用 index 作为 key。在列表动态变化时会导致问题

26. 条件渲染

定义: 条件渲染根据条件决定是否渲染元素。

指令:

指令说明
wx:if条件为 true 时渲染
wx:elif额外条件判断
wx:else条件为 false 时渲染
hidden通过 CSS display 控制显示隐藏

wx:if vs hidden 对比:

维度wx:ifhidden
渲染方式条件为 false 时不渲染(销毁)始终渲染,通过 CSS 控制
切换开销较高(需要创建/销毁)较低
初始渲染较低(条件为 false 时不渲染)较高(始终渲染)
适用场景条件很少切换频繁切换显示状态

代码示例:

<!-- wx:if -->
<view wx:if="{{score >= 90}}">优秀</view>
<view wx:elif="{{score >= 60}}">及格</view>
<view wx:else>不及格</view>

<!-- block wx:if(不渲染自身节点) -->
<block wx:if="{{isLoggedIn}}">
  <view>欢迎回来,{{userName}}</view>
  <button>退出登录</button>
</block>

<!-- hidden -->
<view hidden="{{!isLoading}}">加载中...</view>

27. 事件绑定

定义: 事件绑定是将用户操作(点击、滑动等)与处理函数关联。

绑定方式:

语法说明是否冒泡
bind:eventName绑定事件
bind:eventName / catch:eventNamecatch 阻止冒泡catch 阻止冒泡
mut-bind:eventName互斥事件绑定是,但互斥

常见事件:

事件说明
tap点击
longpress长按(超过 350ms)
touchstart触摸开始
touchmove触摸移动
touchend触摸结束
touchcancel触摸取消
input输入框输入
change值变化

代码示例:

<view bind:tap="onTap" catch:longpress="onLongPress">
  点击或长按
</view>

<input bind:input="onInput" />

<view bind:touchstart="onTouchStart" 
      bind:touchmove="onTouchMove"
      bind:touchend="onTouchEnd">
  触摸区域
</view>
Page({
  onTap(event) {
    console.log('点击', event)
  },
  onLongPress(event) {
    console.log('长按', event)
  },
  onInput(event) {
    this.setData({ value: event.detail.value })
  },
  onTouchStart(event) {
    console.log('触摸开始', event.touches)
  }
})

28. 事件对象

定义: 事件对象包含事件的详细信息,通过事件处理函数的参数传递。

事件对象属性:

属性说明
type事件类型
timeStamp事件生成时的时间戳
target触发事件的组件
currentTarget当前绑定事件的组件
touches触摸点信息数组
changedTouches变化的触摸点信息数组
detail自定义事件携带的额外信息

代码示例:

Page({
  onTap(event) {
    console.log('事件类型:', event.type)
    console.log('触发事件的目标组件:', event.target)
    console.log('当前绑定事件的组件:', event.currentTarget)
    console.log('自定义数据:', event.detail)
    console.log('dataset:', event.currentTarget.dataset)
  }
})
<!-- 通过 data- 传递自定义数据 -->
<view data-id="{{item.id}}" data-name="{{item.name}}" bind:tap="onTap">
  {{item.name}}
</view>
// 获取自定义数据
Page({
  onTap(event) {
    const { id, name } = event.currentTarget.dataset
    console.log('id:', id, 'name:', name)
  }
})

29. 事件传参

定义: 小程序事件传参不能直接在绑定中传参,需要通过 data- 属性或自定义事件。

方式一:data- 属性

<view data-id="{{item.id}}" data-index="{{index}}" bind:tap="handleTap">
  {{item.name}}
</view>
Page({
  handleTap(event) {
    const { id, index } = event.currentTarget.dataset
    console.log('id:', id, 'index:', index)
  }
})

方式二:自定义组件 triggerEvent

// 子组件
Component({
  methods: {
    handleClick() {
      this.triggerEvent('custom-event', {
        id: this.data.id,
        data: this.data.data
      })
    }
  }
})
<!-- 父组件 -->
<child bind:custom-event="onCustomEvent" />
// 父组件
Page({
  onCustomEvent(event) {
    const { id, data } = event.detail
  }
})

30. 事件冒泡 / 事件捕获 / 阻止事件冒泡

定义:

  • 事件冒泡:事件从触发节点向上传递到父节点
  • 事件捕获:事件从外层向内部传递(微信小程序不直接支持)
  • 阻止冒泡:使用 catch 阻止事件继续向上传递

事件冒泡:

<view bind:tap="onOuterTap" style="padding: 30px; background: #f0f0f0;">
  外层
  <view bind:tap="onInnerTap" style="padding: 20px; background: #ccc;">
    内层
    <view bind:tap="onCenterTap" style="padding: 10px; background: #999;">
      中心
    </view>
  </view>
</view>
Page({
  onOuterTap() { console.log('外层触发') },
  onInnerTap() { console.log('内层触发') },
  onCenterTap() { console.log('中心触发') }
})
// 点击"中心"时,依次输出:中心触发 → 内层触发 → 外层触发

阻止冒泡:

<!-- 使用 catch 代替 bind 阻止冒泡 -->
<view catch:tap="onInnerTap">
  <view catch:tap="onCenterTap">中心</view>
</view>
// 点击"中心"时,仅输出:中心触发

mut-bind(互斥事件):

<!-- mut-bind 只触发第一个被触发的事件,其他 mut-bind 失效 -->
<view mut-bind:tap="onTap1">
  <view mut-bind:tap="onTap2">子元素</view>
</view>

五、API 调用

31. 小程序 API 概览

微信小程序提供了丰富的原生能力 API,涵盖网络、媒体、位置、设备、开放接口等多个方面。

API 分类:

分类常用 API
网络wx.request, wx.uploadFile, wx.downloadFile
媒体wx.chooseImage, wx.previewImage, wx.chooseVideo
位置wx.getLocation, wx.openLocation, wx.chooseLocation
设备wx.getSystemInfo, wx.scanCode, wx.setClipboardData
界面wx.showToast, wx.showModal, wx.navigateTo
开放接口wx.login, wx.getUserProfile, wx.requestPayment
数据缓存wx.setStorage, wx.getStorage, wx.clearStorage
文件wx.getFileSystemManager

API 调用规范:

// Promise 风格调用(基础库 2.10.2+)
wx.request({
  url: 'https://api.example.com/data'
}).then(res => {
  console.log('成功', res.data)
}).catch(err => {
  console.error('失败', err)
})

// 回调风格调用
wx.request({
  url: 'https://api.example.com/data',
  success(res) {
    console.log('成功', res.data)
  },
  fail(err) {
    console.error('失败', err)
  },
  complete() {
    console.log('完成')
  }
})

32. 网络请求 / wx.request

定义: wx.request 用于发起 HTTPS 网络请求。

配置要求:

  • 必须在小程序管理后台配置请求域名
  • 仅支持 HTTPS 协议
  • 默认超时 60 秒

基础用法:

wx.request({
  url: 'https://api.example.com/data',
  method: 'GET',
  data: {
    page: 1,
    size: 10
  },
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  success(res) {
    if (res.statusCode === 200) {
      console.log('请求成功', res.data)
    }
  },
  fail(err) {
    console.error('请求失败', err)
  }
})

请求封装最佳实践:

// utils/request.js
const BASE_URL = 'https://api.example.com'

function request(options) {
  const { url, method = 'GET', data = {}, header = {} } = options
  
  // 获取 token
  const token = wx.getStorageSync('token')
  
  return new Promise((resolve, reject) => {
    wx.request({
      url: `${BASE_URL}${url}`,
      method,
      data,
      header: {
        'Content-Type': 'application/json',
        'Authorization': token ? `Bearer ${token}` : '',
        ...header
      },
      timeout: 10000,
      success(res) {
        if (res.statusCode === 200) {
          if (res.data.code === 0) {
            resolve(res.data.data)
          } else {
            wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
            reject(res.data)
          }
        } else if (res.statusCode === 401) {
          // token 过期,跳转登录
          wx.removeStorageSync('token')
          wx.redirectTo({ url: '/pages/login/login' })
          reject(new Error('未授权'))
        } else {
          reject(new Error(`HTTP ${res.statusCode}`))
        }
      },
      fail(err) {
        wx.showToast({ title: '网络异常', icon: 'none' })
        reject(err)
      }
    })
  })
}

module.exports = {
  get: (url, data, header) => request({ url, method: 'GET', data, header }),
  post: (url, data, header) => request({ url, method: 'POST', data, header }),
  put: (url, data, header) => request({ url, method: 'PUT', data, header }),
  delete: (url, data, header) => request({ url, method: 'DELETE', data, header })
}
// 使用
const request = require('../../utils/request')

Page({
  onLoad() {
    request.get('/api/users', { page: 1 }).then(users => {
      this.setData({ users })
    })
  }
})

33. 文件上传 / wx.uploadFile

定义: wx.uploadFile 用于将本地资源上传到服务器。

代码示例:

// 选择图片并上传
Page({
  data: { uploadedUrl: '' },
  
  uploadImage() {
    wx.chooseMedia({
      count: 1,
      mediaType: ['image'],
      success: (res) => {
        const tempFilePath = res.tempFiles[0].tempFilePath
        
        wx.uploadFile({
          url: 'https://api.example.com/upload',
          filePath: tempFilePath,
          name: 'file',
          header: {
            'Authorization': `Bearer ${wx.getStorageSync('token')}`
          },
          formData: {
            type: 'avatar'
          },
          success: (uploadRes) => {
            const data = JSON.parse(uploadRes.data)
            this.setData({ uploadedUrl: data.url })
            wx.showToast({ title: '上传成功', icon: 'success' })
          },
          fail: (err) => {
            wx.showToast({ title: '上传失败', icon: 'none' })
          }
        })
      }
    })
  }
})

34. 文件下载 / wx.downloadFile

定义: wx.downloadFile 用于下载文件资源到本地。

代码示例:

Page({
  downloadFile() {
    wx.downloadFile({
      url: 'https://example.com/file.pdf',
      success: (res) => {
        if (res.statusCode === 200) {
          // 打开文档
          wx.openDocument({
            filePath: res.tempFilePath,
            showMenu: true,
            success() {
              console.log('打开文档成功')
            }
          })
        }
      },
      fail: (err) => {
        wx.showToast({ title: '下载失败', icon: 'none' })
      }
    })
  }
})

35. 图片选择 / wx.chooseImage

定义: wx.chooseImage 用于从本地相册选择图片或使用相机拍照。

代码示例:

Page({
  data: { images: [] },
  
  chooseImages() {
    wx.chooseImage({
      count: 9,              // 最多选择数量
      sizeType: ['compressed'], // original 原图, compressed 压缩图
      sourceType: ['album', 'camera'], // 来源
      success: (res) => {
        this.setData({
          images: [...this.data.images, ...res.tempFilePaths]
        })
      }
    })
  }
})

注意:wx.chooseImage 已被 wx.chooseMedia 替代,建议使用新版 API。


36. 图片预览 / wx.previewImage

定义: wx.previewImage 用于全屏预览图片,支持缩放和滑动切换。

代码示例:

Page({
  data: { images: ['/img/1.jpg', '/img/2.jpg', '/img/3.jpg'] },
  
  previewImage(event) {
    const index = event.currentTarget.dataset.index
    wx.previewImage({
      current: this.data.images[index], // 当前显示图片
      urls: this.data.images            // 所有图片列表
    })
  }
})
<view wx:for="{{images}}" wx:key="*this">
  <image src="{{item}}" bind:tap="previewImage" data-index="{{index}}" />
</view>

37. 扫码 / wx.scanCode

定义: wx.scanCode 用于调起客户端扫码界面进行扫码。

代码示例:

Page({
  scanCode() {
    wx.scanCode({
      onlyFromCamera: false, // 是否只能从相机扫码
      scanType: ['qrCode', 'barCode'],
      success: (res) => {
        console.log('扫码结果:', res.result)
        console.log('码类型:', res.scanType)
        
        // 根据扫码结果处理
        if (res.result.startsWith('http')) {
          wx.navigateToMiniProgram({ appId: 'xxx' })
        } else {
          this.setData({ scanResult: res.result })
        }
      }
    })
  }
})

38. 定位 / wx.getLocation

定义: wx.getLocation 用于获取当前的地理位置信息。

配置要求:

  • 需要在 app.json 中声明 permission
  • 需要用户授权

代码示例:

// app.json
{
  "permission": {
    "scope.userLocation": {
      "desc": "您的位置信息将用于展示附近内容"
    }
  }
}
Page({
  getLocation() {
    wx.getLocation({
      type: 'wgs84', // wgs84 返回 gps 坐标, gcj02 返回可用于 wx.openLocation 的坐标
      altitude: false, // 是否返回高度
      isHighAccuracy: true, // 是否高精度
      highAccuracyExpireTime: 3000, // 高精度有效期
      success: (res) => {
        console.log('经度:', res.longitude)
        console.log('纬度:', res.latitude)
        console.log('速度:', res.speed)
        console.log('精确度:', res.accuracy)
        
        this.setData({
          location: {
            latitude: res.latitude,
            longitude: res.longitude
          }
        })
      },
      fail: (err) => {
        console.error('获取位置失败', err)
        // 引导用户授权
        wx.openSetting()
      }
    })
  }
})

39. 地图 / wx.openLocation

定义: wx.openLocation 用于打开微信内置地图查看位置。

代码示例:

Page({
  openLocation() {
    wx.openLocation({
      latitude: 39.9042,
      longitude: 116.4074,
      name: '天安门',
      address: '北京市东城区长安街',
      scale: 15, // 缩放比例 5-18
      success() {
        console.log('打开地图成功')
      }
    })
  }
})

使用 map 组件:

<map 
  id="myMap"
  latitude="{{latitude}}"
  longitude="{{longitude}}"
  scale="14"
  markers="{{markers}}"
  show-location
  bindmarkertap="onMarkerTap"
  style="width: 100%; height: 400px;"
/>
Page({
  data: {
    latitude: 39.9042,
    longitude: 116.4074,
    markers: [{
      id: 1,
      latitude: 39.9042,
      longitude: 116.4074,
      title: '天安门',
      iconPath: '/images/marker.png',
      width: 30,
      height: 30
    }]
  },
  
  onMarkerTap(event) {
    console.log('点击标记', event.markerId)
  }
})

40. 支付 / wx.requestPayment

定义: wx.requestPayment 用于调起微信支付。

支付流程:

用户下单 → 后端创建订单 → 返回支付参数 → 调起微信支付 → 支付结果回调 → 后端确认支付

代码示例:

Page({
  async makePayment(orderId) {
    try {
      // 1. 调用后端获取支付参数
      const payParams = await request.post('/api/pay', {
        orderId: orderId
      })
      
      // 2. 调起微信支付
      await wx.requestPayment({
        timeStamp: payParams.timeStamp,
        nonceStr: payParams.nonceStr,
        package: payParams.package,
        signType: payParams.signType,
        paySign: payParams.paySign
      })
      
      // 3. 支付成功
      wx.showToast({ title: '支付成功', icon: 'success' })
      this.setData({ payStatus: 'success' })
      
      // 4. 通知后端
      await request.post('/api/pay/success', { orderId })
      
    } catch (err) {
      if (err.errMsg === 'requestPayment:fail cancel') {
        wx.showToast({ title: '取消支付', icon: 'none' })
      } else {
        wx.showToast({ title: '支付失败', icon: 'none' })
      }
      this.setData({ payStatus: 'failed' })
    }
  }
})

41. 登录 / wx.login

定义: wx.login 用于调用接口获取临时登录凭证(code),用于后端换取用户登录态。

登录流程:

小程序 wx.login() → 获取 code → 发送 code 到后端 → 后端用 code + appid + secret 换取 openid 和 session_key → 后端生成自定义登录态返回 token → 小程序存储 token

代码示例:

// app.js
App({
  onLaunch() {
    this.login()
  },
  
  login() {
    return new Promise((resolve, reject) => {
      wx.login({
        success: (res) => {
          if (res.code) {
            // 将 code 发送到后端
            wx.request({
              url: 'https://api.example.com/login',
              method: 'POST',
              data: { code: res.code },
              success: (response) => {
                const { token, userInfo } = response.data
                wx.setStorageSync('token', token)
                this.globalData.token = token
                this.globalData.userInfo = userInfo
                resolve(userInfo)
              },
              fail: reject
            })
          } else {
            reject(new Error('登录失败'))
          }
        }
      })
    })
  },
  
  globalData: {
    token: '',
    userInfo: null
  }
})

42. 获取用户信息 / wx.getUserProfile

定义: wx.getUserProfile 用于获取用户信息(昵称、头像等),需要用户主动触发。

注意: wx.getUserInfo 已废弃,现在使用 wx.getUserProfile 或头像昵称填写组件。

代码示例:

Page({
  getUserInfo() {
    wx.getUserProfile({
      desc: '用于完善用户资料',
      success: (res) => {
        console.log('用户信息:', res.userInfo)
        this.setData({
          userInfo: res.userInfo
        })
        // 保存到本地
        wx.setStorageSync('userInfo', res.userInfo)
      },
      fail: (err) => {
        console.log('用户拒绝授权')
      }
    })
  }
})

使用 button 授权:

<button bind:tap="getUserProfile">获取用户信息</button>

<!-- 或使用新的头像昵称填写组件 -->
<button open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">选择头像</button>
<input type="nickname" placeholder="请输入昵称" />

43. 存储 / wx.setStorage / wx.getStorage / wx.removeStorage

定义: 小程序提供本地存储能力,用于持久化保存数据。

代码示例:

// 异步存储
wx.setStorage({
  key: 'userInfo',
  data: { name: 'Tom', age: 20 },
  success() {
    console.log('存储成功')
  }
})

// 异步获取
wx.getStorage({
  key: 'userInfo',
  success(res) {
    console.log('获取到数据:', res.data)
  },
  fail() {
    console.log('数据不存在')
  }
})

// 异步删除
wx.removeStorage({
  key: 'userInfo',
  success() {
    console.log('删除成功')
  }
})

// 同步操作
wx.setStorageSync('key', 'value')
const value = wx.getStorageSync('key')
wx.removeStorageSync('key')

六、路由与导航

44. 小程序路由概览

定义: 小程序路由用于管理页面之间的跳转和导航,由微信客户端统一管理。

路由方式对比:

API功能页面栈变化使用场景
wx.navigateTo保留当前页面,跳转到新页面页面栈 +1普通页面跳转
wx.redirectTo关闭当前页面,跳转到新页面页面栈不变不需要返回的场景
wx.switchTab跳转到 tabBar 页面清空非 tabBar 页面栈切换 tab
wx.reLaunch关闭所有页面,打开新页面页面栈重置为1重新登录、退出
wx.navigateBack返回上一级或多级页面页面栈 -N返回上一页

45. wx.navigateTo

// 跳转到新页面,保留当前页面
wx.navigateTo({
  url: '/pages/detail/detail?id=123&name=test',
  success() {
    console.log('跳转成功')
  },
  fail(err) {
    console.log('跳转失败', err)
  }
})

限制: 页面栈最多 10 层,超过后 navigateTo 会失败。

46. wx.redirectTo

// 关闭当前页面,跳转到新页面
wx.redirectTo({
  url: '/pages/login/login'
})

与 navigateTo 的区别:

  • navigateTo 保留当前页面,可以返回
  • redirectTo 关闭当前页面,不能返回到当前页面

47. wx.switchTab

// 跳转到 tabBar 页面,关闭其他非 tabBar 页面
wx.switchTab({
  url: '/pages/index/index'
})

注意: switchTab 只能跳转到 tabBar 页面,且不能带参数。

48. wx.reLaunch

// 关闭所有页面,打开新页面
wx.reLaunch({
  url: '/pages/index/index'
})

典型场景: 退出登录、强制重新登录。

49. wx.navigateBack

// 返回上一页
wx.navigateBack({
  delta: 1 // 返回的页面数,默认 1
})

// 返回多级
wx.navigateBack({
  delta: 2 // 返回上两级页面
})

50. 路由传参 / 获取路由参数

传参方式:

// 通过 URL 参数传递
wx.navigateTo({
  url: `/pages/detail/detail?id=${id}&type=${type}`
})

// 通过事件通道传递(基础库 2.7.3+)
wx.navigateTo({
  url: '/pages/detail/detail',
  events: {
    // 监听被打开页面发送到当前页面的数据
    acceptDataFromOpenedPage(data) {
      console.log(data)
    }
  },
  success(res) {
    // 通过 eventChannel 向被打开页面传送数据
    res.eventChannel.emit('acceptDataFromOpenerPage', { 
      source: 'index',
      data: { id: 123 }
    })
  }
})

获取参数:

// pages/detail/detail.js
Page({
  onLoad(options) {
    // 方式一:从 options 获取 URL 参数
    const { id, type } = options
    console.log('URL参数:', id, type)
    
    // 方式二:通过 eventChannel 获取事件通道数据
    const eventChannel = this.getOpenerEventChannel()
    eventChannel.on('acceptDataFromOpenerPage', (data) => {
      console.log('事件通道数据:', data)
    })
    
    // 向上一页发送数据
    eventChannel.emit('acceptDataFromOpenedPage', {
      result: 'success'
    })
  }
})

51. 页面栈

定义: 小程序维护一个页面栈,记录当前所有已打开的页面。

获取页面栈:

const pages = getCurrentPages()
console.log('页面栈长度:', pages.length)
console.log('当前页面:', pages[pages.length - 1])
console.log('上一个页面:', pages[pages.length - 2])

页面栈变化规则:

操作页面栈变化
navigateTo新页面入栈
redirectTo当前页面出栈,新页面入栈
navigateBack页面连续出栈
switchTab清空栈,新页面入栈
reLaunch清空栈,新页面入栈

实战场景:修改上一页数据

Page({
  goBackAndUpdate() {
    const pages = getCurrentPages()
    if (pages.length > 1) {
      const prevPage = pages[pages.length - 2]
      prevPage.setData({
        needRefresh: true
      })
    }
    wx.navigateBack()
  }
})

七、本地存储

52. 本地存储概览

定义: 小程序提供本地存储能力,以键值对形式存储数据,类似于浏览器的 localStorage。

存储方式对比:

API说明是否异步
wx.setStorage设置存储异步
wx.getStorage获取存储异步
wx.removeStorage删除存储异步
wx.clearStorage清空所有存储异步
wx.setStorageSync同步设置存储同步
wx.getStorageSync同步获取存储同步
wx.removeStorageSync同步删除存储同步
wx.clearStorageSync同步清空存储同步
wx.getStorageInfo获取存储信息异步
wx.getStorageInfoSync同步获取存储信息同步

53. 同步存储

// 同步写入
try {
  wx.setStorageSync('token', 'abc123')
  wx.setStorageSync('userInfo', { name: 'Tom', age: 20 })
} catch (e) {
  console.error('写入失败', e)
}

// 同步读取
try {
  const token = wx.getStorageSync('token')
  const userInfo = wx.getStorageSync('userInfo')
} catch (e) {
  console.error('读取失败', e)
}

// 同步删除
wx.removeStorageSync('token')

// 同步清空
wx.clearStorageSync()

54. 异步存储

// 异步写入
wx.setStorage({
  key: 'token',
  data: 'abc123',
  success() { console.log('写入成功') },
  fail(err) { console.error('写入失败', err) }
})

// 异步读取
wx.getStorage({
  key: 'token',
  success(res) { console.log('读取成功', res.data) },
  fail(err) { console.error('读取失败', err) }
})

// 获取存储信息
wx.getStorageInfo({
  success(res) {
    console.log('所有 keys:', res.keys)
    console.log('当前占用空间:', res.currentSize) // KB
    console.log('限制空间:', res.limitSize) // KB
  }
})

55. 存储限制

限制说明:

  • 单个 key 限制:1MB
  • 总容量限制:10MB
  • key 最大长度:1024 字节
  • 支持的数据类型:字符串、数字、布尔值、对象、数组

最佳实践:

// 封装存储工具类
const Storage = {
  // 设置带过期时间的存储
  set(key, value, expireTime) {
    const data = {
      value,
      expireTime: expireTime ? Date.now() + expireTime : null
    }
    wx.setStorageSync(key, JSON.stringify(data))
  },
  
  // 获取并检查过期
  get(key) {
    try {
      const raw = wx.getStorageSync(key)
      if (!raw) return null
      
      const data = JSON.parse(raw)
      if (data.expireTime && Date.now() > data.expireTime) {
        // 已过期,删除
        wx.removeStorageSync(key)
        return null
      }
      return data.value
    } catch (e) {
      return null
    }
  },
  
  // 删除
  remove(key) {
    wx.removeStorageSync(key)
  },
  
  // 清空
  clear() {
    wx.clearStorageSync()
  }
}

// 使用
Storage.set('token', 'abc123', 7 * 24 * 60 * 60 * 1000) // 7天过期
const token = Storage.get('token')

56. 缓存策略

策略分类:

策略说明适用场景
永久缓存数据持久化存储用户信息、偏好设置
时间缓存设置过期时间Token、接口数据
版本缓存加入版本号控制需要强制更新的数据
条件缓存根据条件决定是否缓存需要实时性的数据

实现示例:

// 带版本号的缓存
const Cache = {
  VERSION: '1.0.0',
  
  set(key, value) {
    const data = {
      value,
      version: this.VERSION,
      timestamp: Date.now()
    }
    wx.setStorageSync(key, JSON.stringify(data))
  },
  
  get(key) {
    try {
      const raw = wx.getStorageSync(key)
      if (!raw) return null
      
      const data = JSON.parse(raw)
      if (data.version !== this.VERSION) {
        // 版本不匹配,删除旧缓存
        wx.removeStorageSync(key)
        return null
      }
      return data.value
    } catch (e) {
      return null
    }
  },
  
  clear() {
    wx.clearStorageSync()
  }
}

八、网络请求

57. 网络请求配置

域名配置:

  • 在小程序管理后台配置 request 合法域名
  • 域名必须使用 HTTPS 协议
  • 域名每月修改次数有限制

开发环境设置:

// project.config.json
{
  "setting": {
    "urlCheck": false, // 开发时关闭域名校验
    "es6": true,
    "minified": true
  }
}

58. 请求域名配置

配置要求:

  1. 域名必须已备案
  2. 必须使用 HTTPS 协议
  3. 域名不支持 IP 地址和 localhost
  4. 端口必须为 443(HTTPS)

开发时绕过: 在开发者工具中勾选"不校验合法域名",仅开发环境有效。

59. 请求封装 / 请求拦截 / 响应拦截

完整封装示例:

// utils/http.js
const BASE_URL = 'https://api.example.com'

class HttpRequest {
  constructor() {
    this.interceptors = {
      request: [],
      response: []
    }
  }

  // 请求拦截器
  useRequestInterceptor(fn) {
    this.interceptors.request.push(fn)
  }

  // 响应拦截器
  useResponseInterceptor(fn) {
    this.interceptors.response.push(fn)
  }

  // 发起请求
  request(options) {
    // 执行请求拦截器
    let config = { ...options }
    this.interceptors.request.forEach(fn => {
      config = fn(config) || config
    })

    // 添加公共配置
    config.url = `${BASE_URL}${config.url}`
    config.header = {
      'Content-Type': 'application/json',
      'Authorization': wx.getStorageSync('token') || '',
      ...config.header
    }
    config.timeout = config.timeout || 10000

    return new Promise((resolve, reject) => {
      wx.request({
        ...config,
        success: (res) => {
          // 执行响应拦截器
          let result = res
          this.interceptors.response.forEach(fn => {
            result = fn(result) || result
          })

          if (res.statusCode === 200) {
            if (res.data.code === 0) {
              resolve(res.data.data)
            } else {
              wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
              reject(res.data)
            }
          } else if (res.statusCode === 401) {
            wx.removeStorageSync('token')
            wx.redirectTo({ url: '/pages/login/login' })
            reject(new Error('未授权'))
          } else {
            reject(new Error(`HTTP ${res.statusCode}`))
          }
        },
        fail: (err) => {
          wx.showToast({ title: '网络异常,请检查网络', icon: 'none' })
          reject(err)
        }
      })
    })
  }

  get(url, data, options = {}) {
    return this.request({ url, method: 'GET', data, ...options })
  }

  post(url, data, options = {}) {
    return this.request({ url, method: 'POST', data, ...options })
  }

  put(url, data, options = {}) {
    return this.request({ url, method: 'PUT', data, ...options })
  }

  delete(url, data, options = {}) {
    return this.request({ url, method: 'DELETE', data, ...options })
  }
}

export const http = new HttpRequest()

// 使用示例
http.useRequestInterceptor((config) => {
  console.log('发起请求:', config)
  return config
})

http.useResponseInterceptor((response) => {
  console.log('收到响应:', response)
  return response
})

60. 错误处理

错误类型:

Page({
  fetchData() {
    wx.request({
      url: 'https://api.example.com/data',
      success(res) {
        if (res.statusCode >= 500) {
          // 服务器错误
          this.handleServerError(res)
        } else if (res.statusCode === 404) {
          // 资源不存在
          wx.showToast({ title: '资源不存在', icon: 'none' })
        } else if (res.statusCode === 401) {
          // 未授权
          wx.redirectTo({ url: '/pages/login/login' })
        }
      },
      fail(err) {
        // 网络错误
        switch (err.errMsg) {
          case 'request:fail timeout':
            wx.showToast({ title: '请求超时', icon: 'none' })
            break
          case 'request:fail url not in domain list':
            wx.showToast({ title: '域名未配置', icon: 'none' })
            break
          default:
            wx.showToast({ title: '网络异常', icon: 'none' })
        }
      }
    })
  }
})

61. 请求超时

设置超时时间:

wx.request({
  url: 'https://api.example.com/data',
  timeout: 10000, // 10秒超时
  success(res) {
    console.log('请求成功', res.data)
  },
  fail(err) {
    if (err.errMsg.includes('timeout')) {
      wx.showToast({ title: '请求超时,请重试', icon: 'none' })
    }
  }
})

全局配置:

// app.js
App({
  onLaunch() {
    // 设置全局请求超时时间(仅影响后续创建的 request)
    // 注意:此 API 仅部分平台支持
  }
})

九、小程序开发工具

62. 微信开发者工具

定义: 微信官方提供的小程序开发 IDE,集成了编辑、调试、预览、发布等功能。

主要功能:

  • 代码编辑:语法高亮、智能提示、代码格式化
  • 实时预览:代码修改后自动刷新预览
  • 调试工具:Wxml 面板、Console 面板、Network 面板、Storage 面板
  • 性能分析:Performance 面板、Audits 面板
  • 云开发:支持云开发环境管理

63. 小程序调试

调试方式:

方式说明适用场景
模拟器调试在开发者工具内调试日常开发
真机调试连接手机调试测试真机表现
vConsole移动端调试面板线上问题排查

调试技巧:

// 1. 使用 console 调试
console.log('普通日志')
console.info('信息日志')
console.warn('警告日志')
console.error('错误日志')
console.debug('调试日志')

// 2. 使用 debugger 断点
function test() {
  debugger // 代码执行到此会暂停
  console.log('断点之后')
}

// 3. 使用 vConsole
// 在 app.js 中引入
import vConsole from './miniprogram_npm/vconsole'
const vConsole = new vConsole()

开发者工具调试面板:

  • Console:查看日志和执行 JS
  • Sources:断点调试源码
  • Network:查看网络请求
  • Storage:查看本地存储
  • Wxml:查看和编辑页面结构
  • Sensor:模拟地理位置

64. 真机调试

使用方式:

  1. 在开发者工具点击"真机调试"
  2. 使用微信扫码
  3. 在手机端打开小程序进行调试

特点:

  • 可以在真实设备上测试
  • 可以在开发者工具查看真机日志
  • 支持断点调试
  • 可以看到真机的网络请求和存储

注意事项:

  • 真机调试和线上环境有差异
  • 部分 API 在调试模式下行为不同
  • 需要关注真机性能表现

65. 代码编译

编译流程:

  1. WXML 编译为虚拟 DOM
  2. WXSS 编译为 CSS
  3. JS 编译为可执行代码
  4. 打包为小程序代码包

编译配置:

// project.config.json
{
  "compileType": "miniprogram",
  "setting": {
    "es6": true,         // 是否将 ES6 转 ES5
    "minified": true,    // 是否压缩代码
    "uglifyFileName": false, // 是否压缩文件名
    "autoPushProxy": true,
    "packNpmManually": false, // npm 构建配置
    "packNpmRelationList": []
  }
}

66. 代码上传 / 版本管理

上传流程:

  1. 在开发者工具点击"上传"
  2. 填写版本号和项目备注
  3. 代码上传到微信服务器
  4. 在小程序管理后台查看提交的版本

版本号管理:

  • 版本号格式:major.minor.patch(如 1.0.0)
  • 每次上传版本号必须递增
  • 体验版和正式版共用版本号体系

十、小程序发布与审核

67. 小程序发布

发布流程:

开发完成 → 开发者工具上传 → 管理后台设置体验版 → 提交审核 → 审核通过 → 发布上线

版本类型:

版本说明使用场景
开发版开发者工具直接预览开发阶段测试
体验版上传代码后设置内部测试、产品验收
审核版提交审核后的版本等待微信审核
正式版审核通过后发布线上用户访问

68. 小程序审核

审核流程:

  1. 提交代码到管理后台
  2. 填写审核信息(类目、标签等)
  3. 微信团队进行审核
  4. 审核结果通知(通常 1-7 个工作日)

审核注意事项:

  • 功能完整,无明显 bug
  • 不涉及违法违规内容
  • 用户体验良好
  • 符合小程序运营规范

69. 审核规范

常见审核不通过原因:

  • 存在未完成的功能
  • 诱导分享、关注等行为
  • 收集用户信息未说明用途
  • 涉及需要资质但未提供
  • 小程序内容与服务类目不符
  • 存在虚拟支付(iOS 端)

70. 版本回退

操作方式: 在小程序管理后台的"版本管理"中,可以选择回退到上一个线上版本。

使用场景:

  • 新版本出现严重 bug
  • 新版本用户体验差
  • 新版本审核通过后发现问题

71. 灰度发布

定义: 灰度发布是指将新版本先推送给部分用户,观察效果和反馈后,再逐步扩大推送范围。

操作方式: 在小程序管理后台提交发布时,可以选择"分阶段发布":

  1. 先推送一定比例的用户(如 10%)
  2. 观察数据和反馈
  3. 确认无问题后扩大推送比例
  4. 最终全量发布

72. 体验版 / 开发版

开发版:

  • 通过开发者工具预览
  • 无需上传代码
  • 仅开发者自己可见
  • 有效期较短

体验版:

  • 需要上传代码后设置
  • 可以添加体验成员
  • 体验成员通过扫描二维码访问
  • 有效期较长

十一、小程序登录详解

73. 完整登录流程

┌──────────┐      code       ┌──────────┐    code + appid + secret    ┌──────────┐
│  小程序   │ ─────────────► │  后端    │ ─────────────────────────► │  微信    │
│          │                 │          │ ◄───────────────────────── │  服务器   │
│          │ ◄───────────── │          │     openid + session_key    │          │
│          │    token        │          │                             │          │
└──────────┘                 └──────────┘                             └──────────┘

详细步骤:

  1. 小程序端调用 wx.login()
wx.login({
  success(res) {
    if (res.code) {
      // 将 code 发送给后端
      wx.request({
        url: 'https://your-server.com/api/login',
        method: 'POST',
        data: { code: res.code }
      })
    }
  }
})
  1. 后端接收 code,调用微信接口换取 openid
// Node.js 后端示例
const https = require('https')

function code2Session(code, appId, secret) {
  const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${secret}&js_code=${code}&grant_type=authorization_code`
  
  return new Promise((resolve, reject) => {
    https.get(url, res => {
      let data = ''
      res.on('data', chunk => data += chunk)
      res.on('end', () => resolve(JSON.parse(data)))
    }).on('error', reject)
  })
}

// 登录接口
app.post('/api/login', async (req, res) => {
  const { code } = req.body
  const result = await code2Session(code, APP_ID, SECRET)
  
  if (result.openid) {
    // 生成自定义登录态(token)
    const token = generateToken(result.openid)
    
    // 存储 session_key(不要返回给前端)
    storeSession(result.openid, result.session_key)
    
    res.json({ token, success: true })
  } else {
    res.json({ success: false, msg: '登录失败' })
  }
})
  1. 小程序端存储 token
wx.request({
  url: 'https://your-server.com/api/login',
  method: 'POST',
  data: { code: res.code },
  success(response) {
    if (response.data.success) {
      wx.setStorageSync('token', response.data.token)
    }
  }
})
  1. 后续请求携带 token
wx.request({
  url: 'https://your-server.com/api/user',
  header: {
    'Authorization': `Bearer ${wx.getStorageSync('token')}`
  }
})

74. 登录态维护

静默登录:

// app.js
App({
  async onLaunch() {
    await this.silentLogin()
  },
  
  async silentLogin() {
    try {
      const token = wx.getStorageSync('token')
      if (token) {
        // 检查 token 是否有效
        const valid = await this.checkToken(token)
        if (valid) return
      }
      // 重新登录
      await this.doLogin()
    } catch (e) {
      await this.doLogin()
    }
  },
  
  doLogin() {
    return new Promise((resolve, reject) => {
      wx.login({
        success: async (res) => {
          if (res.code) {
            const result = await wx.request({
              url: 'https://your-server.com/api/login',
              method: 'POST',
              data: { code: res.code }
            })
            if (result.data.success) {
              wx.setStorageSync('token', result.data.token)
              this.globalData.token = result.data.token
              resolve()
            } else {
              reject(new Error('登录失败'))
            }
          }
        }
      })
    })
  },
  
  checkToken(token) {
    return wx.request({
      url: 'https://your-server.com/api/check-token',
      header: { Authorization: `Bearer ${token}` }
    }).then(res => res.data.valid)
  },
  
  globalData: {
    token: ''
  }
})

十二、小程序支付详解

75. 完整支付流程

┌──────────┐   1.下单请求    ┌──────────┐   2.创建订单    ┌──────────┐
│  小程序   │ ────────────► │  后端    │ ──────────────► │  微信     │
│          │ ◄──────────── │          │ ◄────────────── │  支付     │
│          │   支付参数      │          │    预支付结果     │  服务器    │
│          │ ────────────► │          │                 │          │
│          │   3.调起支付    │          │                 │          │
│          │ ◄──────────── │          │                 │          │
│          │                 │          │   5.支付通知       │          │
│          │   6.支付结果     │          │ ◄────────────── │          │
└──────────┘                 └──────────┘                 └──────────┘

代码实现:

// 小程序端
Page({
  async payOrder(orderId) {
    try {
      wx.showLoading({ title: '正在支付...' })
      
      // 1. 请求后端获取支付参数
      const payParams = await request.post('/api/pay/create', {
        orderId: orderId
      })
      
      // 2. 调起微信支付
      await wx.requestPayment({
        timeStamp: payParams.timeStamp,
        nonceStr: payParams.nonceStr,
        package: payParams.package,
        signType: payParams.signType || 'RSA',
        paySign: payParams.paySign
      })
      
      // 3. 支付成功处理
      wx.hideLoading()
      wx.showToast({ title: '支付成功', icon: 'success' })
      
      // 4. 跳转到订单详情或支付结果页
      wx.redirectTo({
        url: `/pages/pay-result/pay-result?orderId=${orderId}&status=success`
      })
      
    } catch (err) {
      wx.hideLoading()
      if (err.errMsg === 'requestPayment:fail cancel') {
        wx.showToast({ title: '已取消支付', icon: 'none' })
      } else {
        wx.showToast({ title: '支付失败', icon: 'none' })
      }
    }
  }
})

76. 支付安全注意事项

  • 不在前端处理签名:签名必须在后端完成
  • 验证支付结果:支付成功后需后端确认
  • 处理支付超时:设置合理的支付超时时间
  • 防止重复支付:使用订单号幂等性控制
  • iOS 虚拟支付限制:iOS 端小程序不支持虚拟物品支付

十三、小程序性能优化

77. 性能优化策略

优化维度:

维度优化策略说明
启动速度分包加载减少首屏加载体积
启动速度精简 app.js减少启动时初始化工作
渲染性能减少 setData 频次合并数据更新
渲染性能减少 setData 数据量使用路径更新
渲染性能虚拟列表长列表按需渲染
网络性能请求合并减少请求次数
网络性能数据缓存减少重复请求
图片优化压缩图片减小图片体积
图片优化懒加载按需加载图片
代码优化删除无用代码减少包体积

78. 分包加载

配置分包:

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/detail/detail"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "name": "A",
      "pages": [
        "pages/a1/a1",
        "pages/a2/a2"
      ],
      "independent": false
    },
    {
      "root": "packageB",
      "name": "B",
      "pages": [
        "pages/b1/b1"
      ],
      "independent": true
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["A"]
    }
  }
}

预加载分包:

// 预加载分包
wx.loadSubpackage({
  name: 'A',
  success() {
    console.log('分包加载成功')
  },
  fail(err) {
    console.error('分包加载失败', err)
  }
})

79. setData 优化

Page({
  data: {
    list: []
  },
  
  // 不推荐:全量更新大列表
  badUpdate() {
    this.setData({ list: newList })
  },
  
  // 推荐:局部更新
  goodUpdate(newItem) {
    const index = this.data.list.length
    this.setData({
      [`list[${index}]`]: newItem
    })
  },
  
  // 推荐:使用节流
  throttledUpdate: throttle(function(value) {
    this.setData({ value })
  }, 100)
})

// 节流函数
function throttle(fn, delay) {
  let timer = null
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null
      }, delay)
    }
  }
}

80. 图片优化

优化方式:

<!-- 使用 lazy-load 懒加载 -->
<image src="{{item.image}}" lazy-load mode="aspectFill" />

<!-- 使用云图片裁剪 -->
<image src="{{item.image}}?x-oss-process=image/resize,w_400" />

<!-- 使用 WebP 格式 -->
<image src="{{item.image}}.webp" />

代码优化建议:

  • 使用 CDN 加速
  • 根据设备像素比加载不同尺寸图片
  • 避免在列表中使用过大的图片

81. 网络优化

// 请求合并
Page({
  async loadAllData() {
    // 不推荐:串行请求
    const users = await request.get('/api/users')
    const orders = await request.get('/api/orders')
    
    // 推荐:并行请求
    const [users, orders] = await Promise.all([
      request.get('/api/users'),
      request.get('/api/orders')
    ])
  }
})

// 数据缓存
const cache = {}
function fetchWithCache(url, ttl = 300000) {
  if (cache[url] && cache[url].expire > Date.now()) {
    return Promise.resolve(cache[url].data)
  }
  
  return request.get(url).then(data => {
    cache[url] = {
      data,
      expire: Date.now() + ttl
    }
    return data
  })
}

82. 性能检测工具

使用 Performance 面板:

  1. 打开开发者工具的 Performance 面板
  2. 点击录制按钮
  3. 执行操作后停止录制
  4. 分析性能数据

关键指标:

  • 首屏渲染时间
  • setData 耗时
  • 页面切换耗时
  • 网络请求耗时

获取性能数据:

// 获取启动性能数据
const launchOptions = wx.getLaunchOptionsSync()

// 自定义性能打点
const performance = wx.getPerformance()
const observer = performance.createObserver((entryList) => {
  console.log('性能数据:', entryList.getEntries())
})
observer.observe({ entryTypes: ['render', 'script', 'navigation'] })

十四、常见面试题总结

83. 小程序双线程模型的优势是什么?

  • 安全性:逻辑层运行在沙盒中,无法直接操作 DOM
  • 性能:渲染和逻辑并行执行
  • 稳定性:逻辑层崩溃不影响渲染层

84. 小程序和 H5 的区别?

  • 运行环境不同:小程序在微信客户端内,H5 在浏览器中
  • 能力不同:小程序有丰富的微信 Native API
  • 审核机制:小程序需审核,H5 不需要
  • 性能:小程序首次加载后缓存,性能更好

85. setData 的原理和优化?

  • 原理:逻辑层序列化数据,通过 JSBridge 传递给渲染层
  • 优化:减少频次、减少数据量、使用路径更新、避免大数据

86. 小程序登录流程?

  • wx.login 获取 code → 发送 code 到后端 → 后端用 code 换取 openid → 后端生成 token 返回 → 小程序存储 token

87. 小程序支付流程?

  • 下单 → 后端创建预支付订单 → 返回支付参数 → 调起微信支付 → 支付结果回调

88. 小程序性能优化方案?

  • 分包加载、减少 setData 频次和数据量、图片优化、网络优化、代码优化

89. 小程序页面间通信方式?

  • URL 参数、EventChannel、globalData、Storage、页面栈操作

90. 小程序本地存储的限制?

  • 单个 key 限制 1MB,总容量 10MB,支持字符串、对象、数组等