全部笔记All notes

微信小程序开发完整指南

阅读 14m 04s14m 04s read

概述

微信小程序是一种轻量级的应用程序,用户可以在微信内直接使用,无需下载安装。小程序提供了接近原生应用的用户体验,同时具有便捷的分享和传播特性。

核心特性

  • 即用即走: 无需安装,扫码即可使用
  • 接近原生: 提供接近原生应用的用户体验
  • 丰富的API: 提供丰富的微信能力和设备能力
  • 便捷分享: 可以方便地分享给好友或群聊
  • 统一管理: 通过微信统一管理和分发

技术架构

层级技术说明
视图层WXML + WXSS页面结构和样式
逻辑层JavaScript业务逻辑处理
配置层JSON页面和应用配置
原生组件微信提供地图、视频、输入框等

开发工具

工具用途特点
微信开发者工具代码编辑、调试、预览官方工具,功能完整
真机调试真实设备测试更接近用户使用环境
云开发后端服务无需搭建服务器

💡 优势: 微信小程序结合了Web开发的便捷性和原生应用的用户体验。


环境准备

开发者账号注册

  1. 注册微信小程序账号:

    • 访问 微信公众平台
    • 选择”小程序”类型进行注册
    • 完成邮箱验证和信息填写
  2. 获取 AppID:

    • 登录小程序管理后台
    • 在”设置” -> “开发设置”中查看 AppID
    • AppID 用于开发工具的项目创建

开发工具安装

  1. 下载微信开发者工具:

  2. 创建项目:

    1. 打开微信开发者工具
    2. 点击"新建项目"
    3. 填入项目目录、AppID 和项目名称
    4. 选择"不使用云服务"(初学者)
    5. 点击"新建"完成项目创建

项目结构

小程序项目/
├── pages/              # 页面文件夹
│   ├── index/          # 首页
│   │   ├── index.js    # 页面逻辑
│   │   ├── index.json  # 页面配置
│   │   ├── index.wxml  # 页面结构
│   │   └── index.wxss  # 页面样式
│   └── logs/           # 其他页面
├── utils/              # 工具函数
├── app.js              # 小程序逻辑
├── app.json            # 全局配置
├── app.wxss            # 全局样式
└── project.config.json # 项目配置

WXSS样式

WXSS(WeiXin Style Sheets)是微信小程序的样式语言,基于 CSS 进行扩展。

与 CSS 的区别

特性WXSSCSS
尺寸单位rpx(响应式像素)px, em, rem
样式导入@import “path”@import url()
选择器部分支持完全支持
适配性自动适配不同设备需要媒体查询

rpx 尺寸单位

rpx(responsive pixel) 是微信小程序独有的尺寸单位,用于解决屏幕适配问题。

实现原理

  • 规定所有设备屏幕宽度为 750rpx
  • 在不同设备上,1rpx 对应不同的物理像素
  • 小程序运行时自动进行换算

换算关系

在 iPhone 6 上:

  • 750rpx = 375px = 750物理像素
  • 1rpx = 0.5px = 1物理像素
设备rpx换算pxpx换算rpx
iPhone 51rpx = 0.42px1px = 2.34rpx
iPhone 61rpx = 0.5px1px = 2rpx
iPhone 6 Plus1rpx = 0.552px1px = 1.81rpx

使用示例

/* 推荐:使用 rpx 进行适配 */
.container {
  width: 750rpx;      /* 全屏宽度 */
  height: 300rpx;     /* 固定高度 */
  padding: 20rpx;     /* 内边距 */
  margin: 10rpx;      /* 外边距 */
  font-size: 28rpx;   /* 字体大小 */
}

/* 在 iPhone 6 上的设计稿换算 */
.box {
  width: 200rpx;      /* 设计稿 100px */
  height: 100rpx;     /* 设计稿 50px */
  border-radius: 10rpx; /* 设计稿 5px */
}

样式导入

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

1. 创建公共样式文件

/* styles/common.wxss */
.btn {
  padding: 20rpx 40rpx;
  border-radius: 10rpx;
  color: #fff;
  text-align: center;
}

.btn-primary {
  background-color: #007aff;
}

.btn-success {
  background-color: #4cd964;
}

.btn-warning {
  background-color: #ff9500;
}

2. 导入样式文件

/* pages/index/index.wxss */
@import "../../styles/common.wxss";

.page-container {
  padding: 20rpx;
  background-color: #f5f5f5;
}

.custom-button {
  margin-top: 20rpx;
}

全局样式和局部样式

全局样式(app.wxss)

