1

基于@ohos/axios学习HarmonyOS Next的网络数据请求

前言

在 HarmonyOS Next 应用开发中,网络请求是一个非常重要的功能。本文将通过分析 @ohos/axios 的实现,深入了解 HarmonyOS Next 的网络数据请求机制。

一、基础知识

1.1 @ohos/axios 简介

@ohos/axios 是 Axios 在 HarmonyOS 平台的适配版本。Axios 是一个基于 Promise 的 HTTP 客户端,可以在浏览器和 Node.js 环境中使用。@ohos/axios 保留了 Axios 的主要特性,同时适配了 HarmonyOS 的网络 API。

1.2 安装与配置
ohpm install @ohos/axios

在命令行中使用 ohpm install @ohos/axios 命令安装 @ohos/axios 模块。

需要在 module.json5 中配置权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

解析:

  • module.json5 是 HarmonyOS 应用的配置文件。
  • requestPermissions 用于声明应用需要的权限,这里声明了 ohos.permission.INTERNET 权限,允许应用访问互联网。

二、基本使用

2.1 创建实例
import axios from '@ohos/axios';

// 创建实例
const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {'X-Custom-Header': 'custom-value'}
});

解析:

  • axios.create 方法创建一个新的 Axios 实例。
  • baseURL 设置所有请求的默认基础 URL。
  • timeout 设置请求超时时间(毫秒)。
  • headers 设置默认的请求头。
2.2 基本请求示例
// GET 请求
async function getData() {
  try {
    const response = await axios.get('/user/123');
    console.info('Response:', response.data);
  } catch (error) {
    console.error('Error:', error);
  }
}

// POST 请求
async function postData() {
  try {
    const response = await axios.post('/user', {
      name: 'John',
      age: 30
    });
    console.info('Response:', response.data);
  } catch (error) {
    console.error('Error:', error);
  }
}

解析:

  • axios.get 方法发起一个 GET 请求,返回一个 Promise 对象。
  • response.data 包含服务器返回的数据。
  • axios.post 方法发起一个 POST 请求,第二个参数是请求体数据。
  • 使用 try...catch 语句处理请求中的异步错误。

三、深入实现原理

3.1 HTTP 请求适配器
3.1.1 基础 HTTP 请求适配器
// library/src/main/ets/components/lib/adapters/ohos/http.js
import http from '@ohos.net.http';

export default function httpAdapter(config) {
  return new Promise((resolve, reject) => {
    let httpRequest = http.createHttp();
    
    // 构建请求配置
    const requestConfig = {
      method: config.method.toUpperCase(),
      header: config.headers,
      readTimeout: config.timeout,
      connectTimeout: config.timeout,
      extraData: config.data ? JSON.stringify(config.data) : undefined
    };

    // 发起请求
    httpRequest.request(
      config.url,
      requestConfig,
      (err, data) => {
        if (!err) {
          resolve({
            data: data.result,
            status: data.responseCode,
            headers: data.header,
            config: config
          });
        } else {
          reject(new AxiosError(
            'Request failed',
            'ECONNABORTED',
            config,
            httpRequest,
            err
          ));
        }
      }
    );
  });
}

解析:

  • httpAdapter 函数是一个适配器,用于处理 HTTP 请求。
  • http.createHttp() 创建一个 HTTP 请求对象。
  • requestConfig 是构建的请求配置对象,包括请求方法、请求头、超时时间、请求体数据等。
  • httpRequest.request 方法发起 HTTP 请求,传入 URL、请求配置和回调函数。
  • 回调函数中,如果没有错误,解析服务器返回的数据并 resolve Promise。
  • 如果有错误,创建一个新的 AxiosError 对象并 reject Promise。
3.1.2 文件下载适配器
// library/src/main/ets/components/lib/adapters/ohos/download.js
import request from '@ohos.request';

