😅 未找到相关内容,请尝试其他关键词
🏗️ 第一部分:架构设计
小程序整体架构概述
双线程模型架构
微信小程序采用双线程模型(Dual Thread Model),这是其与普通 Web 应用最核心的区别:
┌─────────────────────────────────────────────────────┐ │ Native Layer │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ Render Thread │◄──►│ Logic Thread │ │ │ │ (WebView) │ │ (JSCore) │ │ │ │ │ │ │ │ │ │ - WXML渲染 │ │ - JS逻辑执行 │ │ │ │ - WXSS样式计算 │ │ - 数据处理 │ │ │ │ - 页面展示 │ │ - 事件处理 │ │ │ └───────────────────┘ └───────────────────┘ │ │ ▲ ▲ │ │ └─────┬─────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ JS Bridge │ │ │ │ (IPC通信) │ │ │ └──────┬──────┘ │ │ │ │ ├────────────────────────────┼─────────────────────────┤ │ ┌──────▼──────┐ │ │ │ Server │ │ │ │ 微信服务器 │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────┘
核心特点
- 渲染层(View Layer):基于 WebView 运行,负责页面的渲染和展示。每个页面运行在独立的 WebView 中。
- 逻辑层(App Service Layer):基于 JSCore(iOS)/ V8(Android)运行,负责 JavaScript 逻辑的执行。
- 通信机制:两层之间通过微信客户端的 Native 层进行中转通信(使用 evaluateJavaScript 和 postMessage),不存在直接交互。
- 安全性:逻辑层无法直接操作 DOM,所有数据变更必须通过 setData 方式通知渲染层更新。
💡 关键理解:
正因为双线程模型,setData 操作实际上是跨线程的数据序列化传输。频繁或大量的 setData 会成为性能瓶颈。
与传统Web/H5的对比
- 传统Web:单线程(JS与渲染在同一线程),可直接操作DOM
- 小程序:双线程(渲染与逻辑分离),通过 Native Bridge 通信
- 优势:安全隔离、性能可控、避免JS阻塞渲染
- 限制:无法直接操作DOM、通信有延迟、资源受限
项目目录结构设计规范
标准项目结构
├── project.config.json # 项目配置文件
├── sitemap.json # 站点地图配置
├── app.js # 小程序入口(逻辑)
├── app.json # 全局配置
├── app.wxss # 全局样式
├── pages/ # 页面目录
│ ├── index/
│ │ ├── index.js # 页面逻辑
│ │ ├── index.json # 页面配置
│ │ ├── index.wxml # 页面结构
│ │ └── index.wxss # 页面样式
│ ├── user/
│ │ ├── profile/
│ │ ├── settings/
│ │ └── index.js
├── components/ # 自定义组件
│ ├── header/
│ │ ├── header.js
│ │ ├── header.json
│ │ ├── header.wxml
│ │ └── header.wxss
│ └── list-item/
├── utils/ # 工具函数
│ ├── api.js # 接口封装
│ ├── auth.js # 认证工具
│ ├── request.js # 网络请求
│ ├── storage.js # 本地存储
│ └── validate.js # 数据验证
├── services/ # 业务服务层
│ ├── user.service.js
│ ├── order.service.js
│ └── product.service.js
├── models/ # 数据模型
│ ├── user.model.js
│ └── product.model.js
├── constants/ # 常量定义
│ └── index.js
├── assets/ # 静态资源
│ ├── images/
│ ├── icons/
│ └── fonts/
├── styles/ # 公共样式
│ ├── variables.wxss # CSS变量
│ ├── mixins.wxss # 样式混入
│ ├── reset.wxss # 样式重置
│ └── theme.wxss # 主题样式
└── cloud/ # 云开发(可选)
├── functions/
│ ├── login/
│ └── getData/
└── database/
命名规范
- 页面目录:使用小写字母加连字符,如
user-profile - 组件目录:使用小写字母加连字符,如
search-bar - 文件名:与所属目录同名(页面四件套)
- 工具函数:使用驼峰命名,如
formatDate.js - 常量文件:全部大写加下划线,如
API_URL
⚠️ 注意:
小程序对单个包大小有严格限制(主包不超过2MB,总包不超过20MB)。合理的目录结构有助于分包加载。
MVVM设计模式在小程序中的应用
MVVM架构模式
微信小程序本质上遵循 MVVM(Model-View-ViewModel) 模式,但有其独特实现方式:
┌─────────────────────────────────────────────────┐ │ MVVM in 小程序 │ │ │ │ Model层 ←───────────── Service层 │ │ (数据模型) (业务逻辑) │ │ │ │ │ │ ▼ ▼ │ │ ViewModel层 ◄──────── setData() │ │ (Page/Component) │ │ │ │ │ │ │ ▼ ▼ │ │ View层 ◄──── WXML Data Binding │ │ (WXML/WXSS) │ └─────────────────────────────────────────────────┘
各层职责
Model层(数据模型)
- 定义数据结构和接口调用
- 封装网络请求和数据转换
- 管理本地缓存策略
// models/user.model.js
class UserModel {
constructor() {
this.baseURL = 'https://api.example.com';
}
async getUserInfo(userId) {
const response = await request.get(`${this.baseURL}/users/${userId}`);
return this._transformUserData(response.data);
}
_transformUserData(rawData) {
return {
id: rawData.id,
name: rawData.nickname,
avatar: rawData.avatar_url,
isVip: rawData.vip_level > 0,
displayTime: this._formatDate(rawData.created_at)
};
}
}
module.exports = new UserModel();
ViewModel层(页面/组件)
- 管理页面状态和数据
- 处理用户交互事件
- 协调 Model 和 View
// pages/user/user.js
const userService = require('../../services/user.service');
Page({
data: {
userInfo: null,
isLoading: true,
errorMessage: ''
},
async onLoad(options) {
try {
const userInfo = await userService.getUserDetail(options.id);
this.setData({
userInfo,
isLoading: false
});
} catch (error) {
this.setData({
errorMessage: '加载失败,请重试',
isLoading: false
});
}
},
handleFollowUser() {
userService.followUser(this.data.userInfo.id)
.then(() => {
this.setData({
'userInfo.isFollowed': true
});
});
}
});
View层(视图)
- 使用 WXML 描述结构
- 使用 WXSS 描述样式
- 通过数据绑定和事件绑定与 ViewModel 交互
分包加载与性能架构设计
分包加载策略
由于小程序主包大小限制为 2MB(总包 20MB),合理使用分包是大型项目的必要策略。
配置示例
// app.json
{
"pages": [
"pages/index/index",
"pages/user/index"
],
"subpackages": [
{
"root": "pages/packageA",
"name": "packageA",
"pages": [
"pages/detail/index",
"pages/detail/comment"
]
},
{
"root": "pages/packageB",
"name": "packageB",
"pages": [
"pages/shop/index",
"pages/shop/order"
],
"independent": true // 独立分包
}
],
"preloadRule": {
"pages/index/index": {
"network": "wifi",
"packages": ["packageA"]
},
"pages/user/index": {
"network": "all",
"packages": ["packageB"]
}
}
}
分包策略原则
- 主包:只放 tabBar 页面和核心公共逻辑
- 普通分包:按业务功能模块拆分
- 独立分包:可独立运行的功能(如小程序插件化)
- 预下载:在进入某个页面时提前下载相关分包
资源优化架构
- 图片使用 CDN + WebP 格式
- 字体使用 base64 或云存储
- 代码压缩(构建工具自动处理)
- 按需引入组件,避免全局注册过多组件
📘 第二部分:基础入门
开发环境搭建与工具配置
环境准备清单
- 注册小程序账号
- 访问
mp.weixin.qq.com注册开发者账号 - 获取 AppID(小程序ID)
- 配置服务器域名(开发 → 开发设置)
- 访问
- 安装微信开发者工具
- 下载地址:
developers.weixin.qq.com/miniprogram/dev/devtools/download.html - 支持 Windows / macOS / Linux
- 建议使用稳定版(Stable)
- 下载地址:
- 推荐辅助工具
- VS Code + 小程序插件(WXML 语法高亮)
- Postman(API 调试)
- Charles / Fiddler(网络抓包)
- 微信开发者工具 CLI 工具
- 频繁切换:用
hidden(只是 CSS 控制) - 运行时条件很少改变:用
wx:if(减少不必要的组件实例) - rpx 单位:响应式像素,自动适配不同屏幕宽度
- 样式导入:@import 语法
- 内联样式限制:支持 class 和 style,但不支持 ID 选择器
- 数据量控制:单次 setData 数据量建议不超过 256KB
- 避免频繁调用:合并多次 setData 为一次调用
- 局部更新:使用路径表达式更新局部数据
- 避免传递大数据:图片、长列表等考虑按需加载
- 父 → 子:通过 properties 传递
- 子 → 父:通过 triggerEvent 触发事件
- 子组件间:通过父组件中转或使用 selectComponent
- 跨层级:使用 EventBus 或全局状态管理
- 前端创建订单 → 后端生成预支付订单
- 后端调用微信统一下单接口
- 后端返回支付参数给前端
- 前端调用 wx.requestPayment 唤起支付
- 支付完成后,后端接收微信回调通知
- 前端轮询或查询支付结果
- 签名密钥(mch_key)绝对不能放在前端代码中
- 支付金额必须由后端计算,前端不可传递金额参数
- 必须验证微信回调的签名合法性
- 支付结果以后端回调通知为准,不要仅依赖前端结果
- 跨页面/跨组件的数据共享
- 全局状态(用户登录状态、购物车等)管理
- 避免层层传递 props
- 云函数:基于 Node.js 的 Serverless 函数
- 云数据库:JSON 文档型数据库(MongoDB 风格)
- 云存储:文件存储与 CDN 分发
- 云调用:直接调用微信开放接口
- 仅创建者可读写:最安全,每条记录只有创建者能操作
- 仅创建者可写,所有人可读:适合内容类数据
- 仅管理端可写,所有人可读:适合公告类
- 仅管理端可读写:最严格,仅后端可操作
- 简单的数据格式化(日期、价格等)
- 事件响应中的简单逻辑判断
- 触摸事件的高频处理(避免频繁 setData)
- 合并多次 setData 调用为单次
- 使用路径表达式进行局部更新
- 避免在 setData 中传递大量数据
- 使用纯数据字段(pureDataPattern)存储非渲染数据
- 使用 CDN 的图片裁剪参数,按需加载合适尺寸
- 列表图片使用懒加载:
<image lazy-load> - 使用 WebP 格式(比 JPEG 小 25-35%)
- 使用渐进式加载(低质量图 → 高清图)
- 页面卸载时清理定时器、事件监听器
- 避免在全局对象中缓存大量数据
- 使用 WeakRef 或手动管理大型对象
- 定期清理不再使用的缓存数据
- 首屏时间:从点击到首屏渲染完成 < 1.5s
- setData 调用频率:每秒不超过 20 次
- setData 数据量:单次不超过 256KB
- 页面切换耗时:< 300ms
- 体验版测试:上传代码后设为体验版,邀请团队成员测试
- 兼容性测试:在不同机型、不同微信版本上测试
- 网络环境测试:弱网(2G/3G)、无网络、网络切换
- 性能测试:大数据量、长列表、复杂动画
- 边界测试:极端输入、异常状态、并发操作
- ✅ 所有接口域名已配置到白名单
- ✅ 隐私政策已正确配置
- ✅ 敏感信息已移除(测试账号、调试日志等)
- ✅ 代码已压缩(开发者工具自动处理)
- ✅ 包体积检查(主包 < 2MB)
- ✅ 多端兼容性测试完成
- ✅ 性能指标达标
- ✅ 审核材料准备齐全
- 小程序功能完整,无空白页面或死链
- UI 符合微信设计规范
- 有完整的用户协议和隐私政策
- 不存在诱导分享、虚假宣传等行为
- 类目选择正确,所需资质齐全
- 在小程序管理后台 → 设置 → 隐私协议中声明所有用到的权限
- 每个权限需说明用途
- 提供用户拒绝授权的处理方案
- 弹窗过于频繁
- 强制关注公众号才能使用
- 无退出机制或退出困难
- 加载过慢(>5s无响应)
- 开发版:开发调试使用
- 体验版:指定体验成员测试
- 审核版:提交审核的版本
- 线上版:正式发布的版本
- 重要更新先灰度 10% 用户
- 监控灰度版本的错误率和使用数据
- 确认无问题后逐步扩大到全量
- 保留旧版本兼容性至少 1 个月
- 重大变更提前在用户群或公告中预告
- 直接修改 this.data 而没有使用 setData
- 在组件中修改 properties 的值
- 对象深层属性变化但只更新了引用
- URL 参数:适合简单数据,有长度限制
- 全局变量:getApp().globalData,适合全局状态
- 本地缓存:wx.setStorage/getStorage,适合持久化数据
- EventChannel:页面间双向通信(推荐)
- 分层架构:严格遵循 Model-Service-ViewModel-View 分层
- 单一职责:每个文件/模块只负责一个功能
- 依赖注入:通过参数传递依赖而非硬编码
- 配置外部化:环境配置、接口地址等与代码分离
- 使用 ESLint 统一代码风格
- 变量命名语义化,避免缩写
- 函数不超过 50 行,保持短小
- 添加必要的注释说明业务逻辑
- 统一错误处理机制
- 状态管理:MobX-miniprogram(官方推荐)或自研简易 Store
- UI 框架:WeUI、Vant Weapp、TDesign
- 构建工具:Gulp/Webpack + TypeScript
- 后端:云开发(小项目)/ 自建服务(复杂业务)
- 渲染:Skyline 渲染引擎(2024+推荐,性能提升 50%+)
- Skyline 渲染引擎将成为主流(更好的动画和滚动性能)
- 半屏小程序(半屏打开,类似系统弹窗)
- 小程序插件化生态发展
- AI 能力接入(语音、图像识别)
- 跨端方案统一(uni-app / Taro 等框架的成熟)
创建第一个项目
// 1. 打开微信开发者工具
// 2. 选择"项目" → "新建项目"
// 3. 填写信息:
// - 目录:选择项目文件夹
// - AppID:你的小程序 AppID(或使用测试号)
// - 后端服务:选择"不使用云服务"(或使用云开发)
// 4. 点击"确定"
app.json 全局配置详解
{
"pages": [ // 页面路径列表(第一个为首页)
"pages/index/index",
"pages/logs/logs"
],
"window": { // 全局窗口表现
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTitleText": "我的小程序",
"navigationBarTextStyle": "black",
"backgroundColor": "#eeeeee",
"backgroundTextStyle": "dark"
},
"tabBar": { // 底部Tab栏
"color": "#999999",
"selectedColor": "#07c160",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "assets/icons/home.png",
"selectedIconPath": "assets/icons/home-active.png"
},
{
"pagePath": "pages/user/index",
"text": "我的",
"iconPath": "assets/icons/user.png",
"selectedIconPath": "assets/icons/user-active.png"
}
]
},
"networkTimeout": { // 网络超时设置
"request": 10000,
"connectSocket": 10000,
"uploadFile": 10000,
"downloadFile": 10000
},
"permission": { // 权限配置
"scope.userLocation": {
"desc": "需要获取您的位置信息用于附近功能"
}
},
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents" // 按需注入
}
💡 技巧:
2024年起推荐开启
"lazyCodeLoading": "requiredComponents"
,实现按需注入,显著提升启动速度。
WXML 模板语法完全指南
WXML 是什么?
WXML(WeiXin Markup Language)是微信小程序的模板语言,用于描述页面的结构。它类似于 HTML,但有独特的语法特性。
数据绑定
<!-- 简单绑定 -->
<view>{{message}}</view>
<!-- 运算绑定 -->
<view>{{count + 1}}</view>
<view>{{isActive ? '已激活' : '未激活'}}</view>
<!-- 字符串运算 -->
<view>Hello {{name || '陌生人'}}</view>
<!-- 数组运算 -->
<view>{{array[0]}}</view>
<view>{{object.key}}</view>
列表渲染 wx:for
<!-- 基础列表渲染 -->
<view wx:for="{{list}}" wx:key="id">
{{index}}: {{item.name}} - {{item.age}}岁
</view>
<!-- 自定义索引名和项名 -->
<view wx:for="{{list}}" wx:for-index="idx" wx:for-item="entry" wx:key="id">
{{idx}}: {{entry.name}}
</view>
<!-- 嵌套循环 -->
<block wx:for="{{categories}}" wx:key="id">
<h3>{{item.name}}</h3>
<view wx:for="{{item.products}}" wx:for-item="product" wx:key="id">
{{product.title}} - ¥{{product.price}}
</view>
</block>
条件渲染
<!-- wx:if 条件渲染 -->
<view wx:if="{{type === 'A'}}">类型A内容</view>
<view wx:elif="{{type === 'B'}}">类型B内容</view>
<view wx:else>其他类型</view>
<!-- hidden 属性(只是CSS隐藏,组件仍然创建) -->
<view hidden="{{!show}}">隐藏的内容</view>
模板 Template
<!-- 定义模板 -->
<template name="userCard">
<view class="card">
<image src="{{avatar}}" class="avatar"/>
<text>{{name}}</text>
<text class="desc">{{description}}</text>
</view>
</template>
<!-- 使用模板 -->
<template is="userCard" data="{{...user}}"/>
引用 Import / Include
<!-- import:只引入template -->
<import src="templates/user.wxml"/>
<template is="userCard" data="{{...userData}}"/>
<!-- include:引入除了template以外的所有内容 -->
<include src="components/header.wxml"/>
⚠️ wx:if vs hidden 选择指南:
WXSS 样式系统与响应式设计
WXSS 与 CSS 的区别
WXSS(WeiXin Style Sheets)是微信小程序的样式语言,基于 CSS 扩展,主要增加了:
rpx 单位详解
/*
rpx(responsive pixel)规则:
- 设计稿以 750rpx 为屏幕宽度基准
- iPhone 6: 1rpx = 0.5px
- 会自动根据屏幕宽度进行缩放
*/
/* 推荐使用场景 */
.container {
width: 100%;
padding: 20rpx; /* 左右各 20rpx 间距 */
}
.title {
font-size: 32rpx; /* 约等于 16px */
line-height: 1.5;
}
/* 不推荐场景:边框使用rpx可能导致细线模糊 */
.card {
border: 1px solid #eee; /* 推荐用px */
}
样式导入与变量
/* common.wxss */
/* 定义CSS变量 */
page {
--primary-color: #07c160;
--text-color: #333;
--spacing-unit: 16rpx;
}
/* page.wxss */
@import "../common.wxss";
.title {
color: var(--text-color);
padding: var(--spacing-unit);
}
.button-primary {
background: var(--primary-color);
}
响应式设计技巧
/* 媒体查询 */
@media screen and (max-width: 375px) {
.container {
padding: 10rpx;
}
}
/* Flex 布局 */
.flex-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.flex-wrap {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
/* 安全区域适配(iPhone底部安全区) */
.bottom-safe {
padding-bottom: env(safe-area-inset-bottom);
/* 兼容写法 */
padding-bottom: constant(safe-area-inset-bottom);
}
JavaScript 逻辑层与生命周期详解
页面生命周期
Page({
// === 生命周期函数 ===
onLoad(options) {
// 页面加载时执行,只执行一次
// options: 页面路由参数
console.log('页面参数:', options);
// 适合:数据初始化、网络请求预加载
},
onShow() {
// 页面显示时执行(包括从后台切到前台)
// 适合:数据刷新、状态同步、权限检查
},
onReady() {
// 页面初次渲染完成时执行,只执行一次
// 适合:操作DOM节点(使用SelectorQuery)
wx.createSelectorQuery().select('#myCanvas')
.node().exec(res => {
// 可以开始绘制Canvas
});
},
onHide() {
// 页面隐藏时执行(导航到其他页面或进入后台)
// 适合:暂停定时器、保存状态
},
onUnload() {
// 页面卸载时执行,只执行一次
// 适合:清理资源、取消网络请求、解绑事件
},
// === 页面事件处理函数 ===
onPullDownRefresh() {
// 下拉刷新
this.refreshData().then(() => {
wx.stopPullDownRefresh();
});
},
onReachBottom() {
// 触底加载
if (!this.data.isLoadingMore) {
this.loadMore();
}
},
onShareAppMessage() {
// 分享配置
return {
title: this.data.title,
path: `/pages/detail/index?id=${this.data.id}`,
imageUrl: this.data.coverUrl
};
},
onShareTimeline() {
// 分享到朋友圈
return {
title: '分享到朋友',
query: `id=${this.data.id}`
};
}
});
应用生命周期
App({
onLaunch(options) {
// 小程序启动时执行
// options.scene: 场景值(如 1001 发现栏,1047 扫码)
console.log('启动场景:', options.scene);
// 检查更新
this.checkUpdate();
// 初始化全局数据
this.globalData = {
userInfo: null,
systemInfo: wx.getSystemInfoSync()
};
},
onShow(options) {
// 小程序从后台切到前台时执行
console.log('切回前台');
},
onHide() {
// 小程序从前台切到后台时执行
console.log('进入后台');
},
onError(error) {
// 全局错误处理
console.error('全局错误:', error);
// 上报错误日志
this.reportError(error);
},
onPageNotFound(res) {
// 404页面处理
wx.redirectTo({
url: 'pages/404/index'
});
},
globalData: {
userInfo: null
}
});
💡 生命周期执行顺序:
首次进入页面:onLaunch → onLoad → onShow → onReady
再次进入页面:onShow(不会触发onLoad和onReady)
离开页面:onHide → (其他页面onShow)
页面销毁:onUnload
数据绑定与 setData 深入理解
数据绑定机制
小程序使用 单向数据流 + setData 驱动视图更新:
Page({
data: {
message: 'Hello World',
count: 0,
userInfo: {
name: '张三',
age: 25,
hobbies: ['阅读', '编程', '旅行']
}
},
// 更新数据 - 必须使用 setData
updateMessage() {
this.setData({
message: '新消息',
count: this.data.count + 1
});
},
// 路径更新(局部更新,性能更好)
updateUserAge() {
this.setData({
'userInfo.age': 26 // 只更新 age 字段
});
},
// 数组更新
addHobby() {
const hobbies = this.data.userInfo.hobbies;
hobbies.push('游泳');
this.setData({
'userInfo.hobbies': hobbies
});
}
});
setData 原理与性能
逻辑层 (JS) 渲染层 (WebView) │ │ │ setData({key: value}) │ │─────────────────────────────────────►│ │ (序列化数据,通过 Native 传输) │ │ │ │ │ 1. 接收数据 │ │ 2. 差异比较 │ │ 3. 更新虚拟DOM │ │ 4. 渲染视图 │ ◄─────────────────────────────────│ │ (回调函数执行) │ └─────────────────────────────────────┘
setData 性能优化原则
// ❌ 错误示例:频繁 setData
for (let i = 0; i < 100; i++) {
this.setData({ count: this.data.count + 1 });
}
// ✅ 正确示例:合并 setData
this.setData({ count: this.data.count + 100 });
// ❌ 错误示例:传递整个大对象
this.setData({ userList: response.data.allUsers }); // 10000条数据
// ✅ 正确示例:只传递需要的数据
const visibleUsers = response.data.allUsers.slice(0, 20);
this.setData({ userList: visibleUsers });
🧩 第三部分:组件开发
自定义组件开发完全指南
创建自定义组件
组件的四件套文件与页面类似,但 .json 文件中需要声明 "component": true。
// components/star-rating/star-rating.json
{
"component": true
}
组件的 properties 和 data
// components/star-rating/star-rating.js
Component({
// 外部属性(由父组件传入)
properties: {
// 简写方式(默认 String 类型)
title: String,
// 完整写法
score: {
type: Number, // 数据类型
value: 0, // 默认值
observer: function(newVal, oldVal) {
// 属性变化时的监听器
console.log(`分数从 ${oldVal} 变为 ${newVal}`);
this.updateStars();
}
},
maxStars: {
type: Number,
value: 5
},
readonly: {
type: Boolean,
value: false
}
},
// 组件内部数据
data: {
stars: [],
currentScore: 0
},
// 生命周期
lifetimes: {
attached() {
// 组件挂载时执行
this.initStars();
},
detached() {
// 组件卸载时执行
}
},
// 数据监听器
observers: {
'score': function(score) {
this.setData({ currentScore: score });
}
},
// 组件方法
methods: {
initStars() {
const stars = [];
for (let i = 1; i <= this.data.maxStars; i++) {
stars.push({
index: i,
active: i <= this.data.score
});
}
this.setData({ stars });
},
handleTap(e) {
if (this.data.readonly) return;
const index = e.currentTarget.dataset.index;
this.setData({
currentScore: index,
'stars[' + (index - 1) + '].active': true
});
// 触发事件通知父组件
this.triggerEvent('change', { score: index });
},
updateStars() {
this.initStars();
}
}
});
组件的 WXML
<!-- components/star-rating/star-rating.wxml -->
<view class="star-rating">
<text class="title" wx:if="{{title}}">{{title}}</text>
<view class="stars-container">
<view
wx:for="{{stars}}"
wx:key="index"
class="star {{item.active ? 'active' : ''}}"
data-index="{{item.index}}"
bindtap="handleTap"
>
★
</view>
</view>
<text class="score-text">{{currentScore}}/{{maxStars}}</text>
</view>
组件的使用
// 1. 在页面的 JSON 中注册组件
// pages/detail/index.json
{
"usingComponents": {
"star-rating": "/components/star-rating/star-rating"
}
}
// 2. 在 WXML 中使用
<star-rating
title="评分"
score="{{product.score}}"
max-stars="10"
readonly="{{false}}"
bind:change="onRatingChange"
/>
组件间通信方式
组件生命周期与 Behaviors 复用
组件完整生命周期
Component({
lifetimes: {
created() {
// 组件实例刚被创建,此时还不能访问 data 和 properties
// 适合:初始化一些不依赖数据的变量
},
attached() {
// 组件挂载到页面节点树中
// 适合:初始化数据、发起网络请求
console.log('组件挂载完成');
},
ready() {
// 组件在视图层布局完成后执行
// 适合:获取节点信息、初始化动画
},
moved() {
// 组件被移动到节点树的另一个位置
},
detached() {
// 组件从节点树中移除
// 适合:清理定时器、取消请求
},
error(err) {
// 组件方法抛出错误时执行
console.error('组件错误:', err);
}
},
// 旧版生命周期(兼容)
pageLifetimes: {
show() {
// 组件所在页面显示时执行
},
hide() {
// 组件所在页面隐藏时执行
},
resize() {
// 组件所在页面尺寸变化时执行
}
}
});
Behaviors 组件间复用
Behaviors 是用于组件间代码共享的机制,类似 mixin:
// behaviors/pagination.js
module.exports = Behavior({
data: {
page: 1,
pageSize: 20,
total: 0,
hasMore: true,
isLoading: false,
list: []
},
methods: {
async loadPage(reset = false) {
if (this.data.isLoading) return;
if (!reset && !this.data.hasMore) return;
this.setData({ isLoading: true });
if (reset) {
this.setData({ page: 1, list: [] });
}
try {
const result = await this.fetchData({
page: this.data.page,
pageSize: this.data.pageSize
});
const newList = reset
? result.list
: [...this.data.list, ...result.list];
this.setData({
list: newList,
page: this.data.page + 1,
hasMore: result.list.length === this.data.pageSize,
total: result.total,
isLoading: false
});
} catch (error) {
this.setData({ isLoading: false });
wx.showToast({ title: '加载失败', icon: 'none' });
}
},
// 使用此 behavior 的组件需要实现此方法
fetchData(params) {
throw new Error('需要实现 fetchData 方法');
}
}
});
// 在组件中使用
const paginationBehavior = require('../../behaviors/pagination');
Component({
behaviors: [paginationBehavior],
methods: {
async fetchData(params) {
// 实现具体的数据获取逻辑
return await api.getProducts(params);
}
}
});
组件插槽 Slot 与外部样式
默认插槽
<!-- 组件模板 -->
<view class="card">
<view class="card-header">
<slot></slot> <!-- 默认插槽 -->
</view>
<view class="card-body">
<slot name="body"></slot> <!-- 命名插槽 -->
</view>
<view class="card-footer">
<slot name="footer"></slot>
</view>
</view>
<!-- 使用时 -->
<card>
<text>卡片标题</text>
<view slot="body">卡片内容区域</view>
<button slot="footer">确定</button>
</card>
外部样式类 External Classes
// 组件定义
Component({
externalClasses: ['custom-class', 'custom-title-class']
});
// 组件模板
<view class="card custom-class">
<text class="title custom-title-class">{{title}}</text>
</view>
// 父组件使用
<my-card
class="card-wrapper"
custom-class="my-card-style"
custom-title-class="my-title-style"
title="Hello"
/>
样式隔离与穿透
Component({
options: {
// 样式隔离选项
addGlobalClass: true, // 允许全局样式穿透
// 或使用
styleIsolation: 'shared' // 共享样式(推荐)
// 其他值:'isolated'(默认), 'apply-shared'
}
});
/* 父组件样式穿透 */
.my-card-style {
background: #f5f5f5;
border-radius: 12px;
}
:deep(.card-body) { /* 部分工具支持 */
padding: 20px;
}
🔌 第四部分:API与接口
网络请求封装与拦截器设计
统一请求封装
// utils/request.js
const BASE_URL = 'https://api.example.com';
const TIMEOUT = 15000;
class Request {
constructor() {
this.interceptors = {
request: [],
response: []
};
}
// 添加请求拦截器
addRequestInterceptor(fn) {
this.interceptors.request.push(fn);
}
// 添加响应拦截器
addResponseInterceptor(fn) {
this.interceptors.response.push(fn);
}
// 核心请求方法
async request(options) {
let config = {
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
...options.header
},
timeout: options.timeout || TIMEOUT
};
// 执行请求拦截器
for (const interceptor of this.interceptors.request) {
config = await interceptor(config);
}
return new Promise((resolve, reject) => {
wx.request({
...config,
success: async (res) => {
// 执行响应拦截器
let response = res;
for (const interceptor of this.interceptors.response) {
response = await interceptor(response);
}
if (response.statusCode === 200) {
resolve(response.data);
} else {
reject(new Error(`HTTP Error: ${response.statusCode}`));
}
},
fail: (err) => {
reject(err);
}
});
});
}
get(url, params = {}, options = {}) {
return this.request({
url,
method: 'GET',
data: params,
...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, options = {}) {
return this.request({
url,
method: 'DELETE',
...options
});
}
}
// 创建实例并添加拦截器
const request = new Request();
// 请求拦截器:自动添加 token
request.addRequestInterceptor((config) => {
const token = wx.getStorageSync('token');
if (token) {
config.header['Authorization'] = `Bearer ${token}`;
}
return config;
});
// 响应拦截器:统一错误处理
request.addResponseInterceptor((response) => {
if (response.statusCode === 401) {
// Token 过期,跳转登录
wx.removeStorageSync('token');
wx.redirectTo({ url: '/pages/login/index' });
return Promise.reject(new Error('未授权'));
}
return response;
});
module.exports = request;
使用方式
// 在页面中使用
const request = require('../../utils/request');
Page({
async onLoad() {
try {
// GET 请求
const users = await request.get('/api/users', {
page: 1,
size: 20
});
// POST 请求
const result = await request.post('/api/users', {
name: '张三',
email: 'zhangsan@example.com'
});
this.setData({ users });
} catch (error) {
wx.showToast({
title: error.message,
icon: 'none'
});
}
}
});
微信登录与用户授权完整流程
登录流程图
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 小程序 │ │ 微信服务器│ │ 你的服务器│ │ 用户 │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ wx.login() │ │ │ │───────────────►│ │ │ │ │ │ │ │ code │ │ │ │◄───────────────│ │ │ │ │ │ │ │ code + appid │ │ │ │────────────────┼───────────────►│ │ │ │ │ │ │ │ code2session │ │ │ │◄───────────────│ │ │ │ │ │ │ │ session_key │ │ │ │ openid │ │ │ │───────────────►│ │ │ │ │ │ │ custom token │ │ │ │◄───────────────┼────────────────│ │ │ │ │ │ │ 后续请求携带token │ │ │────────────────┼───────────────►│ │ └────────────────┴────────────────┴────────────────┘
登录代码实现
// services/auth.service.js
const request = require('../utils/request');
class AuthService {
// 获取微信登录凭证
async wxLogin() {
return new Promise((resolve, reject) => {
wx.login({
success: (res) => {
if (res.code) {
resolve(res.code);
} else {
reject(new Error('wx.login 失败'));
}
},
fail: reject
});
});
}
// 完整登录流程
async login() {
// 1. 检查本地是否有有效token
const token = wx.getStorageSync('token');
const expireTime = wx.getStorageSync('token_expire');
if (token && expireTime && Date.now() < expireTime) {
return token; // Token 仍然有效
}
// 2. 获取微信 code
const code = await this.wxLogin();
// 3. 向服务器换取自定义 token
const result = await request.post('/api/auth/login', { code });
// 4. 保存 token
wx.setStorageSync('token', result.token);
wx.setStorageSync('token_expire', Date.now() + result.expiresIn * 1000);
wx.setStorageSync('userInfo', result.userInfo);
return result.token;
}
// 检查登录状态
isLoggedIn() {
const expireTime = wx.getStorageSync('token_expire');
return expireTime && Date.now() < expireTime;
}
// 退出登录
logout() {
wx.removeStorageSync('token');
wx.removeStorageSync('token_expire');
wx.removeStorageSync('userInfo');
}
// 获取用户信息(2024年新接口)
async getUserProfile() {
return new Promise((resolve, reject) => {
wx.getUserProfile({
desc: '用于展示用户信息',
success: (res) => resolve(res.userInfo),
fail: reject
});
});
}
}
module.exports = new AuthService();
微信支付接入与回调处理
支付流程
前端支付代码
// services/payment.service.js
class PaymentService {
async pay(orderId) {
try {
// 1. 调用后端接口获取支付参数
const payParams = await request.post('/api/payment/create', {
orderId: orderId
});
// 2. 唤起微信支付
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package, // prepay_id=xxx
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: (res) => {
console.log('支付成功', res);
resolve(res);
},
fail: (err) => {
if (err.errMsg.includes('cancel')) {
reject(new Error('用户取消支付'));
} else {
reject(new Error('支付失败: ' + err.errMsg));
}
}
});
});
} catch (error) {
throw error;
}
}
// 查询支付结果
async queryPayResult(orderId) {
const result = await request.get('/api/payment/status', { orderId });
return result.status; // 'success' | 'pending' | 'failed'
}
}
module.exports = new PaymentService();
⚠️ 安全警告:
文件上传下载与图片处理
图片上传封装
// utils/uploader.js
class Uploader {
constructor(options = {}) {
this.uploadUrl = options.url || 'https://api.example.com/upload';
this.maxSize = options.maxSize || 5 * 1024 * 1024; // 5MB
this.accept = options.accept || ['image/jpeg', 'image/png'];
}
async uploadImage(filePath) {
// 获取文件信息
const fileInfo = await this.getFileInfo(filePath);
// 验证文件大小
if (fileInfo.size > this.maxSize) {
throw new Error(`文件大小不能超过 ${this.maxSize / 1024 / 1024}MB`);
}
// 压缩图片(可选)
const compressedPath = await this.compressImage(filePath);
// 上传文件
return new Promise((resolve, reject) => {
wx.uploadFile({
url: this.uploadUrl,
filePath: compressedPath,
name: 'file',
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
},
formData: {
timestamp: Date.now(),
type: 'image'
},
success: (res) => {
if (res.statusCode === 200) {
const data = JSON.parse(res.data);
resolve(data);
} else {
reject(new Error('上传失败'));
}
},
fail: (err) => reject(err)
});
});
}
compressImage(filePath, quality = 80) {
return new Promise((resolve, reject) => {
wx.compressImage({
src: filePath,
quality: quality,
compressedWidth: 1080,
success: (res) => resolve(res.tempFilePath),
fail: () => resolve(filePath) // 压缩失败则使用原图
});
});
}
getFileInfo(filePath) {
return new Promise((resolve, reject) => {
wx.getFileInfo({
filePath: filePath,
success: resolve,
fail: reject
});
});
}
// 批量上传
async uploadImages(filePaths) {
const results = [];
for (const path of filePaths) {
try {
const result = await this.uploadImage(path);
results.push({ path, url: result.url, status: 'success' });
} catch (error) {
results.push({ path, error: error.message, status: 'failed' });
}
}
return results;
}
}
module.exports = new Uploader({
url: 'https://api.example.com/upload'
});
使用示例
Page({
async handleChooseImage() {
const res = await new Promise((resolve, reject) => {
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album', 'camera'],
sizeType: ['compressed'],
success: resolve,
fail: reject
});
});
wx.showLoading({ title: '上传中...' });
try {
const filePaths = res.tempFiles.map(f => f.tempFilePath);
const results = await uploader.uploadImages(filePaths);
const successResults = results.filter(r => r.status === 'success');
this.setData({
imageList: [...this.data.imageList, ...successResults]
});
wx.showToast({ title: '上传完成' });
} catch (error) {
wx.showToast({ title: '上传失败', icon: 'none' });
} finally {
wx.hideLoading();
}
}
});
🚀 第五部分:进阶技巧
状态管理方案:MobX-miniprogram 实践
为什么需要状态管理?
随着小程序功能复杂度增加,组件间数据共享和状态同步变得越来越困难。全局状态管理可以解决:
MobX-miniprogram 配置
// store/userStore.js
import { observable, action } from 'mobx-miniprogram';
const userStore = observable({
// 可观察状态
userInfo: null,
isLoggedIn: false,
loading: false,
// 计算属性
get displayName() {
return this.userInfo?.nickname || '游客';
},
get isVip() {
return this.userInfo?.vip_level > 0;
},
// actions
setUserInfo: action(function(userInfo) {
this.userInfo = userInfo;
this.isLoggedIn = !!userInfo;
}),
setLoading: action(function(loading) {
this.loading = loading;
}),
logout: action(function() {
this.userInfo = null;
this.isLoggedIn = false;
})
});
export default userStore;
在页面中使用
// pages/profile/index.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import userStore from '../../store/userStore';
Page({
data: {
// MobX 绑定的状态会自动合并到 data 中
},
onLoad() {
// 创建 store 绑定
this.storeBindings = createStoreBindings(this, {
store: userStore,
fields: ['userInfo', 'isLoggedIn', 'displayName', 'isVip'],
actions: ['setUserInfo', 'logout']
});
},
onUnload() {
// 销毁绑定
this.storeBindings.destroyStoreBindings();
},
handleLogout() {
wx.showModal({
title: '确认退出',
success: (res) => {
if (res.confirm) {
this.logout();
wx.reLaunch({ url: '/pages/index/index' });
}
}
});
}
});
<!-- WXML 中直接使用 -->
<view wx:if="{{isLoggedIn}}">
<text>欢迎,{{displayName}}</text>
<view wx:if="{{isVip}}" class="vip-badge">VIP</view>
<button bindtap="handleLogout">退出登录</button>
</view>
<view wx:else>
<button bindtap="handleLogin">登录</button>
</view>
自定义组件高级特性:组件间通信与动态组件
自定义组件间通信
// 父组件获取子组件实例
Component({
methods: {
getChildComponent() {
// 通过 id 获取子组件实例
const child = this.selectComponent('#child-id');
// 调用子组件方法
child.doSomething();
// 获取子组件数据
console.log(child.data);
},
getAllChildren() {
// 获取所有匹配的子组件
const children = this.selectAllComponents('.child-class');
children.forEach(child => {
child.update();
});
}
}
});
组件间事件通道
// 子组件
Component({
methods: {
handleTap() {
// 触发事件并传递数据
this.triggerEvent('customEvent', {
value: 'hello',
timestamp: Date.now()
}, {
bubbles: true, // 事件是否冒泡
composed: true, // 事件是否可以穿越 Shadow DOM 边界
capturePhase: true // 事件是否有捕获阶段
});
}
}
});
// 父组件 WXML
<child-component
bind:customEvent="onCustomEvent"
capture:customEvent="onCaptureEvent"
mut-bind:customEvent="onMutBind"
/>
纯数据字段
Component({
options: {
pureDataPattern: /^_/ // 以 _ 开头的字段为纯数据字段
},
data: {
_internalState: {}, // 不会触发渲染
_cache: new Map(), // 纯数据,不传递到渲染层
visible: true // 正常数据,会触发渲染
},
methods: {
updateInternalState(key, value) {
// 修改纯数据字段不会触发视图更新
this.data._cache.set(key, value);
}
}
});
抽象节点(动态组件)
// 在组件中声明抽象节点
Component({
options: {
componentGenerics: {
// 声明一个抽象节点 "custom-header"
// 使用时可以由外部指定具体组件
}
}
});
// WXML 中使用
<component is="{{currentComponent}}" data="{{componentData}}"/>
// JS 中动态切换
Page({
data: {
currentComponent: 'component-a',
componentData: {}
},
switchComponent(name) {
this.setData({
currentComponent: name,
componentData: this.getInitialData(name)
});
}
});
云开发 Serverless 架构实战
云开发概述
微信云开发提供了一体化的后端解决方案,无需搭建服务器:
云函数示例
// cloud/functions/getProducts/index.js
const cloud = require('wx-server-sdk');
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV // 使用当前环境
});
const db = cloud.database();
const _ = db.command;
exports.main = async (event, context) => {
const { page = 1, pageSize = 20, categoryId } = event;
const wxContext = cloud.getWXContext();
try {
let query = db.collection('products');
// 条件过滤
if (categoryId) {
query = query.where({ categoryId: categoryId });
}
// 分页查询
const countResult = await query.count();
const total = countResult.total;
const listResult = await query
.skip((page - 1) * pageSize)
.limit(pageSize)
.orderBy('createdAt', 'desc')
.get();
return {
code: 0,
data: {
list: listResult.data,
total: total,
page: page,
pageSize: pageSize,
hasMore: page * pageSize < total
},
openId: wxContext.OPENID
};
} catch (error) {
return {
code: -1,
message: error.message
};
}
};
前端调用云函数
Page({
async onLoad() {
try {
const result = await wx.cloud.callFunction({
name: 'getProducts',
data: {
page: 1,
pageSize: 20
}
});
if (result.result.code === 0) {
this.setData({
productList: result.result.data.list
});
}
} catch (error) {
console.error('调用云函数失败:', error);
}
}
});
云数据库权限
自定义组件自定义组件:WXS 脚本与性能优化
WXS 是什么?
WXS(WeiXin Script)是小程序的一套脚本语言,运行在渲染层。与 JavaScript 不同,WXS 可以直接在渲染层执行数据处理,避免了通过 setData 跨线程传输数据的开销。
适用场景
WXS 使用示例
<!-- 内联 WXS -->
<wxs module="filters">
module.exports = {
formatPrice: function(price) {
// 分转元
return (price / 100).toFixed(2);
},
formatDate: function(timestamp) {
var date = getDate(timestamp);
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
return year + '-' + (month < 10 ? '0' + month : month) + '-' + (day < 10 ? '0' + day : day);
},
truncate: function(str, len) {
if (!str) return '';
if (str.length <= len) return str;
return str.substring(0, len) + '...';
}
};
</wxs>
<!-- 使用 WXS -->
<view class="price">¥{{filters.formatPrice(product.price)}}</view>
<view class="date">{{filters.formatDate(product.createdAt)}}</view>
<text class="title">{{filters.truncate(product.title, 20)}}</text>
触摸事件中的 WXS 优化
<!-- 实现跟手滑动效果,避免频繁 setData -->
<wxs module="touchHandler">
module.exports = {
handleTouchMove: function(event, ownerInstance) {
var touch = event.touches[0];
var x = touch.clientX;
// 直接修改样式,不走 setData
ownerInstance.selectComponent('.draggable').setStyle({
transform: 'translateX(' + x + 'px)'
});
}
};
</wxs>
<view class="draggable" bindtouchmove="{{touchHandler.handleTouchMove}}">
拖拽我
</view>
💡 WXS 性能优势:
由于 WXS 直接在渲染层执行,不需要跨线程传输数据。在高频事件处理(如 touchmove、scroll)中,使用 WXS 可以显著降低通信开销,提升流畅度。
⚡ 第六部分:性能优化
启动性能优化与按需注入
启动时间优化
小程序启动时间是用户体验的第一道关卡,优化策略包括:
1. 代码体积优化
// app.json - 开启按需注入
{
"lazyCodeLoading": "requiredComponents"
}
// 效果:
// - 未使用的自定义组件不会被创建
// - 页面切换时销毁不可见的组件
// - 减少内存占用和初始化时间
2. 首屏渲染优化
// 减少首次 setData 数据量
Page({
onLoad() {
// 先展示骨架屏/基础结构
this.setData({
skeletonVisible: true,
basicInfo: { title: '加载中' }
});
// 异步加载详细数据
this.loadDetailData();
},
async loadDetailData() {
const data = await api.getDetail();
// 分批次设置数据,避免一次性传输大量数据
this.setData({
skeletonVisible: false,
'detail.title': data.title,
'detail.summary': data.summary
});
// 延迟加载非必要数据
setTimeout(() => {
this.setData({
'detail.comments': data.comments,
'detail.recommendations': data.recommendations
});
}, 100);
}
});
3. 预加载策略
// 在首页预加载下一页数据
Page({
onReady() {
// 首页渲染完成后预加载下一个常用页面
setTimeout(() => {
wx.preloadSkylineView(); // 预加载 Skyline 视图
}, 2000);
}
});
渲染性能优化与长列表处理
setData 优化最佳实践
长列表性能优化
// 虚拟列表实现思路
Page({
data: {
allItems: [], // 全部数据(不在渲染层)
visibleItems: [], // 可见区域的数据
scrollTop: 0,
itemHeight: 60, // 每项高度
viewportHeight: 0 // 可视区域高度
},
onLoad() {
// 获取可视区域高度
const systemInfo = wx.getSystemInfoSync();
this.setData({
viewportHeight: systemInfo.windowHeight
});
},
onScroll(e) {
const { scrollTop } = e.detail;
const { itemHeight, viewportHeight, allItems } = this.data;
// 计算可见范围
const startIndex = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(viewportHeight / itemHeight);
const endIndex = Math.min(startIndex + visibleCount + 2, allItems.length);
// 只更新可见数据
this.setData({
scrollTop: scrollTop,
visibleItems: allItems.slice(startIndex, endIndex),
paddingOffset: startIndex * itemHeight // 上方占位高度
});
}
});
图片优化策略
<!-- 图片优化示例 -->
<image
src="{{imageUrl}}?x-oss-process=image/resize,w_375/format,webp"
lazy-load
mode="aspectFill"
class="list-image"
bindload="onImageLoad"
binderror="onImageError"
/>
内存优化与性能监控
内存管理要点
Page({
data: {
items: []
},
_timer: null,
onLoad() {
this._timer = setInterval(() => {
this.refreshData();
}, 5000);
},
onUnload() {
// 页面卸载时清理
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// 取消未完成的请求
if (this._requestTask) {
this._requestTask.abort();
}
}
});
性能监控
// 使用 wx.getPerformance() 监控性能
const performance = wx.getPerformance();
const observer = performance.createObserver((entryList) => {
entryList.getEntries().forEach(entry => {
console.log('性能条目:', entry);
// entry 包含:name, entryType, startTime, duration
});
});
observer.observe({ entryTypes: ['render', 'script', 'navigation'] });
// 自定义性能打点
performance.mark('startLoad');
await loadData();
performance.mark('endLoad');
performance.measure('dataLoad', 'startLoad', 'endLoad');
性能面板关键指标
🚀 第七部分:发布上线
测试策略与质量保证
测试类型
自动化测试(Miniprogram-Automator)
// 安装:npm install miniprogram-automator --save-dev
// test/e2e.js
const automator = require('miniprogram-automator');
async function runTest() {
// 启动小程序
const miniProgram = await automator.launch({
projectPath: './miniprogram' // 项目路径
});
// 获取页面
const page = await miniProgram.currentPage();
console.log('当前页面:', page.path);
// 模拟操作
await page.callMethod('addTodo', '测试任务');
// 检查数据
const data = await page.data();
console.log('todos:', data.todos);
// 点击元素
const btn = await page.$('.submit-btn');
await btn.tap();
// 等待渲染
await page.waitFor(500);
// 断言
const result = await page.data('todos');
assert.equal(result.length, 1);
await miniProgram.close();
}
runTest();
发布前检查清单
审核规范与常见驳回原因
审核基本要求
常见驳回原因及解决方案
1. 功能不完整
表现:点击按钮无反应、页面空白、流程断裂
解决:确保所有功能路径可用,提供测试账号供审核人员测试
2. 隐私问题
表现:未声明隐私权限、过度收集用户信息
解决:
3. 类目不符
表现:实际功能与选择的服务类目不匹配
解决:根据实际功能选择正确的类目,部分类目需提供资质证明
4. 内容违规
表现:包含违规内容(政治敏感、色情、赌博等)
解决:接入内容安全审核接口
// 使用微信内容安全接口
async function checkContent(text) {
try {
const result = await wx.cloud.callFunction({
name: 'msgSecCheck',
data: { content: text }
});
return result.result.errCode === 0;
} catch (error) {
return false;
}
}
5. 用户体验问题
版本管理与灰度发布策略
版本管理策略
灰度发布
微信支持分阶段发布,先让一部分用户使用新版本:
// 通过版本号和随机数实现简单灰度
Page({
onLoad() {
this.checkFeatureFlag();
},
checkFeatureFlag() {
// 方式1:使用微信的分阶段发布功能(管理后台设置)
// 方式2:通过后端配置灰度规则
const userId = wx.getStorageSync('userId');
const grayRatio = 0.1; // 10%用户灰度
// 简单灰度判断
const isInGray = (userId % 100) < (grayRatio * 100);
if (isInGray) {
this.setData({ isNewVersion: true });
this.loadNewFeatures();
}
}
});
热更新与强制更新
// app.js - 检查版本更新
App({
onLaunch() {
this.checkUpdate();
},
checkUpdate() {
if (!wx.canIUse('getUpdateManager')) return;
const updateManager = wx.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('发现新版本');
}
});
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,是否重启应用?',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate();
}
}
});
});
updateManager.onUpdateFailed(() => {
wx.showModal({
title: '更新提示',
content: '新版本更新失败,请删除小程序后重新搜索打开',
showCancel: false
});
});
}
});
发布最佳实践
❓ 第八部分:常见问题与最佳实践
常见开发问题排查指南
Q1: setData 不生效或视图不更新?
常见原因:
// ❌ 错误
this.data.list.push(newItem); // 不会触发更新
// ✅ 正确
const list = this.data.list;
list.push(newItem);
this.setData({ list: list });
// ✅ 更优(路径更新)
const index = this.data.list.length;
this.setData({ [`list[${index}]`]: newItem });
Q2: 页面间如何传递数据?
// 使用 EventChannel(页面间通信)
// 页面A
wx.navigateTo({
url: '/pages/B/index',
events: {
// 监听来自B页面的事件
onResult: function(data) {
console.log('收到B页面数据:', data);
}
},
success: function(res) {
// 向B页面发送数据
res.eventChannel.emit('initData', { userId: 123 });
}
});
// 页面B
Page({
onLoad() {
const eventChannel = this.getOpenerEventChannel();
// 监听A页面的数据
eventChannel.on('initData', (data) => {
console.log('收到A页面数据:', data);
});
// 向A页面发送结果
eventChannel.emit('onResult', { selected: true });
}
});
Q3: 如何处理小程序后台运行?
小程序进入后台后只有有限的执行时间(约5分钟),可以通过后台运行接口延长:
// 需要后台持续运行的场景(如音乐播放、导航)
Page({
onShow() {
wx.onAppHide(() => {
console.log('进入后台');
// 申请后台运行
this.taskId = wx.enterBackgroundTask({
expireTime: 10 * 60 * 1000, // 10分钟
success: (res) => {
console.log('后台任务ID:', res.taskId);
}
});
});
},
onHide() {
// 页面隐藏时的处理
}
});
2025年小程序开发最佳实践总结
架构设计最佳实践
编码规范
性能指标目标(2025标准)
| 指标 | 目标值 | 说明 |
|---|---|---|
| 首屏渲染 | < 1.5s | 冷启动首屏可见时间 |
| 页面切换 | < 300ms | 页面间导航耗时 |
| setData 频率 | < 20次/s | 每秒调用次数 |
| 主包体积 | < 1.5MB | 建议低于限制 |
| FPS | ≥ 55 | 交互帧率 |
技术选型建议
💡 2025 关键趋势: