微信小程序开发完全指南:从入门到精通

2023/1/1

点击勘误issues (opens new window),哪吒感谢大家的阅读

# 微信小程序开发完全指南:从入门到精通

# 目录

# 1. 微信小程序基础

# 1.1 什么是微信小程序

微信小程序是一种不需要下载安装即可使用的应用,它实现了「触手可及」的梦想,用户扫一扫或搜一下即可打开应用。小程序提供了一个简单、高效的应用开发框架和丰富的组件及API,帮助开发者在微信中开发具有原生体验的应用。

小程序的主要特点:

  • 无需安装:用户无需下载安装,扫码即用
  • 快速访问:「用完即走」的理念,无需卸载
  • 原生体验:接近原生App的用户体验
  • 微信生态:可以直接调用微信的支付、登录、地图等能力
  • 体积小:单个小程序包大小限制为2MB(主包)

# 1.2 小程序与H5、原生App的对比

特性 微信小程序 H5 原生App
安装方式 无需安装 无需安装 需要安装
更新方式 自动更新 自动更新 需要手动更新
性能表现 接近原生 一般 最佳
设备能力 部分可用 有限 完全可用
开发成本 中等 低 高
跨平台 仅微信生态 全平台 需要分别开发
上架审核 需要审核 无需审核 需要审核
用户获取 微信生态内 全网可访问 应用商店

# 1.3 开发环境搭建

  1. 安装微信开发者工具

  2. 注册小程序账号

  3. 创建小程序项目

    • 打开微信开发者工具
    • 选择「项目」→「新建项目」
    • 填入AppID、项目名称和目录
    • 选择「建立普通快速启动模板」
    • 点击「确定」创建项目

# 1.4 小程序项目结构

一个典型的小程序项目结构如下:

├── app.js              // 小程序逻辑
├── app.json            // 小程序公共配置
├── app.wxss            // 小程序公共样式表
├── project.config.json // 项目配置文件
├── sitemap.json        // 微信索引配置文件
├── pages/              // 页面文件夹
│   └── index/          // index页面
│       ├── index.js    // 页面逻辑
│       ├── index.wxml  // 页面结构
│       ├── index.wxss  // 页面样式表
│       └── index.json  // 页面配置
├── utils/              // 工具函数
├── components/         // 自定义组件
├── images/             // 图片资源
└── miniprogram_npm/    // npm包目录

核心文件说明:

  • app.js:小程序入口文件,包含全局的生命周期函数和全局数据
  • app.json:全局配置,包括页面路径、窗口表现、网络超时、底部tab等
  • app.wxss:全局样式,作用于每一个页面
  • pages目录:存放所有页面,每个页面由四个文件组成(js、wxml、wxss、json)

# 2. 小程序开发基础

# 2.1 WXML与WXSS

WXML(WeiXin Markup Language)是微信小程序的标记语言,类似于HTML,但具有数据绑定等特性。WXSS(WeiXin Style Sheets)是小程序的样式语言,类似于CSS。

WXML示例:

<view class="container">
  <text>{{message}}</text>
  <button bindtap="changeMessage">点击我</button>
</view>

WXSS示例:

.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20rpx;
}

text {
  font-size: 18px;
  margin-bottom: 20rpx;
}

WXML特点:

  • 数据绑定:使用 语法
  • 列表渲染:使用 wx:for 指令
  • 条件渲染:使用 wx:if、wx:elif、wx:else 指令
  • 模板引用:使用 template 和 include 标签

WXSS特点:

  • 尺寸单位:rpx(responsive pixel)自适应单位
  • 样式导入:使用 @import 语句
  • 选择器:支持类选择器、ID选择器、元素选择器等
  • 全局样式与局部样式:app.wxss为全局样式,页面wxss为局部样式

# 2.2 小程序生命周期

小程序有两种生命周期:应用生命周期和页面生命周期。

应用生命周期(App):

App({
  onLaunch: function(options) {
    // 小程序初始化完成时触发,全局只触发一次
    console.log('App onLaunch', options);
  },
  onShow: function(options) {
    // 小程序启动,或从后台进入前台显示时触发
    console.log('App onShow', options);
  },
  onHide: function() {
    // 小程序从前台进入后台时触发
    console.log('App onHide');
  },
  onError: function(msg) {
    // 小程序发生脚本错误或API调用报错时触发
    console.log('App onError', msg);
  },
  globalData: {
    // 全局数据
    userInfo: null
  }
});

页面生命周期(Page):

Page({
  data: {
    // 页面数据
    message: 'Hello World'
  },
  onLoad: function(options) {
    // 页面加载时触发,一个页面只会调用一次
    console.log('Page onLoad', options);
  },
  onShow: function() {
    // 页面显示/切入前台时触发
    console.log('Page onShow');
  },
  onReady: function() {
    // 页面初次渲染完成时触发,一个页面只会调用一次
    console.log('Page onReady');
  },
  onHide: function() {
    // 页面隐藏/切入后台时触发
    console.log('Page onHide');
  },
  onUnload: function() {
    // 页面卸载时触发
    console.log('Page onUnload');
  },
  onPullDownRefresh: function() {
    // 用户下拉刷新时触发
    console.log('Page onPullDownRefresh');
  },
  onReachBottom: function() {
    // 用户上拉触底时触发
    console.log('Page onReachBottom');
  },
  onShareAppMessage: function() {
    // 用户点击右上角分享时触发
    return {
      title: '自定义分享标题',
      path: '/pages/index/index'
    };
  },
  // 自定义方法
  changeMessage: function() {
    this.setData({
      message: 'Hello WeChat MiniProgram!'
    });
  }
});

# 2.3 数据绑定与事件处理

数据绑定:

小程序使用 {{}} 语法进行数据绑定,将JS中的数据展示在页面上。

<!-- WXML -->
<view>{{message}}</view>
<view>{{user.name}}</view>
<view>{{a + b}} + {{c}} = {{a + b + c}}</view>
// JS
Page({
  data: {
    message: 'Hello',
    user: { name: 'John' },
    a: 1,
    b: 2,
    c: 3
  }
});

事件处理:

小程序通过 bind 或 catch 前缀绑定事件处理函数。

<!-- WXML -->
<button bindtap="handleTap">点击我</button>
<view bindtouchstart="handleTouchStart" catchtouchmove="handleTouchMove">触摸区域</view>
// JS
Page({
  handleTap: function(e) {
    console.log('按钮被点击', e);
  },
  handleTouchStart: function(e) {
    console.log('触摸开始', e);
  },
  handleTouchMove: function(e) {
    console.log('触摸移动', e);
    // catch前缀会阻止事件冒泡
  }
});

常用事件:

  • tap:点击事件
  • input:输入事件
  • change:改变事件
  • submit:表单提交事件
  • touchstart/touchmove/touchend:触摸事件

# 2.4 常用组件与API

