网络请求与云开发

💡 核心结论

  1. wx.request发送HTTP请求,域名需要在后台配置白名单

  2. Promise封装wx.request可以使用async/await简化异步代码

  3. 云开发提供数据库、存储、云函数,无需搭建服务器

  4. 云数据库支持权限控制和实时推送

  5. 小程序登录通过code换取openid和session_key


1. 网络请求

1.1 wx.request基础

//pages/index/index.js
Page({
  data: {
    list: []
  },
  
  onLoad() {
    this.loadData()
  },
  
  loadData() {
    wx.showLoading({ title: '加载中' })
    
    wx.request({
      url: 'https://api.example.com/products',
      method: 'GET',
      data: {
        page: 1,
        size: 10
      },
      header: {
        'content-type': 'application/json',
        'Authorization': 'Bearer ' + wx.getStorageSync('token')
      },
      timeout: 10000,
      success: (res) => {
        if (res.statusCode === 200) {
          this.setData({
            list: res.data.list
          })
        } else {
          wx.showToast({
            title: '请求失败',
            icon: 'none'
          })
        }
      },
      fail: (err) => {
        console.error('请求失败', err)
        wx.showToast({
          title: '网络错误',
          icon: 'none'
        })
      },
      complete: () => {
        wx.hideLoading()
      }
    })
  }
})

1.2 Promise封装

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

function request(options) {
  return new Promise((resolve, reject) => {
    wx.request({
      url: baseURL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'content-type': 'application/json',
        'Authorization': 'Bearer ' + wx.getStorageSync('token'),
        ...options.header
      },
      success: (res) => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
        } else {
          reject(res)
        }
      },
      fail: (err) => {
        reject(err)
      }
    })
  })
}

// 封装常用方法
export const http = {
  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 })
  }
}

1.3 使用async/await

// pages/index/index.js
import { http } from '../../utils/request.js'

Page({
  data: {
    products: [],
    page: 1
  },
  
  async onLoad() {
    await this.loadProducts()
  },
  
  async loadProducts() {
    try {
      wx.showLoading({ title: '加载中' })
      
      const data = await http.get('/products', {
        page: this.data.page,
        size: 10
      })
      
      this.setData({
        products: data.list
      })
    } catch (error) {
      console.error(error)
      wx.showToast({
        title: '加载失败',
        icon: 'none'
      })
    } finally {
      wx.hideLoading()
    }
  },
  
  // 上拉加载更多
  async onReachBottom() {
    this.setData({
      page: this.data.page + 1
    })
    
    try {
      const data = await http.get('/products', {
        page: this.data.page,
        size: 10
      })
      
      this.setData({
        products: [...this.data.products, ...data.list]
      })
    } catch (error) {
      console.error(error)
    }
  }
})

1.4 请求拦截器

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

// 请求拦截器
function requestInterceptor(options) {
  // 添加token
  const token = wx.getStorageSync('token')
  if (token) {
    options.header = {
      ...options.header,
      'Authorization': 'Bearer ' + token
    }
  }
  
  // 添加时间戳(防止缓存)
  if (options.method === 'GET') {
    options.data = {
      ...options.data,
      _t: Date.now()
    }
  }
  
  return options
}

// 响应拦截器
function responseInterceptor(res) {
  // 统一处理响应
  if (res.statusCode === 200) {
    // 业务成功
    if (res.data.code === 0) {
      return res.data
    }
    // 业务失败
    else {
      // token过期,重新登录
      if (res.data.code === 401) {
        wx.removeStorageSync('token')
        wx.reLaunch({
          url: '/pages/login/login'
        })
        return Promise.reject(new Error('登录已过期'))
      }
      
      // 其他业务错误
      wx.showToast({
        title: res.data.message || '请求失败',
        icon: 'none'
      })
      return Promise.reject(new Error(res.data.message))
    }
  }
  // HTTP错误
  else {
    wx.showToast({
      title: '网络错误',
      icon: 'none'
    })
    return Promise.reject(new Error('网络错误'))
  }
}

function request(options) {
  // 应用请求拦截器
  options = requestInterceptor(options)
  
  return new Promise((resolve, reject) => {
    wx.request({
      url: baseURL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'content-type': 'application/json',
        ...options.header
      },
      success: (res) => {
        // 应用响应拦截器
        responseInterceptor(res)
          .then(resolve)
          .catch(reject)
      },
      fail: (err) => {
        wx.showToast({
          title: '网络错误',
          icon: 'none'
        })
        reject(err)
      }
    })
  })
}

