概述

uni-app 是一个使用 Vue.js 开发跨平台应用的前端框架,uni.request 是其提供的网络请求 API。本文将详细介绍如何在 uni-app 项目中对 uni.request 进行封装,实现统一管理、错误处理、拦截器等功能。

uni.request 基础使用

基本请求示例

// GET 请求
uni.request({
  url: 'https://api.example.com/users',
  method: 'GET',
  success: (res) => {
    console.log(res.data)
  },
  fail: (err) => {
    console.error(err)
  }
})

// POST 请求
uni.request({
  url: 'https://api.example.com/users',
  method: 'POST',
  data: {
    name: 'John Doe',
    email: 'john@example.com'
  },
  success: (res) => {
    console.log(res.data)
  },
  fail: (err) => {
    console.error(err)
  }
})

Promise 封装基础版本

// 基础 Promise 封装
function request(options) {
  return new Promise((resolve, reject) => {
    uni.request({
      ...options,
      success: (res) => {
        resolve(res)
      },
      fail: (err) => {
        reject(err)
      }
    })
  })
}

// 使用示例
request({
  url: '/api/users',
  method: 'GET'
})
.then(res => {
  console.log(res.data)
})
.catch(err => {
  console.error(err)
})

完整的请求封装

基础封装类

// utils/request.js
class HttpRequest {
  constructor(config = {}) {
    this.config = {
      baseURL: '',
      timeout: 60000,
      header: {
        'Content-Type': 'application/json;charset=utf-8'
      },
      ...config
    }
    
    // 请求队列
    this.queue = []
    
    // 拦截器
    this.interceptors = {
      request: [],
      response: []
    }
  }
  
  // 添加请求拦截器
  addRequestInterceptor(fulfilled, rejected) {
    this.interceptors.request.push({ fulfilled, rejected })
  }
  
  // 添加响应拦截器
  addResponseInterceptor(fulfilled, rejected) {
    this.interceptors.response.push({ fulfilled, rejected })
  }
  
  // 执行请求拦截器
  async runRequestInterceptors(config) {
    for (const interceptor of this.interceptors.request) {
      try {
        config = await interceptor.fulfilled(config)
      } catch (error) {
        if (interceptor.rejected) {
          throw await interceptor.rejected(error)
        }
        throw error
      }
    }
    return config
  }
  
  // 执行响应拦截器
  async runResponseInterceptors(response) {
    for (const interceptor of this.interceptors.response) {
      try {
        response = await interceptor.fulfilled(response)
      } catch (error) {
        if (interceptor.rejected) {
          throw await interceptor.rejected(error)
        }
        throw error
      }
    }
    return response
  }
  
  // 基础请求方法
  async request(options) {
    // 合并配置
    const config = {
      ...this.config,
      ...options,
      url: this.config.baseURL + (options.url || '')
    }
    
    // 执行请求拦截器
    const processedConfig = await this.runRequestInterceptors(config)
    
    return new Promise((resolve, reject) => {
      // 显示加载提示
      if (processedConfig.showLoading !== false) {
        this.showLoading(processedConfig.loadingText)
      }
      
      uni.request({
        ...processedConfig,
        success: async (res) => {
          try {
            // 执行响应拦截器
            const processedResponse = await this.runResponseInterceptors(res)
            resolve(processedResponse)
          } catch (error) {
            reject(error)
          } finally {
            this.hideLoading()
          }
        },
        fail: async (err) => {
          try {
            const processedError = await this.runResponseInterceptors(err)
            reject(processedError)
          } catch (error) {
            reject(error)
          } finally {
            this.hideLoading()
          }
        }
      })
    })
  }
  
  // GET 请求
  get(url, params = {}, config = {}) {
    return this.request({
      url,
      method: 'GET',
      data: params,
      ...config
    })
  }
  
  // POST 请求
  post(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'POST',
      data,
      ...config
    })
  }
  
  // PUT 请求
  put(url, data = {}, config = {}) {
    return this.request({
      url,
      method: 'PUT',
      data,
      ...config
    })
  }
  
  // DELETE 请求
  delete(url, params = {}, config = {}) {
    return this.request({
      url,
      method: 'DELETE',
      data: params,
      ...config
    })
  }
  
  // 显示加载提示
  showLoading(title = '加载中...') {
    this.queue.push(1)
    if (this.queue.length === 1) {
      uni.showLoading({
        title,
        mask: true
      })
    }
  }
  
  // 隐藏加载提示
  hideLoading() {
    this.queue.pop()
    if (this.queue.length === 0) {
      uni.hideLoading()
    }
  }
}

