微信小程序开发入门教程

教程简介

零基础微信小程序开发完整教程,涵盖WXML模板、WXSS样式、JS逻辑、组件化开发、API调用、网络请求、数据缓存、用户授权、微信支付等核心技能,配有电商小程序实战项目,适合移动开发初学者。

微信小程序开发入门教程

本教程面向零基础开发者,从概念到实战,手把手带你掌握微信小程序开发。


目录

  1. 什么是微信小程序
  2. 开发环境搭建
  3. 项目结构详解
  4. WXML 模板语法
  5. WXSS 样式开发
  6. JS 逻辑层开发
  7. 组件化开发
  8. API 调用基础
  9. 网络请求
  10. 数据缓存
  11. 用户授权与登录
  12. 微信支付功能
  13. 实战项目:电商小程序
  14. 发布与审核
  15. 常见问题与调试技巧

1. 什么是微信小程序

微信小程序是一种不需要下载安装即可使用的应用,用户通过微信扫一扫或搜索即可打开。它实现了"触手可及"的应用体验。

小程序的核心特点

特点 说明
轻量级 无需安装,即开即用,用完即走
跨平台 一套代码同时运行在 iOS 和 Android
强社交 依托微信生态,易于分享和传播
丰富能力 可调用微信支付、摄像头、地理位置等原生能力
开发成本低 相比原生 App,开发周期短、成本低

小程序与其他技术的对比

原生 App:功能最强,但开发成本高、需要下载安装
H5 网页:  开发简单,但体验较差、能力受限
小程序:  介于两者之间,体验接近原生,开发成本较低

2. 开发环境搭建

2.1 注册小程序账号

  1. 访问 微信公众平台
  2. 点击"立即注册",选择"小程序"类型
  3. 填写邮箱、密码等信息完成注册
  4. 登录后在"开发管理" → "开发设置"中获取 AppID

💡 提示:个人开发者也可以注册,但部分能力(如微信支付)需要企业资质。

2.2 安装微信开发者工具

微信开发者工具是官方提供的集成开发环境(IDE),支持代码编辑、调试、预览和上传。

下载与安装:

  1. 访问 微信开发者工具下载页
  2. 根据操作系统(Windows / macOS)下载对应版本
  3. 安装后用微信扫码登录

2.3 创建第一个项目

  1. 打开开发者工具,点击"+"号创建新项目
  2. 填写项目名称,例如 my-first-miniprogram
  3. 填入你的 AppID(或选择"测试号")
  4. 选择"不使用云开发"(我们先从基础学起)
  5. 选择"小程序模板"作为起始模板
  6. 点击"确定"即可创建项目

创建成功后,你会看到开发者工具的主界面,左侧是模拟器预览,右侧是代码编辑器。


3. 项目结构详解

一个标准的小程序项目结构如下:

my-miniprogram/
├── pages/                    # 页面目录
│   ├── index/                # 首页
│   │   ├── index.js          # 页面逻辑
│   │   ├── index.json        # 页面配置
│   │   ├── index.wxml        # 页面模板(类似 HTML)
│   │   └── index.wxss        # 页面样式(类似 CSS)
│   └── logs/
│       ├── logs.js
│       ├── logs.json
│       ├── logs.wxml
│       └── logs.wxss
├── utils/                    # 工具函数
│   └── util.js
├── app.js                    # 应用逻辑(入口文件)
├── app.json                  # 应用配置(全局)
├── app.wxss                  # 应用样式(全局)
└── project.config.json       # 项目配置

3.1 核心文件说明

app.json — 全局配置文件

这是小程序的"大脑",定义了页面路径、窗口表现、底部 TabBar 等:

{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window": {
    "navigationBarBackgroundColor": "#ffffff",
    "navigationBarTitleText": "我的小程序",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#f5f5f5"
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#1296db",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "images/home.png",
        "selectedIconPath": "images/home-active.png"
      },
      {
        "pagePath": "pages/logs/logs",
        "text": "日志",
        "iconPath": "images/log.png",
        "selectedIconPath": "images/log-active.png"
      }
    ]
  }
}

app.js — 应用入口文件

App({
  onLaunch() {
    // 小程序初始化时触发(全局只触发一次)
    console.log('小程序启动了!')

    // 检查登录态
    wx.checkSession({
      success() {
        console.log('session 未过期')
      },
      fail() {
        console.log('session 已过期,需要重新登录')
      }
    })
  },

  onShow(options) {
    // 小程序显示时触发(从后台切到前台)
    console.log('小程序显示', options)
  },

  onHide() {
    // 小程序隐藏时触发(从前台切到后台)
    console.log('小程序隐藏')
  },

  // 全局数据,所有页面都可以通过 getApp() 访问
  globalData: {
    userInfo: null,
    baseUrl: 'https://api.example.com'
  }
})

3.2 页面文件的四种类型

每个页面由四个文件组成,各司其职:

文件类型 作用 类比 Web
.wxml 页面结构(模板) HTML
.wxss 页面样式 CSS
.js 页面逻辑 JavaScript
.json 页面配置

4. WXML 模板语法

WXML(WeiXin Markup Language)是微信小程序的模板语言,类似于 HTML,但有一些独特的语法。

4.1 数据绑定

使用双花括号 {{}} 将 JS 中的数据绑定到模板中:

<!-- pages/index/index.wxml -->
<view class="container">
  <text>用户名:{{username}}</text>
  <text>年龄:{{age}}</text>
  <text>是否成人:{{age >= 18 ? '是' : '否'}}</text>

  <!-- 属性绑定 -->
  <image src="{{avatarUrl}}" mode="aspectFill"></image>

  <!-- 运算表达式 -->
  <view>{{a + b}} 的结果是 {{a + b}}</view>
</view>

对应的 JS:

// pages/index/index.js
Page({
  data: {
    username: '张三',
    age: 25,
    avatarUrl: '/images/avatar.png',
    a: 1,
    b: 2
  }
})

4.2 条件渲染

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

<!-- hidden 属性(适合频繁切换的场景) -->
<view hidden="{{!isLoggedIn}}">欢迎回来!</view>
<view hidden="{{isLoggedIn}}">请先登录</view>

💡 wx:if vs hiddenwx:if 会销毁和重建组件,适合条件不常变化的场景;hidden 只是隐藏组件,适合频繁切换显示/隐藏的场景。

4.3 列表渲染

使用 wx:for 遍历数组:

<!-- 基础用法 -->
<view wx:for="{{fruits}}" wx:key="index">
  索引:{{index}},名称:{{item}}
</view>

<!-- 使用 wx:key 提高性能 -->
<view wx:for="{{students}}" wx:key="id">
  <text>姓名:{{item.name}}</text>
  <text>年龄:{{item.age}}</text>
</view>

<!-- 自定义变量名 -->
<view wx:for="{{students}}" wx:for-item="student" wx:for-index="idx" wx:key="id">
  第 {{idx}} 个学生:{{student.name}}
</view>

对应的 JS:

