阅读视图

发现新文章,点击刷新页面。

【WebGL应用开发】浏览器平台文件系统方案

可用方案

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.writeFilefopen() 等都是同步。
    • FS.syncfs(true) 时把 IndexedDB → 内存,syncfs(false) 再刷回去。
    • Unity Player 会自动 mount /idbfs/<hash> 并把 Application.persistentDataPath 指过去。
  • 兼容性:跟 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 配额。

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,不适合二进制。
  • 只建议存极小配置或「上次崩溃标记位」。

选型建议速览

  • 优先级
    1. OPFS(若浏览器全部覆盖且对容量/并发有要求)
    2. IDBFS / IndexedDB(最大兼容且容量较大)
    3. MEMFS + 手动上报/合并(只要临时)
    4. File Picker(需要人手操作)
    5. LocalStorage(极小配置)

Emscripten文件系统深入

Emscripten 把 POSIX-style 文件 API(open / read / write / stat ...)移植到浏览器,做法是:

  1. JavaScript 里实现一个叫 FS 的虚拟文件系统内核;
  2. 允许把 不同后端 “挂载”到目录,后端决定 数据最终落在哪里
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.QuitCleanupsyncfs(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.loaderDownloader)内部已经对 IndexedDB + LocalStorage 做了一层封装,用来缓存远程纹理等。

可选持久层

  1. 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');
  1. OPFS(Chrome 86+) 需要写 Promise 异步或 Worker,同步写只在 SyncAccessHandle(87+)可用。
  2. 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.syncfspostMessage 让 worker flush
OPFS 可通过 window.cc 暴露的桥或自建 JSLib 直接调用;适合 SQLite-WASM、log rolling。

选型指南

  1. 先看引擎是否自带 Emscripten FS
    • → 首选 IDBFS:代码零改动,易于移植。
    • 不带 → 直接用 IndexedDB(兼容最好)或 OPFS(前沿但覆盖率 2025≈85 %)。
  2. 数据量 & 写入模式
    • 几 KB-MB,偶尔写 → LocalStorage/IndexedDB 都够。
    • 多 MB-GB,频繁随机写 → OPFS / IDBFS+batch。
  3. 是否需要同步 API(阻塞式)
    • Unity、Cocos-WASM 这种老 POSIX 逻辑多 → IDBFS 同步 最顺。
    • 纯 JS 项目 → embrace async/await,直接 IndexedDB/OPFS 即可。
  4. 多线程 / Worker
    • IDBFS 仍可用,但要保证 单线程 flush
    • OPFS 的 SyncAccessHandle 允许 Worker 内部真正同步写盘。
❌