/* app.wxss - 作用于所有页面 */
page {
  background-color: #f8f8f8;
  font-size: 28rpx;
  color: #333;
  line-height: 1.5;
}

/* 通用容器样式 */
.container {
  padding: 20rpx;
  min-height: 100vh;
  box-sizing: border-box;
}

/* 通用文本样式 */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }

/* 通用颜色类 */
.text-primary { color: #007aff; }
.text-success { color: #4cd964; }
.text-warning { color: #ff9500; }
.text-danger { color: #ff3b30; }

局部样式(页面.wxss)

/* pages/index/index.wxss - 仅作用于当前页面 */
.hero-section {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 80rpx 40rpx;
  text-align: center;
}

.hero-title {
  font-size: 48rpx;
  font-weight: bold;
  margin-bottom: 20rpx;
}

.hero-subtitle {
  font-size: 28rpx;
  opacity: 0.9;
}

样式优先级

  1. 内联样式 > 局部样式 > 全局样式
  2. 相同优先级时,后定义的覆盖先定义的
  3. 使用 !important 可以提高优先级(谨慎使用)

全局配置

全局配置文件 app.json 用于配置小程序的全局设置。

app.json 结构

{
  "pages": [
    "pages/index/index",
    "pages/profile/profile",
    "pages/settings/settings"
  ],
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "我的小程序",
    "navigationBarTextStyle": "black",
    "backgroundColor": "#f8f8f8",
    "enablePullDownRefresh": false,
    "onReachBottomDistance": 50
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [
      {
        "pagePath": "pages/index/index",
        "iconPath": "icon/home.png",
        "selectedIconPath": "icon/home-active.png",
        "text": "首页"
      },
      {
        "pagePath": "pages/profile/profile",
        "iconPath": "icon/profile.png",
        "selectedIconPath": "icon/profile-active.png",
        "text": "我的"
      }
    ]
  },
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  },
  "debug": false
}

window 配置详解

属性类型默认值说明
navigationBarBackgroundColorHexColor#000000导航栏背景颜色
navigationBarTextStyleStringwhite导航栏标题颜色,仅支持 black/white
navigationBarTitleTextString导航栏标题文字内容
backgroundColorHexColor#ffffff窗口的背景色
backgroundTextStyleStringdark下拉 loading 的样式,仅支持 dark/light
enablePullDownRefreshBooleanfalse是否开启当前页面下拉刷新
onReachBottomDistanceNumber50页面上拉触底事件触发时距页面底部距离,单位为px

tabBar 配置

基础配置

{
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#3cc51f", 
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "position": "bottom",
    "list": [
      {
        "pagePath": "pages/index/index",
        "iconPath": "icon/home.png",
        "selectedIconPath": "icon/home-active.png",
        "text": "首页"
      }
    ]
  }
}

完整的 tabBar 示例

{
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#FF6B4A",
    "borderStyle": "white",
    "backgroundColor": "#ffffff", 
    "position": "bottom",
    "list": [
      {
        "pagePath": "pages/home/home",
        "iconPath": "images/tabbar/home.png",
        "selectedIconPath": "images/tabbar/home-active.png",
        "text": "首页"
      },
      {
        "pagePath": "pages/category/category", 
        "iconPath": "images/tabbar/category.png",
        "selectedIconPath": "images/tabbar/category-active.png",
        "text": "分类"
      },
      {
        "pagePath": "pages/cart/cart",
        "iconPath": "images/tabbar/cart.png", 
        "selectedIconPath": "images/tabbar/cart-active.png",
        "text": "购物车"
      },
      {
        "pagePath": "pages/profile/profile",
        "iconPath": "images/tabbar/profile.png",
        "selectedIconPath": "images/tabbar/profile-active.png", 
        "text": "我的"
      }
    ]
  }
}

网络数据请求

小程序提供了 wx.request() API 来发起网络请求。

域名配置

在小程序管理后台配置合法域名:

  1. 登录小程序管理后台
  2. 进入”开发” -> “开发管理” -> “开发设置”
  3. 在”服务器域名”中添加 request 合法域名

基本用法

GET 请求

// 获取用户列表
function getUserList() {
  wx.request({
    url: 'https://api.example.com/users',
    method: 'GET',
    data: {
      page: 1,
      limit: 10
    },
    header: {
      'Content-Type': 'application/json'
    },
    success(res) {
      console.log('请求成功', res.data)
      // 处理成功响应
    },
    fail(err) {
      console.error('请求失败', err)
      // 处理失败情况
    },
    complete() {
      console.log('请求完成')
      // 无论成功失败都会执行
    }
  })
}

POST 请求