Page({
  data: {
    fruits: ['苹果', '香蕉', '橙子'],
    students: [
      { id: 1, name: '小明', age: 20 },
      { id: 2, name: '小红', age: 21 },
      { id: 3, name: '小刚', age: 19 }
    ]
  }
})

4.4 事件绑定

<!-- 绑定点击事件 -->
<button bindtap="onTap">点击我</button>

<!-- 带参数的事件 -->
<button bindtap="onTapWithArgs" data-id="{{item.id}}" data-name="{{item.name}}">
  删除
</button>

<!-- 表单输入 -->
<input bindinput="onInput" placeholder="请输入内容" value="{{inputValue}}"></input>

<!-- 阻止事件冒泡 -->
<view catchtap="onViewTap">
  <button bindtap="onBtnTap">按钮</button>
</view>
Page({
  data: {
    inputValue: ''
  },

  onTap() {
    console.log('按钮被点击了')
    wx.showToast({ title: '点击成功', icon: 'success' })
  },

  onTapWithArgs(e) {
    // 通过 e.currentTarget.dataset 获取传入的参数
    const { id, name } = e.currentTarget.dataset
    console.log(`删除:ID=${id}, Name=${name}`)
  },

  onInput(e) {
    // 实时获取输入框的值
    this.setData({ inputValue: e.detail.value })
  }
})

4.5 模板(template)

可以定义可复用的模板片段:

<!-- 定义模板 -->
<template name="userCard">
  <view class="card">
    <image src="{{avatar}}" class="avatar"></image>
    <view class="info">
      <text class="name">{{name}}</text>
      <text class="desc">{{desc}}</text>
    </view>
  </view>
</template>

<!-- 使用模板 -->
<template is="userCard" data="{{...userInfo}}"></template>
<template is="userCard" data="{{name: '小明', avatar: '/img/1.png', desc: '前端开发者'}}"></template>

5. WXSS 样式开发

WXSS(WeiXin Style Sheet)类似于 CSS,但有一些扩展特性。

5.1 尺寸单位 rpx

rpx(responsive pixel)是小程序特有的响应式单位,750rpx 恒等于屏幕宽度

/* 在 iPhone 6(375px 宽)上,1rpx = 0.5px */
/* 在 iPhone 6 Plus(414px 宽)上,1rpx ≈ 0.552px */

.container {
  width: 750rpx;        /* 占满屏幕宽度 */
  padding: 30rpx;       /* 约 15px */
  font-size: 28rpx;     /* 约 14px */
}

.box {
  width: 345rpx;        /* 约屏幕的一半 */
  height: 200rpx;
  margin: 20rpx;
}

💡 建议:设计稿通常以 750px 宽度为基准,直接将设计稿上的 px 值写成 rpx 即可自动适配不同屏幕。

5.2 样式导入

使用 @import 导入外部样式文件:

/* common.wxss - 公共样式 */
.text-primary {
  color: #1296db;
}

.text-gray {
  color: #999;
}

/* pages/index/index.wxss */
@import '../../common.wxss';

.page-container {
  background: #f5f5f5;
  min-height: 100vh;
}

5.3 常用布局示例

Flex 布局(推荐):

/* 水平居中 */
.center-h {
  display: flex;
  justify-content: center;
}

/* 垂直居中 */
.center-v {
  display: flex;
  align-items: center;
}