export default HttpRequest

完整配置实例

// utils/http.js
import HttpRequest from './request'
import { getToken, removeToken } from './auth'

// 创建请求实例
const http = new HttpRequest({
  baseURL: 'https://api.example.com',
  timeout: 30000,
  header: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 请求拦截器
http.addRequestInterceptor(
  (config) => {
    // 添加 token
    const token = getToken()
    if (token) {
      config.header.Authorization = `Bearer ${token}`
    }
    
    // 添加时间戳
    if (config.method === 'GET') {
      config.data = {
        ...config.data,
        _t: Date.now()
      }
    }
    
    // 开发环境打印请求信息
    if (process.env.NODE_ENV === 'development') {
      console.log('Request:', {
        url: config.url,
        method: config.method,
        data: config.data,
        header: config.header
      })
    }
    
    return config
  },
  (error) => {
    console.error('Request error:', error)
    return Promise.reject(error)
  }
)

// 响应拦截器
http.addResponseInterceptor(
  (response) => {
    const { data, statusCode } = response
    
    // 开发环境打印响应信息
    if (process.env.NODE_ENV === 'development') {
      console.log('Response:', {
        statusCode,
        data
      })
    }
    
    // 处理 HTTP 状态码
    if (statusCode !== 200) {
      return Promise.reject(new Error(`HTTP Error: ${statusCode}`))
    }
    
    // 处理业务状态码
    if (data.code === 200 || data.success === true) {
      return data
    } else {
      // 处理业务错误
      uni.showToast({
        title: data.message || '请求失败',
        icon: 'none',
        duration: 2000
      })
      return Promise.reject(new Error(data.message || '请求失败'))
    }
  },
  (error) => {
    console.error('Response error:', error)
    
    // 处理网络错误
    let message = '网络错误,请检查网络连接'
    
    if (error.errMsg) {
      if (error.errMsg.includes('timeout')) {
        message = '请求超时,请重试'
      } else if (error.errMsg.includes('fail')) {
        message = '网络连接失败'
      }
    }
    
    uni.showToast({
      title: message,
      icon: 'none',
      duration: 2000
    })
    
    return Promise.reject(error)
  }
)

export default http

API 模块封装

RESTful API 封装

// api/index.js
import http from '@/utils/http'

// 基础请求方法
export const api = {
  get(url, params = {}, config = {}) {
    return http.get(url, params, config)
  },
  
  post(url, data = {}, config = {}) {
    return http.post(url, data, config)
  },
  
  put(url, data = {}, config = {}) {
    return http.put(url, data, config)
  },
  
  delete(url, params = {}, config = {}) {
    return http.delete(url, params, config)
  },
  
  // 文件上传
  upload(url, filePath, formData = {}, config = {}) {
    return new Promise((resolve, reject) => {
      uni.showLoading({
        title: '上传中...',
        mask: true
      })
      
      uni.uploadFile({
        url: http.config.baseURL + url,
        filePath,
        name: 'file',
        formData,
        header: {
          'Authorization': `Bearer ${getToken()}`
        },
        ...config,
        success: (res) => {
          try {
            const data = JSON.parse(res.data)
            if (data.code === 200) {
              resolve(data)
            } else {
              uni.showToast({
                title: data.message || '上传失败',
                icon: 'none'
              })
              reject(new Error(data.message || '上传失败'))
            }
          } catch (error) {
            reject(error)
          }
        },
        fail: (err) => {
          uni.showToast({
            title: '上传失败',
            icon: 'none'
          })
          reject(err)
        },
        complete: () => {
          uni.hideLoading()
        }
      })
    })
  },
  
  // 文件下载
  download(url, params = {}, config = {}) {
    return new Promise((resolve, reject) => {
      const downloadUrl = http.config.baseURL + url + '?' + new URLSearchParams(params).toString()
      
      uni.downloadFile({
        url: downloadUrl,
        header: {
          'Authorization': `Bearer ${getToken()}`
        },
        ...config,
        success: (res) => {
          if (res.statusCode === 200) {
            resolve(res.tempFilePath)
          } else {
            reject(new Error(`下载失败: ${res.statusCode}`))
          }
        },
        fail: (err) => {
          reject(err)
        }
      })
    })
  }
}

业务模块 API

// api/user.js
import { api } from './index'

export const userApi = {
  // 用户登录
  login(data) {
    return api.post('/auth/login', data)
  },
  
  // 用户登出
  logout() {
    return api.post('/auth/logout')
  },
  
  // 获取用户信息
  getUserInfo() {
    return api.get('/auth/userinfo')
  },
  
  // 获取用户列表
  getUserList(params) {
    return api.get('/users', params)
  },
  
  // 获取用户详情
  getUserDetail(id) {
    return api.get(`/users/${id}`)
  },
  
  // 创建用户
  createUser(data) {
    return api.post('/users', data)
  },
  
  // 更新用户
  updateUser(id, data) {
    return api.put(`/users/${id}`, data)
  },
  
  // 删除用户
  deleteUser(id) {
    return api.delete(`/users/${id}`)
  },
  
  // 修改密码
  changePassword(data) {
    return api.post('/auth/change-password', data)
  },
  
  // 上传头像
  uploadAvatar(filePath) {
    return api.upload('/user/avatar', filePath)
  }
}

// api/article.js
export const articleApi = {
  // 获取文章列表
  getArticleList(params) {
    return api.get('/articles', params)
  },
  
  // 获取文章详情
  getArticleDetail(id) {
    return api.get(`/articles/${id}`)
  },
  
  // 创建文章
  createArticle(data) {
    return api.post('/articles', data)
  },
  
  // 更新文章
  updateArticle(id, data) {
    return api.put(`/articles/${id}`, data)
  },
  
  // 删除文章
  deleteArticle(id) {
    return api.delete(`/articles/${id}`)
  },
  
  // 点赞文章
  likeArticle(id) {
    return api.post(`/articles/${id}/like`)
  },
  
  // 收藏文章
  collectArticle(id) {
    return api.post(`/articles/${id}/collect`)
  }
}

Vue 组合式 API 封装

基础 Composable

// composables/useRequest.js
import { ref, reactive } from 'vue'
import { api } from '@/api'

export function useRequest() {
  const loading = ref(false)
  const error = ref(null)
  
  const request = async (apiMethod, ...args) => {
    loading.value = true
    error.value = null
    
    try {
      const result = await apiMethod(...args)
      return result
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    loading,
    error,
    request,
    get: (...args) => request(api.get, ...args),
    post: (...args) => request(api.post, ...args),
    put: (...args) => request(api.put, ...args),
    delete: (...args) => request(api.delete, ...args)
  }
}

分页请求 Composable

// composables/usePagination.js
import { ref, reactive, computed } from 'vue'
import { useRequest } from './useRequest'

export function usePagination(apiMethod, defaultParams = {}) {
  const { loading, error, request } = useRequest()
  
  const data = ref([])
  const total = ref(0)
  const currentPage = ref(1)
  const pageSize = ref(10)
  const params = reactive({ ...defaultParams })
  
  const hasMore = computed(() => {
    return currentPage.value * pageSize.value < total.value
  })
  
  const fetchData = async (page = 1, isLoadMore = false) => {
    if (isLoadMore && !hasMore.value) return
    
    currentPage.value = page
    
    const requestParams = {
      page: currentPage.value,
      pageSize: pageSize.value,
      ...params
    }
    
    try {
      const result = await request(apiMethod, requestParams)
      
      if (isLoadMore) {
        data.value = [...data.value, ...(result.data || result.list || [])]
      } else {
        data.value = result.data || result.list || []
      }
      
      total.value = result.total || result.count || 0
    } catch (err) {
      console.error('Pagination error:', err)
    }
  }
  
  const refresh = () => {
    currentPage.value = 1
    fetchData()
  }
  
  const loadMore = () => {
    if (hasMore.value && !loading.value) {
      fetchData(currentPage.value + 1, true)
    }
  }
  
  const reset = () => {
    currentPage.value = 1
    Object.assign(params, defaultParams)
    data.value = []
    total.value = 0
    fetchData()
  }
  
  return {
    loading,
    error,
    data,
    total,
    currentPage,
    pageSize,
    params,
    hasMore,
    fetchData,
    refresh,
    loadMore,
    reset
  }
}

在页面中使用

<!-- pages/user/list.vue -->
<template>
  <view class="user-list">
    <!-- 搜索栏 -->
    <view class="search-bar">
      <input 
        v-model="params.keyword" 
        placeholder="搜索用户"
        @confirm="reset"
      />
    </view>
    
    <!-- 用户列表 -->
    <view class="user-item" v-for="user in data" :key="user.id">
      <image :src="user.avatar" mode="aspectFill" />
      <view class="user-info">
        <text class="name">{{ user.name }}</text>
        <text class="email">{{ user.email }}</text>
      </view>
    </view>
    
    <!-- 加载状态 -->
    <view v-if="loading" class="loading">
      <text>加载中...</text>
    </view>
    
    <!-- 加载更多 -->
    <view v-if="hasMore && !loading" class="load-more" @tap="loadMore">
      <text>加载更多</text>
    </view>
    
    <!-- 没有更多数据 -->
    <view v-if="!hasMore && data.length > 0" class="no-more">
      <text>没有更多数据了</text>
    </view>
  </view>
</template>

<script setup>
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { userApi } from '@/api/user'
import { usePagination } from '@/composables/usePagination'

const {
  loading,
  data,
  hasMore,
  params,
  fetchData,
  loadMore,
  reset
} = usePagination(userApi.getUserList, {
  status: 'active'
})

// 页面加载
onLoad(() => {
  fetchData()
})

// 触底加载更多
onReachBottom(() => {
  loadMore()
})
</script>

<style scoped>
.user-list {
  padding: 20rpx;
}

.search-bar input {
  width: 100%;
  height: 80rpx;
  background: #f5f5f5;
  border-radius: 40rpx;
  padding: 0 30rpx;
  margin-bottom: 30rpx;
}

.user-item {
  display: flex;
  align-items: center;
  padding: 20rpx 0;
  border-bottom: 1rpx solid #eee;
}

.user-item image {
  width: 80rpx;
  height: 80rpx;
  border-radius: 50%;
  margin-right: 20rpx;
}

.user-info {
  flex: 1;
}

.name {
  font-size: 32rpx;
  font-weight: bold;
  margin-bottom: 10rpx;
}

.email {
  font-size: 26rpx;
  color: #666;
}

.loading, .load-more, .no-more {
  text-align: center;
  padding: 30rpx 0;
  color: #999;
}

.load-more {
  color: #007aff;
}
</style>

环境配置

多环境配置

// config/index.js
const config = {
  development: {
    baseURL: 'http://localhost:3000/api',
    timeout: 30000,
    mock: true
  },
  
  production: {
    baseURL: 'https://api.example.com',
    timeout: 15000,
    mock: false
  },
  
  staging: {
    baseURL: 'https://staging-api.example.com',
    timeout: 20000,
    mock: false
  }
}

// 获取当前环境配置
export const getCurrentConfig = () => {
  // #ifdef H5
  const env = process.env.NODE_ENV
  // #endif
  
  // #ifndef H5
  // 小程序环境通过编译条件判断
  const env = process.env.NODE_ENV || 'development'
  // #endif
  
  return config[env] || config.development
}

export default getCurrentConfig()

动态配置

// utils/http.js
import config from '@/config'

const http = new HttpRequest({
  baseURL: config.baseURL,
  timeout: config.timeout
})

// 根据平台动态调整
// #ifdef MP-WEIXIN
http.config.header['content-type'] = 'application/json'
// #endif

// #ifdef H5
http.config.header['X-Requested-With'] = 'XMLHttpRequest'
// #endif

Mock 数据

Mock 服务

// mock/index.js
const mockData = {
  '/api/auth/login': {
    method: 'POST',
    data: {
      code: 200,
      message: '登录成功',
      data: {
        token: 'mock-token-123456',
        userInfo: {
          id: 1,
          name: '测试用户',
          email: 'test@example.com',
          avatar: 'https://picsum.photos/100/100'
        }
      }
    }
  },
  
  '/api/users': {
    method: 'GET',
    data: {
      code: 200,
      message: '获取成功',
      data: [
        {
          id: 1,
          name: '张三',
          email: 'zhangsan@example.com',
          avatar: 'https://picsum.photos/100/100'
        },
        {
          id: 2,
          name: '李四',
          email: 'lisi@example.com',
          avatar: 'https://picsum.photos/100/100'
        }
      ],
      total: 2
    }
  }
}

// Mock 拦截器
export function setupMock() {
  if (!config.mock) return
  
  // 重写 uni.request
  const originalRequest = uni.request
  
  uni.request = function(options) {
    const mockItem = mockData[options.url]
    
    if (mockItem && mockItem.method === options.method) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            statusCode: 200,
            data: mockItem.data
          })
        }, Math.random() * 500 + 200) // 模拟网络延迟
      })
    }
    
    return originalRequest.call(this, options)
  }
}

