在使用 HarmonyOS NEXT(ArkTS)开发应用的过程中,网络请求是每个项目中不可或缺的部分。鸿蒙官方提供了 @ohos.net.http 模块来实现网络通信,但原始写法通常较为冗长,特别是在需要频繁调用接口、处理 token 和上传文件时,代码极易重复、出错。

为了提高开发效率和项目的可维护性,我封装了一个通用的 Networking 类,支持:

  • GET、POST 请求封装

  • 自动注入 token

  • 统一错误处理

  • 支持文件上传(带沙箱处理)

  • 泛型返回值

这篇文章将手把手教你如何实现,并分享使用方法与注意事项。


📦 为什么要封装?

鸿蒙原生的 http 请求示例如下:

let httpRequest = http.createHttp();
let result = await httpRequest.request("https://api.example.com/data", {
  method: http.RequestMethod.GET,
  header: {
    "Content-Type": "application/json"
  },
  expectDataType: http.HttpDataType.OBJECT
});

这种方式虽然灵活,但在实际开发中,我们通常还需要:

  • 带上登录 token

  • 统一处理错误码(如 403 重定向登录)

  • 上传文件时构造 multipart body

  • 重复导入配置、接口路径等

因此,封装一个 Networking 工具类能极大提升开发体验。


🔧 Networking 工具类核心代码

以下是已经优化过的完整 Networking 类代码,适配鸿蒙 HarmonyOS NEXT,兼容 ArkTS。

注意:文中示例 IP 和路径均为示意,请替换为你自己的服务器地址。

✅ 主要特性

  • get<T>()post<T>() 支持泛型参数

  • 自动注入 token

  • 支持 multipart 文件上传

  • 沙箱文件复制与二进制读取

  • 支持响应体结构统一解析(code/message/data)

📄 完整代码

💡 建议将以下代码保存为:/common/network/Networking.ts

import http from '@ohos.net.http';
import { promptAction, router } from '@kit.ArkUI';
import loginManager from '../../viewModel/Login/LoginManager';
import { JSON } from '@kit.ArkTS';
import util from '@ohos.util';
import fs from '@ohos.file.fs';
import systemDateTime from '@ohos.systemDateTime';
import buffer from '@ohos.buffer';

export interface IResponseModel<T> {
  message: string;
  data?: T;
  code: number;
}

type AnyObject = Record<string, any>;

class Networking {
  private baseURL: string;

  constructor() {
    this.baseURL = 'https://api.example.com/api'; // ← 替换成你自己的 API 地址
  }

  private async createRequestOptions(method: http.RequestMethod, extraData?: AnyObject): Promise<http.HttpRequestOptions> {
    const token = await loginManager.getToken();
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };
    if (token) {
      headers['Authorization'] = `Bearer ${token}`;
    }

