以下是基于 UniApp 的请求封装方案,结合全局和局部 loading
控制、请求拦截、错误处理、超时重试、日志记录、环境切换以及文件上传下载功能。
1. 项目结构设计
project-root/
├── config/
│ └── api.js # 环境配置
├── utils/
│ ├── request.js # 请求封装
│ ├── toast.js # Toast 工具
│ └── loading.js # Loading 工具
├── store/
│ └── loadingStore.js # 全局 Loading 状态管理
├── components/
│ └── GlobalLoading.vue # 全局 Loading 组件
├── App.vue # 全局组件挂载
└── pages/
└── Home.vue # 示例页面
2. 环境配置
在 config/api.js
中定义不同环境下的 API 地址。
config/api.js
const BASE_URL = {
development: 'http://localhost:996',
test: 'https://test-api.yourdomain.com',
production: 'https://api.yourdomain.com'
};
const ENV = process.env.NODE_ENV || 'development'; // 默认为开发环境
export const API_URL = BASE_URL[ENV];
3. 全局 Loading 状态管理
使用 Pinia 或 Vuex 管理全局 loading
状态。
store/loadingStore.js
// 使用 Pinia
import { defineStore } from 'pinia';
export const useLoadingStore = defineStore('loading', {
state: () => ({
isLoading: false
}),
actions: {
showLoading() {
this.isLoading = true;
},
hideLoading() {
this.isLoading = false;
}
}
});
4. 全局 Loading 组件
创建一个全局 Loading 组件,用于显示加载动画。
components/GlobalLoading.vue
<template>
<view v-if="isLoading" class="loading-overlay">
<view class="loading-spinner"></view>
</view>
</template>
<script>
import { useLoadingStore } from '@/store/loadingStore';
export default {
setup() {
const store = useLoadingStore();
return {
isLoading: store.isLoading
};
}
};
</script>
<style scoped>
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.5);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
5. 请求封装
封装请求方法,支持拦截器、超时重试、日志记录等功能。
utils/request.js
import { API_URL } from '@/config/api';
import { useLoadingStore } from '@/store/loadingStore';
import { showToast } from '@/utils/toast';
const loadingStore = useLoadingStore();
// 默认请求配置
const defaultOptions = {
timeout: 15000, // 请求超时时间
dataType: 'json',
header: {
'Content-Type': 'application/json'
},
retry: 3, // 默认重试次数
showErrToast: true // 是否显示错误提示
};
// 请求方法封装
const request = async (options) => {
const { url, method, data, loading = true, retry = defaultOptions.retry, showErrToast } = options;
const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`;
// 设置 Token(如果有)
const token = uni.getStorageSync('token');
if (token) {
options.header = { ...options.header, Authorization: `Bearer ${token}` };
}
// 显示加载动画
if (loading) {
loadingStore.showLoading();
}
try {
const res = await uni.request({
...defaultOptions,
...options,
url: fullUrl,
method,
data
});
loadingStore.hideLoading(); // 隐藏加载动画
if (res.statusCode === 200) {
return res.data;
} else {
throw new Error(`Request failed with status code ${res.statusCode}`);
}
} catch (err) {
loadingStore.hideLoading(); // 隐藏加载动画
if (retry > 0) {
console.warn(`Retrying request... Retries left: ${retry}`);
return request({ ...options, retry: retry - 1 });
} else {
if (showErrToast) {
showToast(`请求失败:${err.message}`);
}
throw err;
}
}
};
// 请求拦截器
const requestInterceptor = (options) => {
console.log(`[Request] ${options.method} ${options.url}`, options.data); // 打印请求日志
return options;
};
// 响应拦截器
const responseInterceptor = (res) => {
console.log(`[Response] ${res.statusCode}`, res); // 打印响应日志
return res;
};
// 封装请求方法
const http = (method, url, data = {}, options = {}) => {
const finalOptions = requestInterceptor({
url,
method,
data,
...options
});
return request(finalOptions).then((res) => responseInterceptor(res));
};
export const get = (url, data = {}, options = {}) => http('GET', url, data, options);
export const post = (url, data = {}, options = {}) => http('POST', url, data, options);
export const put = (url, data = {}, options = {}) => http('PUT', url, data, options);
export const del = (url, data = {}, options = {}) => http('DELETE', url, data, options);
// 文件上传
export const uploadFile = (url, filePath, options = {}) => {
const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`;
const token = uni.getStorageSync('token');
const header = token ? { Authorization: `Bearer ${token}` } : {};
return new Promise((resolve, reject) => {
uni.uploadFile({
url: fullUrl,
filePath,
name: 'file',
header,
...options,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
reject(res);
}
},
fail: (err) => {
reject(err);
}
});
});
};
// 文件下载
export const downloadFile = (url, options = {}) => {
const fullUrl = url.startsWith('http') ? url : `${API_URL}${url}`;
return uni.downloadFile({
url: fullUrl,
...options
});
};
6. Toast 工具
封装一个简单的 Toast
工具用于显示提示信息。
utils/toast.js
export const showToast = (message, icon = 'none') => {
uni.showToast({
title: message,
icon,
duration: 2000
});
};
7. 全局 Loading 组件的使用
在 App.vue
中引入全局 Loading 组件。
App.vue
<template>
<view class="app-container">
<GlobalLoading />
<router-view />
</view>
</template>
<script>
import GlobalLoading from '@/components/GlobalLoading.vue';
export default {
components: {
GlobalLoading
}
};
</script>
<style>
.app-container {
position: relative;
width: 100%;
height: 100%;
}
</style>
8. 使用封装的请求方法
在页面或组件中使用封装的请求方法时,loading
状态会自动显示和隐藏。
示例页面:Home.vue
<template>
<view>
<button @click="fetchData">获取数据</button>
<button @click="submitData">提交数据</button>
<button @click="uploadFile">上传文件</button>
<button @click="downloadFile">下载文件</button>
</view>
</template>
<script>
import { get, post, uploadFile, downloadFile } from '@/utils/request';
export default {
methods: {
async fetchData() {
try {
const res = await get('/api/user', { userId: 123 });
console.log(res);
} catch (err) {
console.error(err);
}
},
async submitData() {
try {
const res = await post('/api/user', { name: 'Kimi', age: 25 });
console.log(res);
} catch (err) {
console.error(err);
}
},
async uploadFile() {
try {
const filePath = '/path/to/file.jpg';
const res = await uploadFile('/api/upload', filePath);
console.log(res);
} catch (err) {
console.error(err);
}
},
async downloadFile() {
try {
const url = '/api/download';
const res = await downloadFile(url);
console.log(res);
} catch (err) {
console.error(err);
}
}
}
};
</script>
总结
通过上述封装,我们实现了以下功能:
-
全局和局部
loading
控制:支持灵活的加载状态管理。 -
请求和响应拦截器:支持拦截器,便于统一处理请求头、Token、错误状态码等。
-
超时重试机制:支持请求超时自动重试。
-
错误统一处理:支持自定义错误处理逻辑。
-
日志记录:记录请求和响应的详细信息,方便调试。
-
环境切换:支持开发、测试、生产环境的无缝切换。
-
支持文件上传和下载:扩展请求功能,支持文件上传和下载。
-
支持自定义配置:允许用户根据需求自定义请求配置,如超时时间、请求头等。