小程序开发

What — 什么是小程序

小程序是一种无需下载安装即可使用的轻量级应用,运行在宿主 App(微信、支付宝、抖音等)提供的运行环境中,具有”用完即走”的特点。

小程序平台生态

平台开发框架渲染方式特色
微信小程序WXML/WXSS/JSWebView + WXCore生态最大,能力最全
支付宝小程序AXML/ACSS/JSWebView金融场景强,芝麻信用
抖音小程序TTML/TTSS/JSWebView + 自研渲染内容分发,直播带货
百度小程序Swan/JSWebView搜索分发,AI 能力
飞书小程序TTML/JSWebView企业办公场景
快手小程序KSML/JSWebView短视频电商
快应用CSS/JS原生渲染手机厂商联盟,系统级入口

核心架构

┌─────────────────────────────────┐
│          宿主 App               │
│  ┌───────────┐  ┌───────────┐  │
│  │ 逻辑层    │  │ 渲染层    │  │
│  │ (JsCore)  │  │ (WebView) │  │
│  │           │  │           │  │
│  │ AppService│  │  WXML/    │  │
│  │ 运行 JS   │  │  WXSS     │  │
│  └─────┬─────┘  └─────┬─────┘  │
│        │   双线程通信   │        │
│        │  (JSBridge)   │        │
│        └───────┬───────┘        │
│            ┌───┴───┐            │
│            │ 原生层 │            │
│            │ Native│            │
│            └───────┘            │
└─────────────────────────────────┘

双线程模型

  • 逻辑层(AppService):运行 JS 代码,处理业务逻辑,无法操作 DOM
  • 渲染层(WebView):运行 WXML/WXSS,负责页面渲染
  • 通信机制:通过 JSBridge 消息通信,逻辑层调用 setData 触发渲染更新

小程序 vs H5 vs 原生 App

维度小程序H5原生 App
安装无需安装无需安装需下载安装
入口宿主 App 内浏览器桌面图标
性能接近原生一般最佳
能力丰富原生 API受限浏览器 API全部系统能力
开发成本
审核需平台审核需应用商店审核
用户体验流畅一般最佳

Why — 为什么选择小程序

适用场景

  • 线下场景:扫码点餐、停车缴费、景区导览
  • 轻量服务:快递查询、天气、计算器
  • 电商导购:商品浏览、下单、拼团
  • 内容消费:资讯阅读、短视频
  • 工具型应用:投票、预约、报名

小程序优势

  1. 获客成本低:无需下载,扫码即用,分享即传播
  2. 开发成本低:一套代码 + Web 技术栈,比原生开发快 3-5 倍
  3. 平台流量:微信 12 亿+ 月活,支付宝 8 亿+ 月活
  4. 原生能力:支付、定位、摄像头、蓝牙、NFC 等系统级 API
  5. 离线能力:缓存机制,弱网环境可用

小程序劣势

  • 平台绑定:受宿主平台规则限制
  • 包体积限制:微信主包 2MB,总包 20MB
  • 性能上限:双线程通信有延迟,大量数据渲染卡顿
  • 审核周期:提交审核需要 1-7 天
  • API 限制:部分 API 需要认证、权限申请

How — 怎么开发

1. 项目结构

├── app.js          // 小程序入口(生命周期)
├── app.json        // 全局配置(页面路由、窗口、tabBar)
├── app.wxss        // 全局样式
├── project.config.json  // 项目配置
├── sitemap.json    // 站点地图(搜索配置)
├── pages/
│   ├── index/
│   │   ├── index.js      // 页面逻辑
│   │   ├── index.json    // 页面配置
│   │   ├── index.wxml    // 页面模板
│   │   └── index.wxss    // 页面样式
│   └── detail/
│       ├── detail.js
│       ├── detail.json
│       ├── detail.wxml
│       └── detail.wxss
├── components/     // 自定义组件
│   └── card/
│       ├── card.js
│       ├── card.json
│       ├── card.wxml
│       └── card.wxss
└── utils/          // 工具函数
    └── util.js

