uni-app 网络请求封装实战:打造高效、可维护的HTTP请求框架

发布于:2025-09-13 ⋅ 阅读:(22) ⋅ 点赞:(0)

前言

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

 

一、网络请求封装的核心目标

  1. 统一管理:集中管理请求配置、拦截器等
  2. 复用性:减少重复代码,提高开发效率
  3. 可扩展性:方便添加新功能如日志记录、错误处理等
  4. 可维护性:清晰的代码结构,便于团队协作

二、封装架构设计

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并挂载到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)

在基础层之上添加:

  • 全局拦截器(请求/响应)
  • 公共参数处理
  • 错误统一处理
  • 默认配置初始化
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}`; // 格式:时间戳-随机串
}