在开发鸿蒙HarmonyOS应用时,网络请求功能是必不可少的。
axios
是一个非常流行的基于Promise的HTTP客户端,适用于浏览器和Node.js环境。本文将介绍如何在鸿蒙HarmonyOS中封装axios
库,使其能够支持文件上传,并提供额外的配置选项以满足不同的业务需求。
封装目的
- 简化网络请求:通过封装,我们可以将常用的HTTP请求操作(如GET、POST等)封装成简洁易用的方法。
- 统一错误处理:封装后的库可以统一处理HTTP请求中的错误,提供更友好的错误提示和处理逻辑。
- 支持文件上传:提供对文件上传的支持,包括单个文件和多个文件的上传。
使用的库
axios
:一个流行的JavaScript库,用于处理HTTP请求。
封装实现
以下是封装后的AxiosHttpRequest
类的实现,支持文件上传:
/*** author:csdn猫哥* qq:534117529* blog:https://blog.csdn.net/yyz_1987*/
//axiosHttp.etsimport axios, {AxiosError,AxiosInstance,AxiosHeaders,AxiosRequestHeaders,AxiosResponse,FormData,AxiosProgressEvent,InternalAxiosRequestConfig
} from "@ohos/axios";interface HttpResponse<T>{data: T;status: number;statusText: string;config: HttpRequestConfig;
}
export type HttpPromise<T> = Promise<HttpResponse<T>>;// 鸿蒙ArkTS文件上传相关接口定义
/**
上传类型支持uri和ArrayBuffer,
uri支持“internal”协议类型和沙箱路径。
"internal://cache/"为必填字段,示例: internal://cache/path/to/file.txt;
沙箱路径示例:cacheDir + '/hello.txt'*/
export interface UploadFile {buffer?: ArrayBuffer;fileName?: string;mimeType?: string;uri?:string;
}export interface FileUploadConfig extends HttpRequestConfig {file?: UploadFile | UploadFile[];fileFieldName?: string; // 文件字段名,默认为 'file'additionalData?: Record<string, any>; // 额外的表单数据onUploadProgress?: (progressEvent: any) => void; // 上传进度回调
}export interface FileInfo {name: string;size: number;type: string;
}
/*** 封装后,不支持传入拦截器* 需要自己定义接口继承 AxiosRequestConfig类型* 从而支持传入拦截器,但拦截器选项应为可选属性* 之后请求实例传入的options为继承了AxiosRequestConfig的自定义类型*/
interface InterceptorHooks {requestInterceptor?: (config: HttpRequestConfig) => Promise<HttpRequestConfig>;requestInterceptorCatch?: (error: any) => any;responseInterceptor?: (response: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;responseInterceptorCatch?: (error: any) => any;
}// @ts-ignore
interface HttpRequestConfig extends InternalAxiosRequestConfig {showLoading?: boolean; //是否展示请求loadingcheckResultCode?: boolean; //是否检验响应结果码checkLoginState?: boolean; //校验用户登陆状态needJumpToLogin?: boolean; //是否需要跳转到登陆页面interceptorHooks?: InterceptorHooks;//拦截器headers?: AxiosRequestHeaders;errorHandler?: (error: any) => void; //错误处理
}/*** 网络请求构造* 基于axios框架实现*/
export class AxiosHttpRequest {config: HttpRequestConfig;interceptorHooks?: InterceptorHooks;instance: AxiosInstance;constructor(options: HttpRequestConfig) {this.config = options;this.interceptorHooks = options.interceptorHooks;this.instance = axios.create(options);this.setupInterceptor()}setupInterceptor(): void {this.instance.interceptors.request.use(// 这里主要是高版本的axios中设置拦截器的时候里面的Config属性必须是InternalAxiosRequestConfig,// 但是InternalAxiosRequestConfig里面的headers是必传,所以在实现的子类我设置成非必传会报错,加了个忽略注解// @ts-ignorethis.interceptorHooks?.requestInterceptor,this.interceptorHooks?.requestInterceptorCatch,);this.instance.interceptors.response.use(this.interceptorHooks?.responseInterceptor,this.interceptorHooks?.responseInterceptorCatch,);}// 类型参数的作用,T决定AxiosResponse实例中data的类型request<T = any>(config: HttpRequestConfig): HttpPromise<T> {return new Promise<HttpResponse<T>>((resolve, reject) => {this.instance.request<any, HttpResponse<T>>(config).then(res => {resolve(res);}).catch((err) => {// 使用传入的 errorHandler 处理错误const errorHandler = config.errorHandler || errorHandlerDefault;errorHandler(err); if (err) {reject(err);}});});}get<T = any>(config: HttpRequestConfig): HttpPromise<T> {return this.request({ ...config, method: 'GET' });}post<T = any>(config: HttpRequestConfig): HttpPromise<T> {return this.request({ ...config, method: 'POST' });}delete<T = any>(config: HttpRequestConfig): HttpPromise<T> {return this.request({ ...config, method: 'DELETE' });}patch<T = any>(config: HttpRequestConfig): HttpPromise<T> {return this.request({ ...config, method: 'PATCH' });}/*** 上传单个文件或多个文件* @param config 文件上传配置* @returns Promise<HttpResponse<T>>*/uploadFile<T = any>(config: FileUploadConfig): HttpPromise<T> {return new Promise<HttpResponse<T>>((resolve, reject) => {if (!config.file) {reject(new Error('文件不能为空'));return;}const formData = new FormData();const fileFieldName = config.fileFieldName || 'file';// 处理单个或多个文件const files = Array.isArray(config.file) ? config.file : [config.file];files.forEach((file, index) => {const fieldName = Array.isArray(config.file) ? `${fileFieldName}[${index}]` : fileFieldName;// 鸿蒙ArkTS FormData.append 支持第三个参数设置文件名和类型if (file.mimeType) {formData.append(fieldName, file.buffer, {filename: file.fileName,type: file.mimeType});} else if (file.buffer){formData.append(fieldName, file.buffer, {filename: file.fileName});}else if (file.uri){formData.append(fieldName, file.uri);}});// 添加额外的表单数据if (config.additionalData) {Object.keys(config.additionalData).forEach(key => {formData.append(key, config.additionalData![key]);});}const uploadConfig: HttpRequestConfig = {...config,method: 'POST',data: formData,headers: new AxiosHeaders({...config.headers,'Content-Type': 'multipart/form-data'})};// 添加上传进度监听if (config.onUploadProgress) {uploadConfig.onUploadProgress = config.onUploadProgress;}this.request<T>(uploadConfig).then(resolve).catch(reject);});}/*** 上传多个文件* @param config 文件上传配置* @returns Promise<HttpResponse<T>>*/uploadFiles<T = any>(config: FileUploadConfig): HttpPromise<T> {if (!Array.isArray(config.file)) {return Promise.reject(new Error('uploadFiles方法需要传入文件数组'));}return this.uploadFile<T>(config);}/*** 获取文件信息* @param file 文件对象* @returns FileInfo*/getFileInfo(file: UploadFile): FileInfo {return {name: file.fileName,size: file.buffer.byteLength,type: file.mimeType || 'application/octet-stream'};}/*** 验证文件类型* @param file 文件对象* @param allowedTypes 允许的文件类型数组* @returns boolean*/validateFileType(file: UploadFile, allowedTypes: string[]): boolean {const fileType = file.mimeType || 'application/octet-stream';return allowedTypes.includes(fileType);}/*** 验证文件大小* @param file 文件对象* @param maxSize 最大文件大小(字节)* @returns boolean*/validateFileSize(file: UploadFile, maxSize: number): boolean {return file.buffer.byteLength <= maxSize;}/*** 创建文件上传配置* @param url 上传地址* @param file 文件对象* @param options 其他配置选项* @returns FileUploadConfig*/createUploadConfig(url: string,file: UploadFile | UploadFile[],options: Partial<FileUploadConfig> = {}): FileUploadConfig {return {url,file,fileFieldName: 'file',...options};}
}function errorHandlerDefault(error: any) {if (error instanceof AxiosError) {//showToast(error.message)} else if (error != undefined && error.response != undefined && error.response.status) {switch (error.response.status) {// 401: 未登录// 未登录则跳转登录页面,并携带当前页面的路径// 在登录成功后返回当前页面,这一步需要在登录页操作。case 401:break;// 403 token过期// 登录过期对用户进行提示// 清除本地token和清空vuex中token对象// 跳转登录页面case 403://showToast("登录过期,请重新登录")// 清除token// localStorage.removeItem('token');break;// 404请求不存在case 404://showToast("网络请求不存在")break;// 其他错误,直接抛出错误提示default://showToast(error.response.data.message)}}
}
export{AxiosRequestHeaders,AxiosError,AxiosHeaders,AxiosProgressEvent,FormData};
export default AxiosHttpRequest;
使用举例
import fs from '@ohos.file.fs';
import { axiosClient, FileUploadConfig, HttpPromise, UploadFile } from '../../utils/axiosClient';
import { AxiosProgressEvent } from '@nutpi/axios';async function uploadSingleFile() {try {// 创建测试文件并读取为ArrayBufferlet context = getContext() as common.UIAbilityContext;const cacheDir = context.cacheDir;const path = cacheDir + '/test.jpg';// 写入测试文件const file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);fs.writeSync(file.fd, "这是一个测试文件内容");fs.fsyncSync(file.fd);fs.closeSync(file.fd);// 读取文件为ArrayBufferconst file2 = fs.openSync(path, 0o2);const stat = fs.lstatSync(path);const buffer = new ArrayBuffer(stat.size);fs.readSync(file2.fd, buffer);fs.fsyncSync(file2.fd);fs.closeSync(file2.fd);const uploadFile:UploadFile = {fileName: 'test.jpg',mimeType: 'text/plain',uri:path};const config:FileUploadConfig = axiosClient.createUploadConfig('upload/image',uploadFile,{context:getContext(),onUploadProgress: (progressEvent:AxiosProgressEvent) => {const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent?.total ?? 1));console.log(`上传进度: ${percentCompleted}%`);}});axiosClient.uploadFile<string>(config).then((res) => {//Log.debug(res.data.code)console.log('文件上传成功:');}).catch((err:BusinessError) => {Log.debug("request","err.code:%d",err.code)Log.debug("request",err.message)console.error('文件上传失败:', err);});} catch (error) {}
}
核心功能解析
1. 文件上传接口
- UploadFile接口:定义了文件上传的数据结构,支持
uri
和ArrayBuffer
两种方式。 - FileUploadConfig接口:继承自
HttpRequestConfig
,增加了对文件和额外表单数据的支持。 - uploadFile方法:实现文件上传逻辑,支持单个文件和多个文件的上传。
- uploadFiles方法:专门用于上传文件数组。
2. 文件信息处理
- getFileInfo方法:获取文件的名称、大小和类型。
- validateFileType方法:验证文件类型是否在允许的类型列表中。
- validateFileSize方法:验证文件大小是否超过指定的最大值。
3. 请求拦截器与响应拦截器
- setupInterceptor方法:设置请求和响应拦截器,以便在请求发送前和响应返回后执行自定义逻辑。
4. 统一错误处理
- errorHandlerDefault函数:定义了默认的错误处理逻辑。根据响应的状态码提供不同的错误提示。
使用示例
const httpRequest = new AxiosHttpRequest({baseURL: 'https://example.com/api',interceptorHooks: {requestInterceptor: (config) => {console.log('Request Interceptor:', config);return config;},responseInterceptor: (response) => {console.log('Response Interceptor:', response);return response;},},errorHandler: (error) => {console.error('Custom Error Handler:', error);}
});const file: UploadFile = {buffer: new ArrayBuffer(1024),fileName: 'example.txt',mimeType: 'text/plain'
};const config = httpRequest.createUploadConfig('https://example.com/api/upload', file, {additionalData: { description: '这是一个示例文件' },onUploadProgress: (progress) => {console.log('上传进度:', progress);}
});httpRequest.uploadFile(config).then((response) => {console.log('上传成功:', response);
}).catch((error) => {console.error('上传失败:', error);
});
总结
通过封装axios
库,我们可以在鸿蒙HarmonyOS中更方便地实现网络请求和文件上传功能。封装后的库提供了统一的错误处理机制和丰富的配置选项,使得我们的网络请求更加高效和灵活。希望本文能帮助大家更好地理解和使用封装后的axios
库。