/* 水平 + 垂直居中 */
.center-all {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 两端对齐 */
.space-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

/* 等分布局 */
.equal-split {
  display: flex;
}

.equal-split > view {
  flex: 1;
}

商品卡片布局示例:

.product-card {
  display: flex;
  padding: 20rpx;
  background: #fff;
  border-radius: 16rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}

.product-card .product-img {
  width: 200rpx;
  height: 200rpx;
  border-radius: 12rpx;
  flex-shrink: 0;
}

.product-card .product-info {
  flex: 1;
  margin-left: 20rpx;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.product-card .product-name {
  font-size: 28rpx;
  color: #333;
  font-weight: bold;
}

.product-card .product-price {
  font-size: 32rpx;
  color: #e74c3c;
  font-weight: bold;
}

6. JS 逻辑层开发

6.1 Page 构造器

每个页面的 JS 文件通过 Page() 注册页面,包含数据和事件处理函数:

Page({
  // 页面数据
  data: {
    count: 0,
    message: 'Hello World',
    list: []
  },

  // 生命周期函数:页面加载
  onLoad(options) {
    console.log('页面加载', options)
    // options 可以获取页面跳转时传递的参数
  },

  // 生命周期函数:页面显示
  onShow() {
    console.log('页面显示')
  },

  // 生命周期函数:页面初次渲染完成
  onReady() {
    console.log('页面初次渲染完成')
  },

  // 生命周期函数:页面隐藏
  onHide() {
    console.log('页面隐藏')
  },

  // 生命周期函数:页面卸载
  onUnload() {
    console.log('页面卸载')
  },

  // 下拉刷新
  onPullDownRefresh() {
    console.log('触发下拉刷新')
    this.loadData().then(() => {
      wx.stopPullDownRefresh() // 停止下拉刷新
    })
  },

  // 上拉加载更多
  onReachBottom() {
    console.log('触底了,加载更多数据')
    this.loadMore()
  },

  // 页面滚动
  onPageScroll(e) {
    // e.scrollTop 为页面滚动距离
  },

  // 自定义方法
  loadData() {
    return new Promise((resolve) => {
      setTimeout(() => {
        this.setData({ list: ['数据1', '数据2', '数据3'] })
        resolve()
      }, 1000)
    })
  },

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

  // 事件处理函数
  onTapButton() {
    // 使用 setData 更新数据(这是触发视图更新的唯一方式)
    this.setData({
      count: this.data.count + 1
    })
  }
})

6.2 setData 详解

setData 是小程序中更新视图的核心方法,它将数据从逻辑层发送到视图层:

// 更新单个字段
this.setData({
  count: 10
})

// 更新嵌套字段(使用路径)
this.setData({
  'userInfo.name': '新名字',
  'userInfo.address.city': '北京'
})

// 更新数组中的某一项
this.setData({
  'list[0].name': '修改后的名字'
})

// 在回调中执行后续操作
this.setData({ count: this.data.count + 1 }, () => {
  console.log('视图已更新')
})

⚠️ 注意事项

  • setData 是同步执行的,但视图更新是异步的
  • 避免一次性设置过大的数据,会影响性能
  • 不要在 setData 中传递函数或不可序列化的数据

6.3 页面间通信

通过 URL 参数传参:

// 跳转并传参
wx.navigateTo({
  url: '/pages/detail/detail?id=123&type=book'
})

// 在目标页面接收参数
Page({
  onLoad(options) {
    const { id, type } = options
    console.log(id, type) // 123, book
  }
})

通过全局数据传参:

// 页面 A:设置全局数据
const app = getApp()
app.globalData.sharedData = { key: 'value' }

// 页面 B:读取全局数据
const app = getApp()
const data = app.globalData.sharedData

7. 组件化开发

小程序提供了丰富的内置组件,同时也支持自定义组件。

7.1 常用内置组件

<!-- 视图容器 -->
<view class="box">这是一个块级容器</view>
<scroll-view scroll-y style="height: 400rpx;">
  <view wx:for="{{list}}" wx:key="index">{{item}}</view>
</scroll-view>
<swiper autoplay circular>
  <swiper-item wx:for="{{banners}}" wx:key="index">
    <image src="{{item.url}}" mode="aspectFill"></image>
  </swiper-item>
</swiper>

<!-- 基础内容 -->
<text>文本内容(支持长按复制)</text>
<rich-text nodes="{{htmlContent}}"></rich-text>
<progress percent="60" stroke-width="6"></progress>

<!-- 表单组件 -->
<input placeholder="请输入" bindinput="onInput"></input>
<textarea placeholder="多行输入" bindinput="onTextareaInput"></textarea>
<switch checked="{{isEnabled}}" bindchange="onSwitchChange"></switch>
<picker mode="date" bindchange="onDateChange">
  <view>选择日期:{{date}}</view>
</picker>

<!-- 媒体组件 -->
<image src="{{imgUrl}}" mode="aspectFill" bindtap="previewImage"></image>
<video src="{{videoUrl}}" controls></video>

7.2 自定义组件

创建自定义组件的步骤:

第一步:创建组件文件

components/
└── product-card/
    ├── product-card.js
    ├── product-card.json
    ├── product-card.wxml
    └── product-card.wxss

第二步:编写组件代码

// components/product-card/product-card.js
Component({
  // 组件属性:接收外部传入的数据
  properties: {
    product: {
      type: Object,
      value: {}
    },
    showPrice: {
      type: Boolean,
      value: true
    }
  },

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

  // 组件方法
  methods: {
    onTapCard() {
      // 触发自定义事件,通知父组件
      this.triggerEvent('tapcard', {
        id: this.properties.product.id
      })
    },

    onTapCollect() {
      this.setData({ isCollected: !this.data.isCollected })
      this.triggerEvent('collect', {
        id: this.properties.product.id,
        collected: this.data.isCollected
      })
    }
  }
})
<!-- components/product-card/product-card.wxml -->
<view class="card" bindtap="onTapCard">
  <image src="{{product.image}}" class="card-img" mode="aspectFill"></image>
  <view class="card-body">
    <text class="card-title">{{product.name}}</text>
    <text class="card-desc">{{product.desc}}</text>
    <view class="card-footer" wx:if="{{showPrice}}">
      <text class="price">¥{{product.price}}</text>
      <view class="collect-btn" catchtap="onTapCollect">
        <text>{{isCollected ? '❤️' : '🤍'}}</text>
      </view>
    </view>
  </view>
</view>
/* components/product-card/product-card.wxss */
.card {
  background: #fff;
  border-radius: 16rpx;
  overflow: hidden;
  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}

.card-img {
  width: 100%;
  height: 360rpx;
}

.card-body {
  padding: 20rpx;
}

.card-title {
  font-size: 28rpx;
  color: #333;
  font-weight: bold;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-desc {
  font-size: 24rpx;
  color: #999;
  margin-top: 8rpx;
}

.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 16rpx;
}

.price {
  font-size: 36rpx;
  color: #e74c3c;
  font-weight: bold;
}

第三步:在页面中使用组件

// pages/index/index.json
{
  "usingComponents": {
    "product-card": "/components/product-card/product-card"
  }
}
<!-- pages/index/index.wxml -->
<product-card
  wx:for="{{products}}"
  wx:key="id"
  product="{{item}}"
  showPrice="{{true}}"
  bindtapcard="onProductTap"
  bindcollect="onProductCollect"
/>
// pages/index/index.js
Page({
  data: {
    products: [
      {
        id: 1,
        name: '无线蓝牙耳机',
        desc: '高品质音效,超长续航',
        price: 199,
        image: '/images/earphone.png'
      }
    ]
  },

  onProductTap(e) {
    wx.navigateTo({
      url: `/pages/detail/detail?id=${e.detail.id}`
    })
  },

  onProductCollect(e) {
    console.log('收藏状态变更:', e.detail)
  }
})

8. API 调用基础

微信小程序提供了丰富的原生 API,以 wx. 开头。

8.1 交互反馈

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

// 加载提示框
wx.showLoading({ title: '加载中...' })
setTimeout(() => wx.hideLoading(), 2000)

// 模态对话框
wx.showModal({
  title: '确认',
  content: '确定要删除吗?',
  success(res) {
    if (res.confirm) {
      console.log('用户点击确定')
    } else if (res.cancel) {
      console.log('用户点击取消')
    }
  }
})

// 操作菜单(从底部弹出)
wx.showActionSheet({
  itemList: ['拍照', '从相册选择', '取消'],
  success(res) {
    console.log('选择了第', res.tapIndex + 1, '项')
  }
})

8.2 路由与页面跳转

// 保留当前页面,跳转到新页面(可返回)
wx.navigateTo({
  url: '/pages/detail/detail?id=123'
})

// 关闭当前页面,跳转到新页面(不可返回)
wx.redirectTo({
  url: '/pages/result/result'
})

// 跳转到 TabBar 页面(必须用 switchTab)
wx.switchTab({
  url: '/pages/index/index'
})

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

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

8.3 设备能力

// 获取网络状态
wx.getNetworkType({
  success(res) {
    console.log('网络类型:', res.networkType) // wifi, 2g, 3g, 4g, 5g, none
  }
})

// 获取系统信息
wx.getSystemInfo({
  success(res) {
    console.log('手机型号:', res.model)
    console.log('系统版本:', res.system)
    console.log('屏幕宽度:', res.screenWidth)
    console.log('屏幕高度:', res.screenHeight)
    console.log('状态栏高度:', res.statusBarHeight)
  }
})

// 扫描二维码
wx.scanCode({
  success(res) {
    console.log('扫描结果:', res.result)
  }
})

// 拨打电话
wx.makePhoneCall({
  phoneNumber: '10086'
})

8.4 地图与位置

// 获取当前地理位置
wx.getLocation({
  type: 'gcj02',
  success(res) {
    console.log('经度:', res.longitude)
    console.log('纬度:', res.latitude)
  }
})

// 使用微信内置地图选择位置
wx.chooseLocation({
  success(res) {
    console.log('选中位置:', res.name, res.address)
    console.log('经度:', res.longitude, '纬度:', res.latitude)
  }
})

9. 网络请求

9.1 基础请求

小程序通过 wx.request 发起 HTTP 请求:

// GET 请求
wx.request({
  url: 'https://api.example.com/products',
  method: 'GET',
  data: {
    page: 1,
    limit: 10
  },
  success(res) {
    console.log('响应数据:', res.data)
  },
  fail(err) {
    console.error('请求失败:', err)
  }
})

// POST 请求
wx.request({
  url: 'https://api.example.com/orders',
  method: 'POST',
  data: {
    productId: 123,
    quantity: 2
  },
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token_xxx'
  },
  success(res) {
    if (res.statusCode === 200) {
      console.log('订单创建成功:', res.data)
    }
  }
})

9.2 封装请求工具

为了统一管理请求,建议封装一个请求工具:

// utils/request.js
const app = getApp()

const request = (options) => {
  return new Promise((resolve, reject) => {
    const { url, method = 'GET', data, header = {} } = options

    // 自动拼接 baseUrl
    const fullUrl = url.startsWith('http') ? url : `${app.globalData.baseUrl}${url}`

    // 自动添加 token
    const token = wx.getStorageSync('token')
    if (token) {
      header['Authorization'] = `Bearer ${token}`
    }

    wx.request({
      url: fullUrl,
      method,
      data,
      header: {
        'Content-Type': 'application/json',
        ...header
      },
      success(res) {
        if (res.statusCode === 200) {
          if (res.data.code === 0) {
            resolve(res.data)
          } else {
            wx.showToast({ title: res.data.message || '请求失败', icon: 'none' })
            reject(res.data)
          }
        } else if (res.statusCode === 401) {
          // Token 过期,跳转登录
          wx.redirectTo({ url: '/pages/login/login' })
          reject(new Error('未授权'))
        } else {
          wx.showToast({ title: '服务器错误', icon: 'none' })
          reject(new Error(`HTTP ${res.statusCode}`))
        }
      },
      fail(err) {
        wx.showToast({ title: '网络错误', icon: 'none' })
        reject(err)
      }
    })
  })
}

// 便捷方法
const get = (url, data) => request({ url, method: 'GET', data })
const post = (url, data) => request({ url, method: 'POST', data })

module.exports = { request, get, post }

使用封装后的请求:

const { get, post } = require('../../utils/request')

Page({
  async onLoad() {
    try {
      // 获取商品列表
      const res = await get('/products', { page: 1, limit: 10 })
      this.setData({ products: res.data.list })

      // 创建订单
      const order = await post('/orders', {
        productId: 123,
        quantity: 1
      })
      console.log('订单号:', order.data.orderId)
    } catch (err) {
      console.error(err)
    }
  }
})

10. 数据缓存

小程序提供了本地存储能力,可以将数据缓存到用户设备上。

10.1 同步 API

// 存储数据
wx.setStorageSync('token', 'abc123')
wx.setStorageSync('userInfo', { name: '张三', age: 25 })

// 读取数据
const token = wx.getStorageSync('token')
const userInfo = wx.getStorageSync('userInfo')
console.log(token, userInfo)

// 删除数据
wx.removeStorageSync('token')

// 清空所有缓存
wx.clearStorageSync()

// 获取缓存信息
const info = wx.getStorageInfoSync()
console.log('缓存的 keys:', info.keys)
console.log('已用大小:', info.currentSize, 'KB')

10.2 异步 API

// 异步存储
wx.setStorage({
  key: 'cartList',
  data: [{ id: 1, name: '商品A', count: 2 }],
  success() {
    console.log('存储成功')
  }
})

// 异步读取
wx.getStorage({
  key: 'cartList',
  success(res) {
    console.log('购物车数据:', res.data)
  }
})

10.3 实战:购物车缓存

// 购物车管理工具
const CartManager = {
  CART_KEY: 'shopping_cart',

  // 获取购物车
  getCart() {
    return wx.getStorageSync(this.CART_KEY) || []
  },

  // 添加商品到购物车
  addItem(product) {
    const cart = this.getCart()
    const existIndex = cart.findIndex(item => item.id === product.id)

    if (existIndex > -1) {
      cart[existIndex].count += 1
    } else {
      cart.push({ ...product, count: 1 })
    }

    wx.setStorageSync(this.CART_KEY, cart)
    return cart
  },

  // 移除商品
  removeItem(productId) {
    let cart = this.getCart()
    cart = cart.filter(item => item.id !== productId)
    wx.setStorageSync(this.CART_KEY, cart)
    return cart
  },

  // 修改数量
  updateCount(productId, count) {
    const cart = this.getCart()
    const item = cart.find(item => item.id === productId)
    if (item) {
      item.count = Math.max(0, count)
      if (item.count === 0) {
        return this.removeItem(productId)
      }
    }
    wx.setStorageSync(this.CART_KEY, cart)
    return cart
  },

  // 获取总数
  getTotalCount() {
    return this.getCart().reduce((sum, item) => sum + item.count, 0)
  },

  // 获取总价
  getTotalPrice() {
    return this.getCart().reduce((sum, item) => sum + item.price * item.count, 0)
  },

  // 清空购物车
  clearCart() {
    wx.removeStorageSync(this.CART_KEY)
  }
}

module.exports = CartManager

11. 用户授权与登录

11.1 微信登录流程

微信小程序的登录流程涉及三方:小程序、微信服务器、你的业务服务器。

┌──────────┐     ┌──────────┐     ┌──────────┐
│  小程序   │────>│ 微信服务器 │────>│ 业务服务器 │
│          │<────│          │<────│          │
└──────────┘     └──────────┘     └──────────┘
  1. wx.login()    2. 返回 code     3. 用 code 换 openid
                                   4. 自定义登录态
  5. 存储 token <─────────────────────┘

11.2 实现登录

// pages/login/login.js
Page({
  data: {
    isLoggedIn: false,
    userInfo: null
  },

  // 第一步:调用微信登录获取 code
  wxLogin() {
    wx.login({
      success(res) {
        if (res.code) {
          console.log('登录 code:', res.code)

          // 第二步:将 code 发送到业务服务器
          wx.request({
            url: 'https://api.example.com/auth/login',
            method: 'POST',
            data: { code: res.code },
            success(response) {
              // 第三步:保存服务器返回的自定义登录态
              const { token, userInfo } = response.data
              wx.setStorageSync('token', token)
              wx.setStorageSync('userInfo', userInfo)

              // 登录成功,返回上一页或跳转首页
              wx.switchTab({ url: '/pages/index/index' })
            }
          })
        }
      }
    })
  },

  // 获取用户头像和昵称(需要用户主动点击按钮授权)
  onChooseAvatar(e) {
    const avatarUrl = e.detail.avatarUrl
    this.setData({ 'userInfo.avatarUrl': avatarUrl })
  },

  onInputNickname(e) {
    const nickName = e.detail.value
    this.setData({ 'userInfo.nickName': nickName })
  },

  // 保存用户信息
  async saveUserInfo() {
    const app = getApp()
    const userInfo = this.data.userInfo

    try {
      wx.request({
        url: 'https://api.example.com/user/update',
        method: 'POST',
        header: {
          'Authorization': `Bearer ${wx.getStorageSync('token')}`
        },
        data: userInfo,
        success(res) {
          app.globalData.userInfo = userInfo
          wx.showToast({ title: '保存成功', icon: 'success' })
        }
      })
    } catch (err) {
      wx.showToast({ title: '保存失败', icon: 'none' })
    }
  }
})

登录页面模板:

<!-- pages/login/login.wxml -->
<view class="login-page">
  <view class="logo">
    <image src="/images/logo.png" class="logo-img"></image>
    <text class="app-name">我的电商小程序</text>
  </view>

  <view class="login-form">
    <!-- 头像选择 -->
    <button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
      <image wx:if="{{userInfo.avatarUrl}}" src="{{userInfo.avatarUrl}}" class="avatar-preview"></image>
      <text wx:else>选择头像</text>
    </button>

    <!-- 昵称输入 -->
    <input type="nickname" placeholder="请输入昵称" bindinput="onInputNickname" class="nickname-input"></input>

    <!-- 登录按钮 -->
    <button type="primary" bindtap="wxLogin" class="login-btn">微信登录</button>
  </view>
</view>

11.3 权限管理

某些 API 需要用户授权后才能使用:

// 检查权限
wx.getSetting({
  success(res) {
    // 检查是否已授权地理位置
    if (!res.authSetting['scope.userLocation']) {
      // 首次使用,会弹出授权弹窗
      wx.authorize({
        scope: 'scope.userLocation',
        success() {
          console.log('已授权地理位置')
        },
        fail() {
          // 用户拒绝授权,引导去设置页
          wx.showModal({
            title: '提示',
            content: '需要地理位置权限,请在设置中开启',
            success(res) {
              if (res.confirm) {
                wx.openSetting({
                  success(settingRes) {
                    console.log('设置结果:', settingRes.authSetting)
                  }
                })
              }
            }
          })
        }
      })
    }
  }
})

12. 微信支付功能

12.1 支付流程概述

1. 用户在小程序内选择商品,点击"下单"
2. 小程序调用业务服务器创建订单
3. 业务服务器调用微信支付统一下单 API,获取预支付信息
4. 业务服务器将支付参数返回给小程序
5. 小程序调用 wx.requestPayment 发起支付
6. 用户在微信内完成支付
7. 微信服务器通知业务服务器支付结果

12.2 前端实现

// pages/order-confirm/order-confirm.js
const { post } = require('../../utils/request')

Page({
  data: {
    cartItems: [],
    totalPrice: 0,
    address: null,
    isSubmitting: false
  },

  onLoad() {
    // 加载购物车数据和地址
    this.loadCartData()
    this.loadAddress()
  },

  loadCartData() {
    const cart = wx.getStorageSync('shopping_cart') || []
    const totalPrice = cart.reduce((sum, item) => sum + item.price * item.count, 0)
    this.setData({ cartItems: cart, totalPrice })
  },

  loadAddress() {
    const address = wx.getStorageSync('default_address')
    this.setData({ address })
  },

  // 选择地址
  chooseAddress() {
    wx.chooseAddress({
      success(res) {
        const address = {
          name: res.userName,
          phone: res.telNumber,
          province: res.provinceName,
          city: res.cityName,
          district: res.countyName,
          detail: res.detailInfo
        }
        wx.setStorageSync('default_address', address)
        this.setData({ address })
      }
    })
  },

  // 提交订单并发起支付
  async submitOrder() {
    if (!this.data.address) {
      wx.showToast({ title: '请先选择收货地址', icon: 'none' })
      return
    }

    if (this.data.isSubmitting) return
    this.setData({ isSubmitting: true })

    try {
      // 1. 调用业务服务器创建订单
      const orderRes = await post('/orders/create', {
        items: this.data.cartItems,
        address: this.data.address,
        totalPrice: this.data.totalPrice
      })

      const { orderId, payParams } = orderRes.data

      // 2. 调用微信支付
      const paymentResult = await this.wxPay(payParams)

      // 3. 支付成功
      wx.showToast({ title: '支付成功', icon: 'success' })

      // 清空购物车
      wx.removeStorageSync('shopping_cart')

      // 跳转到订单详情页
      setTimeout(() => {
        wx.redirectTo({
          url: `/pages/order-detail/order-detail?id=${orderId}`
        })
      }, 1500)

    } catch (err) {
      console.error('下单失败:', err)
      wx.showToast({ title: err.message || '下单失败', icon: 'none' })
    } finally {
      this.setData({ isSubmitting: false })
    }
  },

  // 封装微信支付为 Promise
  wxPay(payParams) {
    return new Promise((resolve, reject) => {
      wx.requestPayment({
        timeStamp: payParams.timeStamp,
        nonceStr: payParams.nonceStr,
        package: payParams.package,
        signType: payParams.signType || 'MD5',
        paySign: payParams.paySign,
        success(res) {
          resolve(res)
        },
        fail(err) {
          if (err.errMsg.includes('cancel')) {
            reject(new Error('用户取消支付'))
          } else {
            reject(new Error('支付失败'))
          }
        }
      })
    })
  }
})

⚠️ 重要提醒:微信支付需要企业资质,并在微信商户平台完成签约。个人开发者无法使用支付功能。


13. 实战项目:电商小程序

下面我们整合前面所学,构建一个简化版的电商小程序。

13.1 项目结构

shop-miniprogram/
├── components/
│   ├── product-card/          # 商品卡片组件
│   ├── cart-item/             # 购物车商品组件
│   └── tab-bar/               # 自定义底部导航
├── pages/
│   ├── index/                 # 首页(商品列表)
│   ├── category/              # 分类页
│   ├── cart/                  # 购物车页
│   ├── mine/                  # 我的页面
│   ├── detail/                # 商品详情页
│   ├── order-confirm/         # 订单确认页
│   └── login/                 # 登录页
├── utils/
│   ├── request.js             # 网络请求封装
│   └── cart.js                # 购物车管理
├── images/                    # 图片资源
├── app.js
├── app.json
└── app.wxss

13.2 全局配置

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/cart/cart",
    "pages/mine/mine",
    "pages/detail/detail",
    "pages/order-confirm/order-confirm",
    "pages/login/login"
  ],
  "window": {
    "navigationBarBackgroundColor": "#ff6b6b",
    "navigationBarTitleText": "小商城",
    "navigationBarTextStyle": "white",
    "backgroundColor": "#f5f5f5"
  },
  "tabBar": {
    "color": "#999",
    "selectedColor": "#ff6b6b",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首页",
        "iconPath": "images/tab/home.png",
        "selectedIconPath": "images/tab/home-active.png"
      },
      {
        "pagePath": "pages/category/category",
        "text": "分类",
        "iconPath": "images/tab/category.png",
        "selectedIconPath": "images/tab/category-active.png"
      },
      {
        "pagePath": "pages/cart/cart",
        "text": "购物车",
        "iconPath": "images/tab/cart.png",
        "selectedIconPath": "images/tab/cart-active.png"
      },
      {
        "pagePath": "pages/mine/mine",
        "text": "我的",
        "iconPath": "images/tab/mine.png",
        "selectedIconPath": "images/tab/mine-active.png"
      }
    ]
  },
  "permission": {
    "scope.userLocation": {
      "desc": "用于获取您的收货地址"
    }
  }
}