2. 全局配置 app.json

{
  "pages": [
    "pages/index/index",
    "pages/detail/detail"
  ],
  "window": {
    "navigationBarTitleText": "我的小程序",
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#f5f5f5",
    "backgroundTextStyle": "dark"
  },
  "tabBar": {
    "color": "#999",
    "selectedColor": "#1890ff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "assets/home.png",
        "selectedIconPath": "assets/home-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "assets/mine.png",
        "selectedIconPath": "assets/mine-active.png"
      }
    ]
  },
  "subpackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/order/order",
        "pages/pay/pay"
      ]
    }
  ],
  "lazyCodeLoading": "requiredComponents"
}

3. 生命周期

// App 生命周期
App({
  onLaunch(options) {
    // 小程序启动时触发(全局只触发一次)
    console.log('启动参数', options)
    // 获取场景值
    console.log('场景值', options.scene)
  },
  onShow(options) {
    // 小程序从后台进入前台
  },
  onHide() {
    // 小程序从前台进入后台
  },
  onError(msg) {
    // 小程序发生脚本错误
    console.error(msg)
  },
  globalData: {
    userInfo: null
  }
})

// Page 生命周期
Page({
  onLoad(options) {
    // 页面加载时触发,获取路由参数
    console.log('页面参数', options.id)
  },
  onShow() {
    // 页面显示/切入前台
  },
  onReady() {
    // 页面初次渲染完成
  },
  onHide() {
    // 页面隐藏/切入后台
  },
  onUnload() {
    // 页面卸载
  },
  onPullDownRefresh() {
    // 下拉刷新
    this.fetchData().then(() => {
      wx.stopPullDownRefresh()
    })
  },
  onReachBottom() {
    // 上拉触底
    this.loadMore()
  },
  onShareAppMessage() {
    // 用户点击分享
    return {
      title: '分享标题',
      path: '/pages/index/index?id=123',
      imageUrl: '/assets/share.png'
    }
  },
  onShareTimeline() {
    // 分享到朋友圈
    return {
      title: '分享标题',
      query: 'id=123'
    }
  }
})

4. 数据绑定与事件

<!-- WXML 数据绑定 -->
<view class="container">
  <!-- 简单绑定 -->
  <text>{{message}}</text>

  <!-- 属性绑定 -->
  <view id="item-{{id}}">属性绑定</view>

  <!-- 条件渲染 -->
  <view wx:if="{{type === 'A'}}">类型A</view>
  <view wx:elif="{{type === 'B'}}">类型B</view>
  <view wx:else>其他类型</view>

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

  <!-- 事件绑定 -->
  <button bindtap="handleTap">点击</button>
  <view catchtap="handleCatch">阻止冒泡</view>
  <button bind:tap="handleTapWithData" data-id="{{item.id}}" data-name="{{item.name}}">
    携带数据
  </button>

  <!-- 双向绑定 -->
  <input model:value="{{value}}" />

  <!-- 计算属性(WXS) -->
  <wxs module="filters">
    module.exports = {
      formatPrice: function(price) {
        return '¥' + price.toFixed(2)
      }
    }
  </wxs>
  <text>{{filters.formatPrice(price)}}</text>
</view>
// 页面逻辑
Page({
  data: {
    message: 'Hello',
    items: [],
    value: ''
  },

  handleTap() {
    console.log('点击了按钮')
  },

  handleTapWithData(e) {
    // 获取 data-* 传参
    const { id, name } = e.currentTarget.dataset
    console.log(id, name)
  },

  // setData 最佳实践
  updateData() {
    // ❌ 频繁 setData
    // for (let i = 0; i < 100; i++) {
    //   this.setData({ count: i })
    // }

    // ✅ 合并 setData
    this.setData({
      count: 100,
      name: 'test',
      list: newList
    })

    // ✅ 只更新需要变化的数据(路径更新)
    this.setData({
      'list[0].name': '新名称',
      'userInfo.age': 25
    })
  }
})

