---
title: "WebGL浏览器平台文件系统"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/webgl-browser-filesystem/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/WebGL/WebGL浏览器平台文件系统.md"
content_hash: 605765247d859f24b952a4296a7392c8c1fc9eeea6bc08b05f571766074ed62a
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# 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 同版本也已带上。

* **典型用法**

```ts
// 打开 / 创建沙箱根目录
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，移动端同理。
* **速查片段**

```ts
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` 指过去。

* **兼容性**：跟 IndexedDB 一致；无需任何额外权限或标头。

* **快速上手**

  ```js
  // 仅首次加载
  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`）；浏览器刷新即丢。

* **代码示例**

  ```js
  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. 允许把 *不同后端* “挂载”到目录，后端决定 **数据最终落在哪里**。

```js
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。**

### 挂载阶段

```js
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：

```cpp
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 中使用

```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** 做了一层封装，用来缓存远程纹理等。

### 可选持久层

1. **IndexedDB**

```ts
import { openDB } from 'idb';
const db = await openDB('game-db', 1, { upgrade(db){ db.createObjectStore('files'); } });
await db.put('files', blob, 'crash_0625.dmp');
```
2. **OPFS**（Chrome 86+）
   需要写 Promise 异步或 Worker，同步写只在 `SyncAccessHandle`（87+）可用。
3. **LocalStorage**
   仅文本 / 小量数据（≤5 MiB）。

> 若想要 **POSIX 风格同步 API**，可引入三方库（如 [BrowserFS](https://github.com/jvilk/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。                                           |


---

## 选型指南

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 内部**真正同步**写盘。
