uni-app封装网络请求
概述
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. 性能优化
- 避免重复请求
- 合理使用缓存
- 控制并发请求数量
- 及时清理不需要的数据
7 0
评论 (0)
请先登录后再评论
暂无评论