// 用户登录
function userLogin(username, password) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: 'https://api.example.com/auth/login',
      method: 'POST',
      data: {
        username: username,
        password: password
      },
      header: {
        'Content-Type': 'application/json'
      },
      success(res) {
        if (res.statusCode === 200) {
          resolve(res.data)
        } else {
          reject(new Error('登录失败'))
        }
      },
      fail(err) {
        reject(err)
      }
    })
  })
}

// 使用示例
userLogin('admin', '123456')
  .then(data => {
    console.log('登录成功', data)
    // 保存token
    wx.setStorageSync('token', data.token)
  })
  .catch(err => {
    console.error('登录失败', err)
    wx.showToast({
      title: '登录失败',
      icon: 'error'
    })
  })

封装网络请求

创建一个通用的网络请求工具:

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

function request(options) {
  return new Promise((resolve, reject) => {
    // 显示加载提示
    wx.showLoading({
      title: '加载中...',
      mask: true
    })

    wx.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'Content-Type': 'application/json',
        'Authorization': wx.getStorageSync('token') || '',
        ...options.header
      },
      success(res) {
        wx.hideLoading()
        
        if (res.statusCode === 200) {
          resolve(res.data)
        } else if (res.statusCode === 401) {
          // token 过期,跳转到登录页
          wx.removeStorageSync('token')
          wx.navigateTo({
            url: '/pages/login/login'
          })
          reject(new Error('登录已过期'))
        } else {
          reject(new Error(`请求失败: ${res.statusCode}`))
        }
      },
      fail(err) {
        wx.hideLoading()
        wx.showToast({
          title: '网络错误',
          icon: 'error'
        })
        reject(err)
      }
    })
  })
}

// 导出具体的请求方法
module.exports = {
  get(url, data) {
    return request({
      url,
      method: 'GET',
      data
    })
  },
  
  post(url, data) {
    return request({
      url,
      method: 'POST', 
      data
    })
  },
  
  put(url, data) {
    return request({
      url,
      method: 'PUT',
      data
    })
  },
  
  delete(url, data) {
    return request({
      url,
      method: 'DELETE',
      data
    })
  }
}

使用封装的请求工具:

// 页面中使用
const api = require('../../utils/request')

Page({
  data: {
    userList: []
  },
  
  async onLoad() {
    await this.loadUserList()
  },
  
  async loadUserList() {
    try {
      const result = await api.get('/users', {
        page: 1,
        limit: 20
      })
      
      this.setData({
        userList: result.data
      })
    } catch (error) {
      console.error('加载用户列表失败', error)
    }
  },
  
  async createUser(userData) {
    try {
      await api.post('/users', userData)
      wx.showToast({
        title: '创建成功',
        icon: 'success'
      })
      // 重新加载列表
      await this.loadUserList()
    } catch (error) {
      console.error('创建用户失败', error)
    }
  }
})

页面导航

小程序提供了声明式导航和编程式导航两种方式。

声明式导航

使用 navigator 组件进行页面跳转:

<!-- 跳转到普通页面 -->
<navigator url="/pages/detail/detail" open-type="navigate">
  查看详情
</navigator>

<!-- 跳转到 tabBar 页面 -->
<navigator url="/pages/home/home" open-type="switchTab">
  返回首页
</navigator>

<!-- 返回上一页 -->
<navigator open-type="navigateBack" delta="1">
  返回
</navigator>

<!-- 带参数跳转 -->
<navigator url="/pages/detail/detail?id=123&type=product">
  查看商品详情
</navigator>

编程式导航

使用 wx API 进行页面跳转:

基本跳转方法

Page({
  // 跳转到普通页面
  navigateToDetail() {
    wx.navigateTo({
      url: '/pages/detail/detail?id=123'
    })
  },
  
  // 跳转到 tabBar 页面
  switchToHome() {
    wx.switchTab({
      url: '/pages/home/home'
    })
  },
  
  // 重定向到页面(关闭当前页面)
  redirectToLogin() {
    wx.redirectTo({
      url: '/pages/login/login'
    })
  },
  
  // 重新启动到页面(关闭所有页面)
  reLaunchToIndex() {
    wx.reLaunch({
      url: '/pages/index/index'
    })
  },
  
  // 返回上一页
  goBack() {
    wx.navigateBack({
      delta: 1
    })
  }
})

带参数跳转

// 跳转时传递参数
goToUserProfile(userId) {
  wx.navigateTo({
    url: `/pages/profile/profile?userId=${userId}&from=list`
  })
}