13.3 全局样式

/* app.wxss */
page {
  background: #f5f5f5;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 28rpx;
  color: #333;
  box-sizing: border-box;
}

/* 通用工具类 */
.container {
  padding: 20rpx;
}

.flex {
  display: flex;
}

.flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.flex-between {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.text-ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.btn-primary {
  background: linear-gradient(135deg, #ff6b6b, #ee5a24);
  color: #fff;
  border: none;
  border-radius: 50rpx;
  font-size: 28rpx;
  padding: 20rpx 60rpx;
}

.btn-primary::after {
  border: none;
}

13.4 首页实现

// pages/index/index.js
const { get } = require('../../utils/request')

Page({
  data: {
    banners: [
      { id: 1, image: '/images/banner1.jpg' },
      { id: 2, image: '/images/banner2.jpg' }
    ],
    categories: [
      { id: 1, name: '数码', icon: '📱' },
      { id: 2, name: '服饰', icon: '👕' },
      { id: 3, name: '食品', icon: '🍜' },
      { id: 4, name: '家居', icon: '🏠' }
    ],
    products: [],
    page: 1,
    hasMore: true,
    isLoading: false
  },

  onLoad() {
    this.loadProducts()
  },

  // 下拉刷新
  onPullDownRefresh() {
    this.setData({ page: 1, hasMore: true, products: [] })
    this.loadProducts().then(() => {
      wx.stopPullDownRefresh()
    })
  },

  // 上拉加载更多
  onReachBottom() {
    if (this.data.hasMore && !this.data.isLoading) {
      this.setData({ page: this.data.page + 1 })
      this.loadProducts()
    }
  },

  async loadProducts() {
    if (this.data.isLoading) return
    this.setData({ isLoading: true })

    try {
      const res = await get('/products', {
        page: this.data.page,
        limit: 10
      })

      const newProducts = this.data.products.concat(res.data.list)
      this.setData({
        products: newProducts,
        hasMore: res.data.list.length >= 10
      })
    } catch (err) {
      console.error('加载商品失败:', err)
    } finally {
      this.setData({ isLoading: false })
    }
  },

  // 点击分类
  onCategoryTap(e) {
    const { id } = e.currentTarget.dataset
    wx.navigateTo({
      url: `/pages/category/category?id=${id}`
    })
  },

  // 点击商品
  onProductTap(e) {
    const { id } = e.currentTarget.dataset
    wx.navigateTo({
      url: `/pages/detail/detail?id=${id}`
    })
  },

  // 搜索
  onSearchTap() {
    wx.navigateTo({ url: '/pages/search/search' })
  }
})
<!-- pages/index/index.wxml -->
<view class="index-page">
  <!-- 搜索栏 -->
  <view class="search-bar" bindtap="onSearchTap">
    <view class="search-input">
      <text class="search-icon">🔍</text>
      <text class="search-placeholder">搜索商品</text>
    </view>
  </view>

  <!-- 轮播图 -->
  <swiper class="banner" autoplay circular indicator-dots indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#fff">
    <swiper-item wx:for="{{banners}}" wx:key="id">
      <image src="{{item.image}}" class="banner-img" mode="aspectFill"></image>
    </swiper-item>
  </swiper>

  <!-- 分类入口 -->
  <view class="categories">
    <view class="category-item" wx:for="{{categories}}" wx:key="id"
          bindtap="onCategoryTap" data-id="{{item.id}}">
      <text class="category-icon">{{item.icon}}</text>
      <text class="category-name">{{item.name}}</text>
    </view>
  </view>

  <!-- 商品列表 -->
  <view class="section">
    <view class="section-title">热门推荐</view>
    <view class="product-grid">
      <view class="product-item" wx:for="{{products}}" wx:key="id"
            bindtap="onProductTap" data-id="{{item.id}}">
        <image src="{{item.image}}" class="product-img" mode="aspectFill"></image>
        <view class="product-info">
          <text class="product-name text-ellipsis">{{item.name}}</text>
          <view class="product-bottom">
            <text class="product-price">¥{{item.price}}</text>
            <text class="product-sales">已售{{item.sales}}件</text>
          </view>
        </view>
      </view>
    </view>
  </view>

  <!-- 加载状态 -->
  <view class="loading-tip" wx:if="{{isLoading}}">加载中...</view>
  <view class="loading-tip" wx:elif="{{!hasMore}}">没有更多了</view>
</view>
/* pages/index/index.wxss */
.search-bar {
  padding: 16rpx 20rpx;
  background: #ff6b6b;
}

.search-input {
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, 0.9);
  border-radius: 32rpx;
  padding: 14rpx 24rpx;
}

.search-icon {
  margin-right: 12rpx;
}

.search-placeholder {
  color: #999;
  font-size: 26rpx;
}

.banner {
  width: 100%;
  height: 360rpx;
}

.banner-img {
  width: 100%;
  height: 100%;
}

.categories {
  display: flex;
  justify-content: space-around;
  padding: 30rpx 20rpx;
  background: #fff;
  margin-bottom: 20rpx;
}

.category-item {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.category-icon {
  font-size: 48rpx;
  margin-bottom: 8rpx;
}

.category-name {
  font-size: 24rpx;
  color: #666;
}

.section {
  padding: 0 20rpx;
}

.section-title {
  font-size: 32rpx;
  font-weight: bold;
  padding: 20rpx 0;
}

.product-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}

.product-item {
  width: 345rpx;
  background: #fff;
  border-radius: 16rpx;
  overflow: hidden;
  margin-bottom: 20rpx;
}

.product-img {
  width: 345rpx;
  height: 345rpx;
}

.product-info {
  padding: 16rpx;
}

.product-name {
  font-size: 26rpx;
  color: #333;
}

.product-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 12rpx;
}