基础组件:

  • view:视图容器,类似div
  • text:文本组件
  • button:按钮组件
  • image:图片组件
  • input:输入框组件
  • scroll-view:可滚动视图区域
  • swiper:滑块视图容器
  • navigator:页面链接

常用API:

  • 路由导航:wx.navigateTo、wx.redirectTo、wx.switchTab
  • 网络请求:wx.request
  • 数据存储:wx.setStorage、wx.getStorage
  • 界面交互:wx.showToast、wx.showModal、wx.showLoading
  • 用户信息:wx.getUserProfile
  • 支付功能:wx.requestPayment

API使用示例:

// 发起网络请求
wx.request({
  url: 'https://api.example.com/data',
  method: 'GET',
  data: { id: 1 },
  success: function(res) {
    console.log('请求成功', res.data);
  },
  fail: function(err) {
    console.error('请求失败', err);
  }
});

// 显示提示框
wx.showToast({
  title: '操作成功',
  icon: 'success',
  duration: 2000
});

// 页面跳转
wx.navigateTo({
  url: '/pages/detail/detail?id=1'
});

# 3. 小程序登录与授权

# 3.1 登录流程详解

微信小程序的登录流程主要包括以下步骤:

  1. 前端调用 wx.login() 获取临时登录凭证 code
  2. 将 code 发送到开发者服务器
  3. 开发者服务器通过 code 向微信接口服务获取 openid 和 session_key
  4. 开发者服务器自定义登录态,返回自定义登录态给小程序
  5. 小程序保存登录态,后续请求携带登录态

登录流程代码示例:

// 小程序端
wx.login({
  success: function(res) {
    if (res.code) {
      // 发送 res.code 到后端
      wx.request({
        url: 'https://api.example.com/login',
        method: 'POST',
        data: {
          code: res.code
        },
        success: function(result) {
          // 保存登录态
          wx.setStorageSync('token', result.data.token);
          console.log('登录成功');
        }
      });
    } else {
      console.error('登录失败:' + res.errMsg);
    }
  }
});

后端处理流程(Node.js示例):

const axios = require('axios');

async function login(code) {
  // 小程序 appId 和 appSecret
  const appId = 'your_appid';
  const appSecret = 'your_appsecret';
  
  try {
    // 请求微信接口获取 openid 和 session_key
    const result = await axios.get(
      `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`
    );
    
    const { openid, session_key } = result.data;
    
    // 生成自定义登录态 token
    const token = generateToken(openid);
    
    // 保存用户信息和 session_key 到数据库
    await saveUserInfo(openid, session_key, token);
    
    return { token };
  } catch (error) {
    console.error('登录失败', error);
    throw error;
  }
}

// 生成 token 的函数
function generateToken(openid) {
  // 实际应用中应使用更安全的方式生成 token
  return `token_${openid}_${Date.now()}`;
}

// 保存用户信息到数据库
async function saveUserInfo(openid, session_key, token) {
  // 实现数据库存储逻辑
  // ...
}

# 3.2 获取用户信息

从2021年4月13日起,微信调整了获取用户信息的接口,废弃了 wx.getUserInfo 接口,改为使用 wx.getUserProfile 接口,且必须由用户主动触发。

获取用户信息示例:

<!-- WXML -->
<button bindtap="getUserProfile">获取用户信息</button>
// JS
Page({
  getUserProfile: function() {
    wx.getUserProfile({
      desc: '用于完善会员资料', // 声明获取用户个人信息后的用途
      success: (res) => {
        const userInfo = res.userInfo;
        console.log('用户信息', userInfo);
        
        // 保存用户信息
        this.setData({
          userInfo: userInfo,
          hasUserInfo: true
        });
        
        // 可以将用户信息发送到后端保存
        this.updateUserInfo(userInfo);
      },
      fail: (err) => {
        console.error('获取用户信息失败', err);
      }
    });
  },
  
  updateUserInfo: function(userInfo) {
    const token = wx.getStorageSync('token');
    
    wx.request({
      url: 'https://api.example.com/user/update',
      method: 'POST',
      header: {
        'Authorization': `Bearer ${token}`
      },
      data: userInfo,
      success: function(res) {
        console.log('用户信息更新成功');
      }
    });
  }
});

# 3.3 授权最佳实践

授权策略:

  1. 分步授权:根据功能需要逐步请求授权,避免一次性请求多个权限
  2. 明确用途:在请求授权时明确说明用途,提高用户接受度
  3. 优雅降级:用户拒绝授权时提供替代方案,不影响核心功能使用
  4. 授权引导:对于关键权限,提供图文并茂的引导说明

授权状态检查:

wx.getSetting({
  success: function(res) {
    if (res.authSetting['scope.userInfo']) {
      // 已经授权获取用户信息
      console.log('已授权用户信息');
    }
    
    if (res.authSetting['scope.userLocation']) {
      // 已经授权获取位置信息
      console.log('已授权位置信息');
    }
  }
});

打开设置页引导用户授权:

wx.showModal({
  title: '提示',
  content: '需要您的位置权限才能提供附近服务,是否前往设置?',
  success: function(res) {
    if (res.confirm) {
      wx.openSetting({
        success: function(settingRes) {
          console.log('设置页操作结果', settingRes);
        }
      });
    }
  }
});

# 4. 小程序支付功能实现

# 4.1 支付流程概述

微信小程序支付流程主要包括以下步骤:

  1. 用户在小程序内选择商品下单
  2. 小程序调用后端接口创建订单
  3. 后端调用微信支付统一下单API获取支付参数
  4. 后端将支付参数返回给小程序
  5. 小程序调用 wx.requestPayment() 发起支付
  6. 用户完成支付操作
  7. 微信服务器通知商户支付结果
  8. 商户系统更新订单状态

支付流程图

# 4.2 后端接口开发

统一下单接口(Node.js示例):

const crypto = require('crypto');
const axios = require('axios');

async function createOrder(req, res) {
  try {
    const { openid, totalFee, body, orderId } = req.body;
    
    // 微信支付参数
    const appid = 'your_appid';
    const mchid = 'your_mchid'; // 商户号
    const mchKey = 'your_mch_key'; // 商户密钥
    const notifyUrl = 'https://api.example.com/pay/notify'; // 支付结果通知地址
    const nonceStr = generateNonceStr();
    const timestamp = Math.floor(Date.now() / 1000).toString();
    
    // 构建统一下单请求参数
    const params = {
      appid,
      mch_id: mchid,
      nonce_str: nonceStr,
      body,
      out_trade_no: orderId,
      total_fee: totalFee, // 单位:分
      spbill_create_ip: req.ip,
      notify_url: notifyUrl,
      trade_type: 'JSAPI',
      openid
    };
    
    // 签名
    const sign = generateSign(params, mchKey);
    params.sign = sign;
    
    // 将参数转为XML格式
    const xmlData = jsonToXml(params);
    
    // 调用微信统一下单接口
    const result = await axios.post(
      'https://api.mch.weixin.qq.com/pay/unifiedorder',
      xmlData,
      { headers: { 'Content-Type': 'text/xml' } }
    );
    
    // 解析XML响应
    const responseData = xmlToJson(result.data);
    
    if (responseData.return_code === 'SUCCESS' && responseData.result_code === 'SUCCESS') {
      // 生成小程序调用支付需要的参数
      const payParams = {
        appId: appid,
        timeStamp: timestamp,
        nonceStr,
        package: `prepay_id=${responseData.prepay_id}`,
        signType: 'MD5'
      };
      
      // 再次签名
      payParams.paySign = generateSign(payParams, mchKey);
      
      // 删除appId,避免小程序端签名校验失败
      delete payParams.appId;
      
      res.json({
        code: 0,
        message: '创建订单成功',
        data: payParams
      });
    } else {
      throw new Error(responseData.return_msg || responseData.err_code_des || '创建订单失败');
    }
  } catch (error) {
    console.error('创建订单失败', error);
    res.json({
      code: -1,
      message: error.message || '创建订单失败'
    });
  }
}