// 接收参数的页面
Page({
  onLoad(options) {
    const { userId, from } = options
    console.log('用户ID:', userId)
    console.log('来源页面:', from)
    
    // 根据参数加载数据
    this.loadUserData(userId)
  },
  
  loadUserData(userId) {
    // 加载用户数据
  }
})

封装导航工具

// utils/navigation.js
module.exports = {
  // 跳转到页面
  navigateTo(url, params = {}) {
    const queryString = Object.keys(params)
      .map(key => `${key}=${encodeURIComponent(params[key])}`)
      .join('&')
    
    const fullUrl = queryString ? `${url}?${queryString}` : url
    
    wx.navigateTo({
      url: fullUrl,
      fail(err) {
        console.error('页面跳转失败', err)
      }
    })
  },
  
  // 跳转到 tabBar
  switchTab(url) {
    wx.switchTab({
      url,
      fail(err) {
        console.error('tabBar跳转失败', err)
      }
    })
  },
  
  // 返回上一页
  goBack(delta = 1) {
    wx.navigateBack({ delta })
  },
  
  // 确认后返回
  confirmGoBack(message = '确定要返回吗?') {
    wx.showModal({
      title: '提示',
      content: message,
      success: (res) => {
        if (res.confirm) {
          this.goBack()
        }
      }
    })
  }
}

生命周期

小程序包含应用生命周期和页面生命周期。

应用生命周期

在 app.js 中定义:

// app.js
App({
  // 小程序初始化完成时触发(全局只触发一次)
  onLaunch(options) {
    console.log('小程序启动', options)
    
    // 检查更新
    this.checkUpdate()
    
    // 获取用户信息
    this.getUserInfo()
  },
  
  // 小程序启动,或从后台进入前台显示时触发
  onShow(options) {
    console.log('小程序显示', options)
    
    // 刷新数据
    this.refreshData()
  },
  
  // 小程序从前台进入后台时触发
  onHide() {
    console.log('小程序隐藏')
    
    // 保存数据
    this.saveData()
  },
  
  // 小程序发生脚本错误或API调用报错时触发
  onError(error) {
    console.error('小程序错误', error)
    
    // 错误上报
    this.reportError(error)
  },
  
  // 页面不存在时触发
  onPageNotFound(res) {
    console.log('页面不存在', res)
    
    wx.redirectTo({
      url: '/pages/index/index'
    })
  },
  
  // 检查更新
  checkUpdate() {
    if (wx.canIUse('getUpdateManager')) {
      const updateManager = wx.getUpdateManager()
      
      updateManager.onCheckForUpdate((res) => {
        console.log('检查更新结果', res.hasUpdate)
      })
      
      updateManager.onUpdateReady(() => {
        wx.showModal({
          title: '更新提示',
          content: '新版本已准备好,是否重启应用?',
          success: (res) => {
            if (res.confirm) {
              updateManager.applyUpdate()
            }
          }
        })
      })
      
      updateManager.onUpdateFailed(() => {
        console.error('更新失败')
      })
    }
  },
  
  globalData: {
    userInfo: null,
    token: null
  }
})

页面生命周期

// pages/index/index.js
Page({
  data: {
    loading: true,
    list: []
  },
  
  // 页面加载时触发,一个页面只会调用一次
  onLoad(options) {
    console.log('页面加载', options)
    
    // 获取页面参数
    const { id, type } = options
    
    // 设置页面标题
    wx.setNavigationBarTitle({
      title: '页面标题'
    })
    
    // 初始化数据
    this.initData()
  },
  
  // 页面显示/切入前台时触发
  onShow() {
    console.log('页面显示')
    
    // 刷新数据
    this.refreshData()
  },
  
  // 页面初次渲染完成时触发,一个页面只会调用一次
  onReady() {
    console.log('页面渲染完成')
    
    // 获取组件实例
    this.selectComponent('#my-component')
  },
  
  // 页面隐藏/切入后台时触发
  onHide() {
    console.log('页面隐藏')
    
    // 暂停音视频
    // 保存表单数据
  },
  
  // 页面卸载时触发
  onUnload() {
    console.log('页面卸载')
    
    // 清理定时器
    if (this.timer) {
      clearInterval(this.timer)
    }
    
    // 清理事件监听
    wx.off('customEvent', this.handleCustomEvent)
  },
  
  // 下拉刷新时触发
  onPullDownRefresh() {
    console.log('下拉刷新')
    
    this.refreshData().finally(() => {
      wx.stopPullDownRefresh()
    })
  },
  
  // 上拉触底时触发
  onReachBottom() {
    console.log('上拉触底')
    
    this.loadMore()
  },
  
  // 页面滚动时触发
  onPageScroll(e) {
    // console.log('页面滚动', e.scrollTop)
    
    // 控制返回顶部按钮显示
    this.setData({
      showBackTop: e.scrollTop > 500
    })
  },
  
  // 用户点击右上角分享
  onShareAppMessage() {
    return {
      title: '分享标题',
      path: '/pages/index/index',
      imageUrl: '/images/share.png'
    }
  },
  
  // 初始化数据
  async initData() {
    try {
      this.setData({ loading: true })
      
      const data = await this.fetchData()
      
      this.setData({
        list: data,
        loading: false
      })
    } catch (error) {
      console.error('初始化数据失败', error)
      this.setData({ loading: false })
    }
  },
  
  // 刷新数据
  async refreshData() {
    try {
      const data = await this.fetchData()
      this.setData({ list: data })
    } catch (error) {
      console.error('刷新数据失败', error)
    }
  },
  
  // 加载更多
  async loadMore() {
    if (this.data.loading) return
    
    try {
      this.setData({ loading: true })
      
      const newData = await this.fetchData(this.data.list.length)
      
      this.setData({
        list: [...this.data.list, ...newData],
        loading: false
      })
    } catch (error) {
      console.error('加载更多失败', error)
      this.setData({ loading: false })
    }
  }
})