高级功能

请求重试

// utils/retry.js
export class RetryRequest {
  constructor(maxRetries = 3, retryDelay = 1000) {
    this.maxRetries = maxRetries
    this.retryDelay = retryDelay
  }
  
  async request(requestFn, ...args) {
    let lastError
    
    for (let i = 0; i <= this.maxRetries; i++) {
      try {
        return await requestFn(...args)
      } catch (error) {
        lastError = error
        
        if (i === this.maxRetries) {
          break
        }
        
        // 判断是否需要重试
        if (!this.shouldRetry(error)) {
          break
        }
        
        // 等待后重试
        await this.delay(this.retryDelay * Math.pow(2, i))
      }
    }
    
    throw lastError
  }
  
  shouldRetry(error) {
    // 网络错误或 5xx 错误时重试
    return !error.statusCode || error.statusCode >= 500
  }
  
  delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms))
  }
}

请求队列管理

// utils/queue.js
export class RequestQueue {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent
    this.queue = []
    this.running = 0
  }
  
  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        requestFn,
        resolve,
        reject
      })
      
      this.process()
    })
  }
  
  async process() {
    if (this.running >= this.maxConcurrent || this.queue.length === 0) {
      return
    }
    
    this.running++
    
    const { requestFn, resolve, reject } = this.queue.shift()
    
    try {
      const result = await requestFn()
      resolve(result)
    } catch (error) {
      reject(error)
    } finally {
      this.running--
      this.process()
    }
  }
}

