前言
在uniapp开发中,网络请求是每个应用都必不可少的功能模块。一个优秀的网络请求封装不仅能提高开发效率,还能增强代码的可维护性和可扩展性。本文将基于实际项目经验,详细介绍如何封装一个高效、可维护的Uniapp网络请求框架,并结合Bing图片API的调用示例,展示其完整使用流程。

一、网络请求封装的核心目标
- 统一管理:集中管理请求配置、拦截器等
- 复用性:减少重复代码,提高开发效率
- 可扩展性:方便添加新功能如日志记录、错误处理等
- 可维护性:清晰的代码结构,便于团队协作
二、封装架构设计
1.项目结构规划
项目根目录下新建文件夹common,在其目录下新建 api 文件夹以及 vmeitime-http 文件夹 ,如下
common/
├── api/ # API接口管理
│ └── index.js # 接口统一出口
└── vmeitime-http/ # HTTP核心封装
├── http.js # 请求核心实现
└── interface.js # 基础请求方法
然后再项目根目录下的main.js下引用
javascript
//api
import api from '@/common/api/index.js'
Vue.prototype.$api = api
2.核心组件解析
(1) 基础请求层 (interface.js)
这是整个框架的基石,实现了:
- 基础请求方法(GET/POST/PUT/DELETE)
- 请求超时处理
- 请求ID生成(便于日志追踪)
- 基础配置管理
javascript
/**
* 通用uni-app网络请求
* 基于 Promise 对象实现更简单的 request 使用方式,支持请求和响应拦截
*/
export default {
config: {
baseUrl: "",
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: {},
method: "POST",
dataType: "json",
/* 如设为json,会对返回的数据做一次 JSON.parse */
responseType: "text",
timeout: 15000, // 全局超时时间 15 秒
fail() {},
complete() {}
},
interceptor: {
request: null,
response: null
},
request(options) {
if (!options) {
options = {}
}
// 1. 生成唯一请求ID并挂载到options
const requestId = generateRequestId();
options.requestId = requestId; // 给请求配置添加ID
options.baseUrl = options.baseUrl || this.config.baseUrl
options.dataType = options.dataType || this.config.dataType
options.url = options.baseUrl + options.url
options.data = options.data || {}
options.method = options.method || this.config.method
options.timeout = options.timeout || this.config.timeout; // 使用配置的超时时间
return new Promise((resolve, reject) => {
let _config = null
let timeoutHandle = null;
// 超时处理
if (options.timeout) {
timeoutHandle = setTimeout(() => {
reject({
errMsg: "request:fail timeout",
timeout: true,
requestId: requestId // 超时错误也携带ID,方便定位
});
}, options.timeout);
}
options.complete = (response) => {
// 无论成功失败,都清除超时计时器
clearTimeout(timeoutHandle);
_config = Object.assign({}, this.config, options);
response.config = _config; // 给响应挂载配置,供拦截器使用
// 执行响应拦截器
if (this.interceptor.response) {
const interceptedResponse = this.interceptor.response(response);
if (interceptedResponse !== undefined) {
response = interceptedResponse;
}
}
// 统一的响应日志记录
_reslog(response)
if (response.statusCode === 200) { //成功
resolve(response);
} else {
reject(response)
}
}
// 失败回调(网络错误等)
options.fail = (error) => {
clearTimeout(timeoutHandle);
error.requestId = requestId; // 网络错误携带ID
console.error(`【网络请求失败】ID: ${requestId}`, error);
uni.showToast({
title: error.timeout ? "请求超时" : "网络连接失败,请检查网络",
icon: "none"
});
reject(error);
};
// 执行请求拦截器
_config = Object.assign({}, this.config, options);
if (this.interceptor.request) {
const interceptedConfig = this.interceptor.request(_config);
if (interceptedConfig !== undefined) {
_config = interceptedConfig;
}
}
// 统一的请求日志记录
_reqlog(_config)
uni.request(_config);
});
},
get(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'GET'
return this.request(options)
},
post(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'POST'
return this.request(options)
},
put(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'PUT'
return this.request(options)
},
delete(url, data, options) {
if (!options) {
options = {}
}
options.url = url
options.data = data
options.method = 'DELETE'
return this.request(options)
}
}
/**
* 请求接口日志记录
*/
function _reqlog(req) {
//开发环境日志打印
if (process.env.NODE_ENV === 'development') {
const reqId = req.requestId || '未知ID'; // 读取请求ID
const reqUrl = req.url || '未知URL'; // 读取完整URL
console.log(`【${reqId}】 地址:${reqUrl}`);
if (req.data) {
console.log("【" + (req.requestId || '未知ID') + "】 请求参数:", req.data);
}
}
}
/**
* 响应接口日志记录
*/
function _reslog(res) {
if (!res) {
console.error("【日志错误】响应对象为空");
return;
}
const requestId = res.config?.requestId || '未知请求ID';
const url = res.config?.url || '未知URL';
const statusCode = res.statusCode || '未知状态码';
console.log(`【${requestId}】 接口: ${url} | 业务状态码: ${statusCode}`);
// 打印响应数据
if (res.data) {
console.log(`【${requestId}】 响应数据:`, res.data);
}
// 错误处理逻辑
switch (statusCode) {
case "401":
console.error(`【${requestId}】 未授权错误`);
break;
case "404":
console.error(`【${requestId}】 接口不存在`);
break;
case "500":
console.error(`【${requestId}】 服务器错误`);
break;
default:
}
}
/**
* 生成唯一请求ID(时间戳+随机数,避免重复)
*/
function generateRequestId() {
const timestamp = Date.now().toString(36); // 时间戳转36进制(缩短长度)
const randomStr = Math.random().toString(36).slice(2, 8); // 6位随机字符串
return `${timestamp}-${randomStr}`; // 格式:时间戳-随机串(如:1h8z2x-9k7a3b)
}
(2) 请求增强层 (http.js)
在基础层之上添加:
- 全局拦截器(请求/响应)
- 公共参数处理
- 错误统一处理
- 默认配置初始化
javascript
import uniRequest from './interface'
uniRequest.config.baseUrl = "https://cn.bing.com"
uniRequest.config.header = {
'Content-Type': 'application/x-www-form-urlencoded'
}
// 公共参数(所有请求都会携带)
const commonParams = {
};
//设置请求前拦截器
uniRequest.interceptor.request = (config) => {
//添加通用参数
let token = uni.getStorageSync('token');
if (token) {
config.header["X-Token"] = token;
}
// 合并公共参数和业务参数
const mergedData = {
...commonParams, // 公共参数
...config.data // 业务参数(会覆盖公共参数)
};
config.data = mergedData;
return config;
}
//设置请求结束后拦截器
uniRequest.interceptor.response = (response) => {
return response;
}
export default uniRequest
(3) API接口层 (api/index.js)
javascript
import http from '@/common/vmeitime-http/http.js'
const $api = {
//查看必应图片
byImageList(data) {
return http.request({
url: '/HPImageArchive.aspx?format=js&idx=0&n=1',
method: 'GET',
data,
})
}
}
export default $api
三、使用示例:调用Bing图片API
javascript
<template>
<view>
<!-- 显示加载状态 -->
<view v-if="loading" class="loading">{{ tooltips.loading }}</view>
<!-- 显示Bing图片 -->
<image v-else :src="fullImageUrl" mode="widthFix" class="bing-image"></image>
</view>
</template>
<script>
export default {
data() {
return {
loading: true, // 加载状态
fullImageUrl: "", // 拼接后的完整图片URL
};
},
onLoad() {
uni.showLoading({
title: '请稍后',
mask: true // 防止用户重复操作
});
this.fetchBingImage()
.finally(() => {
this.loading = false; // 确保 loading 被关闭
uni.hideLoading(); // 无论成功失败都关闭加载框
});
},
methods: {
fetchBingImage() {
return this.$api.byImageList()
.then(res => {
console.log("请求成功:", res);
console.log('images 数组:', res.data.images);
// 2. 提取 images 数组的第一项
const imageData = res.data.images[0];
console.log('images图片:', imageData.url);
// 3. 拼接完整URL(http://cn.bing.com + url)
this.fullImageUrl = `https://cn.bing.com${imageData.url}`;
console.log('images图片地址:', this.fullImageUrl);
})
.catch(err => {
// 此时的err只可能是:HTTP错误、解析异常、业务失败(result≠0000)
console.error("请求失败:", err);
uni.showToast({
title: err.error || err.timeout ? '请求超时' : '请求失败',
icon: 'none',
duration: 2000
});
return Promise.reject(err); // 继续抛出,供上层处理
});
},
},
};
</script>
<style>
.loading {
font-size: 16px;
color: #999;
text-align: center;
margin-top: 50px;
}
.bing-image {
width: 100%;
height: auto;
}
</style>
四、高级功能实现
1. 日志系统
javascript
// 请求日志
function _reqlog(req) {
if (process.env.NODE_ENV === 'development') {
console.log(`【${req.requestId}】 地址:${req.url}`);
if (req.data) {
console.log("请求参数:", req.data);
}
}
}
// 响应日志
function _reslog(res) {
const requestId = res.config?.requestId || '未知请求ID';
console.log(`【${requestId}】 接口: ${res.config?.url} | 状态码: ${res.statusCode}`);
// 错误状态码处理
switch (res.statusCode) {
case 401: console.error(`未授权错误`); break;
case 404: console.error(`接口不存在`); break;
case 500: console.error(`服务器错误`); break;
}
}
2. 请求ID生成算法
javascript
function generateRequestId() {
const timestamp = Date.now().toString(36); // 时间戳转36进制
const randomStr = Math.random().toString(36).slice(2, 8); // 6位随机字符串
return `${timestamp}-${randomStr}`; // 格式:时间戳-随机串
}