You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter如何实现API获取的图片与.mp3文件本地存储

批量存储接口返回图片、音频到本地实现方案

核心逻辑是遍历接口返回的资源列表,过滤无效地址后控制并发拉取资源二进制内容,按命名规则写入本地即可,以下是可直接复用的实现:

通用实现流程

  • 过滤无效资源:遍历接口返回的data数组,剔除image_src、audio_src/audio_file字段为空、null的项,避免无效请求
  • 并发控制:同时发起的下载请求数控制在3-5个,避免请求量过大打挂资源服务器、或者触发浏览器请求限制
  • 资源落盘:拿到资源二进制内容后,按自定义规则命名(建议绑定数据唯一id避免重名覆盖),存储到指定路径,单个文件下载/存储失败做异常捕获,不中断整个批量任务
  • 结果统计:所有任务执行完成后,输出成功、失败的资源清单,方便后续重试或者做用户提示

代码实现

浏览器端(纯前端/网页环境,触发浏览器下载到用户本地)

// 并发控制函数,限制同时下载的请求数量
async function asyncPool(concurrency, items, iteratorFn) {
  const ret = [];
  const executing = new Set();
  for (const item of items) {
    const p = Promise.resolve().then(() => iteratorFn(item));
    ret.push(p);
    executing.add(p);
    const clean = () => executing.delete(p);
    p.then(clean).catch(clean);
    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }
  return Promise.all(ret);
}

// 单个文件下载方法
async function downloadFile(url, fileName) {
  if (!url) return { success: false, url, msg: '资源地址为空' };
  try {
    const res = await fetch(url);
    const blob = await res.blob();
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(a.href);
    return { success: true, fileName, url };
  } catch (err) {
    return { success: false, url, msg: err.message };
  }
}

// 批量处理入口
async function batchSaveResources(apiData) {
  const taskList = [];
  apiData.data.forEach(item => {
    // 组装图片下载任务
    if (item.image_src) {
      const imgSuffix = item.image_src.split('.').pop().split('?')[0];
      taskList.push({
        url: item.image_src,
        fileName: `${item.id}_${item.title}.${imgSuffix}`
      })
    }
    // 组装音频下载任务,兼容接口两个音频字段
    const audioUrl = item.audio_src || item.audio_file;
    if (audioUrl) {
      const audioSuffix = audioUrl.includes('.mp3') ? 'mp3' : audioUrl.split('.').pop().split('?')[0];
      taskList.push({
        url: audioUrl,
        fileName: `${item.id}_${item.title}.${audioSuffix}`
      })
    }
  })

  // 控制3个并发执行下载
  const result = await asyncPool(3, taskList, (task) => downloadFile(task.url, task.fileName));
  const successList = result.filter(i => i.success);
  const failList = result.filter(i => !i.success);
  console.log(`下载完成:成功${successList.length}个,失败${failList.length}个`, failList);
  return { successList, failList };
}

// 调用时直接传入接口返回的完整响应对象即可
// batchSaveResources(yourApiResponseData)

Node.js/Electron环境(直接写入本地磁盘指定目录)

并发控制方法asyncPool直接复用上面浏览器端代码里的实现即可,Node18+版本自带全局fetch,低版本需要自行安装node-fetch依赖。

const fs = require('fs/promises');
const path = require('path');

async function downloadFileToDisk(url, savePath) {
  if (!url) return { success: false, url, msg: '资源地址为空' };
  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`请求失败,状态码${res.status}`);
    const arrayBuffer = await res.arrayBuffer();
    const buffer = Buffer.from(arrayBuffer);
    await fs.writeFile(savePath, buffer);
    return { success: true, savePath, url };
  } catch (err) {
    return { success: false, url, msg: err.message };
  }
}

async function batchSaveResourcesToDisk(apiData, saveDir = './download_resource') {
  // 初始化存储目录,自动分类图片、音频
  await fs.mkdir(saveDir, { recursive: true });
  const imgDir = path.join(saveDir, 'images');
  const audioDir = path.join(saveDir, 'audios');
  await fs.mkdir(imgDir, { recursive: true });
  await fs.mkdir(audioDir, { recursive: true });

  const taskList = [];
  apiData.data.forEach(item => {
    if (item.image_src) {
      const imgSuffix = item.image_src.split('.').pop().split('?')[0];
      const savePath = path.join(imgDir, `${item.id}_${item.title}.${imgSuffix}`);
      taskList.push(downloadFileToDisk(item.image_src, savePath));
    }
    const audioUrl = item.audio_src || item.audio_file;
    if (audioUrl) {
      const audioSuffix = audioUrl.includes('.mp3') ? 'mp3' : audioUrl.split('.').pop().split('?')[0];
      const savePath = path.join(audioDir, `${item.id}_${item.title}.${audioSuffix}`);
      taskList.push(downloadFileToDisk(audioUrl, savePath));
    }
  })

  const result = await asyncPool(3, taskList, (task) => task);
  const successList = result.filter(i => i.success);
  const failList = result.filter(i => !i.success);
  console.log(`存储完成:成功${successList.length}个,失败${failList.length}个`, failList);
  return { successList, failList };
}

注意事项

  • 纯前端环境如果遇到跨域报错,需要让资源服务配置CORS规则,或者通过自己的后端服务做一层资源代理
  • 提取文件后缀的时候记得去掉url后面的查询参数(比如?x-oss-process=xxx这类),避免后缀识别错误
  • 如果是存储大体积音频文件,可以额外加分片下载、断点续传逻辑,普通几M的学习类音频直接整包下载即可
  • 失败的资源可以单独收集,支持单独重试,不用重复下载已经成功的文件

内容的提问来源于stack exchange,提问作者Sanjay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 19:36:45