export const http = {
  get(url, data = {}) {
    return request({ url, method: 'GET', data })
  },
  
  post(url, data = {}) {
    return request({ url, method: 'POST', data })
  }
}

1.5 错误处理

// utils/errorHandler.js
export const errorHandler = {
  // 处理网络错误
  handleNetworkError(error) {
    if (error.errMsg.includes('timeout')) {
      wx.showToast({
        title: '请求超时',
        icon: 'none'
      })
    } else if (error.errMsg.includes('fail')) {
      wx.showToast({
        title: '网络连接失败',
        icon: 'none'
      })
    } else {
      wx.showToast({
        title: '网络错误',
        icon: 'none'
      })
    }
  },
  
  // 处理业务错误
  handleBusinessError(code, message) {
    switch (code) {
      case 400:
        wx.showToast({
          title: message || '请求参数错误',
          icon: 'none'
        })
        break
      case 401:
        wx.showToast({
          title: '登录已过期,请重新登录',
          icon: 'none'
        })
        setTimeout(() => {
          wx.reLaunch({
            url: '/pages/login/login'
          })
        }, 1500)
        break
      case 403:
        wx.showToast({
          title: '没有权限',
          icon: 'none'
        })
        break
      case 404:
        wx.showToast({
          title: '资源不存在',
          icon: 'none'
        })
        break
      case 500:
        wx.showToast({
          title: '服务器错误',
          icon: 'none'
        })
        break
      default:
        wx.showToast({
          title: message || '操作失败',
          icon: 'none'
        })
    }
  }
}

// 使用
import { errorHandler } from '../../utils/errorHandler.js'

Page({
  async loadData() {
    try {
      const data = await http.get('/products')
      this.setData({ products: data.list })
    } catch (error) {
      if (error.errMsg) {
        errorHandler.handleNetworkError(error)
      } else {
        errorHandler.handleBusinessError(error.code, error.message)
      }
    }
  }
})

1.6 API接口管理

// api/product.js
import { http } from '../utils/request.js'

export const productAPI = {
  // 获取商品列表
  getList(params) {
    return http.get('/products', params)
  },
  
  // 获取商品详情
  getDetail(id) {
    return http.get(`/products/${id}`)
  },
  
  // 创建商品
  create(data) {
    return http.post('/products', data)
  },
  
  // 更新商品
  update(id, data) {
    return http.put(`/products/${id}`, data)
  },
  
  // 删除商品
  delete(id) {
    return http.delete(`/products/${id}`)
  }
}

// 使用
import { productAPI } from '../../api/product.js'

Page({
  async loadProducts() {
    try {
      const data = await productAPI.getList({ page: 1, size: 10 })
      this.setData({ products: data.list })
    } catch (error) {
      console.error('加载商品失败', error)
    }
  }
})

2. 用户登录

2.1 微信登录流程

// app.js
App({
  globalData: {
    userInfo: null,
    token: ''
  },
  
  onLaunch() {
    this.login()
  },
  
  // 登录
  async login() {
    try {
      // 1. 获取code
      const loginRes = await wx.login()
      const code = loginRes.code
      
      // 2. 发送code到后端
      const res = await wx.request({
        url: 'https://api.example.com/login',
        method: 'POST',
        data: { code }
      })
      
      // 3. 保存token
      this.globalData.token = res.data.token
      wx.setStorageSync('token', res.data.token)
      
      // 4. 获取用户信息
      await this.getUserProfile()
    } catch (error) {
      console.error('登录失败', error)
    }
  },
  
  // 获取用户信息(需要用户授权)
  async getUserProfile() {
    try {
      const res = await wx.getUserProfile({
        desc: '用于完善会员资料'
      })
      
      this.globalData.userInfo = res.userInfo
      wx.setStorageSync('userInfo', res.userInfo)
      
      // 发送到后端
      await wx.request({
        url: 'https://api.example.com/user/profile',
        method: 'POST',
        data: res.userInfo,
        header: {
          'Authorization': 'Bearer ' + this.globalData.token
        }
      })
    } catch (error) {
      console.error('获取用户信息失败', error)
    }
  }
})

2.2 后端接口(Node.js示例)

// 后端:login接口
const axios = require('axios')
const jwt = require('jsonwebtoken')