export default function downloadAdapter(config) {
  return new Promise((resolve, reject) => {
    const downloadConfig = {
      url: config.url,
      header: config.headers,
      filePath: config.filePath
    };

    request.downloadFile(context, downloadConfig)
      .then((downloadTask) => {
        // 监听下载进度
        downloadTask.on('progress', (receivedSize, totalSize) => {
          if (config.onDownloadProgress) {
            config.onDownloadProgress({
              loaded: receivedSize,
              total: totalSize
            });
          }
        });

        // 监听下载完成
        downloadTask.on('complete', (uri) => {
          resolve({
            data: uri,
            status: 200,
            config: config
          });
        });
      })
      .catch(error => {
        reject(new AxiosError(
          'Download failed',
          'DOWNLOAD_ERROR',
          config,
          null,
          error
        ));
      });
  });
}

解析:

  • downloadAdapter 函数用于处理文件下载请求。
  • downloadConfig 是构建的下载配置对象,包括文件的 URL、请求头和文件保存路径。
  • request.downloadFile 方法发起文件下载请求,返回一个 downloadTask 对象。
  • downloadTask.on('progress') 监听下载进度,调用 config.onDownloadProgress 回调函数。
  • downloadTask.on('complete') 监听下载完成,解析下载结果并 resolve Promise。
  • 如果下载过程中出现错误,创建一个新的 AxiosError 对象并 reject Promise。
3.1.3 文件上传适配器
// library/src/main/ets/components/lib/adapters/ohos/upload.js
import request from '@ohos.request';

export default function uploadAdapter(config) {
  return new Promise((resolve, reject) => {
    const uploadConfig = {
      url: config.url,
      header: config.headers,
      method: config.method,
      files: [],
      data: []
    };

    // 处理 FormData
    if (config.data instanceof FormData) {
      config.data.forEach((value, key) => {
        if (value instanceof File) {
          uploadConfig.files.push({
            filename: value.name,
            name: key,
            uri: value.uri,
            type: value.type
          });
        } else {
          uploadConfig.data.push({
            name: key,
            value: value
          });
        }
      });
    }

    request.uploadFile(context, uploadConfig)
      .then((uploadTask) => {
        // 监听上传进度
        uploadTask.on('progress', (uploadedSize, totalSize) => {
          if (config.onUploadProgress) {
            config.onUploadProgress({
              loaded: uploadedSize,
              total: totalSize
            });
          }
        });

        // 监听上传完成
        uploadTask.on('complete', (response) => {
          resolve({
            data: response,
            status: 200,
            config: config
          });
        });
      })
      .catch(error => {
        reject(new AxiosError(
          'Upload failed',
          'UPLOAD_ERROR',
          config,
          null,
          error
        ));
      });
  });
}

解析:

  • uploadAdapter 函数用于处理文件上传请求。
  • uploadConfig 是构建的上传配置对象,包括文件的 URL、请求头、请求方法、文件列表和普通数据列表。
  • config.data 如果是 FormData 类型,会遍历并分别处理文件和普通数据。
  • request.uploadFile 方法发起文件上传请求,返回一个 uploadTask 对象。
  • uploadTask.on('progress') 监听上传进度,调用 config.onUploadProgress 回调函数。
  • uploadTask.on('complete') 监听上传完成,解析上传结果并 resolve Promise。
  • 如果上传过程中出现错误,创建一个新的 AxiosError 对象并 reject Promise。
3.2 拦截器实现
// 请求拦截器
axios.interceptors.request.use(
  config => {
    // 在发送请求之前做些什么
    config.headers['Authorization'] = `Bearer ${getToken()}`;
    return config;
  },
  error => {
    // 对请求错误做些什么
    return Promise.reject(error);
  }
);

// 响应拦截器
axios.interceptors.response.use(
  response => {
    // 对响应数据做点什么
    return response.data;
  },
  error => {
    // 对响应错误做点什么
    if (error.response.status === 401) {
      // 处理认证错误
    }
    return Promise.reject(error);
  }
);

解析:

  • axios.interceptors.request.use 方法注册请求拦截器。
  • 请求拦截器的第一个参数是在发送请求之前对请求配置进行处理的函数,可以添加或修改请求头等。
  • 请求拦截器的第二个参数是在请求错误时处理错误的函数。
  • axios.interceptors.response.use 方法注册响应拦截器。
  • 响应拦截器的第一个参数是在收到响应后对响应数据进行处理的函数。
  • 响应拦截器的第二个参数是在响应错误时处理错误的函数,可以根据不同的错误状态码进行不同的处理。