.product-price {
  font-size: 32rpx;
  color: #e74c3c;
  font-weight: bold;
}

.product-sales {
  font-size: 22rpx;
  color: #999;
}

.loading-tip {
  text-align: center;
  padding: 30rpx;
  color: #999;
  font-size: 24rpx;
}

13.5 商品详情页

// pages/detail/detail.js
const { get, post } = require('../../utils/request')
const CartManager = require('../../utils/cart')

Page({
  data: {
    product: null,
    selectedSku: null,
    quantity: 1,
    showSkuPopup: false,
    cartCount: 0,
    isCollected: false
  },

  onLoad(options) {
    this.loadProductDetail(options.id)
    this.updateCartCount()
  },

  onShow() {
    this.updateCartCount()
  },

  async loadProductDetail(id) {
    try {
      wx.showLoading({ title: '加载中' })
      const res = await get(`/products/${id}`)
      this.setData({
        product: res.data,
        selectedSku: res.data.skus ? res.data.skus[0] : null
      })
      wx.setNavigationBarTitle({ title: res.data.name })
    } catch (err) {
      wx.showToast({ title: '加载失败', icon: 'none' })
    } finally {
      wx.hideLoading()
    }
  },

  // 预览图片
  previewImage(e) {
    const { current } = e.currentTarget.dataset
    wx.previewImage({
      current,
      urls: this.data.product.images
    })
  },

  // 选择 SKU
  selectSku(e) {
    const { index } = e.currentTarget.dataset
    this.setData({
      selectedSku: this.data.product.skus[index]
    })
  },

  // 修改数量
  changeQuantity(e) {
    const { action } = e.currentTarget.dataset
    let quantity = this.data.quantity

    if (action === 'minus') {
      quantity = Math.max(1, quantity - 1)
    } else {
      quantity = Math.min(this.data.selectedSku.stock, quantity + 1)
    }

    this.setData({ quantity })
  },

  // 显示 SKU 弹窗(用于加入购物车 / 立即购买)
  showSku(e) {
    const { action } = e.currentTarget.dataset
    this.setData({ showSkuPopup: true, buyAction: action })
  },

  hideSku() {
    this.setData({ showSkuPopup: false })
  },

  // 加入购物车
  addToCart() {
    const { product, selectedSku, quantity } = this.data
    const cartItem = {
      id: product.id,
      skuId: selectedSku.id,
      name: product.name,
      image: product.images[0],
      price: selectedSku.price,
      skuName: selectedSku.name,
      count: quantity
    }

    CartManager.addItem(cartItem)
    this.updateCartCount()
    this.hideSku()
    wx.showToast({ title: '已加入购物车', icon: 'success' })
  },

  // 立即购买
  buyNow() {
    const { product, selectedSku, quantity } = this.data
    const orderItem = {
      id: product.id,
      skuId: selectedSku.id,
      name: product.name,
      image: product.images[0],
      price: selectedSku.price,
      skuName: selectedSku.name,
      count: quantity
    }

    // 将购买信息存入缓存,跳转到订单确认页
    wx.setStorageSync('buy_now_item', orderItem)
    this.hideSku()
    wx.navigateTo({ url: '/pages/order-confirm/order-confirm?from=buyNow' })
  },

  // 收藏
  toggleCollect() {
    this.setData({ isCollected: !this.data.isCollected })
    wx.showToast({
      title: this.data.isCollected ? '已收藏' : '已取消收藏',
      icon: 'none'
    })
  },

  updateCartCount() {
    this.setData({ cartCount: CartManager.getTotalCount() })
  },

  // 跳转购物车
  goCart() {
    wx.switchTab({ url: '/pages/cart/cart' })
  },

  // 分享
  onShareAppMessage() {
    return {
      title: this.data.product.name,
      path: `/pages/detail/detail?id=${this.data.product.id}`,
      imageUrl: this.data.product.images[0]
    }
  }
})
<!-- pages/detail/detail.wxml -->
<view class="detail-page" wx:if="{{product}}">
  <!-- 商品图片轮播 -->
  <swiper class="detail-swiper" indicator-dots autoplay>
    <swiper-item wx:for="{{product.images}}" wx:key="index">
      <image src="{{item}}" mode="aspectFill" class="detail-img"
             bindtap="previewImage" data-current="{{item}}"></image>
    </swiper-item>
  </swiper>

  <!-- 价格信息 -->
  <view class="price-section">
    <view class="current-price">
      <text class="price-symbol">¥</text>
      <text class="price-value">{{selectedSku.price || product.price}}</text>
      <text class="original-price" wx:if="{{selectedSku.originalPrice}}">¥{{selectedSku.originalPrice}}</text>
    </view>
    <text class="sales-count">已售{{product.sales}}件</text>
  </view>

  <!-- 商品名称 -->
  <view class="name-section">
    <text class="product-title">{{product.name}}</text>
  </view>

  <!-- SKU 选择 -->
  <view class="sku-section" bindtap="showSku" data-action="cart">
    <text class="sku-label">已选:</text>
    <text class="sku-value">{{selectedSku.name}} × {{quantity}}</text>
    <text class="sku-arrow">›</text>
  </view>

  <!-- 商品详情 -->
  <view class="detail-section">
    <view class="section-title">商品详情</view>
    <rich-text nodes="{{product.detail}}"></rich-text>
  </view>

  <!-- SKU 弹窗 -->
  <view class="sku-popup-mask" wx:if="{{showSkuPopup}}" bindtap="hideSku"></view>
  <view class="sku-popup {{showSkuPopup ? 'show' : ''}}">
    <view class="sku-popup-header">
      <image src="{{product.images[0]}}" class="sku-preview-img"></image>
      <view class="sku-popup-info">
        <text class="sku-popup-price">¥{{selectedSku.price}}</text>
        <text class="sku-popup-stock">库存:{{selectedSku.stock}}</text>
      </view>
      <text class="sku-popup-close" bindtap="hideSku">✕</text>
    </view>

    <view class="sku-options">
      <text class="sku-options-title">规格</text>
      <view class="sku-list">
        <view class="sku-tag {{selectedSku.id === item.id ? 'active' : ''}}"
              wx:for="{{product.skus}}" wx:key="id"
              bindtap="selectSku" data-index="{{index}}">
          {{item.name}}
        </view>
      </view>
    </view>

    <view class="quantity-section">
      <text class="quantity-title">数量</text>
      <view class="quantity-control">
        <view class="qty-btn" bindtap="changeQuantity" data-action="minus">−</view>
        <text class="qty-value">{{quantity}}</text>
        <view class="qty-btn" bindtap="changeQuantity" data-action="plus">+</view>
      </view>
    </view>

    <view class="sku-popup-footer">
      <button class="btn-add-cart" bindtap="addToCart">加入购物车</button>
      <button class="btn-buy-now" bindtap="buyNow">立即购买</button>
    </view>
  </view>

  <!-- 底部操作栏 -->
  <view class="bottom-bar">
    <view class="bottom-icons">
      <view class="bottom-icon-item" bindtap="toggleCollect">
        <text>{{isCollected ? '❤️' : '🤍'}}</text>
        <text class="icon-label">收藏</text>
      </view>
      <view class="bottom-icon-item" bindtap="goCart">
        <view class="cart-badge" wx:if="{{cartCount > 0}}">{{cartCount}}</view>
        <text>🛒</text>
        <text class="icon-label">购物车</text>
      </view>
    </view>
    <view class="bottom-btns">
      <view class="bottom-btn add-cart" bindtap="showSku" data-action="cart">加入购物车</view>
      <view class="bottom-btn buy-now" bindtap="showSku" data-action="buy">立即购买</view>
    </view>
  </view>
