Chapter 07

网络与数据

封装生产级 HTTP 请求层,掌握 uniCloud 云开发的数据库与云函数体系

7.1 uni.request 基础

uni.request 是 uni-app 提供的跨端 HTTP 请求 API,在 H5 端封装为 XMLHttpRequest,在 App 端使用原生 HTTP 库,在小程序端调用各平台的网络 API。

// 基础用法(Callback 风格)
uni.request({
  url: 'https://api.example.com/users/1',
  method: 'GET',
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  },
  success(res) {
    console.log(res.data)   // 响应数据
    console.log(res.statusCode)  // HTTP 状态码
  },
  fail(err) {
    console.error(err)
  }
})

// Promise 风格(uni-app 官方支持)
const res = await uni.request({
  url: 'https://api.example.com/users/1'
})
console.log(res.data)
小程序合法域名

微信小程序要求所有网络请求的域名必须在微信公众平台的「合法域名」中配置。开发阶段可以在微信开发者工具的「详情 → 本地设置」中勾选"不校验合法域名",但发布前必须正式配置

7.2 生产级请求封装

直接使用 uni.request 存在很多问题:每次都要写 baseURL、token、错误处理等重复代码。封装一个请求实例是工程化的必要步骤。

// utils/request.ts — 生产级请求封装
import { useUserStore } from '@/stores/user'

// 响应数据格式约定
interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
}

const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://api.example.com'
const TIMEOUT = 15000  // 15 秒超时

function request<T = any>(
  url: string,
  options: Partial<UniApp.RequestOptions> = {}
): Promise<T> {
  return new Promise((resolve, reject) => {
    const userStore = useUserStore()

    // 请求拦截:添加 token
    const header: Record<string, string> = {
      'Content-Type': 'application/json',
      ...options.header as Record<string, string>
    }
    if (userStore.token) {
      header['Authorization'] = `Bearer ${userStore.token}`
    }

    uni.request({
      url: `${BASE_URL}${url}`,
      timeout: TIMEOUT,
      ...options,
      header,
      success(res) {
        // 响应拦截:统一处理 HTTP 状态码
        if (res.statusCode === 401) {
          userStore.logout()  // token 过期,强制退出登录
          reject(new Error('登录已过期'))
          return
        }

        if (res.statusCode >= 400) {
          reject(new Error(`HTTP ${res.statusCode}`))
          return
        }

        const body = res.data as ApiResponse<T>

        // 业务错误码处理
        if (body.code !== 0) {
          uni.showToast({ title: body.message, icon: 'none' })
          reject(new Error(body.message))
          return
        }

        resolve(body.data)
      },
      fail(err) {
        // 网络错误(超时、无网络等)
        if (err.errMsg?.includes('timeout')) {
          uni.showToast({ title: '请求超时,请重试', icon: 'none' })
        } else {
          uni.showToast({ title: '网络异常,请检查网络', icon: 'none' })
        }
        reject(err)
      }
    })
  })
}

// 导出 HTTP 方法
export const http = {
  get<T>(url: string, params?: Record<string, any>) {
    return request<T>(url, { method: 'GET', data: params })
  },
  post<T>(url: string, data?: Record<string, any>) {
    return request<T>(url, { method: 'POST', data })
  },
  put<T>(url: string, data?: Record<string, any>) {
    return request<T>(url, { method: 'PUT', data })
  },
  delete<T>(url: string) {
    return request<T>(url, { method: 'DELETE' })
  }
}

// 使用
const user = await http.get<UserInfo>('/api/users/me')
const order = await http.post<Order>('/api/orders', { productId: 42 })

7.3 文件上传与下载