四、高级特性

4.1 并发请求
async function makeMultipleRequests() {
  try {
    const [users, posts] = await Promise.all([
      axios.get('/users'),
      axios.get('/posts')
    ]);
    
    console.info('Users:', users.data);
    console.info('Posts:', posts.data);
  } catch (error) {
    console.error('Error:', error);
  }
}

解析:

  • Promise.all 方法用于并发执行多个请求,并等待所有请求完成。
  • axios.get('/users')axios.get('/posts') 分别发起两个 GET 请求。
  • 两个请求的结果将被解构赋值给 usersposts
  • 使用 try...catch 语句处理并发请求中的异步错误。
4.2 请求配置
const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json'
  },
  // 自定义转换数据
  transformRequest: [(data) => {
    // 对发送的数据进行转换
    return JSON.stringify(data);
  }],
  transformResponse: [(data) => {
    // 对接收的数据进行转换
    return JSON.parse(data);
  }]
});

解析:

  • axios.create 方法创建一个新的 Axios 实例。
  • transformRequest 是一个数组,包含一个或多个函数,这些函数在请求发送之前对请求体数据进行处理。
  • transformResponse 是一个数组,包含一个或多个函数,这些函数在响应数据接收之后对数据进行处理。
  • JSON.stringify(data) 将数据转换为 JSON 字符串。
  • JSON.parse(data) 将 JSON 字符串转换为 JavaScript 对象。
4.3 错误处理
async function handleRequestWithError() {
  try {
    await axios.get('/api/data');
  } catch (error) {
    if (error.response) {
      // 服务器返回错误状态码
      console.error('Status:', error.response.status);
      console.error('Data:', error.response.data);
    } else if (error.request) {
      // 请求已发送但没有收到响应
      console.error('No response:', error.request);
    } else {
      // 请求配置出错
      console.error('Error:', error.message);
    }
  }
}

解析:

  • try...catch 语句用于捕获请求中的异步错误。
  • error.response 对象包含服务器返回的错误信息,包括状态码和数据。
  • error.request 对象包含已发送的请求信息,但没有收到响应。
  • error.message 包含请求配置出错的信息。

五、最佳实践

5.1 封装 API 服务
// api/index.ets
import axios from '@ohos/axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});

export const userService = {
  getUser: (id: string) => api.get(`/users/${id}`),
  createUser: (userData: any) => api.post('/users', userData),
  updateUser: (id: string, userData: any) => api.put(`/users/${id}`, userData),
  deleteUser: (id: string) => api.delete(`/users/${id}`)
};

解析:

  • api 是一个 Axios 实例,设置了基础 URL 和超时时间。
  • userService 是一个对象,封装了用户相关的 API 请求方法。
  • 每个方法都使用 api 实例发起请求,传入不同的参数。
5.2 统一错误处理

在 API 请求中,统一错误处理是非常重要的,它可以帮助我们更好地管理和响应应用程序中的异常。通过使用 axios 的拦截器功能,我们可以集中地处理所有的请求和响应错误,从而简化代码逻辑,提高代码的可维护性。

5.2.1 错误拦截器的实现

setupErrorHandler 函数中,我们定义了一个响应拦截器,该拦截器会检查每个响应的状态码,并根据状态码执行相应的错误处理逻辑。

// utils/errorHandler.ets
import axios from '@ohos/axios';