</view>

13.6 购物车页面

// pages/cart/cart.js
const CartManager = require('../../utils/cart')

Page({
  data: {
    cartItems: [],
    isAllSelected: false,
    totalPrice: 0,
    totalCount: 0,
    isEditing: false
  },

  onShow() {
    this.refreshCart()
  },

  refreshCart() {
    const cartItems = CartManager.getCart().map(item => ({
      ...item,
      selected: true
    }))
    this.setData({ cartItems })
    this.calcTotal()
  },

  // 切换选中状态
  toggleSelect(e) {
    const { index } = e.currentTarget.dataset
    const key = `cartItems[${index}].selected`
    this.setData({
      [key]: !this.data.cartItems[index].selected
    })
    this.calcTotal()
  },

  // 全选/取消全选
  toggleSelectAll() {
    const isAllSelected = !this.data.isAllSelected
    const cartItems = this.data.cartItems.map(item => ({
      ...item,
      selected: isAllSelected
    }))
    this.setData({ cartItems, isAllSelected })
    this.calcTotal()
  },

  // 修改数量
  changeQuantity(e) {
    const { index, action } = e.currentTarget.dataset
    const item = this.data.cartItems[index]
    let newCount = item.count

    if (action === 'minus') {
      newCount = Math.max(1, item.count - 1)
    } else {
      newCount = item.count + 1
    }

    CartManager.updateCount(item.id, newCount)
    this.setData({ [`cartItems[${index}].count`]: newCount })
    this.calcTotal()
  },

  // 删除商品
  deleteItem(e) {
    const { index } = e.currentTarget.dataset
    const item = this.data.cartItems[index]

    wx.showModal({
      title: '确认删除',
      content: `确定要删除"${item.name}"吗?`,
      success: (res) => {
        if (res.confirm) {
          CartManager.removeItem(item.id)
          this.refreshCart()
        }
      }
    })
  },

  // 计算总价
  calcTotal() {
    const selectedItems = this.data.cartItems.filter(item => item.selected)
    const totalPrice = selectedItems.reduce((sum, item) => sum + item.price * item.count, 0)
    const totalCount = selectedItems.reduce((sum, item) => sum + item.count, 0)

    this.setData({
      totalPrice: totalPrice.toFixed(2),
      totalCount,
      isAllSelected: selectedItems.length === this.data.cartItems.length && this.data.cartItems.length > 0
    })
  },

  // 切换编辑模式
  toggleEdit() {
    this.setData({ isEditing: !this.data.isEditing })
  },

  // 去结算
  goCheckout() {
    const selectedItems = this.data.cartItems.filter(item => item.selected)
    if (selectedItems.length === 0) {
      wx.showToast({ title: '请选择商品', icon: 'none' })
      return
    }

    wx.setStorageSync('checkout_items', selectedItems)
    wx.navigateTo({ url: '/pages/order-confirm/order-confirm' })
  },

  // 清空购物车
  clearCart() {
    wx.showModal({
      title: '确认清空',
      content: '确定要清空购物车吗?',
      success: (res) => {
        if (res.confirm) {
          CartManager.clearCart()
          this.refreshCart()
        }
      }
    })
  }
})