5. 自定义组件

// components/card/card.js
Component({
  // 组件属性(对外接口)
  properties: {
    title: {
      type: String,
      value: '默认标题'
    },
    list: {
      type: Array,
      value: []
    }
  },

  // 组件内部数据
  data: {
    expanded: false
  },

  // 数据监听器
  observers: {
    'list.**': function(list) {
      this.setData({ count: list.length })
    }
  },

  // 生命周期
  lifetimes: {
    attached() {
      // 组件进入页面节点树
    },
    detached() {
      // 组件离开页面节点树
    }
  },

  pageLifetimes: {
    show() {
      // 所在页面显示
    }
  },

  methods: {
    handleToggle() {
      this.setData({ expanded: !this.data.expanded })
      // 触发事件(向父组件通信)
      this.triggerEvent('toggle', { expanded: this.data.expanded })
    }
  }
})
<!-- components/card/card.wxml -->
<view class="card">
  <view class="card-header" bindtap="handleToggle">
    <text>{{title}}</text>
    <text>{{expanded ? '收起' : '展开'}}</text>
  </view>
  <view class="card-body" wx:if="{{expanded}}">
    <slot></slot>
  </view>
</view>
// components/card/card.json
{
  "component": true,
  "usingComponents": {}
}
<!-- 使用组件 -->
<card title="我的卡片" bind:toggle="onCardToggle">
  <text>插槽内容</text>
</card>
// 页面 json 中注册组件
{
  "usingComponents": {
    "card": "/components/card/card"
  }
}

6. 网络请求封装

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

const request = (options) => {
  return new Promise((resolve, reject) => {
    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
      },
      success(res) {
        if (res.statusCode === 200) {
          if (res.data.code === 0) {
            resolve(res.data.data)
          } else {
            wx.showToast({ title: res.data.message, icon: 'none' })
            reject(res.data)
          }
        } else if (res.statusCode === 401) {
          // token 过期,跳转登录
          wx.removeStorageSync('token')
          wx.redirectTo({ url: '/pages/login/login' })
          reject(res.data)
        } else {
          reject(res.data)
        }
      },
      fail(err) {
        wx.showToast({ title: '网络异常', icon: 'none' })
        reject(err)
      }
    })
  })
}

// 并发请求
const requestAll = (requests) => {
  return Promise.all(requests.map(req => request(req)))
}

module.exports = { request, requestAll }
// 使用
const { request } = require('../../utils/request')

Page({
  async onLoad() {
    try {
      const data = await request({
        url: '/api/user/info',
        method: 'GET'
      })
      this.setData({ userInfo: data })
    } catch (err) {
      console.error(err)
    }
  }
})

7. 分包加载

// app.json — 分包配置
{
  "pages": [
    "pages/index/index",
    "pages/mine/mine"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "name": "order",
      "pages": [
        "pages/order/order",
        "pages/pay/pay"
      ],
      "independent": false
    },
    {
      "root": "packageB",
      "name": "activity",
      "pages": [
        "pages/activity/detail"
      ]
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["order"]
    }
  }
}
// 跳转分包页面
wx.navigateTo({ url: '/packageA/pages/order/order?id=123' })

分包注意事项

  • 主包 2MB 限制,单个分包最大 2MB,总包 20MB
  • 分包不能引用其他分包的资源,可以引用主包
  • tabBar 页面必须在主包
  • 独立分包(independent: true)可以不下载主包直接运行

8. 性能优化

// 1. 减少 setData 数据量
// ❌ 传输整个列表
this.setData({ list: newList })

// ✅ 只传输变化项
this.setData({ 'list[2].status': 'done' })

// 2. 合并 setData 调用
// ❌ 多次调用
this.setData({ a: 1 })
this.setData({ b: 2 })

// ✅ 一次调用
this.setData({ a: 1, b: 2 })