缓存管理

// utils/cache.js
export class RequestCache {
  constructor(ttl = 5 * 60 * 1000) { // 默认5分钟
    this.cache = new Map()
    this.ttl = ttl
  }
  
  get(key) {
    const item = this.cache.get(key)
    
    if (!item) {
      return null
    }
    
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key)
      return null
    }
    
    return item.data
  }
  
  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
  }
  
  clear() {
    this.cache.clear()
  }
  
  delete(key) {
    this.cache.delete(key)
  }
  
  // 生成缓存键
  generateKey(url, method, data) {
    return `${method}:${url}:${JSON.stringify(data)}`
  }
}

TypeScript 支持

类型定义

// types/api.ts
export interface ApiResponse<T = any> {
  code: number
  message: string
  data: T
  success: boolean
}

export interface PaginationParams {
  page: number
  pageSize: number
  [key: string]: any
}

export interface PaginationResponse<T> {
  data: T[]
  total: number
  page: number
  pageSize: number
}

export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: string
  updatedAt: string
}

export interface RequestConfig {
  url?: string
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  data?: any
  header?: Record<string, string>
  timeout?: number
  showLoading?: boolean
  loadingText?: string
}

类型化封装

// utils/http.ts
import type { RequestConfig, ApiResponse } from '@/types/api'