export function setupErrorHandler(axiosInstance) {
  axiosInstance.interceptors.response.use(
    (response) => {
      // 如果请求成功,直接返回响应数据
      return response;
    },
    (error) => {
      // 如果请求失败,处理错误
      if (error.response) {
        // 服务器返回了错误状态码
        switch (error.response.status) {
          case 401:
            // 处理未授权错误
            console.error('未授权访问,请重新登录');
            // 例如,可以重定向到登录页面
            // navigateToLogin();
            break;
          case 403:
            // 处理禁止访问错误
            console.error('禁止访问该资源');
            // 可以显示一个禁止访问的提示
            // showAlert('禁止访问该资源');
            break;
          case 404:
            // 处理资源未找到错误
            console.error('请求的资源未找到');
            // 可以显示一个资源未找到的提示
            // showAlert('请求的资源未找到');
            break;
          case 500:
            // 处理服务器内部错误
            console.error('服务器内部错误');
            // 可以显示一个服务器错误的提示
            // showAlert('服务器内部错误');
            break;
          default:
            // 处理其他状态码的错误
            console.error('未知错误:', error.response.status, error.response.data);
            // 可以显示一个通用的错误提示
            // showAlert('未知错误');
            break;
        }
      } else if (error.request) {
        // 请求已发送但没有收到响应
        console.error('请求超时或网络问题');
        // 可以显示一个请求超时或网络问题的提示
        // showAlert('请求超时或网络问题');
      } else {
        // 请求配置出错
        console.error('请求配置错误:', error.message);
        // 可以显示一个请求配置错误的提示
        // showAlert('请求配置错误');
      }

      // 继续向下抛出错误,以便上层代码可以处理
      return Promise.reject(error);
    }
  );
}
5.2.2 使用错误拦截器

在创建 axios 实例时,我们可以调用 setupErrorHandler 函数来设置错误拦截器。

// api/index.ets
import axios from '@ohos/axios';
import { setupErrorHandler } from './utils/errorHandler';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});

// 设置错误拦截器
setupErrorHandler(api);

export const userService = {
  getUser: (id: string) => api.get(`/users/${id}`),
  createUser: (userData: any) => api.post('/users', userData),
  updateUser: (id: string, userData: any) => api.put(`/users/${id}`, userData),
  deleteUser: (id: string) => api.delete(`/users/${id}`)
};
5.2.2.1 axios.create 的配置
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});
  • baseURL: 设置基础 URL,所有请求都会在这个基础 URL 上进行拼接。
  • timeout: 设置请求的超时时间,单位是毫秒。如果请求超过这个时间还没有响应,请求会被取消。
5.2.2.2 设置错误拦截器
setupErrorHandler(api);

这行代码调用了 setupErrorHandler 函数,并将 axios 实例 api 作为参数传递。setupErrorHandler 函数会在 api 实例上添加一个响应拦截器。

5.2.2.3 响应拦截器的逻辑
axiosInstance.interceptors.response.use(
  (response) => {
    // 如果请求成功,直接返回响应数据
    return response;
  },
  (error) => {
    // 如果请求失败,处理错误
    if (error.response) {
      // 服务器返回了错误状态码
      switch (error.response.status) {
        case 401:
          // 处理未授权错误
          console.error('未授权访问,请重新登录');
          // 例如,可以重定向到登录页面
          // navigateToLogin();
          break;
        case 403:
          // 处理禁止访问错误
          console.error('禁止访问该资源');
          // 可以显示一个禁止访问的提示
          // showAlert('禁止访问该资源');
          break;
        case 404:
          // 处理资源未找到错误
          console.error('请求的资源未找到');
          // 可以显示一个资源未找到的提示
          // showAlert('请求的资源未找到');
          break;
        case 500:
          // 处理服务器内部错误
          console.error('服务器内部错误');
          // 可以显示一个服务器错误的提示
          // showAlert('服务器内部错误');
          break;
        default:
          // 处理其他状态码的错误
          console.error('未知错误:', error.response.status, error.response.data);
          // 可以显示一个通用的错误提示
          // showAlert('未知错误');
          break;
      }
    } else if (error.request) {
      // 请求已发送但没有收到响应
      console.error('请求超时或网络问题');
      // 可以显示一个请求超时或网络问题的提示
      // showAlert('请求超时或网络问题');
    } else {
      // 请求配置出错
      console.error('请求配置错误:', error.message);
      // 可以显示一个请求配置错误的提示
      // showAlert('请求配置错误');
    }

    // 继续向下抛出错误,以便上层代码可以处理
    return Promise.reject(error);
  }
);
  • response:成功响应时的处理函数。如果请求成功,直接返回响应数据。
  • error:失败响应时的处理函数。根据 error 的不同属性,我们可以区分不同类型的错误:

    • error.response:服务器返回了错误状态码。我们可以使用 switch 语句来处理不同状态码的错误,并执行相应的逻辑。
    • error.request:请求已发送但没有收到响应。这种情况通常是由于网络问题或请求超时引起的。
    • error.message:请求配置出错。例如,URL 生成错误或请求头配置错误。
