HarmonyOS APP开发—"菜谱汇"美食菜谱App,需要用到这个库

做一个菜谱聚合 App,从多个美食 API 拉菜谱、下载菜品高清图、拦截器统一加 Token。@ohos/axios 的并发请求 + 拦截器 + 下载进度回调,让菜谱加载体验丝滑。

📦 仓库地址:https://gitcode.com/CPF-ApplicationTPC/ohos_axios | 安装:ohpm install @ohos/axios


写在前面

"菜谱汇"要从三个美食数据源聚合菜谱,每个菜谱有封面图需要下载到本地缓存。核心需求:

  • 多源并发请求,合并后按评分排序
  • 菜品封面图下载带进度条
  • 统一鉴权头(每个源 Token 不同)

如果用原生 @ohos.net.http,每个请求都要手写 header、手写回调嵌套、手写文件流处理——三个源就是三份重复代码。

@ohos/axiosaxios.all 并发 + 拦截器统一鉴权 + onDownloadProgress 进度回调,三件事一把梭。而且 API 和前端 axios 完全一致,写过前端的人零学习成本。

这篇文章聊什么

  1. 多源聚合——axios.all 并发拉三个菜谱源
  2. 统一鉴权——多实例拦截器,各源 Token 独立管理
  3. 封面图下载——进度回调驱动 UI 进度条

菜谱汇 App

axios.all 并发

源A: 下厨房

源B: 美食杰

源C: 豆果

合并+按评分排序

渲染菜谱列表

点击下载封面

onDownloadProgress

进度条更新


第一步:安装与权限

ohpm install @ohos/axios
"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET",
    "reason": "$string:network_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  }
]

第二步:多实例拦截器管理不同源的 Token

三个数据源的鉴权方式不同,用 axios.create() 建独立实例,各自配拦截器:

import axios, { AxiosResponse, AxiosError } from '@ohos/axios'

interface Recipe {
  id: string
  title: string
  cover: string
  rating: number
  source: string
}

// 源 A:Token 放 Header
const sourceA = axios.create({ baseURL: 'https://api.xiachufang.com', timeout: 10000 })
sourceA.interceptors.request.use((config) => {
  config.headers['Authorization'] = 'Bearer ' + getTokenA()
  return config
})

// 源 B:Token 放 Query 参数
const sourceB = axios.create({ baseURL: 'https://api.meishij.com', timeout: 10000 })
sourceB.interceptors.request.use((config) => {
  config.params = { ...config.params, api_key: getTokenB() }
  return config
})

// 源 C:需要签名
const sourceC = axios.create({ baseURL: 'https://api.douguo.com', timeout: 10000 })
sourceC.interceptors.request.use((config) => {
  config.headers['X-Sign'] = generateSign(config.url, config.params)
  return config
})

// 统一的错误兜底(三个实例共用逻辑)
function attachErrorHandler(instance: typeof axios) {
  instance.interceptors.response.use(
    (response) => response,
    (error: AxiosError) => {
      if (error.response?.status === 401) {
        console.warn('Token 过期,需要刷新')
      } else if (error.code === 'ECONNABORTED') {
        console.warn('请求超时')
      }
      return Promise.reject(error)
    }
  )
}
attachErrorHandler(sourceA)
attachErrorHandler(sourceB)
attachErrorHandler(sourceC)

关键设计:每个源一个实例,鉴权逻辑各自独立,互不干扰。错误处理抽成公共函数复用。


第三步:并发拉取三个源并合并

async function fetchAllRecipes(category: string): Promise<Recipe[]> {
  try {
    const [resA, resB, resC] = await axios.all([
      sourceA.get<Recipe[], AxiosResponse<Recipe[]>, null>(`/recipes?category=${category}`),
      sourceB.get<Recipe[], AxiosResponse<Recipe[]>, null>(`/recipes?category=${category}`),
      sourceC.get<Recipe[], AxiosResponse<Recipe[]>, null>(`/recipes?category=${category}`),
    ])

    // 合并 + 按评分降序 + 去重(同名菜谱只留评分最高的)
    const merged = [...resA.data, ...resB.data, ...resC.data]
      .sort((a, b) => b.rating - a.rating)

    const seen = new Set<string>()
    return merged.filter((r) => {
      if (seen.has(r.title)) return false
      seen.add(r.title)
      return true
    })
  } catch (err) {
    console.error('聚合失败: ' + JSON.stringify(err))
    return []
  }
}