app.post('/login', async (req, res) => {
  const { code } = req.body
  
  // 1. 用code换取openid和session_key
  const wechatRes = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
    params: {
      appid: 'your_appid',
      secret: 'your_secret',
      js_code: code,
      grant_type: 'authorization_code'
    }
  })
  
  const { openid, session_key } = wechatRes.data
  
  // 2. 查询或创建用户
  let user = await User.findOne({ openid })
  if (!user) {
    user = await User.create({ openid })
  }
  
  // 3. 生成JWT token
  const token = jwt.sign(
    { userId: user.id, openid },
    'your_secret_key',
    { expiresIn: '7d' }
  )
  
  res.json({ token, userId: user.id })
})

3. 云开发

3.1 初始化云开发

// app.js
App({
  onLaunch() {
    // 初始化云开发
    wx.cloud.init({
      env: 'your-env-id',  // 云开发环境ID
      traceUser: true
    })
  }
})

3.2 云数据库

创建数据

// pages/add/add.js
Page({
  async onAdd() {
    const db = wx.cloud.database()
    
    try {
      const res = await db.collection('todos').add({
        data: {
          title: '学习小程序',
          done: false,
          createTime: db.serverDate()  // 服务器时间
        }
      })
      
      console.log('添加成功', res._id)
      wx.showToast({ title: '添加成功' })
    } catch (error) {
      console.error('添加失败', error)
    }
  }
})

查询数据

Page({
  async loadTodos() {
    const db = wx.cloud.database()
    const _ = db.command
    
    try {
      // 简单查询
      const res = await db.collection('todos')
        .where({
          done: false
        })
        .orderBy('createTime', 'desc')
        .limit(20)
        .get()
      
      this.setData({
        todos: res.data
      })
      
      // 条件查询
      const res2 = await db.collection('todos')
        .where({
          // 等于
          status: 'active',
          // 大于
          score: _.gt(60),
          // 包含
          tags: _.in(['work', 'study'])
        })
        .get()
      
      // 正则匹配
      const res3 = await db.collection('todos')
        .where({
          title: db.RegExp({
            regexp: '学习',
            options: 'i'  // 不区分大小写
          })
        })
        .get()
    } catch (error) {
      console.error('查询失败', error)
    }
  }
})

更新数据

Page({
  async updateTodo(id) {
    const db = wx.cloud.database()
    
    try {
      await db.collection('todos').doc(id).update({
        data: {
          done: true,
          updateTime: db.serverDate()
        }
      })
      
      wx.showToast({ title: '更新成功' })
    } catch (error) {
      console.error('更新失败', error)
    }
  },
  
  // 数组操作
  async addTag(id) {
    const db = wx.cloud.database()
    const _ = db.command
    
    await db.collection('todos').doc(id).update({
      data: {
        tags: _.push(['new-tag'])  // 添加到数组
      }
    })
  }
})

删除数据

Page({
  async deleteTodo(id) {
    const db = wx.cloud.database()
    
    try {
      await db.collection('todos').doc(id).remove()
      wx.showToast({ title: '删除成功' })
    } catch (error) {
      console.error('删除失败', error)
    }
  }
})

3.3 云存储

Page({
  // 上传图片
  async uploadImage() {
    try {
      // 选择图片
      const chooseRes = await wx.chooseImage({
        count: 1,
        sizeType: ['compressed'],
        sourceType: ['album', 'camera']
      })
      
      const filePath = chooseRes.tempFilePaths[0]
      
      // 上传到云存储
      const uploadRes = await wx.cloud.uploadFile({
        cloudPath: `images/${Date.now()}-${Math.random()}.png`,
        filePath: filePath
      })
      
      console.log('上传成功', uploadRes.fileID)
      
      // 获取临时链接
      const tempURLRes = await wx.cloud.getTempFileURL({
        fileList: [uploadRes.fileID]
      })
      
      const imageURL = tempURLRes.fileList[0].tempFileURL
      this.setData({ imageURL })
    } catch (error) {
      console.error('上传失败', error)
    }
  },
  
  // 下载文件
  async downloadFile(fileID) {
    try {
      const res = await wx.cloud.downloadFile({
        fileID: fileID
      })
      
      console.log('临时文件路径', res.tempFilePath)
    } catch (error) {
      console.error('下载失败', error)
    }
  },
  
  // 删除文件
  async deleteFile(fileID) {
    try {
      await wx.cloud.deleteFile({
        fileList: [fileID]
      })
      
      wx.showToast({ title: '删除成功' })
    } catch (error) {
      console.error('删除失败', error)
    }
  }
})

3.4 云函数

创建云函数

// cloudfunctions/getProducts/index.js
const cloud = require('wx-server-sdk')
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV })

const db = cloud.database()