实战案例

本地生活小程序

这是一个综合性的实战案例,展示小程序开发的核心技术。

功能需求

  1. 首页轮播图展示
  2. 九宫格导航
  3. 商家列表展示
  4. 下拉刷新和上拉加载
  5. 页面跳转和参数传递

项目结构

local-life/
├── pages/
│   ├── index/              # 首页
│   ├── category/           # 分类页
│   ├── detail/             # 详情页
│   └── my/                 # 我的页面
├── components/             # 自定义组件
├── utils/                  # 工具函数
├── images/                 # 图片资源
└── styles/                 # 公共样式

首页实现

index.wxml:

<view class="container">
  <!-- 轮播图 -->
  <swiper class="banner-swiper" indicator-dots autoplay circular>
    <swiper-item wx:for="{{banners}}" wx:key="id">
      <image src="{{item.image}}" mode="aspectFill" bindtap="onBannerTap" data-url="{{item.url}}" />
    </swiper-item>
  </swiper>
  
  <!-- 九宫格导航 -->
  <view class="grid-nav">
    <view wx:for="{{categories}}" wx:key="id" class="grid-item" bindtap="onCategoryTap" data-id="{{item.id}}">
      <image src="{{item.icon}}" class="grid-icon" />
      <text class="grid-text">{{item.name}}</text>
    </view>
  </view>
  
  <!-- 商家列表 -->
  <view class="shop-list">
    <view class="section-title">推荐商家</view>
    <view wx:for="{{shops}}" wx:key="id" class="shop-item" bindtap="onShopTap" data-id="{{item.id}}">
      <image src="{{item.image}}" class="shop-image" />
      <view class="shop-info">
        <text class="shop-name">{{item.name}}</text>
        <text class="shop-desc">{{item.description}}</text>
        <view class="shop-meta">
          <text class="shop-rating">⭐ {{item.rating}}</text>
          <text class="shop-distance">{{item.distance}}km</text>
        </view>
      </view>
    </view>
  </view>
  
  <!-- 加载提示 -->
  <view wx:if="{{loading}}" class="loading">
    <text>加载中...</text>
  </view>
</view>

index.js:

const api = require('../../utils/request')

Page({
  data: {
    banners: [],
    categories: [],
    shops: [],
    loading: false,
    page: 1,
    hasMore: true
  },
  
  async onLoad() {
    await this.initData()
  },
  
  async onPullDownRefresh() {
    await this.refreshData()
    wx.stopPullDownRefresh()
  },
  
  async onReachBottom() {
    if (!this.data.hasMore || this.data.loading) return
    await this.loadMoreShops()
  },
  
  // 初始化数据
  async initData() {
    try {
      this.setData({ loading: true })
      
      const [banners, categories, shops] = await Promise.all([
        api.get('/banners'),
        api.get('/categories'),
        api.get('/shops', { page: 1, limit: 10 })
      ])
      
      this.setData({
        banners: banners.data,
        categories: categories.data,
        shops: shops.data,
        loading: false,
        page: 1,
        hasMore: shops.data.length === 10
      })
    } catch (error) {
      console.error('初始化数据失败', error)
      this.setData({ loading: false })
    }
  },
  
  // 刷新数据
  async refreshData() {
    this.setData({ page: 1 })
    await this.initData()
  },
  
  // 加载更多商家
  async loadMoreShops() {
    try {
      this.setData({ loading: true })
      
      const nextPage = this.data.page + 1
      const result = await api.get('/shops', { 
        page: nextPage, 
        limit: 10 
      })
      
      this.setData({
        shops: [...this.data.shops, ...result.data],
        page: nextPage,
        hasMore: result.data.length === 10,
        loading: false
      })
    } catch (error) {
      console.error('加载更多失败', error)
      this.setData({ loading: false })
    }
  },
  
  // 轮播图点击
  onBannerTap(e) {
    const { url } = e.currentTarget.dataset
    if (url) {
      wx.navigateTo({ url })
    }
  },
  
  // 分类点击
  onCategoryTap(e) {
    const { id } = e.currentTarget.dataset
    wx.navigateTo({
      url: `/pages/category/category?id=${id}`
    })
  },
  
  // 商家点击
  onShopTap(e) {
    const { id } = e.currentTarget.dataset
    wx.navigateTo({
      url: `/pages/detail/detail?shopId=${id}`
    })
  },
  
  // 分享
  onShareAppMessage() {
    return {
      title: '本地生活服务',
      path: '/pages/index/index'
    }
  }
})

