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

基于@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 属性,以便为每个请求提供不同的错误处理逻辑。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容