Files
miniapp-template-ts/miniprogram/utils/request.ts
T

239 lines
6.4 KiB
TypeScript
Raw Normal View History

2026-01-09 09:35:44 +08:00
/**
* 网络请求封装 - 基于 axios-miniprogram
*
* 功能特性:
* - 自动添加 Token
* - 统一错误处理
* - 智能加载动画管理
* - 完整的 TypeScript 类型支持
* - 文件上传功能
*/
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios-miniprogram'
import type { StandardResponse } from '../api/types'
// ==================== 类型定义 ====================
/**
* 自定义请求配置接口
* 扩展 axios 的请求配置,添加自定义参数
*/
interface CustomRequestConfig extends AxiosRequestConfig {
loading?: boolean // 是否显示加载提示,默认 true
upload?: boolean // 是否为上传请求
}
/**
* 文件上传配置接口
*/
interface UploadOptions {
apiUrl: string // 接口路径
filePath: string // 文件本地路径
name?: string // 文件对应的 key,默认 'file'
formData?: Record<string, unknown> // 额外参数
loading?: boolean // 是否显示加载提示,默认 true
onUploadProgress?: (event: unknown) => void // 上传进度回调
}
// ==================== 基础配置 ====================
// 配置 baseURL
axios.defaults.baseURL = '你的后端域名'
// 配置超时时间(30秒)
axios.defaults.timeout = 30000
// 配置默认请求头
axios.defaults.headers.common['Content-Type'] = 'application/json'
// ==================== 请求白名单 ====================
/**
* 请求白名单
* 白名单内的接口不需要添加 Authorization 请求头
* 例如:登录接口、注册接口等
*/
const requestWhiteList: string[] = []
// ==================== 加载动画管理 ====================
/**
* 网络请求计数器
* 用于管理多个并发请求的加载状态
*/
let netWorkNum = 0
/**
* 开启加载动画
* 使用计数器机制,防止多个请求导致的 loading 闪烁
* @param title 加载提示文字
*/
function startLoading(title: string) {
if (netWorkNum === 0) {
wx.showLoading({
title: title,
mask: true,
})
}
netWorkNum++
}
/**
* 关闭加载动画
* 延迟 500ms 关闭,确保用户体验
*/
function endLoading() {
netWorkNum--
setTimeout(() => {
if (netWorkNum === 0) {
wx.hideLoading()
wx.stopPullDownRefresh() // 停止当前页面下拉刷新
}
}, 500)
}
// ==================== 请求拦截器 ====================
axios.interceptors.request.use(
(config: CustomRequestConfig) => {
// 加载提示
if (config.loading ?? true) {
startLoading('请稍等...')
}
// Content-Type 设置
// 注意:上传文件时 axios 会自动设置正确的 Content-Type
if (config.headers && !config.headers['Content-Type']) {
config.headers['Content-Type'] = 'application/json'
}
// Token 自动注入(非白名单接口)
if (!requestWhiteList.includes(config.url || '')) {
const app = getApp<IAppOption>()
if (app.globalData.token && config.headers) {
config.headers['Authorization'] = app.globalData.token
}
}
// 详细日志记录
/* eslint-disable no-console */
console.log('============发送请求 start============')
console.log(`[${new Date().toISOString()}] [INFO] 请求地址: ${config.baseURL}${config.url}`)
console.log(`[${new Date().toISOString()}] [INFO] 请求方式: ${config.method?.toUpperCase()}`)
console.log(`[${new Date().toISOString()}] [INFO] 上传模式: ${config.upload ? '是' : '否'}`)
console.log(`[${new Date().toISOString()}] [INFO] params参数: ${JSON.stringify(config.params)}`)
console.log(`[${new Date().toISOString()}] [INFO] data参数: ${JSON.stringify(config.data)}`)
console.log('============发送请求 end============')
/* eslint-enable no-console */
return config
},
(error) => {
// 请求错误处理
endLoading()
return Promise.reject(error)
},
)
// ==================== 响应拦截器 ====================
axios.interceptors.response.use(
(response: AxiosResponse) => {
// 关闭加载动画
const config = response.config as CustomRequestConfig
if (config.loading ?? true) {
endLoading()
}
// 标准格式:{ code, data, message }
const result = response.data as StandardResponse
if (result.code === 200) {
// 只返回 data 字段(修改 response.data 为提取后的 data
return { ...response, data: result.data } as unknown as AxiosResponse
} else {
// 业务错误处理
wx.showToast({
title: result.message || '请求失败',
icon: 'none',
})
return Promise.reject(new Error(result.message))
}
},
(error: unknown) => {
// 关闭加载动画
const axiosError = error as {
config?: CustomRequestConfig
response?: AxiosResponse
message?: string
}
const config = axiosError.config
if (config?.loading ?? true) {
endLoading()
}
// 401 未授权,跳转登录页
if (axiosError.response?.status === 401) {
wx.showToast({
title: '未授权,请重新登录',
icon: 'none',
})
wx.reLaunch({
url: '/pages/login/login',
})
return Promise.reject(error)
}
// 统一错误提示
const errorData = axiosError.response?.data as StandardResponse | undefined
const message = errorData?.message || errorData?.data || axiosError.message || '网络请求失败'
wx.showToast({
title: message as string,
icon: 'none',
})
return Promise.reject(error)
},
)
// ==================== 文件上传封装 ====================
/**
* 文件上传函数
* 基于 axios-miniprogram 内置的 upload 功能
* @param options 上传配置
* @returns Promise<T> 返回 data 字段的类型
*/
function upload<T = unknown>(options: UploadOptions): Promise<T> {
const {
apiUrl,
filePath,
name = 'file',
formData = {},
loading = true,
onUploadProgress,
} = options
return axios({
url: apiUrl,
method: 'POST',
upload: true, // 启用上传模式
data: {
name, // 文件对应的 key
filePath, // 要上传文件资源的路径
...formData, // 额外的 formData 属性
},
loading, // 自定义 loading 参数(会被拦截器识别)
onUploadProgress, // 上传进度回调
} as CustomRequestConfig).then((res) => res.data as T)
}
// ==================== 导出接口 ====================
// 默认导出 axios 实例(支持完整的泛型类型推导)
export default axios
// 命名导出 upload 函数(支持泛型)
export { upload }