纯内存虚拟文件系统
纯内存虚拟文件系统
背景需求
我的项目有 obsidian 插件分支和 Tauri2 软件分支等。
为了方便对 obsidian 分支快速调试,我创建了一个 browser_app 的子项目用于快速调试。
其中对于核心模块来哦,需要重新实现一些通用 api。如:
import { global_setting } from "../../../Core/shared/setting";
export function initApi() {
global_setting.platform = 'browser'
global_setting.api.readFolder = async (relPath: string, recursion_depth?: number): Promise<string[]> => {
return []
}
global_setting.api.readFile = async (relPath: string): Promise<string | null> => {
return null
}
global_setting.api.writeFile = async (relPath: string, content: string, _isappend?: boolean): Promise<boolean> => {
return false
}
}该api初始化是必须的,用于处理多个平台使用核心模块时的差异。那么对于 browser_app 来说,可能会用于快速调试或提供在线演示。
我想有两种方案:
- 一是使用一个本地后端服务器,用来代理解决这里的文件读写 api(弊端是可能无法改良成在线演示版本,但这个版本我不一定会去做,也可能不做)
- 二是使用虚拟文件系统,不需要后端。直接预设好一个带文件内容demo的对象,读写该对象来模拟文件读写(不确定这个需求有无什么好的库,还是自己写)
- 或者还有其他方法?
先不急着写代码,先分析多种方案哪种更好,以及给出大致的解决思路和方案。
答案
by deepseek-v4-pro
针对你的 browser_app 子项目需要实现核心文件 API 的场景,可以归纳出几种典型方案。我们按“快速调试”和“可能的在线演示”两个目标,从维护成本、功能模拟度、可扩展性、部署便利性等维度来分析。
几种方案
方案一:本地后端服务器代理
思路
在 browser_app 本地开发时,启动一个轻量 HTTP 服务器(如 Node/Deno 的简易静态 + API 服务),将前端的 readFolder / readFile / writeFile 请求代理到真实文件系统。
优点
- 读写的是真实的项目文件,调试时修改文件后刷新即可直接反映,无需手动更新测试数据。
- 与 Obsidian / Tauri 分支的行为完全一致,适合验证文件边界情况。
缺点
- 必须额外启动后端,丧失“纯前端一键跑”的便利性。
- 无法直接作为在线演示版本(除非你愿意部署一个带后端沙箱的服务,成本高)。
- 调试时强依赖后端,脱离该环境会完全瘫痪。
适用场景
- 主要需求是本地深度调试文件相关逻辑,且不打算对外在线演示。
- 你愿意接受多一个进程带来的环境成本。
方案二:纯内存虚拟文件系统
思路
在浏览器内存中维护一个对象(如 Record<string, string>),预设一些 demo 文件内容,读写都操作这个对象,刷新即丢失。
优点
- 零依赖、零启动成本,直接打开
index.html即可调试。 - 完全可以自己做,也可以借助类似
memfs这样的小库提供类 Nodefs接口。 - 非常适合“只是跑一下 UI / 核心逻辑,不关心文件持久化”的快速验证。
缺点
- 数据不持久,刷新后恢复为预设状态;无法模拟真实文件系统的大量行为(如大文件、文件锁、部分写入等)。
- 需要手工维护预设文件树,如果调试过程中频繁新增/修改 demo 文件,需要修改代码。
- 没有真正的文件夹层次结构(需要自己实现路径解析和递归遍历),虽然不难,但会增加一些胶水代码。
适用场景
- 纯 UI / 纯逻辑阶段的快速调试。
- 在线演示本身是只读或“一次性”体验(不要求刷新后保留用户修改)。
- 不打算做持久化演示时,这是最简单且最轻的方案。
方案三:基于 OPFS(Origin Private File System)的持久化虚拟文件系统
思路
使用浏览器内置的 OPFS(Origin Private File System),它提供沙盒化的真实目录和文件句柄,支持异步读写,数据持久存储在用户浏览器中。
优点
- 真实目录结构,支持
getDirectoryHandle、getFileHandle、createWritable等,能很好地模拟readFolder/readFile/writeFile。 - 数据持久化(同一浏览器同一域名下数据不丢失),刷新后仍保留用户操作结果,非常适合在线演示。
- 纯前端方案,无需任何后端,部署只需静态文件。
- 浏览器支持良好(现代 Chrome/Edge/Firefox 都支持,需在安全上下文使用,
localhost也可)。 - 初始时可以从静态资源加载 demo 文件写入 OPFS,以后直接从 OPFS 读写;用户体验类似真实本地存储。
缺点
- 存储限制根据浏览器配额,但对 demo 场景绰绰有余。
- API 是异步的,且基于
FileSystemFileHandle和FileSystemDirectoryHandle,需要封装一层以匹配global_setting.api的签名。 - 无法直接访问真实本地文件系统(与方案一相比,不能实时反映源码改动)。
适用场景
- 需要在线演示、且希望保留用户操作状态。
- 不想搭建后端,但希望有接近真实文件系统的行为和持久化能力。
- 依然可以用于本地快速调试,只是需要一次写入 demo 文件的初始化(可在首次检测到 OPFS 为空时自动完成)。
方案四:基于 IndexedDB 的持久化存储
思路
用 IndexedDB 存储文件路径和内容,通过封装实现文件系统操作。
优点
- 持久化,支持大量数据存储,兼容性好。
- 可自由组织数据模型。
缺点
- 需自己实现文件夹遍历、路径解析、递归等,开发量较大。
- 不如 OPFS 贴近真实文件系统 API,更像是“数据库模拟文件树”。
结论
- 与 OPFS 相比没有突出优势,仅在需要兼容更旧浏览器时考虑。目前 OPFS 已经完全够用,更推荐 OPFS。
方案五:Service Worker + 内存/缓存
思路
利用 Service Worker 拦截请求模拟文件系统,配合 Cache API 持久化。但这更像把文件抽象成 HTTP 资源,与你的 API 接口形态不匹配,会引入更多复杂度,不推荐。
综合评估与推荐
| 维度 | 本地后端 | 内存虚拟FS | OPFS 持久化 |
|---|---|---|---|
| 调试启动成本 | 高(需启动服务) | 极低 | 低 |
| 在线演示能力 | 差 | 一般(无持久化) | 优秀 |
| 文件行为模拟度 | 完全真实 | 低 | 高(接近真实) |
| 开发工作量 | 中等 | 低~中 | 中等 |
| 持久化 | 是 | 否 | 是 |
| 扩展为生产级 | 困难 | 困难 | 可平滑扩展 |
推荐策略:分层适配,按需切换
短期快速调试:直接用内存虚拟文件系统。
- 实现一个简单的
VirtualFS类,维护一个Map<string, string | null>(null表示目录)结构。 - 预设一些 demo 路径和内容,实现
readFolder、readFile、writeFile。 - 这种零依赖方案让你立刻可以跑通
browser_app,验证核心逻辑。
- 实现一个简单的
一旦需要在线演示或保持状态:切换到 OPFS 实现。
- 封装一个
OPFSFileSystem适配器,实现与内存版完全一致的 API。 - 首次初始化时,将内存预设文件树写入 OPFS;之后所有操作都落在 OPFS 上。
- 该适配器可以完全替换内存版,对外暴露相同的接口。
- 封装一个
保留本地后端作为特殊调试模式(可选)。
- 仅在开发环境,通过环境变量或 URL 参数切换到一个
HttpFS适配器,前端请求本地localhost服务。 - 这不会干扰其他两套方案,可以共用同一套 API 签名。
- 仅在开发环境,通过环境变量或 URL 参数切换到一个
因此,你不必二选一,而是设计一个“文件系统适配器”接口,让 browser_app 在初始化时根据配置加载不同实现:
memory– 内存 FS(调试)opfs– 持久化 FS(演示 & 调试)local-server– 代理本地文件系统(深度文件调试,可选)
大致的解决思路与结构
1. 抽象文件系统接口
基于你已有的 global_setting.api 签名,定义三个核心方法不变:
interface FileSystemAPI {
readFolder: (relPath: string, recursion_depth?: number) => Promise<string[]>;
readFile: (relPath: string) => Promise<string | null>;
writeFile: (relPath: string, content: string, isAppend?: boolean) => Promise<boolean>;
}2. 实现内存适配器(用于快速调试)
- 使用一个
Map存储所有路径(键如"/","/note.md","/folder/","/folder/child.md")。 - 目录以
"/"结尾标识,值可以是null或任意占位。 readFolder通过过滤键实现,recursion_depth控制遍历深度。writeFile直接更新 Map,并可自动创建父目录占位符。- 预设 demo 文件树,代码量小,易于维护。
3. 实现 OPFS 适配器(用于在线演示)
- 初始化时获取 OPFS 根目录句柄:
navigator.storage.getDirectory()。 - 封装
pathToHandle工具:将相对路径按/分割,逐级获取目录句柄,最后文件部分使用getFileHandle。 readFile:拿到文件句柄后getFile()→text()。writeFile:用createWritable()写入,isAppend则先读旧内容拼接。readFolder:遍历目录句柄的keys(),根据recursion_depth递归。- 预设 demo 文件:在首次启动时,检查一个标记文件是否存在,若不存在则循环写入预定义的文件集合。
4. 初始化选择逻辑
在 initApi() 中,根据一个配置常量(例如 STORAGE_MODE)或 URL 参数决定使用哪个适配器:
import { MemoryFS } from './adapters/memory';
import { OPFSAdapter } from './adapters/opfs';
// import { HttpFS } from './adapters/http';
const mode = new URLSearchParams(location.search).get('fs') || 'memory'; // 或 'opfs'
if (mode === 'opfs') {
await OPFSAdapter.initDemoFiles(DEMO_FILES);
setApi(OPFSAdapter);
} else if (mode === 'http') {
setApi(new HttpFS('http://localhost:4000'));
} else {
setApi(new MemoryFS(DEMO_FILES));
}这样,你可以在 package.json 中配置不同启动命令,方便切换。
总结
- 最快上手:内存虚拟文件系统。
- 最佳在线演示体验:OPFS 持久化虚拟文件系统,纯前端、持久化、无后端。
- 可选增强:本地后端用于深度文件操作调试。
- 根本思路:不把方案限定死,用适配器模式隔离实现,让 browser_app 可在不同场景下灵活运行。
这种架构既能满足当前快速调试需求,又为未来可能的在线演示版本提供了低成本升级路径,无需大幅修改核心代码。
OPFS
OPFS(Origin Private File System),中文译名 源私有文件系统
使用浏览器内置。它提供沙盒化的真实目录和文件句柄,支持异步读写,数据持久存储在用户浏览器中。