// 生成随机字符串
function generateNonceStr() {
  return Math.random().toString(36).substr(2, 15);
}

// 生成签名
function generateSign(params, key) {
  // 按字典序排序参数
  const sortedParams = Object.keys(params).sort().reduce((result, key) => {
    result[key] = params[key];
    return result;
  }, {});
  
  // 拼接参数字符串
  let stringA = '';
  for (const k in sortedParams) {
    if (sortedParams[k] !== '' && sortedParams[k] !== undefined) {
      stringA += `${k}=${sortedParams[k]}&`;
    }
  }
  
  // 拼接key
  const stringSignTemp = stringA + `key=${key}`;
  
  // MD5加密并转为大写
  return crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase();
}

// 支付结果通知处理
async function handlePayNotify(req, res) {
  try {
    // 解析微信支付结果通知
    const notifyData = xmlToJson(req.body);
    
    // 验证签名
    const sign = notifyData.sign;
    delete notifyData.sign;
    const calculatedSign = generateSign(notifyData, 'your_mch_key');
    
    if (calculatedSign !== sign) {
      throw new Error('签名验证失败');
    }
    
    if (notifyData.return_code === 'SUCCESS' && notifyData.result_code === 'SUCCESS') {
      // 支付成功,更新订单状态
      const orderId = notifyData.out_trade_no;
      const transactionId = notifyData.transaction_id;
      
      // 更新订单状态逻辑
      await updateOrderStatus(orderId, transactionId);
      
      // 返回成功响应
      res.send('<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>');
    } else {
      throw new Error('支付失败');
    }
  } catch (error) {
    console.error('处理支付通知失败', error);
    res.send('<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理失败]]></return_msg></xml>');
  }
}

// 更新订单状态
async function updateOrderStatus(orderId, transactionId) {
  // 实现数据库更新逻辑
  // ...
}

# 4.3 前端支付实现

小程序端发起支付:

Page({
  data: {
    goodsId: '',
    goodsName: '',
    price: 0
  },
  
  onLoad: function(options) {
    this.setData({
      goodsId: options.id
    });
    
    // 获取商品详情
    this.getGoodsDetail(options.id);
  },
  
  getGoodsDetail: function(goodsId) {
    wx.request({
      url: `https://api.example.com/goods/${goodsId}`,
      success: (res) => {
        this.setData({
          goodsName: res.data.name,
          price: res.data.price
        });
      }
    });
  },
  
  // 创建订单并发起支付
  createOrder: function() {
    // 显示加载中
    wx.showLoading({
      title: '正在创建订单'
    });
    
    // 获取用户openid(假设已经在登录时保存)
    const openid = wx.getStorageSync('openid');
    const token = wx.getStorageSync('token');
    
    // 创建订单参数
    const orderParams = {
      openid,
      goodsId: this.data.goodsId,
      totalFee: this.data.price * 100, // 转为分
      body: this.data.goodsName,
      orderId: `ORDER_${Date.now()}` // 实际应用中应由后端生成
    };
    
    // 请求后端创建订单
    wx.request({
      url: 'https://api.example.com/order/create',
      method: 'POST',
      header: {
        'Authorization': `Bearer ${token}`
      },
      data: orderParams,
      success: (res) => {
        wx.hideLoading();
        
        if (res.data.code === 0) {
          // 获取支付参数
          const payParams = res.data.data;
          
          // 调起微信支付
          this.requestPayment(payParams);
        } else {
          wx.showToast({
            title: res.data.message || '创建订单失败',
            icon: 'none'
          });
        }
      },
      fail: () => {
        wx.hideLoading();
        wx.showToast({
          title: '网络错误,请重试',
          icon: 'none'
        });
      }
    });
  },
  
  // 发起微信支付
  requestPayment: function(params) {
    wx.requestPayment({
      timeStamp: params.timeStamp,
      nonceStr: params.nonceStr,
      package: params.package,
      signType: params.signType,
      paySign: params.paySign,
      success: (res) => {
        console.log('支付成功', res);
        wx.showToast({
          title: '支付成功'
        });
        
        // 跳转到订单详情页
        setTimeout(() => {
          wx.navigateTo({
            url: '/pages/order/detail?id=' + orderParams.orderId
          });
        }, 1500);
      },
      fail: (err) => {
        console.log('支付失败', err);
        if (err.errMsg === 'requestPayment:fail cancel') {
          wx.showToast({
            title: '支付已取消',
            icon: 'none'
          });
        } else {
          wx.showToast({
            title: '支付失败,请重试',
            icon: 'none'
          });
        }
      }
    });
  }
});

# 4.4 支付结果处理

支付成功后的业务处理:

  1. 前端处理:

    • 显示支付成功提示
    • 跳转到订单详情页
    • 更新本地订单状态
  2. 后端处理:

    • 接收微信支付结果通知
    • 验证签名和支付状态
    • 更新订单状态
    • 触发后续业务流程(发货、记录等)

订单查询接口(用于前端主动查询订单状态):

// 后端实现
async function queryOrder(req, res) {
  try {
    const { orderId } = req.params;
    
    // 从数据库查询订单
    const order = await Order.findOne({ orderId });
    
    if (!order) {
      return res.json({
        code: -1,
        message: '订单不存在'
      });
    }
    
    res.json({
      code: 0,
      data: {
        orderId: order.orderId,
        status: order.status,
        payTime: order.payTime,
        amount: order.amount
      }
    });
  } catch (error) {
    console.error('查询订单失败', error);
    res.json({
      code: -1,
      message: '查询订单失败'
    });
  }
}