index.wxss:

.container {
  background-color: #f5f5f5;
}

/* 轮播图样式 */
.banner-swiper {
  width: 100%;
  height: 300rpx;
}

.banner-swiper image {
  width: 100%;
  height: 100%;
}

/* 九宫格导航 */
.grid-nav {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20rpx;
  padding: 30rpx;
  background-color: #fff;
  margin-bottom: 20rpx;
}

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

.grid-icon {
  width: 80rpx;
  height: 80rpx;
  margin-bottom: 10rpx;
}

.grid-text {
  font-size: 26rpx;
  color: #333;
}

/* 商家列表 */
.shop-list {
  background-color: #fff;
  padding: 0 30rpx;
}

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

.shop-item {
  display: flex;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #f0f0f0;
}

.shop-image {
  width: 120rpx;
  height: 120rpx;
  border-radius: 10rpx;
  margin-right: 20rpx;
}

.shop-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}

.shop-name {
  font-size: 30rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 10rpx;
}

.shop-desc {
  font-size: 26rpx;
  color: #666;
  margin-bottom: 10rpx;
}

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

.shop-rating {
  font-size: 24rpx;
  color: #ff6b35;
}

.shop-distance {
  font-size: 24rpx;
  color: #999;
}

/* 加载提示 */
.loading {
  text-align: center;
  padding: 40rpx;
  color: #999;
}

性能优化

1. 代码优化

避免频繁的 setData

// ❌ 不好的做法
for (let i = 0; i < 100; i++) {
  this.setData({
    [`list[${i}]`]: newData[i]
  })
}

// ✅ 好的做法
const updates = {}
for (let i = 0; i < 100; i++) {
  updates[`list[${i}]`] = newData[i]
}
this.setData(updates)

// ✅ 更好的做法
this.setData({
  list: newData
})

减少数据传输量

// ❌ 传输不必要的数据
this.setData({
  userInfo: {
    id: 1,
    name: 'John',
    email: 'john@example.com',
    avatar: 'http://example.com/avatar.jpg',
    // 大量其他不需要的字段...
    unnecessary_field_1: '...',
    unnecessary_field_2: '...'
  }
})

// ✅ 只传输需要的数据
const { id, name, avatar } = userInfo
this.setData({
  userInfo: { id, name, avatar }
})

2. 图片优化

图片懒加载

<image 
  wx:for="{{imageList}}" 
  wx:key="id"
  src="{{item.url}}" 
  lazy-load="{{true}}"
  mode="aspectFill" />

合理的图片格式和大小

// 根据设备像素比选择合适的图片
const pixelRatio = wx.getSystemInfoSync().pixelRatio
const imageSize = pixelRatio > 2 ? '@3x' : pixelRatio > 1 ? '@2x' : ''
const imageUrl = `https://example.com/image${imageSize}.jpg`

3. 网络优化

请求合并

// ❌ 多个独立请求
const user = await api.get('/user')
const posts = await api.get('/posts')
const comments = await api.get('/comments')

// ✅ 合并请求
const [user, posts, comments] = await Promise.all([
  api.get('/user'),
  api.get('/posts'), 
  api.get('/comments')
])

缓存策略

// utils/cache.js
class Cache {
  constructor() {
    this.cache = new Map()
    this.expire = new Map()
  }
  
  set(key, value, ttl = 5 * 60 * 1000) { // 默认5分钟
    this.cache.set(key, value)
    this.expire.set(key, Date.now() + ttl)
  }
  