class HttpRequest {
  // ... 其他代码
  
  get<T = any>(url: string, params = {}, config: RequestConfig = {}): Promise<ApiResponse<T>> {
    return this.request({
      url,
      method: 'GET',
      data: params,
      ...config
    })
  }
  
  post<T = any>(url: string, data = {}, config: RequestConfig = {}): Promise<ApiResponse<T>> {
    return this.request({
      url,
      method: 'POST',
      data,
      ...config
    })
  }
  
  // ... 其他方法
}

最佳实践

1. 项目结构

src/
├── api/
│   ├── index.js          # 基础API方法
│   ├── user.js           # 用户相关API
│   ├── article.js        # 文章相关API
│   └── modules/          # 其他模块API
├── utils/
│   ├── request.js        # 请求类封装
│   ├── http.js           # HTTP实例配置
│   ├── auth.js           # 认证工具
│   ├── cache.js          # 缓存工具
│   ├── retry.js          # 重试工具
│   └── queue.js          # 队列管理
├── composables/
│   ├── useRequest.js     # 请求组合式API
│   └── usePagination.js  # 分页组合式API
├── config/
│   └── index.js          # 环境配置
└── types/
    └── api.ts            # TypeScript类型定义

2. 使用建议

  • 统一的错误处理和用户提示
  • 合理使用加载状态和缓存
  • 根据平台特性进行适配
  • 做好请求重试和队列管理
  • 使用 TypeScript 提高代码质量

3. 性能优化

  • 避免重复请求
  • 合理使用缓存
  • 控制并发请求数量
  • 及时清理不需要的数据