【WebGL应用开发】浏览器平台文件系统方案
2025年6月25日 18:28
可用方案
Origin-Private File System(OPFS)——浏览器里的 “ext4”
-
原理
- File System Access API 在站点沙箱分区创建真文件;数据直接落磁盘块,不占用 IndexedDB 配额。
- 主线程异步 I/O;Worker 可用
getSyncAccessHandle()
获得互斥同步 I/O(≈ fread/fwrite)。
-
兼容性
- Chrome / Edge 86+、Opera 对齐;Firefox 111+ 默认开启;Safari 17+ 全面支持(桌面 + iOS)。
- Android WebView 基于 Chromium 同版本也已带上。
-
典型用法
// 打开 / 创建沙箱根目录
const root = await navigator.storage.getDirectory();
// 1) 异步写
const fh = await root.getFileHandle('crash.dmp', { create: true });
const w = await fh.createWritable();
await w.write(new Uint8Array(coreDump));
await w.close();
// 2) Worker 同步写(C/C++重度场景)
const sync = await fh.createSyncAccessHandle();
sync.write(buffer, { at: 0 });
sync.flush();
sync.close();
- 适合场景 大体积或高频随机读写:崩溃转储、SQLite-WASM、视频缓存等。
File Picker 系列——让用户自己选文件 / 目录
-
showOpenFilePicker()
showSaveFilePicker()
showDirectoryPicker()
- 原理:浏览器弹系统对话框,返回 FileSystemFileHandle;会话结束或权限收回后失效。
- 兼容性:与 OPFS 基本一致,但 Safari 17 仍只读/只选文件。
- 适合场景:手动导入/导出存档、MOD、截图等。不适合后台自动日志。
IndexedDB(IDB)——浏览器级 NoSQL 仓库
-
原理
- Web 标准对象数据库,B+Tree/LSM 事务结构;异步 API。
- 每域配额:Chrome/Safari ≈ 60 % 空闲盘;Firefox 10 GiB 或 10 %;超限触发逐步清理。
- 兼容性:所有现代浏览器 + 老 Safari,移动端同理。
- 速查片段
const req = indexedDB.open('uwa-crash', 1);
req.onupgradeneeded = () => req.result.createObjectStore('logs');
req.onsuccess = () => {
const db = req.result;
const tx = db.transaction('logs', 'readwrite');
tx.objectStore('logs').put(blobCore, 'core-20250624');
tx.oncomplete = () => console.log('saved');
};
-
适合场景
- 10 MiB ~ GB 数据;跨刷新持久;最广兼容。
- 频繁写入会有事务开销,上层可聚合写(批量 / worker)。
IDBFS(Emscripten / Unity)——把 POSIX 调用映射到 IndexedDB
-
原理
- Emscripten 在内存建一棵 MEMFS;你调用
FS.writeFile
、fopen()
等都是同步。 - 调
FS.syncfs(true)
时把 IndexedDB → 内存,syncfs(false)
再刷回去。 - Unity Player 会自动 mount
/idbfs/<hash>
并把Application.persistentDataPath
指过去。
- Emscripten 在内存建一棵 MEMFS;你调用
-
兼容性:跟 IndexedDB 一致;无需任何额外权限或标头。
-
快速上手
// 仅首次加载 FS.mkdir('/Crash'); FS.mount(IDBFS, {}, '/Crash'); FS.syncfs(true, () => { // 拉取已存在的数据 FS.writeFile('/Crash/log.txt', 'new line\n', { encoding: 'utf8' }); window.addEventListener('pagehide', () => FS.syncfs(false, () => console.log('flushed'))); });
-
适合场景
- 现有 C/C++ / Unity 代码直接用
fopen
/File.WriteAllBytes
,不想改 JS。 - 日志、存档、配置;容量≈ IndexedDB 配额。
- 现有 C/C++ / Unity 代码直接用
MEMFS——纯内存临时文件
-
原理:全部驻留 JS Heap(
Uint8Array
);浏览器刷新即丢。 -
代码示例
FS.writeFile('/tmp/foo.txt', '42'); console.log(FS.readFile('/tmp/foo.txt', {encoding:'utf8'}));
-
适合场景:每帧生成的小文件、shader 缓存、测试 stub。
LocalStorage / SessionStorage
- 同步
key ⇄ string
,容量 ≤ 5 ~ 10 MiB。 - 严重阻塞主线程,且自动转 UTF-16,不适合二进制。
- 只建议存极小配置或「上次崩溃标记位」。
选型建议速览
-
优先级
- OPFS(若浏览器全部覆盖且对容量/并发有要求)
- IDBFS / IndexedDB(最大兼容且容量较大)
- MEMFS + 手动上报/合并(只要临时)
- File Picker(需要人手操作)
- LocalStorage(极小配置)
Emscripten文件系统深入
Emscripten 把 POSIX-style 文件 API(open / read / write / stat ...
)移植到浏览器,做法是:
- 在 JavaScript 里实现一个叫
FS
的虚拟文件系统内核; - 允许把 不同后端 “挂载”到目录,后端决定 数据最终落在哪里。
FS.mkdir('/save');
FS.mount(IDBFS , {}, '/save'); // /save 目录 → IndexedDB
FS.mount(MEMFS , {}, '/tmp'); // /tmp → 纯内存
FS.mount(WORKERFS,{ files: …},'/rom'); // /rom → 只读打包资源
常见后端一览
后端 | 数据落点 | 特性 | 典型用途 |
---|---|---|---|
MEMFS | JS Heap | 同步最快;刷新即丢 | 临时缓冲 / 单元测试 |
IDBFS | IndexedDB |
持久化; 同步 API + FS.syncfs 异步落盘 |
存档、日志、Unity persistentDataPath
|
WORKERFS | 只读 HTML <input type=file> / Drop |
读取用户选中文件 | MOD / 关卡导入 |
NODEFS | Node.js 真文件系统 | 仅 Node 环境可用 | CLI 工具 |
PROXYFS | 通过 postMessage 代理到 Worker |
多线程 WASM | pthread 模式 |
IDBFS 深潜:如何把 POSIX 调用落到 IndexedDB
IDBFS =【同步 POSIX API】+【IndexedDB 持久层】 对 Unity WebGL & 传统 C/C++ 项目是当前最稳妥的文件系统解决方案; 若追求极致性能和最新浏览器覆盖,可开始关注 OPFS 后端 / WASMFS 动向。
原理: 在内存里跑一个 MEMFS,读写秒级同步;你调用 FS.syncfs()
时再把 脏块 批量落到 IndexedDB。
挂载阶段
FS.mkdir('/idb');
FS.mount(IDBFS, {}, '/idb'); // 现在 /idb 看起来就像一个普通磁盘
FS.syncfs(true, onReady); // populate=true ➜ IndexedDB ➜ 内存
-
mount()
创建一个 IDBFS mount 结构体,内部再开一棵 MEMFS 作为 cache。 - 会在 IndexedDB 里打开数据库
EM_FS_<origin_path>
,对象仓库FILE_DATA
。 - 每个文件序列化为
{ path, timestamp, mode, contents(Blob) }
。
正常读写
所有 POSIX 调用全是同步,因为此时只是动 JS Heap:
int fd = open("/idb/user/crash.dmp", O_WRONLY | O_CREAT);
write(fd, buf, len); // <1ms 内存写
close(fd);
Emscripten 在写入时为每个节点打 “dirty” 标记。
FS.syncfs(populate, cb)
—— 真正 I/O 的时刻
步骤 | populate = true | populate = false |
---|---|---|
① 开 IndexedDB 事务 | readonly |
readwrite |
② 遍历对象仓库 | 反序列化所有条目 → 填充 MEMFS | 找到 dirty Node,序列化成 Blob |
③ 事务结束 | JS Heap ←→ IndexedDB 对齐 | IndexedDB 持久化完成后 cb(null)
|
⚠️ 如果你忘记在页面退出前
syncfs(false)
,脏数据就会永远留在内存!
Unity WebGL 中的默认流程
- Unity 的模板脚本在
unityFileSystemInit()
里自动执行FS.mkdir('/idbfs'); FS.mount(IDBFS,{},'/idbfs');
-
Application.persistentDataPath
指向'/idbfs/UnityCache/xxx'
。 - Loader 在首帧
syncfs(true)
,在Module.QuitCleanup
时syncfs(false)
。
结果:C# 侧可以直接 File.WriteAllBytes
而无需关心浏览器差异。
优缺点
优点 | 说明 |
---|---|
同步 API → 对老 C/C++ / Unity 代码 0 改动 | POSIX 语义完整(锁、权限除外) |
数据真正持久化,配额≈ IndexedDB | Chrome/Safari ≈ 60 % 空闲盘;Firefox 10 GiB 上限 |
批量事务,写放大极小 | 每次 flush 统一写入 |
局限 | 规避方案 |
---|---|
刷盘必须显式 FS.syncfs() ;频繁调用有异步开销 |
定时或 pagehide / visibilitychange 时 flush |
单线程同步写 vs. Worker | 若需要高并发,考虑 OPFS + SyncAccessHandle 或 WASMFS 多后端 |
旧 Safari 15- 对 IndexedDB 文件 Blob 支持不齐 | Polyfill(base64)或降级 LocalStorage |
其他后端一瞥
MEMFS
- 零依赖纯 JS;容量受 JS Heap 限制。断电就丢。
WORKERFS
- 把
<input type=file>
传来的File
数组映射为只读节点,让 C/C++ 代码也能open("/rom/hero.png")
。
PROXYFS / OPFS backend(实验)
- 在 pthread / SharedArrayBuffer 场景,将 FS 调用代理到专用 Worker;或直接把 OPFS handle 当作块设备使用,跳过 IndexedDB 额外拷贝。
为什么 Unity 选择 IDBFS
- 兼容性广:自 2012 年起所有桌面 / 移动浏览器都内置 IndexedDB。
- 无需额外权限:不像 OPFS 需要较新的浏览器或 HTTPS。
- 同步 API:引擎层可在主线程 File I/O,不必改成回调或 Promise。
- 崩溃安全:transaction 级落盘,写时先 copy-on-write。
在 JSLib / 自己的 JS 中使用
mergeInto(LibraryManager.library, {
SaveCrashReport: function(ptr, len) {
const data = new Uint8Array(Module.HEAPU8.buffer, ptr, len);
FS.writeFile('/idbfs/crash/' + Date.now() + '.dmp', data);
FS.syncfs(false, ()=> console.log('flushed'));
}
});
只要 FS
已经是全局变量,你就可以随意调用:
FS.readdir
, FS.stat
, FS.readFile
, FS.unlink
… 像 Node.js 的 fs
一样同步易用。
浏览器端持久文件系统对三大游戏引擎的适配要点
结论
- Unity WebGL → 直接用 IDBFS(已内置)最稳;向上可平滑升级到 OPFS
- Cocos2d-JS / Cocos2d-HTML5 → 本身纯 JS,没有 Emscripten;需走 IndexedDB / OPFS API 或社区封装
- Cocos Creator 3.x (WASM backend) → 既能用 IDBFS(引擎内部已挂)、又能调用浏览器原生 OPFS;留意多线程与刷盘时机
Unity (WebGL) — Emscripten 100 % 驱动
目标 | 推荐方案 | 接入成本 | 说明 |
---|---|---|---|
通用持久化 (Application.persistentDataPath , 存档/崩溃转储) |
IDBFS (默认已经 /idbfs 挂载)
|
0 行引擎改动 少量 JS 调 FS.syncfs()
|
File.WriteAllBytes() → 内存 → syncfs(false) → IndexedDB |
高性能随机更新 (SQLite-WASM、日志实时刷新) | OPFS + SyncAccessHandle | 自定义 JSLib/plug-in 异步刷新或 Worker 同步 |
仅 Chrome 86+/Firefox 111+/Safari 17+;需 HTTPS |
让玩家导出/导入文件 |
File Picker (showOpenFilePicker ) |
少量 JS 桥 | 返回 FileSystemFileHandle ,可流式读/写,关闭标签即失效 |
Cocos2d-JS / Cocos2d-HTML5 (2.x 及旧 Creator 1.x)
- 引擎运行时 完全是 JavaScript,没有 Emscripten → 也就 没有 FS / IDBFS。
- 官方资源缓存模块(
cc.loader
、Downloader
)内部已经对 IndexedDB + LocalStorage 做了一层封装,用来缓存远程纹理等。
可选持久层
- IndexedDB
import { openDB } from 'idb';
const db = await openDB('game-db', 1, { upgrade(db){ db.createObjectStore('files'); } });
await db.put('files', blob, 'crash_0625.dmp');
-
OPFS(Chrome 86+)
需要写 Promise 异步或 Worker,同步写只在
SyncAccessHandle
(87+)可用。 - LocalStorage 仅文本 / 小量数据(≤5 MiB)。
若想要 POSIX 风格同步 API,可引入三方库(如 BrowserFS) 再让游戏逻辑调用 Node-like
fs
。
Cocos Creator 3.x (原生 C++ Engine → WebAssembly)
关键词 | 说明 |
---|---|
Emscripten 构建链 | v3.x 默认为 wasm backend → 编译时同样带入了 FS 模块 |
默认挂载 | 引擎启动脚本会把 IDBFS 部署在 remote/ 或 gamecaches/ 目录用于 AssetBundle 缓存 |
多线程(WebAssembly - pthread) | 写文件时需避免主线程卡顿: • 在 Worker 侧用 FS.write() • 退出时在 主线程 FS.syncfs 或 postMessage 让 worker flush |
OPFS | 可通过 window.cc 暴露的桥或自建 JSLib 直接调用;适合 SQLite-WASM、log rolling。 |
选型指南
-
先看引擎是否自带 Emscripten FS
- 带 → 首选 IDBFS:代码零改动,易于移植。
- 不带 → 直接用 IndexedDB(兼容最好)或 OPFS(前沿但覆盖率 2025≈85 %)。
-
数据量 & 写入模式
- 几 KB-MB,偶尔写 → LocalStorage/IndexedDB 都够。
- 多 MB-GB,频繁随机写 → OPFS / IDBFS+batch。
-
是否需要同步 API(阻塞式)
- Unity、Cocos-WASM 这种老 POSIX 逻辑多 → IDBFS 同步 最顺。
- 纯 JS 项目 → embrace async/await,直接 IndexedDB/OPFS 即可。
-
多线程 / Worker
- IDBFS 仍可用,但要保证 单线程 flush。
- OPFS 的
SyncAccessHandle
允许 Worker 内部真正同步写盘。