// 3. 虚拟列表(长列表优化)
// 使用 recycle-view 组件
// npm install miniprogram-recycle-view

// 4. 图片优化
// <image mode="aspectFill" lazy-load="{{true}}" />

// 5. 骨架屏
// page.json
{
  "usingComponents": {
    "skeleton": "/components/skeleton/skeleton"
  }
}
<!-- 骨架屏 -->
<skeleton wx:if="{{loading}}" />
<view wx:else>
  <!-- 真实内容 -->
</view>
// 6. 分页加载(上拉加载更多)
Page({
  data: {
    list: [],
    page: 1,
    hasMore: true,
    loading: false
  },

  async loadList() {
    if (this.data.loading || !this.data.hasMore) return
    this.setData({ loading: true })

    const data = await request({
      url: '/api/list',
      data: { page: this.data.page, size: 20 }
    })

    this.setData({
      list: [...this.data.list, ...data.items],
      page: this.data.page + 1,
      hasMore: data.items.length === 20,
      loading: false
    })
  },

  onReachBottom() {
    this.loadList()
  }
})

9. 微信登录流程

用户 → 点击登录 → wx.login() → 获取 code

                            发送 code 到后端

                            后端调用微信 API
                            (code + appId + appSecret)

                            获取 openid + session_key

                            后端生成自定义 token 返回前端

                            前端存储 token,后续请求携带
// 登录流程封装
async function login() {
  // 1. 获取 code
  const { code } = await wx.login()

  // 2. 获取用户信息(需用户授权)
  const { userInfo } = await wx.getUserProfile({
    desc: '用于完善个人资料'
  })

  // 3. 发送到后端
  const data = await request({
    url: '/api/auth/wx-login',
    method: 'POST',
    data: { code, userInfo }
  })

  // 4. 存储 token
  wx.setStorageSync('token', data.token)
  return data
}

// 检查登录态
async function checkSession() {
  try {
    await wx.checkSession()
    return true // session 未过期
  } catch {
    return false // session 已过期,需要重新登录
  }
}

10. 支付流程

async function pay(orderId) {
  // 1. 后端创建订单,获取支付参数
  const payParams = await request({
    url: '/api/pay/create',
    method: 'POST',
    data: { orderId }
  })

  // 2. 调起微信支付
  return new Promise((resolve, reject) => {
    wx.requestPayment({
      timeStamp: payParams.timeStamp,
      nonceStr: payParams.nonceStr,
      package: payParams.package,
      signType: 'RSA',
      paySign: payParams.paySign,
      success: resolve,
      fail: reject
    })
  })
}

11. 多端框架对比

框架语法支持平台特色
原生WXML/WXSS/JS仅微信性能最优,能力最全
uni-appVue微信/支付宝/抖音/H5/App生态丰富,跨端能力强
TaroReact/Vue微信/支付宝/抖音/H5/RN京东出品,React 友好
mpvueVue微信(已停止维护)Vue 语法,不再更新
kboneVue/React微信 + H5同构方案,微信和 H5 共用代码
RemaxReact微信/支付宝运行时方案,React 完整支持

12. 常用原生能力

// 扫码
wx.scanCode({
  success(res) {
    console.log(res.result) // 扫码结果
  }
})

// 定位
wx.getLocation({
  type: 'gcj02',
  success(res) {
    console.log(res.latitude, res.longitude)
  }
})

// 选择图片
wx.chooseMedia({
  count: 9,
  mediaType: ['image'],
  success(res) {
    const tempFilePaths = res.tempFiles.map(f => f.tempFilePath)
  }
})

// 获取手机号(需企业认证)
// <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">
//   获取手机号
// </button>

// 分享
wx.shareAppMessage({
  title: '分享标题',
  path: '/pages/index/index'
})

// 订阅消息
wx.requestSubscribeMessage({
  tmplIds: ['template_id_1', 'template_id_2'],
  success(res) {
    console.log(res)
  }
})

