uni-app 网络请求封装实战:打造高效、可维护的HTTP请求框架
前言
在uniapp开发中,网络请求是每个应用都必不可少的功能模块。一个优秀的网络请求封装不仅能提高开发效率,还能增强代码的可维护性和可扩展性。本文将基于实际项目经验,详细介绍如何封装一个高效、可维护的Uniapp网络请求框架,并结合Bing图片API的调用示例,展示其完整使用流程。
一、网络请求封装的核心目标
- 统一管理:集中管理请求配置、拦截器等
- 复用性:减少重复代码,提高开发效率
- 可扩展性:方便添加新功能如日志记录、错误处理等
- 可维护性:清晰的代码结构,便于团队协作
二、封装架构设计
1.项目结构规划
项目根目录下新建文件夹common,在其目录下新建 api 文件夹以及 vmeitime-http 文件夹 ,如下
common/ ├── api/ # API接口管理 │ └── index.js # 接口统一出口 └── vmeitime-http/ # HTTP核心封装├── http.js # 请求核心实现└── interface.js # 基础请求方法
然后再项目根目录下的main.js下引用
//api
import api from '@/common/api/index.js'
Vue.prototype.$api = api
2.核心组件解析
(1) 基础请求层 (interface.js)
这是整个框架的基石,实现了:
- 基础请求方法(GET/POST/PUT/DELETE)
- 请求超时处理
- 请求ID生成(便于日志追踪)
- 基础配置管理
/*** 通用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并挂载到optionsconst requestId = generateRequestId();options.requestId = requestId; // 给请求配置添加IDoptions.baseUrl = options.baseUrl || this.config.baseUrloptions.dataType = options.dataType || this.config.dataTypeoptions.url = options.baseUrl + options.urloptions.data = options.data || {}options.method = options.method || this.config.methodoptions.timeout = options.timeout || this.config.timeout; // 使用配置的超时时间return new Promise((resolve, reject) => {let _config = nulllet 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; // 网络错误携带IDconsole.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 = urloptions.data = dataoptions.method = 'GET'return this.request(options)},post(url, data, options) {if (!options) {options = {}}options.url = urloptions.data = dataoptions.method = 'POST'return this.request(options)},put(url, data, options) {if (!options) {options = {}}options.url = urloptions.data = dataoptions.method = 'PUT'return this.request(options)},delete(url, data, options) {if (!options) {options = {}}options.url = urloptions.data = dataoptions.method = 'DELETE'return this.request(options)}
}/*** 请求接口日志记录*/function _reqlog(req) {//开发环境日志打印if (process.env.NODE_ENV === 'development') {const reqId = req.requestId || '未知ID'; // 读取请求IDconst reqUrl = req.url || '未知URL'; // 读取完整URLconsole.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)
在基础层之上添加:
- 全局拦截器(请求/响应)
- 公共参数处理
- 错误统一处理
- 默认配置初始化
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)
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
<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. 日志系统
// 请求日志
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生成算法
function generateRequestId() {const timestamp = Date.now().toString(36); // 时间戳转36进制const randomStr = Math.random().toString(36).slice(2, 8); // 6位随机字符串return `${timestamp}-${randomStr}`; // 格式:时间戳-随机串
}