// 选择图片并上传
async function uploadAvatar() {
  // 1. 选择图片
  const { tempFilePaths } = await uni.chooseImage({
    count: 1,
    sizeType: ['compressed'],
    sourceType: ['album', 'camera']
  })

  // 2. 显示上传进度
  uni.showLoading({ title: '上传中...' })

  try {
    // 3. 上传文件
    const userStore = useUserStore()
    const res = await uni.uploadFile({
      url: 'https://api.example.com/upload/avatar',
      filePath: tempFilePaths[0],
      name: 'file',
      header: { 'Authorization': `Bearer ${userStore.token}` },
      formData: { type: 'avatar' }
    })
    const data = JSON.parse(res.data) as { url: string }
    console.log('上传成功:', data.url)
  } finally {
    uni.hideLoading()
  }
}

// 下载文件并预览
async function downloadPDF(url: string) {
  const res = await uni.downloadFile({ url })
  if (res.statusCode === 200) {
    await uni.openDocument({
      filePath: res.tempFilePath,
      fileType: 'pdf'
    })
  }
}

7.4 uniCloud 云开发

uniCloud 是 DCloud 推出的云开发平台,提供数据库、云函数、文件存储等服务,类似微信云开发但跨平台。它让开发者无需自建服务器,即可完成完整的前后端开发。

云函数

云函数是运行在服务端的 JavaScript 函数,可以安全地访问数据库(绕过客户端的安全限制)。

// cloudfunctions/getUserList/index.js
'use strict'
const db = uniCloud.database()

exports.main = async (event, context) => {
  // event:客户端传来的参数
  // context:请求上下文(uid、appId 等)
  const { page = 1, pageSize = 20, keyword } = event

  const collection = db.collection('users')
  let query = collection

  if (keyword) {
    // 正则搜索
    query = query.where({
      name: db.RegExp({ regexp: keyword, options: 'i' })
    })
  }

  const [total, list] = await Promise.all([
    query.count(),
    query.skip((page - 1) * pageSize).limit(pageSize).get()
  ])

  return {
    code: 0,
    data: {
      list: list.data,
      total: total.total,
      hasMore: page * pageSize < total.total
    }
  }
}

在客户端调用云函数

// 调用云函数
const result = await uniCloud.callFunction({
  name: 'getUserList',
  data: { page: 1, pageSize: 20, keyword: '张' }
})

if (result.result.code === 0) {
  list.value = result.result.data.list
}

云数据库直接操作(clientDB)

clientDB 允许客户端直接操作数据库,通过数据库权限规则控制安全性:

// 客户端直接查询(需要在数据库权限中配置允许)
const db = uniCloud.database()
const articles = db.collection('articles')

// 查询列表(分页)
const res = await articles
  .where({ status: 'published' })
  .orderBy('createTime', 'desc')
  .skip(0)
  .limit(10)
  .field('title,summary,cover,createTime')  // 只返回需要的字段
  .get()

// 新增文档
await articles.add({
  title: '新文章',
  content: '...',
  status: 'draft',
  createTime: db.serverDate()  // 服务器时间
})

// 更新文档
await articles.doc('articleId123').update({
  views: db.command.inc(1)  // 原子操作:浏览量 +1
})

7.5 数据缓存策略

合理的缓存策略可以显著改善用户体验(快速显示内容)并减少不必要的网络请求:

// composables/useRequest.ts — 带缓存的请求 Composable
import { ref } from 'vue'
import { http } from '@/utils/request'
import { storage } from '@/utils/storage'

interface CacheItem<T> {
  data: T
  expireAt: number
}

export function useRequest<T>(
  cacheKey: string,
  fetcher: () => Promise<T>,
  ttl = 5 * 60 * 1000  // 默认缓存 5 分钟
) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  async function fetch(force = false) {
    // 检查缓存
    if (!force) {
      const cached = storage.get<CacheItem<T>>(cacheKey)
      if (cached && Date.now() < cached.expireAt) {
        data.value = cached.data
        return
      }
    }

    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
      // 写入缓存
      storage.set(cacheKey, { data: data.value, expireAt: Date.now() + ttl })
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  return { data, loading, error, fetch }
}

// 使用
const { data: banners, loading, fetch } = useRequest(
  'home_banners',
  () => http.get<Banner[]>('/api/banners'),
  10 * 60 * 1000  // 缓存 10 分钟
)
onLoad(() => fetch())

7.6 小结