💡 容错建议:如果希望"某个源挂了也能展示其他源的数据",用 Promise.allSettled 替代 axios.all,单独处理每个源的结果。

// 部分失败也能出数据
const results = await Promise.allSettled([
  sourceA.get<Recipe[], AxiosResponse<Recipe[]>, null>(url),
  sourceB.get<Recipe[], AxiosResponse<Recipe[]>, null>(url),
  sourceC.get<Recipe[], AxiosResponse<Recipe[]>, null>(url),
])

const recipes: Recipe[] = []
results.forEach((r, i) => {
  if (r.status === 'fulfilled') {
    recipes.push(...r.value.data)
  } else {
    console.warn(`${i} 失败,已跳过: ${r.reason}`)
  }
})

第四步:封面图下载带进度

用户点开菜谱详情,下载高清封面图到本地缓存:

import fs from '@ohos.file.fs'

@State downloadPercent: number = 0

async function downloadCover(recipe: Recipe): Promise<string> {
  const filePath = getContext(this).cacheDir + `/cover_${recipe.id}.jpg`

  // 已缓存则直接返回,不重复下载
  try {
    fs.accessSync(filePath)
    return filePath
  } catch (e) { /* 文件不存在,继续下载 */ }

  const res = await axios.get<ArrayBuffer, AxiosResponse<ArrayBuffer>, null>(recipe.cover, {
    responseType: 'array_buffer',
    onDownloadProgress: (e) => {
      this.downloadPercent = e?.total ? Math.ceil(e.loaded / e.total * 100) : 0
    },
  })

  // 写入本地缓存
  const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
  fs.writeSync(file.fd, res.data)
  fs.closeSync(file)

  return filePath
}

UI 上显示进度条:

Column() {
  if (this.downloadPercent > 0 && this.downloadPercent < 100) {
    Progress({ value: this.downloadPercent, total: 100 })
      .width('80%')
    Text(`下载高清封面 ${this.downloadPercent}%`).fontSize(12).fontColor('#999')
  } else {
    Image(this.localCoverPath).width('100%').aspectRatio(1.5)
  }
}

第五步:预加载列表页的缩略图

列表页滑动时提前把即将进入视口的封面拉进缓存:

// 预加载下 5 条菜谱的封面
function preloadCovers(recipes: Recipe[], currentIndex: number) {
  recipes.slice(currentIndex + 1, currentIndex + 6).forEach((r) => {
    axios.get<ArrayBuffer, AxiosResponse<ArrayBuffer>, null>(r.cover, {
      responseType: 'array_buffer',
      // 预加载用低优先级,不抢当前请求带宽
      priority: 1,
    }).catch(() => { /* 预加载失败静默处理 */ })
  })
}

为什么"菜谱汇"选了 axios?

需求原生 @ohos.net.http@ohos/axios
多源并发手写 Promise.all + 回调axios.all
各源独立鉴权每次手动加 header✅ 多实例拦截器
错误兜底每个请求各写 catch✅ 响应拦截器统一
下载进度手写流式处理onDownloadProgress
部分失败容错手写Promise.allSettled

总结

"菜谱汇"这个场景里,@ohos/axios 解决了三件事:

  1. 多源聚合——axios.all 并发拉三个源,allSettled 保证部分失败也能出数据
  2. 鉴权隔离——每个源独立实例 + 独立拦截器,Token 管理不串味
  3. 下载体验——onDownloadProgress 驱动进度条,缓存命中直接跳过下载

如果你也在做聚合类、资讯类、电商类需要多接口并发的鸿蒙 App,@ohos/axios 的拦截器体系能把重复的鉴权和错误处理代码全部收敛掉。

Logo

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

更多推荐