// 小程序端实现
Page({
  data: {
    orderId: '',
    orderStatus: '',
    orderAmount: 0,
    payTime: ''
  },
  
  onLoad: function(options) {
    this.setData({
      orderId: options.id
    });
    
    this.queryOrderStatus();
  },
  
  queryOrderStatus: function() {
    const token = wx.getStorageSync('token');
    
    wx.request({
      url: `https://api.example.com/order/${this.data.orderId}`,
      header: {
        'Authorization': `Bearer ${token}`
      },
      success: (res) => {
        if (res.data.code === 0) {
          this.setData({
            orderStatus: res.data.data.status,
            orderAmount: res.data.data.amount / 100, // 转为元
            payTime: res.data.data.payTime
          });
        }
      }
    });
  }
});

# 5. 订阅消息推送

# 5.1 消息模板申请

微信小程序订阅消息需要先申请消息模板,步骤如下:

  1. 登录微信公众平台
  2. 进入「功能」→「订阅消息」
  3. 选择「公共模板库」或「自定义模板」
  4. 选择合适的模板并提交申请
  5. 审核通过后获取模板ID

消息模板申请

订阅消息类型:

  • 一次性订阅消息:用户订阅后,开发者可在不限时间内下发一条消息
  • 长期订阅消息:用户订阅一次后,开发者可长期下发多条消息(仅向特定类目开放)

# 5.2 前端订阅实现

订阅消息前端实现:

<!-- WXML -->
<button bindtap="subscribeMessage">订阅消息通知</button>
// JS
Page({
  data: {
    tmplIds: ['模板ID1', '模板ID2', '模板ID3'] // 最多可添加3个模板ID
  },
  
  // 订阅消息
  subscribeMessage: function() {
    wx.requestSubscribeMessage({
      tmplIds: this.data.tmplIds,
      success: (res) => {
        console.log('订阅结果', res);
        
        // 检查订阅结果
        let acceptedTemplates = [];
        this.data.tmplIds.forEach(tmplId => {
          if (res[tmplId] === 'accept') {
            acceptedTemplates.push(tmplId);
          }
        });
        
        if (acceptedTemplates.length > 0) {
          // 将接受的模板ID发送到后端保存
          this.saveSubscription(acceptedTemplates);
          
          wx.showToast({
            title: '订阅成功',
            icon: 'success'
          });
        } else {
          wx.showToast({
            title: '您拒绝了订阅',
            icon: 'none'
          });
        }
      },
      fail: (err) => {
        console.error('订阅失败', err);
        wx.showToast({
          title: '订阅失败,请重试',
          icon: 'none'
        });
      }
    });
  },
  
  // 保存订阅信息到后端
  saveSubscription: function(acceptedTemplates) {
    const token = wx.getStorageSync('token');
    
    wx.request({
      url: 'https://api.example.com/subscription/save',
      method: 'POST',
      header: {
        'Authorization': `Bearer ${token}`
      },
      data: {
        templateIds: acceptedTemplates
      },
      success: (res) => {
        console.log('订阅信息保存成功', res.data);
      }
    });
  }
});

# 5.3 后端推送实现

发送订阅消息(Node.js示例):

async function sendSubscribeMessage(req, res) {
  try {
    const { openid, templateId, data, page } = req.body;
    
    // 获取接口调用凭证
    const accessToken = await getAccessToken();
    
    // 构建请求参数
    const params = {
      touser: openid,
      template_id: templateId,
      page, // 可选,点击消息卡片后跳转的页面
      data // 模板数据
    };
    
    // 调用发送订阅消息接口
    const result = await axios.post(
      `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`,
      params
    );
    
    if (result.data.errcode === 0) {
      res.json({
        code: 0,
        message: '发送成功'
      });
    } else {
      throw new Error(`发送失败:${result.data.errmsg}`);
    }
  } catch (error) {
    console.error('发送订阅消息失败', error);
    res.json({
      code: -1,
      message: error.message || '发送失败'
    });
  }
}

