📱 微信小程序设计架构与完整开发教程

IT 技术 75 阅读 更新于 2026-09-25 17:37

😅 未找到相关内容,请尝试其他关键词

🏗️ 第一部分:架构设计

小程序整体架构概述

双线程模型架构

微信小程序采用双线程模型(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 或云存储
  • 代码压缩(构建工具自动处理)
  • 按需引入组件,避免全局注册过多组件

📘 第二部分:基础入门

开发环境搭建与工具配置

环境准备清单

  1. 注册小程序账号
    • 访问 mp.weixin.qq.com 注册开发者账号
    • 获取 AppID(小程序ID)
    • 配置服务器域名(开发 → 开发设置)
  2. 安装微信开发者工具
    • 下载地址:developers.weixin.qq.com/miniprogram/dev/devtools/download.html
    • 支持 Windows / macOS / Linux
    • 建议使用稳定版(Stable)
  3. 推荐辅助工具
    • VS Code + 小程序插件(WXML 语法高亮)
    • Postman(API 调试)
    • Charles / Fiddler(网络抓包)
    • 微信开发者工具 CLI 工具

创建第一个项目

// 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 选择指南:

  • 频繁切换:用 hidden(只是 CSS 控制)
  • 运行时条件很少改变:用 wx:if(减少不必要的组件实例)

WXSS 样式系统与响应式设计

WXSS 与 CSS 的区别

WXSS(WeiXin Style Sheets)是微信小程序的样式语言,基于 CSS 扩展,主要增加了:

  • rpx 单位:响应式像素,自动适配不同屏幕宽度
  • 样式导入:@import 语法
  • 内联样式限制:支持 class 和 style,但不支持 ID 选择器

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 数据量建议不超过 256KB
  • 避免频繁调用:合并多次 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"
/>

组件间通信方式

  • 父 → 子:通过 properties 传递
  • 子 → 父:通过 triggerEvent 触发事件
  • 子组件间:通过父组件中转或使用 selectComponent
  • 跨层级:使用 EventBus 或全局状态管理

组件生命周期与 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();

微信支付接入与回调处理

支付流程

  1. 前端创建订单 → 后端生成预支付订单
  2. 后端调用微信统一下单接口
  3. 后端返回支付参数给前端
  4. 前端调用 wx.requestPayment 唤起支付
  5. 支付完成后,后端接收微信回调通知
  6. 前端轮询或查询支付结果

前端支付代码

// 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();

⚠️ 安全警告:

  • 签名密钥(mch_key)绝对不能放在前端代码中
  • 支付金额必须由后端计算,前端不可传递金额参数
  • 必须验证微信回调的签名合法性
  • 支付结果以后端回调通知为准,不要仅依赖前端结果

文件上传下载与图片处理

图片上传封装

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

为什么需要状态管理?

随着小程序功能复杂度增加,组件间数据共享和状态同步变得越来越困难。全局状态管理可以解决:

  • 跨页面/跨组件的数据共享
  • 全局状态(用户登录状态、购物车等)管理
  • 避免层层传递 props

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 架构实战

云开发概述

微信云开发提供了一体化的后端解决方案,无需搭建服务器:

  • 云函数:基于 Node.js 的 Serverless 函数
  • 云数据库:JSON 文档型数据库(MongoDB 风格)
  • 云存储:文件存储与 CDN 分发
  • 云调用:直接调用微信开放接口

云函数示例

// 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 跨线程传输数据的开销。

适用场景

  • 简单的数据格式化(日期、价格等)
  • 事件响应中的简单逻辑判断
  • 触摸事件的高频处理(避免频繁 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 优化最佳实践

  • 合并多次 setData 调用为单次
  • 使用路径表达式进行局部更新
  • 避免在 setData 中传递大量数据
  • 使用纯数据字段(pureDataPattern)存储非渲染数据

长列表性能优化

// 虚拟列表实现思路
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  // 上方占位高度
    });
  }
});

图片优化策略

  • 使用 CDN 的图片裁剪参数,按需加载合适尺寸
  • 列表图片使用懒加载:<image lazy-load>
  • 使用 WebP 格式(比 JPEG 小 25-35%)
  • 使用渐进式加载(低质量图 → 高清图)
<!-- 图片优化示例 -->
<image
  src="{{imageUrl}}?x-oss-process=image/resize,w_375/format,webp"
  lazy-load
  mode="aspectFill"
  class="list-image"
  bindload="onImageLoad"
  binderror="onImageError"
/>

内存优化与性能监控