  get(key) {
    if (this.expire.get(key) < Date.now()) {
      this.cache.delete(key)
      this.expire.delete(key)
      return null
    }
    return this.cache.get(key)
  }
  
  async getOrFetch(key, fetchFn, ttl) {
    let data = this.get(key)
    if (!data) {
      data = await fetchFn()
      this.set(key, data, ttl)
    }
    return data
  }
}

const cache = new Cache()

// 使用缓存
async function getUserInfo(userId) {
  return await cache.getOrFetch(
    `user_${userId}`,
    () => api.get(`/users/${userId}`),
    10 * 60 * 1000 // 10分钟缓存
  )
}

4. 长列表优化

使用虚拟列表减少DOM节点:

<!-- 使用 recycle-view 组件 -->
<recycle-view batch="{{batchSetRecycleData}}" id="recycleId">
  <recycle-item wx:for="{{recycleList}}" wx:key="id">
    <view class="item">{{item.name}}</view>
  </recycle-item>
</recycle-view>

最佳实践

1. 代码组织

模块化开发

// utils/validator.js - 表单验证工具
module.exports = {
  isEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  },
  
  isPhone(phone) {
    return /^1[3-9]\d{9}$/.test(phone)
  },
  
  isRequired(value) {
    return value !== null && value !== undefined && value !== ''
  }
}

// utils/format.js - 格式化工具
module.exports = {
  formatDate(date, format = 'YYYY-MM-DD') {
    // 日期格式化逻辑
  },
  
  formatNumber(num) {
    return num.toLocaleString()
  },
  
  formatFileSize(bytes) {
    const sizes = ['Bytes', 'KB', 'MB', 'GB']
    if (bytes === 0) return '0 Bytes'
    const i = Math.floor(Math.log(bytes) / Math.log(1024))
    return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
  }
}

组件化开发

// components/loading/loading.js
Component({
  properties: {
    show: {
      type: Boolean,
      value: false
    },
    text: {
      type: String,
      value: '加载中...'
    }
  },
  
  data: {
    // 组件内部数据
  },
  
  methods: {
    // 组件方法
  }
})

2. 错误处理

全局错误处理

// app.js
App({
  onError(error) {
    console.error('全局错误:', error)
    
    // 错误上报
    this.reportError(error)
    
    // 用户提示
    wx.showToast({
      title: '系统繁忙,请稍后重试',
      icon: 'none'
    })
  },
  
  reportError(error) {
    // 发送错误信息到服务器
    wx.request({
      url: 'https://api.example.com/errors',
      method: 'POST',
      data: {
        error: error.toString(),
        stack: error.stack,
        userAgent: wx.getSystemInfoSync(),
        timestamp: Date.now()
      }
    })
  }
})

页面错误边界

Page({
  data: {
    hasError: false,
    errorMessage: ''
  },
  
  async onLoad() {
    try {
      await this.initData()
    } catch (error) {
      this.handleError(error)
    }
  },
  
  handleError(error) {
    console.error('页面错误:', error)
    
    this.setData({
      hasError: true,
      errorMessage: '页面加载失败,请重试'
    })
  },
  
  onRetry() {
    this.setData({
      hasError: false,
      errorMessage: ''
    })
    this.onLoad()
  }
})

3. 用户体验优化

Loading 状态管理

// utils/loading.js
class LoadingManager {
  constructor() {
    this.loadingCount = 0
  }
  
  show(title = '加载中...') {
    if (this.loadingCount === 0) {
      wx.showLoading({ title, mask: true })
    }
    this.loadingCount++
  }
  
  hide() {
    this.loadingCount--
    if (this.loadingCount <= 0) {
      this.loadingCount = 0
      wx.hideLoading()
    }
  }
}

const loadingManager = new LoadingManager()
module.exports = loadingManager

防抖和节流

// utils/debounce.js
function debounce(func, wait) {
  let timeout
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout)
      func(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  }
}