14. 发布与审核

14.1 发布前检查清单

✅ 功能完整性:所有功能正常运行,无明显 Bug
✅ 界面适配:在不同屏幕尺寸上显示正常
✅ 性能优化:页面加载速度、图片大小合理
✅ 错误处理:网络异常、空数据等情况有友好提示
✅ 隐私合规:用户数据收集有明确告知和授权
✅ 内容合规:不涉及违规内容
✅ 体验评分:开发者工具中"体验评分"达到 90 分以上

14.2 上传与提交审核

  1. 在开发者工具中,点击右上角"上传"按钮
  2. 填写版本号(如 1.0.0)和项目备注
  3. 登录微信公众平台 → "版本管理"
  4. 在"开发版本"中找到刚上传的版本
  5. 点击"提交审核",填写相关信息
  6. 等待审核通过(通常 1-7 个工作日)
  7. 审核通过后,点击"发布"即可上线

14.3 版本管理建议

版本号规范:主版本.次版本.修订号(如 1.2.3)
- 主版本:重大功能更新或架构变化
- 次版本:新功能添加
- 修订号:Bug 修复、小改动

15. 常见问题与调试技巧

15.1 常见问题

Q:页面白屏怎么办?

  • 检查 app.json 中页面路径是否正确
  • 检查页面 JS 文件是否有语法错误
  • 查看控制台是否有报错信息

