第 32 章 专辑封面、缓存与 custom:// 协议
本章目标
- 注册
cover://<hash>协议,让前端<img>直接渲染 cache 里的封面。 - 明白 Tauri 2.x 注册自定义协议的两种方式。
- 处理缩略图、懒加载、CSP 白名单。
一、为什么要 custom 协议
前端 <img src="..."> 只能走:
http:///https://:需要 CORS。data:...:base64 过大。asset:/// Tauri 内置:只能访问打包进去的资源。- 本地绝对路径:不安全且会被 CSP 拒绝。
所以我们自定义 cover://<song_or_hash>,Rust 侧读磁盘返回字节,前端直接 src="cover://abc123"。
二、注册 URI Scheme
// src-tauri/src/lib.rs
use tauri::{http::Response, Manager};
tauri::Builder::default()
// ... plugins, setup
.register_uri_scheme_protocol("cover", |app, req| {
let handle = app.clone();
let uri = req.uri().clone();
tauri::async_runtime::block_on(async move {
let path = serve_cover(&handle, &uri).await;
match path {
Ok((bytes, mime)) => Response::builder()
.status(200)
.header("Content-Type", mime)
.header("Access-Control-Allow-Origin", "*")
.body(bytes).unwrap(),
Err(_) => Response::builder().status(404).body(Vec::new()).unwrap(),
}
})
})
serve_cover:
async fn serve_cover(app: &tauri::AppHandle, uri: &tauri::http::Uri) -> anyhow::Result<(Vec<u8>, &'static str)> {
// cover://fallback 或 cover://<hash>
let host = uri.host().unwrap_or("fallback");
if host == "fallback" {
return Ok((include_bytes!("../../icons/placeholder-cover.png").to_vec(), "image/png"));
}
let cache = app.path().app_cache_dir()?.join("covers");
for ext in ["jpg","png","webp"] {
let p = cache.join(format!("{}.{}", host, ext));
if p.exists() {
let bytes = tokio::fs::read(&p).await?;
let mime = match ext { "png" => "image/png", "webp" => "image/webp", _ => "image/jpeg" };
return Ok((bytes, mime));
}
}
anyhow::bail!("not found")
}
三、CSP 修改
tauri.conf.json 的 security.csp:
"csp": "default-src 'self'; img-src 'self' data: blob: cover: https: http: asset:; media-src 'self' asset: cover: https: http:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ipc: http: https:"
img-src 和 media-src 中加入 cover: scheme。
四、前端用法
<img src={`cover://${song.fileHash}`} className="w-12 h-12 rounded" />
<img src="cover://fallback" />
简单、快速、零 IPC。
五、缩略图生成(可选优化)
大封面(2000×2000 JPG)每次加载可能 500KB+。生成 128×128 缩略图:
use image::imageops::FilterType;
pub async fn ensure_thumb(cache: &Path, hash: &str) -> anyhow::Result<()> {
let src = cache.join(format!("{}.jpg", hash));
let thumb = cache.join(format!("{}-thumb.webp", hash));
if thumb.exists() { return Ok(()); }
let img = image::open(&src)?;
let small = img.resize(128, 128, FilterType::Triangle);
small.save_with_format(&thumb, image::ImageFormat::WebP)?;
Ok(())
}
协议中按 host 前缀判断:cover://thumb/abc123。
六、懒加载 & 并发限制
列表一屏可能有几十张封面。浏览器 loading="lazy" + IntersectionObserver 控制。Rust 侧对 serve_cover 用 semaphore 限流。
<img loading="lazy" src={`cover://${song.fileHash}`} />
七、另一个坑:本地音频文件能不能也用协议?
能。原理一样:注册 track:// 协议,返回 audio/mp3 bytes。但大文件推荐走 asset://(Tauri 内置,支持 Range 请求),或者前端只做列表,实际播放在 Rust 完成(CloudTone 即如此)。
本章小结
register_uri_scheme_protocol是 Tauri 最强大的扩展点之一。- CSP 必须同步更新。
- 封面缓存 + 协议 = 前端零成本显示。
动手时刻
-
实现
cover://协议。 - Library 页每一行显示真实封面。
- 切换到深色/浅色主题,fallback 封面也跟着换。
下一章:搜索。