    return {
      method,
      header: headers,
      expectDataType: http.HttpDataType.OBJECT,
      extraData,
    };
  }

  private handleResponse<T>(resData: IResponseModel<T>): IResponseModel<T> {
    if (resData.code !== 200) {
      if (resData.code === 404 || resData.code === 403) {
        promptAction.showToast({ message: 'Token失效,请重新登录' });
        router.replaceUrl({ url: 'pages/login/Login' });
      } else {
        promptAction.showToast({ message: resData.message });
      }
    }
    return resData;
  }

  async post<T>(url: string, extraData?: AnyObject): Promise<IResponseModel<T>> {
    try {
      const req = http.createHttp();
      const options = await this.createRequestOptions(http.RequestMethod.POST, extraData);
      const res = await req.request(this.baseURL + url, options);
      const resData = res.result as IResponseModel<T>;
      return this.handleResponse(resData);
    } catch (err) {
      promptAction.showToast({ message: '请求网络错误' });
      console.error('Networking POST Error:', JSON.stringify(err));
      return Promise.reject(err);
    }
  }

  async get<T>(url: string, extraData?: AnyObject): Promise<IResponseModel<T>> {
    try {
      const req = http.createHttp();
      const options = await this.createRequestOptions(http.RequestMethod.GET, extraData);
      const res = await req.request(this.baseURL + url, options);
      const resData = res.result as IResponseModel<T>;
      return this.handleResponse(resData);
    } catch (err) {
      promptAction.showToast({ message: '请求网络错误' });
      console.error('Networking GET Error:', JSON.stringify(err));
      return Promise.reject(err);
    }
  }

  async upload<T>(url: string, uploadFilePath: string, uploadFileName: string, fieldName: string = 'images'): Promise<IResponseModel<T>> {
    try {
      const boundary = '----Boundary' + (await systemDateTime.getCurrentTime(true)).toString();
      const sandFile = await this.copyToSandbox(uploadFilePath, uploadFileName);
      const fileContent = new Uint8Array(this.readContentFromFile(sandFile));
      const bodyContent = this.buildBodyContent(boundary, fieldName, uploadFileName, fileContent);

      const req = http.createHttp();
      const token = await loginManager.getToken();
      const headers: Record<string, string> = {
        'Content-Type': `multipart/form-data; boundary=${boundary}`,
        'Content-Length': bodyContent.byteLength.toString(),
        'Accept': 'application/json',
      };
      if (token) {
        headers['Authorization'] = `Bearer ${token}`;
      }

      const options: http.HttpRequestOptions = {
        method: http.RequestMethod.POST,
        header: headers,
        extraData: bodyContent,
      };

      const res = await req.request(this.baseURL + url, options);
      const resData = JSON.parse(res.result as string) as IResponseModel<T>;
      return this.handleResponse(resData);
    } catch (err) {
      promptAction.showToast({ message: '上传失败,请检查网络' });
      console.error('Networking Upload Error:', JSON.stringify(err));
      return Promise.reject(err);
    }
  }

  private buildBodyContent(boundary: string, fieldName: string, fileName: string, content: Uint8Array, contentType: string = 'image/jpeg'): ArrayBuffer {
    const textEncoder = new util.TextEncoder();
    const preFileContent = `--${boundary}\r\nContent-Disposition: form-data; name="${fieldName}"; filename="${fileName}"\r\nContent-Type: ${contentType}\r\n\r\n`;
    const preArray = textEncoder.encodeInto(preFileContent);
    const aftArray = textEncoder.encodeInto(`\r\n--${boundary}--\r\n`);
    const bodyBuf = buffer.concat([preArray, content, aftArray]);
    return bodyBuf.buffer;
  }

  private async copyToSandbox(srcUri: string, fileName: string): Promise<string> {
    const context = getContext(this);
    const realUri = `${context.cacheDir}/${fileName}`;
    try {
      const file = await fs.open(srcUri);
      fs.copyFileSync(file.fd, realUri);
      fs.close(file);
    } catch (err) {
      console.error('Copy to Sandbox Error:', JSON.stringify(err));
    }
    return realUri;
  }

  private readContentFromFile(fileUri: string): ArrayBuffer {
    const file = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
    const fsStat = fs.lstatSync(fileUri);
    const buf = new ArrayBuffer(fsStat.size);
    fs.readSync(file.fd, buf);
    fs.fsyncSync(file.fd);
    fs.closeSync(file);
    return buf;
  }
}

const networking = new Networking();
export default networking;

✅ 使用示例

1. 普通 GET 请求

interface UserProfile {
  username: string;
  avatar: string;
}

const res = await networking.get<UserProfile>('/users/profile');
console.log('用户名:', res.data?.username);

2. POST 请求(登录)

interface LoginResult {
  token: string;
  userId: string;
}

const loginData = { username: 'test', password: '123456' };
const res = await networking.post<LoginResult>('/users/login', loginData);
console.log('登录成功,Token:', res.data?.token);

3. 文件上传(头像或图片)

interface UploadResponse {
  url: string;
}

const res = await networking.upload<UploadResponse>('/upload/avatar', '/storage/media/file.jpg', 'file.jpg', 'avatar');
console.log('文件已上传:', res.data?.url);

🛡 安全建议

  • ❗ 请勿将服务器 IP 写死为 localhost,使用实际 IP 或域名

  • ⚠️ token 失效应统一跳转登录(已封装)

  • ✅ 后端建议统一使用 code/message/data 格式返回

  • 🚫 避免重复文件上传,建议使用 MD5 去重策略


🔚 总结

这个 Networking 封装类几乎涵盖了中小项目中所有的网络请求场景,它不仅简化了调用逻辑,还提高了代码的健壮性和复用性。如果你正在写 ArkTS 项目,完全可以拷贝并集成这套封装逻辑,让你专注业务开发。

希望这篇博客对你有帮助!


如果你有其他模块封装需求,欢迎留言,我会继续更新组件化开发经验 

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