// 小程序跳转
wx.navigateToMiniProgram({
  appId: 'other_app_id',
  path: 'pages/index/index?id=123'
})

常见问题与踩坑

问题原因解决方案
setData 卡顿传输数据量过大或调用过于频繁只传变化数据,合并调用,使用路径更新
图片模糊未使用 2x 图或 mode 不对使用 CDN 参数适配 DPR,设置 mode="aspectFill"
苹果底部安全区iPhone X 底部 Home 指示条遮挡env(safe-area-inset-bottom) 适配
分包预加载失败preloadRule 网络限制设置 network: "all",或在 Wi-Fi 下测试
分享图片不显示imageUrl 使用网络图但未配置域名后台配置 downloadFile 合法域名
wx.getUserInfo 废弃平台隐私规范调整使用 wx.getUserProfile 或头像昵称填写能力
长列表卡顿渲染节点过多使用虚拟列表(recycle-view),按需渲染
iOS 音频自动播放iOS 限制自动播放必须用户交互触发 wx.createInnerAudioContext()

最佳实践

  1. 分包策略:主包只放首页和 tabBar 页,其他页面分包,使用 preloadRule 预加载
  2. setData 优化:只传变化的数据,合并调用,避免频繁触发渲染
  3. 图片优化:CDN + WebP 格式 + 懒加载 + 适当压缩质量
  4. 请求优化:封装统一请求,token 自动刷新,请求重试,请求取消
  5. 缓存策略:合理使用 wx.setStorageSync 缓存接口数据,减少重复请求
  6. 骨架屏:首屏使用骨架屏,提升用户感知速度
  7. 体验优化:使用 wx.nextTick 延迟非关键操作,避免页面切换卡顿

面试题

1. 小程序的双线程模型是什么?为什么要这样设计?

:小程序采用逻辑层(JsCore)和渲染层(WebView)双线程架构,通过 JSBridge 通信。逻辑层负责业务逻辑和数据处理,渲染层负责 WXML/WXSS 解析和页面渲染。这样设计的原因:(1) 安全管控——逻辑层无法直接操作 DOM,杜绝了开发者使用 DOM API 绕过平台审核的风险;(2) 稳定性——JS 执行阻塞不会导致页面渲染卡死,反之亦然;(3) 性能——渲染线程和逻辑线程可以并行执行;(4) 多页面管理——每个页面一个 WebView 实例,页面切换更流畅。代价是 setData 通信有开销,需要优化数据传输量。


2. setData 为什么会影响性能?如何优化?

:setData 的性能问题源于:(1) 通信开销——数据从逻辑层通过 JSBridge 序列化传到渲染层,数据量越大耗时越长;(2) 重渲染——渲染层收到数据后重新执行 WXML 模板对比和节点更新;(3) 阻塞——JSBridge 通信是同步的,频繁调用会阻塞逻辑层。优化策略:(1) 只传变化数据——this.setData({ 'list[0].name': 'new' }) 而非整个 list;(2) 合并调用——多个状态更新合并为一次 setData;(3) 减少数据量——不需要渲染的数据不通过 setData 传递,使用 this._data 或实例变量存储;(4) 延迟非关键更新——wx.nextTick 延迟执行;(5) 长列表虚拟化——使用 recycle-view 只渲染可见区域。


3. 小程序的分包策略是什么?独立分包有什么区别?

:分包将小程序拆分为一个主包和多个子包,主包包含首页和 tabBar 页面,子包按业务模块划分。加载时先下载主包,进入分包页面时再下载对应子包。好处:(1) 减少首次加载时间;(2) 突破 2MB 限制(总包可达 20MB);(3) 按需加载,节省流量。独立分包(independent: true)是特殊分包,可以不依赖主包独立运行——从独立分包页面进入小程序时,不需要下载主包。适用场景:分享页、广告落地页等需要快速打开的页面。限制:独立分包不能引用主包资源(图片、JS、组件),getApp() 获取不到主包 App 实例,需要使用 getApp({ allowDefault: true }) 处理。