function throttle(func, limit) {
  let inThrottle
  return function() {
    const args = arguments
    const context = this
    if (!inThrottle) {
      func.apply(context, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

module.exports = { debounce, throttle }

常见问题

问题1:页面跳转失败

❓ 现象: 使用 wx.navigateTo 跳转页面失败

可能原因和解决方案:

  1. 页面路径错误:

    // ❌ 错误的路径
    wx.navigateTo({
      url: 'pages/detail/detail' // 缺少开头的 /
    })
    
    // ✅ 正确的路径
    wx.navigateTo({
      url: '/pages/detail/detail'
    })
  2. 页面栈层级过深:

    // 页面栈最多10层,超出后需要使用 redirectTo
    wx.redirectTo({
      url: '/pages/detail/detail'
    })
  3. 跳转到 tabBar 页面:

    // ❌ 错误:用 navigateTo 跳转 tabBar 页面
    wx.navigateTo({
      url: '/pages/home/home'
    })
    
    // ✅ 正确:用 switchTab 跳转 tabBar 页面
    wx.switchTab({
      url: '/pages/home/home'
    })

问题2:数据不更新

❓ 现象: 调用 setData 后页面数据没有更新

解决方案:

  1. 检查数据路径:

    // ❌ 错误的数组更新方式
    this.data.list.push(newItem)
    this.setData({
      list: this.data.list
    })
    
    // ✅ 正确的数组更新方式
    this.setData({
      list: [...this.data.list, newItem]
    })
  2. 深层对象更新:

    // ❌ 错误:直接修改对象属性
    this.data.user.name = 'New Name'
    this.setData({
      user: this.data.user
    })
    
    // ✅ 正确:使用对象展开或指定路径
    this.setData({
      'user.name': 'New Name'
    })
    
    // 或者
    this.setData({
      user: { ...this.data.user, name: 'New Name' }
    })

问题3:网络请求失败

❓ 现象: wx.request 请求返回失败

解决方案:

  1. 检查域名配置:

    • 确保请求域名已在小程序管理后台配置
    • 域名必须是 HTTPS 协议
  2. 开发环境跳过域名校验:

    // 在开发者工具中勾选"不校验合法域名"
  3. 处理跨域问题:

    // 小程序不存在跨域问题,但服务器需要正确处理 OPTIONS 请求

问题4:图片加载失败

❓ 现象: 图片无法显示或加载很慢

解决方案:

  1. 使用合适的 mode 属性:

    <!-- 保持比例完整显示 -->
    <image src="{{imageUrl}}" mode="aspectFit" />
    
    <!-- 保持比例裁剪填充 -->
    <image src="{{imageUrl}}" mode="aspectFill" />
  2. 添加默认图片和错误处理:

    <image 
      src="{{imageUrl}}" 
      mode="aspectFill"
      binderror="onImageError"
      class="image" />
    onImageError(e) {
      const { index } = e.currentTarget.dataset
      this.setData({
        [`imageList[${index}].url`]: '/images/default.png'
      })
    }
  3. 使用图片懒加载:

    <image 
      src="{{imageUrl}}" 
      lazy-load="{{true}}"
      mode="aspectFill" />

问题5:性能问题

❓ 现象: 页面卡顿、响应慢

解决方案:

  1. 优化 setData 使用:

    // ❌ 频繁调用 setData
    for (let i = 0; i < 1000; i++) {
      this.setData({
        [`list[${i}]`]: data[i]
      })
    }
    
    // ✅ 批量更新
    this.setData({
      list: data
    })
  2. 避免在页面滚动时频繁更新:

    // ❌ 每次滚动都更新
    onPageScroll(e) {
      this.setData({
        scrollTop: e.scrollTop
      })
    }
    
    // ✅ 使用节流
    onPageScroll: throttle(function(e) {
      this.setData({
        scrollTop: e.scrollTop
      })
    }, 100)

相关文章

基础开发

移动开发技术

  • 微信公众号开发 - 微信生态开发

开发工具

  • Git版本控制 - 代码版本管理
  • 微信开发者工具使用 - 官方开发环境
  • 小程序调试技巧 - 开发调试方法

数据库集成

部署运维


总结

微信小程序作为移动端轻量级应用的重要形式,为开发者提供了便捷的开发体验和丰富的功能特性。

🎯 核心优势

  • 开发简便: 基于Web技术栈,学习成本低
  • 生态丰富: 微信生态支持,用户触达便捷
  • 性能优异: 接近原生应用的使用体验
  • 功能完整: 丰富的API和组件支持

🛠️ 技术要点

  1. WXML模板: 数据绑定、条件渲染、列表渲染
  2. WXSS样式: rpx适配单位、样式导入
  3. JavaScript逻辑: 页面生命周期、事件处理
  4. 组件化开发: 自定义组件、组件通信
  5. 网络请求: 数据获取、状态管理
  6. 页面导航: 路由跳转、参数传递

🚀 最佳实践

  • 性能优化: 合理使用setData,优化渲染
  • 用户体验: 加载状态、错误处理、交互反馈
  • 代码质量: 模块化开发、组件复用、错误处理
  • 项目管理: 版本控制、代码规范、团队协作

相关文章

移动开发技术

实战项目

  • 保持对新技术的敏感度