Q:setData 后视图没有更新?

  • 确认 setData 的数据格式正确
  • 检查是否在正确的 this 上调用
  • 确认 WXML 中绑定的字段名是否一致

Q:图片不显示?

  • 检查图片路径是否正确(小程序中路径区分大小写)
  • 确认图片格式是否支持(jpg、png、gif、webp)
  • 检查图片大小是否超出限制

Q:网络请求失败?

  • 在微信公众平台配置合法域名(开发管理 → 开发设置 → 服务器域名)
  • 勾选开发者工具中的"不校验合法域名"选项(仅开发阶段)
  • 检查请求地址是否正确

15.2 调试技巧

  1. 使用 Console 面板:查看 console.log 输出和错误信息
  2. 使用 Network 面板:查看所有网络请求的状态和数据
  3. 使用 AppData 面板:实时查看和修改页面数据
  4. 使用 Wxml 面板:检查页面元素结构和样式
  5. 使用 Performance 面板:分析页面性能瓶颈
// 性能监控示例
const startTime = Date.now()

// 在页面渲染完成后
Page({
  onReady() {
    const renderTime = Date.now() - startTime
    console.log(`页面渲染耗时:${renderTime}ms`)

    if (renderTime > 1000) {
      console.warn('页面渲染较慢,建议优化')
    }
  }
})

15.3 性能优化建议

  1. 减少 setData 调用:合并多次 setData 为一次
  2. 精简数据传输:只传递需要更新的数据字段
  3. 图片优化:使用 WebP 格式、合理压缩、懒加载
  4. 分包加载:将非核心页面放入分包,减少主包体积
  5. 合理使用缓存:减少重复的网络请求
// 分包配置示例 app.json
{
  "pages": [
    "pages/index/index",
    "pages/cart/cart"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "name": "shop",
      "pages": [
        "pages/detail/detail",
        "pages/order/order"
      ]
    },
    {
      "root": "packageB",
      "name": "user",
      "pages": [
        "pages/mine/mine",
        "pages/address/address"
      ]
    }
  ]
}

总结

本教程从零基础出发,涵盖了微信小程序开发的核心知识:

知识点 重要程度
项目结构与配置 ⭐⭐⭐⭐⭐
WXML 模板语法 ⭐⭐⭐⭐⭐
WXSS 样式(rpx) ⭐⭐⭐⭐
JS 逻辑与 setData ⭐⭐⭐⭐⭐
组件化开发 ⭐⭐⭐⭐
网络请求封装 ⭐⭐⭐⭐
数据缓存 ⭐⭐⭐
用户授权与登录 ⭐⭐⭐⭐
微信支付 ⭐⭐⭐
性能优化 ⭐⭐⭐

推荐学习路线

入门阶段:项目结构 → WXML → WXSS → 基础 JS → 简单页面
进阶阶段:组件化 → API 调用 → 网络请求 → 数据缓存
实战阶段:完整项目 → 用户登录 → 支付接入 → 性能优化
高级阶段:自定义组件 → 分包加载 → 云开发 → 插件开发

推荐资源


📝 本教程所有代码示例均为原创,可直接复制使用。 建议结合微信开发者工具实际运行,加深理解。祝你开发愉快!

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录