4. 小程序登录流程是怎样的?session_key 有什么作用?

:登录流程:(1) 前端调用 wx.login() 获取临时 code;(2) 将 code 发送到后端;(3) 后端用 code + appId + appSecret 调用微信服务器 jscode2session 接口;(4) 获取 openid(用户唯一标识)和 session_key(会话密钥);(5) 后端生成自定义 token 返回前端存储。session_key 的作用:(1) 解密用户敏感数据(手机号、地址等);(2) 作为用户会话的凭证。注意:session_key 会过期,需要通过 wx.checkSession() 检查有效性,过期后重新调用 wx.login()重要安全规则:session_key 绝不能传到前端,只能保存在后端;code 只能使用一次。


5. WXS 是什么?为什么需要它?

:WXS(WeiXin Script)是小程序的一套脚本语言,运行在渲染层(WebView)中,可以在 WXML 中直接使用。为什么需要:(1) 性能优化——普通 JS 运行在逻辑层,调用数据处理函数需要通过 JSBridge 通信(约 20-50ms 延迟),WXS 运行在渲染层可以直接操作数据,无通信开销;(2) 动画优化——<wxs> 处理的动画可以响应 touch 事件实现 60fps 的实时交互(如拖拽、滑动删除)。限制:WXS 语法是 JS 子集,不支持 ES6+,不能调用小程序 API,不能使用 DOM,只能做纯数据处理。典型场景:价格格式化、时间格式化、列表过滤、手势动画。


6. 小程序与 H5 的通信方式有哪些?

:三种通信方式:(1) URL 参数——H5 页面通过 URL 传递参数给小程序 wx.miniProgram.navigateTo({ url: '/pages/web/index?data=xxx' });(2) postMessage——H5 调用 wx.miniProgram.postMessage({ data: { key: 'value' } }),小程序在 <web-view>bindmessage 事件中接收(注意只在特定时机触发:后退、组件销毁、分享时);(3) 微信 JSSDK——H5 通过 wx.miniProgram 对象调用小程序 API,如 wx.miniProgram.navigateTowx.miniProgram.switchTab。反向通信(小程序 → H5):通过修改 web-view 的 src URL 参数,或注入 JS(evaluateJs 方法,基础库 2.22.1+)。


7. 如何实现小程序的埋点和数据统计?

:(1) 全局拦截——在 App 的 onShow/onHide 中自动采集页面访问数据;封装 Page 构造器,自动注入 onLoad/onShow 等生命周期埋点;(2) 事件代理——重写 Component 构造器,在 methods 中代理事件方法,自动上报用户行为;(3) 请求拦截——封装 wx.request,自动上报接口性能(耗时、状态码、错误信息);(4) 性能采集——wx.getPerformance() 获取页面加载性能数据;(5) 自定义埋点——业务关键路径手动埋点(如按钮点击、表单提交)。常用方案:微信官方”小程序数据助手”、友盟、神策、自建埋点 SDK。注意埋点数据量控制,避免影响主业务性能。


8. 小程序的审核被拒常见原因有哪些?如何避免?

:常见拒审原因及规避:(1) 虚拟支付——iOS 上不得包含非苹果渠道的虚拟商品支付(如会员、课程、道具),必须使用苹果内购或改为实物/线下服务;Android 不受限制;(2) 诱导分享——“分享后解锁""分享领红包”等强制分享行为被禁止,分享功能必须是用户自愿的;(3) 用户隐私——收集用户信息(位置、手机号、通讯录等)未说明用途或未经授权,必须在隐私协议中明确说明;(4) 内容审核——UGC 内容需要接入微信内容安全 API(msgSecCheck)进行文本和图片审核;(5) 类目不符——小程序实际功能与申请类目不匹配,选择正确类目;(6) 网页内嵌——<web-view> 内嵌的 H5 页面不能是纯展示型网站,必须有交互功能。建议提审前对照《微信小程序平台运营规范》自查。


相关链接