// 获取接口调用凭证
async function getAccessToken() {
  const appId = 'your_appid';
  const appSecret = 'your_appsecret';
  
  try {
    const result = await axios.get(
      `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
    );
    
    if (result.data.access_token) {
      return result.data.access_token;
    } else {
      throw new Error('获取access_token失败');
    }
  } catch (error) {
    console.error('获取access_token失败', error);
    throw error;
  }
}

// 使用示例
// 审核结果通知
app.post('/notify/audit-result', async (req, res) => {
  try {
    const { userId, auditResult, auditTime, title } = req.body;
    
    // 获取用户openid
    const user = await User.findById(userId);
    
    if (!user || !user.openid) {
      return res.json({
        code: -1,
        message: '用户不存在或未绑定openid'
      });
    }
    
    // 发送订阅消息
    await sendSubscribeMessage({
      body: {
        openid: user.openid,
        templateId: 'TknXJKdubbcKDpmLGDkvoxeDn-4ReD3dLDmHRHwSB_M', // 审核结果通知模板ID
        page: 'pages/audit/detail?id=123',
        data: {
          thing1: { value: title }, // 问题标题
          phrase2: { value: auditResult }, // 审批结果
          time3: { value: auditTime } // 发起时间
        }
      }
    }, res);
  } catch (error) {
    console.error('发送审核结果通知失败', error);
    res.json({
      code: -1,
      message: '发送通知失败'
    });
  }
});

# 6. 小程序发布与上线

# 6.1 代码审核与发布

小程序发布流程:

  1. 代码上传:在开发者工具中点击「上传」按钮
  2. 填写版本信息:版本号、项目备注等
  3. 提交审核:在微信公众平台提交代码审核
  4. 等待审核:审核时间通常为1-3个工作日
  5. 发布上线:审核通过后点击「发布」按钮

版本管理最佳实践:

  • 版本号规范:采用语义化版本号(Semantic Versioning)
  • 灰度发布:先发布给部分用户,确认稳定后再全量发布
  • 版本日志:详细记录每个版本的变更内容
  • 回滚机制:保留回滚到之前版本的能力

# 6.2 自动化部署CI

微信小程序支持使用 miniprogram-ci 工具实现自动化部署,可以集成到 CI/CD 流程中。

安装 miniprogram-ci:

npm install --save-dev miniprogram-ci

创建上传脚本(upload.js):

const ci = require('miniprogram-ci');
const path = require('path');

// 读取命令行参数
const version = process.argv[2] || '1.0.0';
const desc = process.argv[3] || '自动化部署';

(async () => {
  try {
    // 创建项目对象
    const project = new ci.Project({
      appid: 'your_appid',
      type: 'miniProgram',
      projectPath: path.resolve('./'), // 项目路径
      privateKeyPath: path.resolve('./private.key'), // 密钥路径
      ignores: ['node_modules/**/*']
    });
    
    // 上传代码
    const uploadResult = await ci.upload({
      project,
      version,
      desc,
      setting: {
        es6: true,
        minify: true,
        autoPrefixWXSS: true
      },
      onProgressUpdate: console.log
    });
    
    console.log('上传成功', uploadResult);
  } catch (error) {
    console.error('上传失败', error);
    process.exit(1);
  }
})();

集成到 package.json:

{
  "scripts": {
    "upload": "node upload.js",
    "upload:prod": "node upload.js 1.0.0 '正式版发布'",
    "upload:test": "node upload.js 1.0.0-beta '测试版发布'"
  }
}

集成到 GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy MiniProgram

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '14'
      
      - name: Install dependencies
        run: npm install
      
      - name: Create private key file
        run: echo "${{ secrets.PRIVATE_KEY }}" > private.key
      
      - name: Upload to WeChat
        run: npm run upload:prod

# 6.3 版本管理策略

版本号规范:

  • 主版本号:不兼容的API修改
  • 次版本号:向下兼容的功能性新增
  • 修订号:向下兼容的问题修正

发布策略:

  1. 体验版:内部测试使用
  2. 开发版:开发过程中的版本
  3. 审核版:提交审核的版本
  4. 线上版:正式发布的版本

多环境配置:

// config.js
const env = wx.getAccountInfoSync().miniProgram.envVersion;

const config = {
  development: {
    apiBaseUrl: 'https://dev-api.example.com',
    debug: true
  },
  trial: {
    apiBaseUrl: 'https://test-api.example.com',
    debug: true
  },
  release: {
    apiBaseUrl: 'https://api.example.com',
    debug: false
  }
};

// 根据环境选择配置
const currentConfig = {
  development: config.development, // 开发版
  trial: config.trial, // 体验版
  release: config.release // 正式版
}[env] || config.development;

export default currentConfig;

# 7. uni-app跨平台开发

# 7.1 uni-app简介

uni-app是一个使用Vue.js开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/飞书/QQ/快手/钉钉/淘宝)等多个平台。

uni-app优势:

  • 跨平台:一套代码,多端发布
  • 高性能:基于原生渲染,性能体验接近原生App
  • 生态丰富:支持NPM、支持小程序组件和SDK
  • 开发效率:HBuilderX内置相关环境,开箱即用

# 7.2 开发与打包流程

环境准备:

  1. 安装HBuilderX:

    • 下载并安装最新版HBuilderX:https://www.dcloud.io/hbuilderx.html
  2. 创建uni-app项目:

    • 打开HBuilderX → 文件 → 新建 → 项目
    • 选择uni-app模板
    • 填写项目名称和存储路径
    • 选择Vue版本和默认模板

配置manifest.json:

{
  "name": "应用名称",
  "appid": "__UNI__XXXXXXX",
  "description": "应用描述",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "app-plus": {
    "usingComponents": true,
    "nvueCompiler": "uni-app",
    "compilerVersion": 3,
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    },
    "modules": {},
    "distribute": {
      "android": {
        "permissions": [],
        "abiFilters": ["armeabi-v7a", "arm64-v8a"]
      },
      "ios": {
        "dSYMs": false
      },
      "sdkConfigs": {}
    }
  },
  "quickapp": {},
  "mp-weixin": {
    "appid": "wx开头的微信小程序appid",
    "setting": {
      "urlCheck": false
    },
    "usingComponents": true
  },
  "mp-alipay": {
    "usingComponents": true
  },
  "mp-baidu": {
    "usingComponents": true
  },
  "mp-toutiao": {
    "usingComponents": true
  },
  "uniStatistics": {
    "enable": false
  }
}

打包发布流程:

  1. 微信小程序打包:

    • HBuilderX → 发行 → 小程序-微信
    • 填写小程序名称和AppID
    • 点击发行
    • 使用微信开发者工具上传代码
  2. Android打包:

    • HBuilderX → 发行 → 原生App-云打包
    • 选择Android平台
    • 配置证书信息
    • 点击发行
  3. iOS打包:

    • HBuilderX → 发行 → 原生App-云打包
    • 选择iOS平台
    • 配置证书和描述文件
    • 点击发行

# 7.3 多端适配策略

条件编译:

uni-app提供了条件编译功能,可以根据不同平台编写特定代码。

<template>
  <view>
    <!-- 所有平台通用 -->
    <view>所有平台都显示的内容</view>
    
    <!-- 仅在微信小程序显示 -->
    <view v-if="mpPlatform === 'mp-weixin'">仅微信小程序显示</view>
    
    <!-- #ifdef MP-WEIXIN -->
    <view>仅微信小程序显示(条件编译)</view>
    <!-- #endif -->
    
    <!-- #ifdef APP-PLUS -->
    <view>仅App显示(条件编译)</view>
    <!-- #endif -->
  </view>
</template>

<script>
export default {
  data() {
    return {
      mpPlatform: ''
    }
  },
  onLoad() {
    // 获取当前平台
    // #ifdef MP-WEIXIN
    this.mpPlatform = 'mp-weixin';
    // #endif
    
    // #ifdef MP-ALIPAY
    this.mpPlatform = 'mp-alipay';
    // #endif
    
    // #ifdef APP-PLUS
    this.mpPlatform = 'app-plus';
    // #endif
  },
  methods: {
    // 条件编译示例
    platformSpecificMethod() {
      // #ifdef MP-WEIXIN
      // 微信小程序特有代码
      wx.showShareMenu({
        withShareTicket: true
      });
      // #endif
      
      // #ifdef APP-PLUS
      // App特有代码
      plus.device.vibrate(100);
      // #endif
    }
  }
}
</script>

<style>
/* 所有平台通用样式 */
view {
  padding: 10px;
}

/* #ifdef MP-WEIXIN */
/* 微信小程序特有样式 */
.wx-only {
  color: #07C160;
}
/* #endif */

/* #ifdef APP-PLUS */
/* App特有样式 */
.app-only {
  color: #007AFF;
}
/* #endif */
</style>

API差异处理:

// 封装平台差异的API
const platform = {
  // 获取位置信息
  getLocation() {
    return new Promise((resolve, reject) => {
      uni.getLocation({
        type: 'gcj02',
        success: res => {
          resolve(res);
        },
        fail: err => {
          reject(err);
        }
      });
    });
  },
  
  // 分享功能
  share(options) {
    // #ifdef MP-WEIXIN
    wx.showShareMenu({
      withShareTicket: true,
      menus: ['shareAppMessage', 'shareTimeline']
    });
    // #endif
    
    // #ifdef APP-PLUS
    plus.share.sendWithSystem({
      type: 'text',
      content: options.title,
      href: options.path
    });
    // #endif
  }
};

export default platform;

样式适配:

/* 样式适配示例 */

/* 通用样式 */
.container {
  padding: 20rpx;
}

/* 小程序样式 */
/* #ifdef MP */
.container {
  background-color: #f8f8f8;
}
/* #endif */

/* App样式 */
/* #ifdef APP-PLUS */
.container {
  background-color: #ffffff;
}
/* #endif */

/* iOS样式 */
/* #ifdef APP-PLUS-IOS */
.safe-area-inset {
  padding-bottom: constant(safe-area-inset-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}
/* #endif */

/* Android样式 */
/* #ifdef APP-PLUS-ANDROID */
.android-only {
  margin-top: 10rpx;
}
/* #endif */

# 8. 小程序性能优化

# 8.1 启动性能优化

减少启动时间的策略:

  1. 控制代码包大小:

    • 分包加载
    • 压缩图片资源
    • 移除未使用的代码和资源
  2. 优化app.js:

    • 减少app.js中的同步代码
    • 将非必要的初始化逻辑延迟执行
    • 使用异步API代替同步API
  3. 预加载策略:

    • 使用预加载接口提前下载分包
    • 使用周期性更新机制保持本地数据最新

代码示例:

// app.js 优化示例
App({
  onLaunch: function() {
    // 只保留必要的初始化逻辑
    this.initUserInfo();
    
    // 延迟执行非关键任务
    setTimeout(() => {
      this.initNonCriticalTasks();
    }, 2000);
    
    // 预加载分包
    wx.loadSubpackage({
      name: 'packageA',
      success: function() {
        console.log('分包预加载成功');
      },
      fail: function() {
        console.log('分包预加载失败');
      }
    });
  },
  
  initUserInfo: function() {
    // 用户信息初始化(关键任务)
    const token = wx.getStorageSync('token');
    if (token) {
      this.globalData.isLoggedIn = true;
    }
  },
  
  initNonCriticalTasks: function() {
    // 非关键任务初始化
    this.checkForUpdates();
    this.initAnalytics();
    this.loadRemoteConfig();
  },
  
  globalData: {
    isLoggedIn: false
  }
});

# 8.2 渲染性能优化

提升渲染性能的策略:

  1. 避免频繁setData:

    • 合并多次setData操作
    • 只传输必要数据
    • 避免传输大量数据
  2. 使用懒加载:

    • 图片懒加载
    • 列表分页加载
  3. 优化WXML结构:

    • 减少节点层级和数量
    • 使用block标签包裹
    • 适当使用wx:if和wx:for

代码示例:

// 优化前
Page({
  updateUI: function() {
    this.setData({ a: 1 });
    this.setData({ b: 2 });
    this.setData({ c: 3 });
  }
});

// 优化后
Page({
  updateUI: function() {
    this.setData({
      a: 1,
      b: 2,
      c: 3
    });
  }
});
<!-- 优化前 -->
<view wx:for="{{list}}" wx:key="id">
  <view class="item">
    <view class="title">{{item.title}}</view>
    <view class="desc">{{item.desc}}</view>
    <view class="footer">
      <view class="time">{{item.time}}</view>
      <view class="author">{{item.author}}</view>
    </view>
  </view>
</view>

<!-- 优化后 -->
<block wx:for="{{list}}" wx:key="id">
  <view class="item">
    <text class="title">{{item.title}}</text>
    <text class="desc">{{item.desc}}</text>
    <view class="footer">
      <text class="time">{{item.time}}</text>
      <text class="author">{{item.author}}</text>
    </view>
  </view>
</block>

# 8.3 网络请求优化

网络请求优化策略:

  1. 请求合并:

    • 合并多个相关请求
    • 使用批量接口
  2. 缓存策略:

    • 缓存不常变化的数据
    • 使用Storage存储API响应
    • 设置合理的缓存过期时间
  3. 预加载数据:

    • 提前请求可能需要的数据
    • 页面切换时预加载下一页数据

代码示例:

// 网络请求封装(带缓存)
const request = (options) => {
  return new Promise((resolve, reject) => {
    // 缓存key
    const cacheKey = `cache_${options.url}_${JSON.stringify(options.data || {})}`;
    
    // 缓存过期时间(毫秒)
    const expireTime = options.cache ? (options.expireTime || 5 * 60 * 1000) : 0;
    
    // 检查是否有缓存
    if (options.cache) {
      const cacheData = wx.getStorageSync(cacheKey);
      if (cacheData && cacheData.expire > Date.now()) {
        resolve(cacheData.data);
        return;
      }
    }
    
    // 发起请求
    wx.request({
      url: options.url,
      method: options.method || 'GET',
      data: options.data,
      header: options.header || {},
      success: (res) => {
        // 缓存结果
        if (options.cache && res.statusCode === 200) {
          wx.setStorageSync(cacheKey, {
            data: res.data,
            expire: Date.now() + expireTime
          });
        }
        resolve(res.data);
      },
      fail: (err) => {
        reject(err);
      }
    });
  });
};

// 使用示例
Page({
  onLoad: function() {
    // 带缓存的请求,缓存5分钟
    request({
      url: 'https://api.example.com/data',
      cache: true,
      expireTime: 5 * 60 * 1000
    }).then(data => {
      this.setData({ list: data });
    });
  }
});

# 9. 小程序安全最佳实践

# 9.1 数据安全

保护数据安全的策略:

  1. 敏感数据处理:

    • 避免在本地存储敏感信息
    • 使用加密存储必要的敏感数据
    • 及时清理不需要的数据
  2. 数据传输安全:

    • 使用HTTPS进行网络通信
    • 对敏感数据进行加密传输
    • 实现请求签名机制
  3. 防止数据泄露:

    • 避免在日志中打印敏感信息
    • 控制数据访问权限
    • 定期审计数据访问记录

代码示例:

// 数据加密存储
const CryptoJS = require('crypto-js');

// 加密函数
function encrypt(data, key) {
  return CryptoJS.AES.encrypt(JSON.stringify(data), key).toString();
}

// 解密函数
function decrypt(ciphertext, key) {
  const bytes = CryptoJS.AES.decrypt(ciphertext, key);
  return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
}

// 安全存储敏感数据
function secureStorage(key, data) {
  const encryptionKey = 'your-secret-key'; // 实际应用中应使用更安全的密钥管理
  const encryptedData = encrypt(data, encryptionKey);
  wx.setStorageSync(key, encryptedData);
}

// 安全读取敏感数据
function secureRead(key) {
  const encryptionKey = 'your-secret-key';
  const encryptedData = wx.getStorageSync(key);
  if (!encryptedData) return null;
  return decrypt(encryptedData, encryptionKey);
}

// 使用示例
Page({
  saveUserData: function(userData) {
    secureStorage('userData', userData);
  },
  
  getUserData: function() {
    return secureRead('userData');
  },
  
  clearUserData: function() {
    wx.removeStorageSync('userData');
  }
});

# 9.2 通信安全

保障通信安全的策略:

  1. 请求签名:

    • 对请求参数进行签名
    • 验证请求来源和完整性
    • 防止请求被篡改
  2. 防重放攻击:

    • 请求添加时间戳
    • 实现请求有效期机制
    • 使用一次性nonce值
  3. 传输加密:

    • 使用HTTPS协议
    • 敏感字段单独加密
    • 使用安全的加密算法

代码示例:

// 请求签名实现
function generateSignature(params, secretKey) {
  // 1. 按字典序排序参数
  const sortedKeys = Object.keys(params).sort();
  
  // 2. 拼接参数字符串
  let signStr = '';
  sortedKeys.forEach(key => {
    if (key !== 'sign' && params[key] !== undefined && params[key] !== null) {
      signStr += `${key}=${params[key]}&`;
    }
  });
  
  // 3. 添加密钥
  signStr += `key=${secretKey}`;
  
  // 4. MD5加密并转大写
  return CryptoJS.MD5(signStr).toString().toUpperCase();
}

// 发送带签名的请求
function secureRequest(url, params, method = 'GET') {
  // 添加公共参数
  const requestParams = {
    ...params,
    timestamp: Date.now(),
    nonce: Math.random().toString(36).substr(2, 10),
    appId: 'your-app-id'
  };
  
  // 生成签名
  const secretKey = 'your-secret-key'; // 实际应用中应使用更安全的密钥管理
  requestParams.sign = generateSignature(requestParams, secretKey);
  
  // 发送请求
  return new Promise((resolve, reject) => {
    wx.request({
      url,
      method,
      data: requestParams,
      success: res => {
        if (res.statusCode === 200) {
          resolve(res.data);
        } else {
          reject(new Error(`请求失败:${res.statusCode}`));
        }
      },
      fail: err => {
        reject(err);
      }
    });
  });
}

// 使用示例
Page({
  getData: function() {
    secureRequest('https://api.example.com/data', {
      userId: '123',
      action: 'query'
    }).then(data => {
      console.log('请求成功', data);
    }).catch(err => {
      console.error('请求失败', err);
    });
  }
});

# 9.3 敏感信息处理

敏感信息处理策略:

  1. 最小化收集:

    • 只收集必要的敏感信息
    • 明确告知用户收集目的
    • 提供隐私政策说明
  2. 安全展示:

    • 敏感信息脱敏显示
    • 提供查看完整信息的选项
    • 避免截屏泄露风险
  3. 安全传输与存储:

    • 传输过程加密
    • 存储时加密
    • 使用后及时清除

代码示例:

// 信息脱敏工具函数
const maskUtil = {
  // 手机号脱敏
  maskPhone: function(phone) {
    if (!phone) return '';
    return phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
  },
  
  // 身份证号脱敏
  maskIdCard: function(idCard) {
    if (!idCard) return '';
    return idCard.replace(/^(\d{4})\d{10}(\d{4})$/, '$1**********$2');
  },
  
  // 姓名脱敏
  maskName: function(name) {
    if (!name) return '';
    if (name.length <= 1) return name;
    if (name.length === 2) return name.substr(0, 1) + '*';
    return name.substr(0, 1) + '*'.repeat(name.length - 2) + name.substr(-1);
  },
  
  // 银行卡号脱敏
  maskBankCard: function(cardNo) {
    if (!cardNo) return '';
    return cardNo.replace(/^(\d{4})\d+(\d{4})$/, '$1 **** **** $2');
  },
  
  // 邮箱脱敏
  maskEmail: function(email) {
    if (!email) return '';
    const parts = email.split('@');
    if (parts.length !== 2) return email;
    
    const name = parts[0];
    const domain = parts[1];
    
    let maskedName = '';
    if (name.length <= 2) {
      maskedName = name.substr(0, 1) + '*';
    } else {
      maskedName = name.substr(0, 2) + '*'.repeat(name.length - 2);
    }
    
    return `${maskedName}@${domain}`;
  }
};

// 使用示例
Page({
  data: {
    userInfo: {
      name: '张三',
      phone: '13812345678',
      idCard: '110101199001011234',
      email: 'zhangsan@example.com',
      bankCard: '6222021234567890123'
    },
    showFullInfo: false
  },
  
  onLoad: function() {
    this.updateDisplayInfo();
  },
  
  updateDisplayInfo: function() {
    if (this.data.showFullInfo) {
      this.setData({
        displayInfo: this.data.userInfo
      });
    } else {
      this.setData({
        displayInfo: {
          name: maskUtil.maskName(this.data.userInfo.name),
          phone: maskUtil.maskPhone(this.data.userInfo.phone),
          idCard: maskUtil.maskIdCard(this.data.userInfo.idCard),
          email: maskUtil.maskEmail(this.data.userInfo.email),
          bankCard: maskUtil.maskBankCard(this.data.userInfo.bankCard)
        }
      });
    }
  },
  
  toggleInfoDisplay: function() {
    this.setData({
      showFullInfo: !this.data.showFullInfo
    }, () => {
      this.updateDisplayInfo();
    });
  }
});

# 10. 高级开发技巧

# 10.1 分包加载

分包加载是优化小程序启动速度的重要手段,可以将小程序划分为多个子包,在需要时按需加载。

分包配置示例:

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/cat/cat",
        "pages/dog/dog"
      ]
    },
    {
      "root": "packageB",
      "name": "pack2",
      "pages": [
        "pages/apple/apple",
        "pages/banana/banana"
      ]
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["packageA"]
    }
  }
}

分包策略:

  1. 按功能模块分包:将不同功能模块划分到不同分包
  2. 按访问频率分包:高频功能放主包,低频功能放分包
  3. 按加载时机分包:启动必需的放主包,其他放分包

分包预加载:

// 手动预加载分包
Page({
  onLoad: function() {
    // 预加载分包
    wx.loadSubpackage({
      name: 'packageB', // 分包的name
      success: function() {
        console.log('分包加载成功');
      },
      fail: function() {
        console.log('分包加载失败');
      }
    });
  }
});

# 10.2 自定义组件

自定义组件可以提高代码复用性和可维护性,是小程序开发中的重要技术。

组件定义示例:

// components/custom-card/custom-card.js
Component({
  properties: {
    title: {
      type: String,
      value: '默认标题'
    },
    content: {
      type: String,
      value: ''
    },
    imageUrl: {
      type: String,
      value: ''
    }
  },
  data: {
    isExpanded: false
  },
  methods: {
    toggleExpand: function() {
      this.setData({
        isExpanded: !this.data.isExpanded
      });
      
      // 触发自定义事件
      this.triggerEvent('expand', { isExpanded: this.data.isExpanded });
    }
  },
  lifetimes: {
    attached: function() {
      // 组件被添加到页面时执行
      console.log('组件attached');
    },
    detached: function() {
      // 组件从页面中移除时执行
      console.log('组件detached');
    }
  },
  pageLifetimes: {
    show: function() {
      // 页面被展示时执行
      console.log('页面show');
    },
    hide: function() {
      // 页面被隐藏时执行
      console.log('页面hide');
    }
  }
});
<!-- components/custom-card/custom-card.wxml -->
<view class="card {{isExpanded ? 'expanded' : ''}}">
  <view class="card-header" bindtap="toggleExpand">
    <text class="card-title">{{title}}</text>
    <view class="card-icon">{{isExpanded ? '↑' : '↓'}}</view>
  </view>
  
  <view class="card-body" wx:if="{{isExpanded}}">
    <image wx:if="{{imageUrl}}" src="{{imageUrl}}" mode="aspectFill" class="card-image"></image>
    <text class="card-content">{{content}}</text>
    <slot></slot>
  </view>
</view>
/* components/custom-card/custom-card.wxss */
.card {
  margin: 20rpx;
  border-radius: 10rpx;
  background-color: #fff;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx;
  border-bottom: 1rpx solid #eee;
}

.card-title {
  font-size: 32rpx;
  font-weight: bold;
}

.card-icon {
  font-size: 24rpx;
  color: #999;
}

.card-body {
  padding: 20rpx;
}

.card-image {
  width: 100%;
  height: 300rpx;
  margin-bottom: 20rpx;
}

.card-content {
  font-size: 28rpx;
  color: #666;
  line-height: 1.5;
}

.expanded {
  box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
}

组件使用示例:

// pages/index/index.json
{
  "usingComponents": {
    "custom-card": "/components/custom-card/custom-card"
  }
}
<!-- pages/index/index.wxml -->
<view class="container">
  <custom-card 
    title="自定义卡片" 
    content="这是一个自定义卡片组件示例" 
    image-url="/images/example.jpg"
    bind:expand="onCardExpand"
  >
    <view class="custom-content">这是通过slot插入的内容</view>
  </custom-card>
</view>
// pages/index/index.js
Page({
  onCardExpand: function(e) {
    console.log('卡片展开状态:', e.detail.isExpanded);
  }
});

# 10.3 云开发应用

微信小程序云开发提供了数据库、存储、云函数等能力,可以大幅简化后端开发。

云开发初始化:

// app.js
App({
  onLaunch: function() {
    if (!wx.cloud) {
      console.error('请使用 2.2.3 或以上的基础库以使用云能力');
    } else {
      wx.cloud.init({
        env: 'your-env-id', // 云开发环境ID
        traceUser: true // 是否将用户访问记录到云开发控制台
      });
    }
  }
});

云数据库操作:

// 添加数据
function addData() {
  const db = wx.cloud.database();
  db.collection('todos').add({
    data: {
      title: '新的待办事项',
      description: '这是一个示例待办事项',
      done: false,
      createTime: db.serverDate()
    },
    success: function(res) {
      console.log('添加成功,记录ID:', res._id);
    },
    fail: function(err) {
      console.error('添加失败', err);
    }
  });
}

// 查询数据
function queryData() {
  const db = wx.cloud.database();
  db.collection('todos')
    .where({
      done: false
    })
    .orderBy('createTime', 'desc')
    .limit(10)
    .get()
    .then(res => {
      console.log('查询结果', res.data);
    })
    .catch(err => {
      console.error('查询失败', err);
    });
}

// 更新数据
function updateData(id) {
  const db = wx.cloud.database();
  db.collection('todos').doc(id).update({
    data: {
      done: true,
      updateTime: db.serverDate()
    },
    success: function() {
      console.log('更新成功');
    },
    fail: function(err) {
      console.error('更新失败', err);
    }
  });
}

// 删除数据
function deleteData(id) {
  const db = wx.cloud.database();
  db.collection('todos').doc(id).remove({
    success: function() {
      console.log('删除成功');
    },
    fail: function(err) {
      console.error('删除失败', err);
    }
  });
}

云存储操作:

// 上传文件
function uploadFile() {
  wx.chooseImage({
    count: 1,
    sizeType: ['compressed'],
    sourceType: ['album', 'camera'],
    success: function(res) {
      const filePath = res.tempFilePaths[0];
      
      // 上传至云存储
      wx.cloud.uploadFile({
        cloudPath: `images/${Date.now()}-${Math.floor(Math.random() * 1000)}.png`, // 云存储路径
        filePath: filePath, // 本地文件路径
        success: res => {
          console.log('上传成功', res);
          const fileID = res.fileID;
          
          // 可以将fileID保存到数据库
          saveFileInfo(fileID, filePath);
        },
        fail: err => {
          console.error('上传失败', err);
        }
      });
    }
  });
}

// 保存文件信息到数据库
function saveFileInfo(fileID, filePath) {
  const db = wx.cloud.database();
  db.collection('files').add({
    data: {
      fileID: fileID,
      uploadTime: db.serverDate()
    },
    success: function(res) {
      console.log('文件信息保存成功');
    }
  });
}

// 获取文件临时链接
function getTempFileURL(fileID) {
  wx.cloud.getTempFileURL({
    fileList: [fileID],
    success: res => {
      console.log('文件下载链接', res.fileList[0].tempFileURL);
    },
    fail: err => {
      console.error('获取链接失败', err);
    }
  });
}

// 删除文件
function deleteFile(fileID) {
  wx.cloud.deleteFile({
    fileList: [fileID],
    success: res => {
      console.log('删除成功', res);
    },
    fail: err => {
      console.error('删除失败', err);
    }
  });
}

云函数调用:

// 调用云函数
function callCloudFunction() {
  wx.cloud.callFunction({
    name: 'login', // 云函数名称
    data: {}, // 传递给云函数的参数
    success: res => {
      console.log('云函数调用成功', res);
    },
    fail: err => {
      console.error('云函数调用失败', err);
    }
  });
}

云函数定义示例(login函数):

// 云函数 login/index.js
const cloud = require('wx-server-sdk');

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV
});

exports.main = async (event, context) => {
  // 获取用户OpenID
  const wxContext = cloud.getWXContext();
  
  // 可以在这里进行用户注册、登录等逻辑
  const db = cloud.database();
  
  // 查询用户是否已存在
  const userCollection = db.collection('users');
  const user = await userCollection.where({
    openid: wxContext.OPENID
  }).get();
  
  // 用户不存在则创建新用户
  if (user.data.length === 0) {
    await userCollection.add({
      data: {
        openid: wxContext.OPENID,
        createTime: db.serverDate(),
        lastLoginTime: db.serverDate()
      }
    });
  } else {
    // 更新登录时间
    await userCollection.where({
      openid: wxContext.OPENID
    }).update({
      data: {
        lastLoginTime: db.serverDate()
      }
    });
  }
  
  return {
    openid: wxContext.OPENID,
    appid: wxContext.APPID,
    unionid: wxContext.UNIONID,
    env: cloud.DYNAMIC_CURRENT_ENV
  };
};

# 总结

本指南全面介绍了微信小程序开发的各个方面,从基础知识到高级技巧,涵盖了小程序开发的完整生命周期。通过学习本指南,开发者可以掌握小程序开发的核心技能,包括基础组件使用、登录授权、支付功能、订阅消息、性能优化、安全实践等。

对于跨平台开发需求,uni-app提供了一套代码多端发布的解决方案,可以大幅提高开发效率。而对于后端服务需求,微信云开发则提供了简单易用的云服务能力,让开发者可以专注于业务逻辑实现。

希望本指南能够帮助开发者快速上手微信小程序开发,构建出高质量、高性能的小程序应用。