如何实现缓存音频功能(App端详解)
📦 音频缓存与播放流程说明
项目目标
实现App端音频资源的自动缓存与播放:
- 获取远程音频URL
- 下载文件至本地
_downloads/
目录 - 优先使用缓存音频进行播放
- 提供缓存管理功能(大小查看、清理)
关键问题与解决方案
_downloads
plus.io.resolveLocalFileSystemURL
操作路径safeFileName()
函数过滤非法字符一、获取音频URL流程
安全优化方案
#mermaid-svg-N44aIRVt6ZVqo0mV {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV .error-icon{fill:#552222;}#mermaid-svg-N44aIRVt6ZVqo0mV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N44aIRVt6ZVqo0mV .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-N44aIRVt6ZVqo0mV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N44aIRVt6ZVqo0mV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N44aIRVt6ZVqo0mV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N44aIRVt6ZVqo0mV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N44aIRVt6ZVqo0mV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N44aIRVt6ZVqo0mV .marker.cross{stroke:#333333;}#mermaid-svg-N44aIRVt6ZVqo0mV svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N44aIRVt6ZVqo0mV .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV .cluster-label text{fill:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV .cluster-label span{color:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV .label text,#mermaid-svg-N44aIRVt6ZVqo0mV span{fill:#333;color:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV .node rect,#mermaid-svg-N44aIRVt6ZVqo0mV .node circle,#mermaid-svg-N44aIRVt6ZVqo0mV .node ellipse,#mermaid-svg-N44aIRVt6ZVqo0mV .node polygon,#mermaid-svg-N44aIRVt6ZVqo0mV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N44aIRVt6ZVqo0mV .node .label{text-align:center;}#mermaid-svg-N44aIRVt6ZVqo0mV .node.clickable{cursor:pointer;}#mermaid-svg-N44aIRVt6ZVqo0mV .arrowheadPath{fill:#333333;}#mermaid-svg-N44aIRVt6ZVqo0mV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N44aIRVt6ZVqo0mV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N44aIRVt6ZVqo0mV .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-N44aIRVt6ZVqo0mV .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-N44aIRVt6ZVqo0mV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N44aIRVt6ZVqo0mV .cluster text{fill:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV .cluster span{color:#333;}#mermaid-svg-N44aIRVt6ZVqo0mV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N44aIRVt6ZVqo0mV :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}发送单词通过前端后端服务安全校验生成加密签名请求音频API返回音频URL
实施要点:
- 前端仅传输必要参数
- 敏感操作(签名生成)在后端完成
- 增加请求合法性验证
二、缓存与播放核心流程
主流程实现(playAudio函数)
async function playAudio(id, remoteUrl) {// 平台检测if (isH5Platform()) {return playRemoteAudio(remoteUrl); // H5直接播放}// 生成缓存文件名const fileName = `audio-cache-${safeFileName(id)}.mp3`;const localPath = `_downloads/${fileName}`;// 检查缓存是否存在if (await fileExists(localPath)) {return playLocalAudio(localPath);}// 下载并缓存try {const tempPath = await downloadFile(remoteUrl);const permanentPath = await saveToDownloads(tempPath, fileName);playLocalAudio(permanentPath);} catch (error) {playRemoteAudio(remoteUrl); // 降级方案}}
文件名安全处理
function safeFileName(id) {// 保留安全字符,过滤特殊符号return id.replace(/[^a-zA-Z0-9\\-_\\.]/g, \'\');}
三、本地缓存操作详解
缓存大小获取
async function getAudioCacheSize() {return new Promise((resolve) => {getDirectoryEntries(\'_downloads/\', (entries) => {const cacheFiles = entries.filter(e => e.name.startsWith(\'audio-cache-\'));const totalSize = calculateTotalSize(cacheFiles);resolve(formatFileSize(totalSize));});});}
缓存清理
async function clearAudioCache() {return new Promise((resolve) => {getDirectoryEntries(\'_downloads/\', (entries) => {const deletions = entries.filter(e => e.name.startsWith(\'audio-cache-\')).map(file => deleteFile(file));Promise.all(deletions).then(() => resolve(true));});});}
缓存保存流程
#mermaid-svg-eQ2B7GVhIyYjmOPC {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC .error-icon{fill:#552222;}#mermaid-svg-eQ2B7GVhIyYjmOPC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eQ2B7GVhIyYjmOPC .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-eQ2B7GVhIyYjmOPC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eQ2B7GVhIyYjmOPC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eQ2B7GVhIyYjmOPC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eQ2B7GVhIyYjmOPC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eQ2B7GVhIyYjmOPC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eQ2B7GVhIyYjmOPC .marker.cross{stroke:#333333;}#mermaid-svg-eQ2B7GVhIyYjmOPC svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eQ2B7GVhIyYjmOPC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eQ2B7GVhIyYjmOPC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-eQ2B7GVhIyYjmOPC .actor-line{stroke:grey;}#mermaid-svg-eQ2B7GVhIyYjmOPC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC .sequenceNumber{fill:white;}#mermaid-svg-eQ2B7GVhIyYjmOPC #sequencenumber{fill:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC .messageText{fill:#333;stroke:#333;}#mermaid-svg-eQ2B7GVhIyYjmOPC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eQ2B7GVhIyYjmOPC .labelText,#mermaid-svg-eQ2B7GVhIyYjmOPC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-eQ2B7GVhIyYjmOPC .loopText,#mermaid-svg-eQ2B7GVhIyYjmOPC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-eQ2B7GVhIyYjmOPC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eQ2B7GVhIyYjmOPC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-eQ2B7GVhIyYjmOPC .noteText,#mermaid-svg-eQ2B7GVhIyYjmOPC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-eQ2B7GVhIyYjmOPC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eQ2B7GVhIyYjmOPC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eQ2B7GVhIyYjmOPC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eQ2B7GVhIyYjmOPC .actorPopupMenu{position:absolute;}#mermaid-svg-eQ2B7GVhIyYjmOPC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-eQ2B7GVhIyYjmOPC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eQ2B7GVhIyYjmOPC .actor-man circle,#mermaid-svg-eQ2B7GVhIyYjmOPC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-eQ2B7GVhIyYjmOPC :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}AppTempStoragePermanentStorageAudioPlayer下载文件返回临时路径请求文件转移返回永久路径播放音频AppTempStoragePermanentStorageAudioPlayer
关键步骤:
- 下载文件到临时存储
- 通过
copyTo
操作转移到永久目录 - 系统自动清理临时文件
🔚 功能总结
playAudio(id, url)
统一入口_downloads/audio-cache-{safeID}.mp3
getAudioCacheSize()
和 clearAudioCache()
注意事项
- 平台权限:确保App有本地存储权限
- 缓存策略:
- 建议添加缓存上限(如100MB)
- 实现LRU(最近最少使用)清理机制
- 网络优化:
- 大文件下载显示进度条
- 支持暂停/恢复下载
- 安全存储:敏感内容建议加密存储