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
相关产品推荐
相关产品推荐