5.2.3 重试机制

在实际应用中,我们可能希望在某些情况下自动重试请求,例如网络不稳定时。我们可以通过扩展错误拦截器来实现这一功能。

// utils/errorHandler.ets
export function setupErrorHandler(axiosInstance) {
  let retryCount = 0;
  const maxRetries = 3;

  axiosInstance.interceptors.response.use(
    (response) => {
      return response;
    },
    (error) => {
      const originalRequest = error.config;

      if (error.response) {
        switch (error.response.status) {
          case 401:
            console.error('未授权访问,请重新登录');
            // 例如,可以重定向到登录页面
            // navigateToLogin();
            break;
          case 403:
            console.error('禁止访问该资源');
            // 可以显示一个禁止访问的提示
            // showAlert('禁止访问该资源');
            break;
          case 404:
            console.error('请求的资源未找到');
            // 可以显示一个资源未找到的提示
            // showAlert('请求的资源未找到');
            break;
          case 500:
            console.error('服务器内部错误');
            // 可以显示一个服务器错误的提示
            // showAlert('服务器内部错误');
            break;
          default:
            console.error('未知错误:', error.response.status, error.response.data);
            // 可以显示一个通用的错误提示
            // showAlert('未知错误');
            break;
        }
      } else if (error.request) {
        console.error('请求超时或网络问题');
        // 可以显示一个请求超时或网络问题的提示
        // showAlert('请求超时或网络问题');

        // 重试机制
        if (retryCount < maxRetries) {
          retryCount++;
          return axiosInstance(originalRequest);
        }
      } else {
        console.error('请求配置错误:', error.message);
        // 可以显示一个请求配置错误的提示
        // showAlert('请求配置错误');
      }

      // 继续向下抛出错误,以便上层代码可以处理
      return Promise.reject(error);
    }
  );

  // 重置重试计数器
  axiosInstance.interceptors.request.use(
    (config) => {
      retryCount = 0;
      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );
}
  • retryCount:记录当前请求的重试次数。
  • maxRetries:设置最大重试次数。
  • error.request 分支中,我们检查 retryCount 是否小于 maxRetries,如果小于,则递增 retryCount 并重新发起请求。
  • axiosInstance.interceptors.request.use 中,我们重置 retryCount,以确保每次新的请求时计数器从 0 开始。
5.2.4 自定义错误处理

在某些情况下,我们可能希望为不同的 API 提供不同的错误处理逻辑。我们可以通过在服务层定义自定义的错误处理函数来实现这一点。

// api/index.ets
import axios from '@ohos/axios';
import { setupErrorHandler } from './utils/errorHandler';

const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000
});

// 设置默认错误拦截器
setupErrorHandler(api);

// 自定义错误处理
api.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    if (error.config && error.config.customErrorHandling) {
      // 调用自定义错误处理函数
      error.config.customErrorHandling(error);
    }

    return Promise.reject(error);
  }
);

export const userService = {
  getUser: (id: string) => api.get(`/users/${id}`, {
    customErrorHandling: (error) => {
      console.error('自定义错误处理:', error);
      // 可以在这里添加自定义的错误处理逻辑
    }
  }),
  createUser: (userData: any) => api.post('/users', userData),
  updateUser: (id: string, userData: any) => api.put(`/users/${id}`, userData),
  deleteUser: (id: string) => api.delete(`/users/${id}`)
};
  • api.interceptors.response.use 中,我们检查 error.config 是否包含 customErrorHandling 属性。
  • 如果存在 customErrorHandling 属性,我们调用该函数并传递错误对象。
  • userService 中,我们在每个请求的配置对象中添加 customErrorHandling 属性,以便为每个请求提供不同的错误处理逻辑。

帅比九日
7 声望2 粉丝