内存管理要点

  • 页面卸载时清理定时器、事件监听器
  • 避免在全局对象中缓存大量数据
  • 使用 WeakRef 或手动管理大型对象
  • 定期清理不再使用的缓存数据
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');

性能面板关键指标

  • 首屏时间:从点击到首屏渲染完成 < 1.5s
  • setData 调用频率:每秒不超过 20 次
  • setData 数据量:单次不超过 256KB
  • 页面切换耗时:< 300ms

🚀 第七部分:发布上线

测试策略与质量保证

测试类型

  • 体验版测试:上传代码后设为体验版,邀请团队成员测试
  • 兼容性测试:在不同机型、不同微信版本上测试
  • 网络环境测试:弱网(2G/3G)、无网络、网络切换
  • 性能测试:大数据量、长列表、复杂动画
  • 边界测试:极端输入、异常状态、并发操作

自动化测试(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. ✅ 代码已压缩(开发者工具自动处理)
  5. ✅ 包体积检查(主包 < 2MB)
  6. ✅ 多端兼容性测试完成
  7. ✅ 性能指标达标
  8. ✅ 审核材料准备齐全

审核规范与常见驳回原因

审核基本要求

  • 小程序功能完整,无空白页面或死链
  • UI 符合微信设计规范
  • 有完整的用户协议和隐私政策
  • 不存在诱导分享、虚假宣传等行为
  • 类目选择正确,所需资质齐全

常见驳回原因及解决方案

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. 用户体验问题

  • 弹窗过于频繁
  • 强制关注公众号才能使用
  • 无退出机制或退出困难
  • 加载过慢(>5s无响应)

版本管理与灰度发布策略

版本管理策略

  • 开发版:开发调试使用
  • 体验版:指定体验成员测试
  • 审核版:提交审核的版本
  • 线上版:正式发布的版本

灰度发布

微信支持分阶段发布,先让一部分用户使用新版本:

// 通过版本号和随机数实现简单灰度
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
      });
    });
  }
});

发布最佳实践

  • 重要更新先灰度 10% 用户
  • 监控灰度版本的错误率和使用数据
  • 确认无问题后逐步扩大到全量
  • 保留旧版本兼容性至少 1 个月
  • 重大变更提前在用户群或公告中预告

❓ 第八部分:常见问题与最佳实践

常见开发问题排查指南

Q1: setData 不生效或视图不更新?

常见原因:

  • 直接修改 this.data 而没有使用 setData
  • 在组件中修改 properties 的值
  • 对象深层属性变化但只更新了引用
// ❌ 错误
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: 页面间如何传递数据?

  • URL 参数:适合简单数据,有长度限制
  • 全局变量:getApp().globalData,适合全局状态
  • 本地缓存:wx.setStorage/getStorage,适合持久化数据
  • EventChannel:页面间双向通信(推荐)
// 使用 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年小程序开发最佳实践总结

架构设计最佳实践

  1. 分层架构:严格遵循 Model-Service-ViewModel-View 分层
  2. 单一职责:每个文件/模块只负责一个功能
  3. 依赖注入:通过参数传递依赖而非硬编码
  4. 配置外部化:环境配置、接口地址等与代码分离

编码规范

  1. 使用 ESLint 统一代码风格
  2. 变量命名语义化,避免缩写
  3. 函数不超过 50 行,保持短小
  4. 添加必要的注释说明业务逻辑
  5. 统一错误处理机制

性能指标目标(2025标准)

指标目标值说明
首屏渲染< 1.5s冷启动首屏可见时间
页面切换< 300ms页面间导航耗时
setData 频率< 20次/s每秒调用次数
主包体积< 1.5MB建议低于限制
FPS≥ 55交互帧率

技术选型建议

  • 状态管理:MobX-miniprogram(官方推荐)或自研简易 Store
  • UI 框架:WeUI、Vant Weapp、TDesign
  • 构建工具:Gulp/Webpack + TypeScript
  • 后端:云开发(小项目)/ 自建服务(复杂业务)
  • 渲染:Skyline 渲染引擎(2024+推荐,性能提升 50%+)

💡 2025 关键趋势:

  • Skyline 渲染引擎将成为主流(更好的动画和滚动性能)
  • 半屏小程序(半屏打开,类似系统弹窗)
  • 小程序插件化生态发展
  • AI 能力接入(语音、图像识别)
  • 跨端方案统一(uni-app / Taro 等框架的成熟)
← 返回IT 技术 yicool 百科 · 📱 微信小程序设计架构与完整开发教程

评论 0