在开发鸿蒙HarmonyOS应用时,网络请求功能是必不可少的。
axios
是一个非常流行的基于Promise的HTTP客户端,适用于浏览器和Node.js环境。本文将介绍如何在鸿蒙HarmonyOS中封装axios
库,使其能够支持文件上传,并提供额外的配置选项以满足不同的业务需求。
封装目的
- 简化网络请求:通过封装,我们可以将常用的HTTP请求操作(如GET、POST等)封装成简洁易用的方法。
- 统一错误处理:封装后的库可以统一处理HTTP请求中的错误,提供更友好的错误提示和处理逻辑。
- 支持文件上传:提供对文件上传的支持,包括单个文件和多个文件的上传。
使用的库
axios
:一个流行的JavaScript库,用于处理HTTP请求。
封装实现
以下是封装后的AxiosHttpRequest
类的实现,支持文件上传:
typescript
/**
* author:csdn猫哥
* qq:534117529
* blog:https://blog.csdn.net/yyz_1987
*/
//axiosHttp.ets
import 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; //是否展示请求loading
checkResultCode?: 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-ignore
this.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;
使用举例
javascript
import fs from '@ohos.file.fs';
import { axiosClient, FileUploadConfig, HttpPromise, UploadFile } from '../../utils/axiosClient';
import { AxiosProgressEvent } from '@nutpi/axios';
async function uploadSingleFile() {
try {
// 创建测试文件并读取为ArrayBuffer
let 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);
// 读取文件为ArrayBuffer
const 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函数:定义了默认的错误处理逻辑。根据响应的状态码提供不同的错误提示。
使用示例
typescript
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
库。