exports.main = async (event, context) => {
  const { page = 1, size = 10 } = event
  
  try {
    // 可以突破小程序端20条的限制
    const res = await db.collection('products')
      .skip((page - 1) * size)
      .limit(size)
      .orderBy('createTime', 'desc')
      .get()
    
    return {
      code: 0,
      data: res.data,
      total: res.data.length
    }
  } catch (error) {
    return {
      code: -1,
      error: error.message
    }
  }
}

调用云函数

// pages/index/index.js
Page({
  async loadProducts() {
    try {
      wx.showLoading({ title: '加载中' })
      
      const res = await wx.cloud.callFunction({
        name: 'getProducts',
        data: {
          page: 1,
          size: 20
        }
      })
      
      if (res.result.code === 0) {
        this.setData({
          products: res.result.data
        })
      }
    } catch (error) {
      console.error('调用失败', error)
    } finally {
      wx.hideLoading()
    }
  }
})

4. WebSocket

// pages/chat/chat.js
Page({
  data: {
    messages: [],
    socketOpen: false
  },
  
  onLoad() {
    this.connectWebSocket()
  },
  
  onUnload() {
    this.closeWebSocket()
  },
  
  // 连接WebSocket
  connectWebSocket() {
    wx.connectSocket({
      url: 'wss://example.com/ws',
      header: {
        'Authorization': 'Bearer ' + wx.getStorageSync('token')
      }
    })
    
    // 连接成功
    wx.onSocketOpen(() => {
      console.log('WebSocket连接已打开')
      this.setData({ socketOpen: true })
      
      // 发送心跳
      this.startHeartbeat()
    })
    
    // 接收消息
    wx.onSocketMessage((res) => {
      const message = JSON.parse(res.data)
      this.setData({
        messages: [...this.data.messages, message]
      })
    })
    
    // 连接关闭
    wx.onSocketClose(() => {
      console.log('WebSocket连接已关闭')
      this.setData({ socketOpen: false })
      
      // 重连
      setTimeout(() => {
        this.connectWebSocket()
      }, 3000)
    })
    
    // 错误
    wx.onSocketError((error) => {
      console.error('WebSocket错误', error)
    })
  },
  
  // 发送消息
  sendMessage(content) {
    if (!this.data.socketOpen) {
      wx.showToast({
        title: '连接已断开',
        icon: 'none'
      })
      return
    }
    
    wx.sendSocketMessage({
      data: JSON.stringify({
        type: 'message',
        content: content
      })
    })
  },
  
  // 心跳
  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.data.socketOpen) {
        wx.sendSocketMessage({
          data: JSON.stringify({ type: 'ping' })
        })
      }
    }, 30000)  // 30秒
  },
  
  // 关闭连接
  closeWebSocket() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
    }
    wx.closeSocket()
  }
})

5. 文件操作

5.1 图片选择和上传

Page({
  async chooseAndUpload() {
    try {
      // 选择图片
      const chooseRes = await wx.chooseImage({
        count: 9,
        sizeType: ['compressed'],
        sourceType: ['album', 'camera']
      })
      
      const tempFilePaths = chooseRes.tempFilePaths
      
      // 批量上传
      const uploadPromises = tempFilePaths.map(filePath => {
        return wx.uploadFile({
          url: 'https://api.example.com/upload',
          filePath: filePath,
          name: 'file',
          header: {
            'Authorization': 'Bearer ' + wx.getStorageSync('token')
          }
        })
      })
      
      const results = await Promise.all(uploadPromises)
      
      const imageUrls = results.map(res => {
        const data = JSON.parse(res.data)
        return data.url
      })
      
      this.setData({ images: imageUrls })
      wx.showToast({ title: '上传成功' })
    } catch (error) {
      console.error('上传失败', error)
    }
  }
})

5.2 图片预览

Page({
  previewImage(current) {
    wx.previewImage({
      current: current,
      urls: this.data.images
    })
  }
})

6. 常见问题

Q1: 如何配置合法域名?

A:

  1. 登录小程序后台

  2. 开发 → 开发管理 → 开发设置 → 服务器域名

  3. 配置request、uploadFile、downloadFile、socket域名

Q2: 云开发和传统后端的区别?

A:

  • 云开发:无需搭建服务器,按量付费,快速开发

  • 传统后端:自己搭建,成本高,灵活性强

Q3: 如何处理并发请求?

A:

Promise.all([
  request1(),
  request2(),
  request3()
]).then(results => {
  // 所有请求完成
})

参考资源

  • 微信小程序网络API文档

  • 云开发官方文档

  • WebSocket协议规范