Onlook 浏览器文件系统解析:基于 ZenFS 与 IndexedDB 的 `@onlook/file-system` 完整指南
Onlook 浏览器文件系统解析基于 ZenFS 与 IndexedDB 的onlook/file-system完整指南【免费下载链接】onlookThe Cursor for Designers • An Open-Source AI-First Design tool • Visually build, style, and edit your React App with AI项目地址: https://gitcode.com/GitHub_Trending/on/onlook导读onlook/file-system是 Onlook开源 AI 优先的设计工具在浏览器端实现的虚拟文件系统抽象层它基于 ZenFS 将 IndexedDB 挂载为根文件系统让前端代码可以像操作本地磁盘一样创建、读写、移动、删除文件和目录并提供了文件变更监听与一组 React Hooks。本文以 packages/file-system/README.md 为主线结合 fs.ts、config.ts、code-fs.ts 与 hooks 等源码实现完整讲解其 API 用法、文本/二进制自动检测、递归目录遍历、事件监听语义以及面向代码编辑的CodeFileSystem扩展读完你可以在自己的 Web 项目中直接复现这套浏览器端文件管理方案。一、定位为什么浏览器里需要一套文件系统Onlook 允许用户在浏览器中可视化地查看、编辑 React 项目的源码这意味着被编辑的项目文件必须能存放到浏览器可访问的地方。onlook/file-system正是为此设计的抽象底层使用ZenFSzenfs/core2.4.0作为可插拔的虚拟文件系统内核存储后端使用IndexedDBzenfs/dom1.2.5数据持久化在浏览器数据库里刷新页面后依然存在对外暴露一个简化、统一、Promise 风格的 API并自动处理路径前缀、父目录创建、递归遍历等细节。从 config.ts 可以看到挂载配置根路径/使用IndexedDB后端storeName固定为browser-fsimport ZenFS, { configure } from zenfs/core; import { IndexedDB } from zenfs/dom; let configPromise: Promisevoid | null null; export async function getFS(): Promisetypeof ZenFS { // 用一个共享 Promise 保证全局只配置一次 configPromise ?? configure({ mounts: { /: { backend: IndexedDB, storeName: browser-fs, }, }, }).catch((err) { configPromise null; // 出错后重置允许重试 throw err; }); await configPromise; return ZenFS; }值得注意的是configPromise ?? ...这种单例 Promise写法整个应用生命周期内configure只会被调用一次后续所有getFS()调用都复用同一次初始化结果一旦失败会自动重置允许下次调用重试避免错误状态被永久缓存。二、快速开始基础用法按照 README 的示例先创建FileSystem实例并初始化然后就可以像操作本地文件系统一样工作import { FileSystem } from onlook/file-system; // 创建一个以 /my-project 为根目录的文件系统实例 const fs new FileSystem(/my-project); await fs.initialize(); // 创建文件和目录父目录会自动创建 await fs.createFile(/src/index.js, console.log(Hello World);); await fs.createDirectory(/src/components); // 读取文件自动识别文本文件并返回字符串 const content await fs.readFile(/src/index.js); console.log(content); // console.log(Hello World); // 递归列出目录内容 const files await fs.readDirectory(/);2.1 根目录语义路径前缀自动管理FileSystem构造时会把传入的rootDir作为整个实例的虚拟根constructor(rootDir: string) { this.basePath path.resolve(/, rootDir); }在 fs.ts 的注释中明确了这一语义当new FileSystem(/my-project)后调用readFile(/src/index.ts)实际读取的是/my-project/src/index.ts。所有公开 API 的入参都是相对这个虚拟根的逻辑路径底层通过path.join(this.basePath, inputPath)换算成 ZenFS 中的真实路径调用方完全不用关心前缀问题。initialize()会通过mkdir(this.basePath, { recursive: true })确保根目录存在并用isInitialized标志保证重复调用是幂等的。2.2 读写文件父目录自动创建createFile与writeFile在写入前都会对目标路径执行path.dirname()并递归mkdirasync createFile(inputPath: string, content ): Promisevoid { const fullPath path.join(this.basePath, inputPath); const dir path.dirname(fullPath); if (dir) { await this.fs.promises.mkdir(dir, { recursive: true }); } await this.fs.promises.writeFile(fullPath, content); }这意味着你可以直接写入/src/components/Button.tsx这类深层路径无需手动先建目录。此外还提供了批量写入writeFiles(files)内部按顺序逐个调用writeFile以避免并发写同一元数据文件时的竞态条件单个文件失败会被console.error记录而不会中断整个批次。2.3 文本 / 二进制自动检测readFile的返回类型是Promisestring | Uint8Array判断依据是 fs.ts 中的isTextContent私有方法只检查缓冲区前 512 字节Math.min(512, buffer.length)出现空字节byte 0直接判定为二进制出现除 Tab9、换行10、回车13以外的控制字符byte 32判定为二进制高字节 127密集出现连续 10 字节窗口内超过 7 个也判定为二进制。private isTextContent(buffer: Uint8Array): boolean { const checkLength Math.min(512, buffer.length); for (let i 0; i checkLength; i) { const byte buffer[i]; if (byte 0 || byte undefined) return false; if (byte 32 byte ! 9 byte ! 10 byte ! 13) return false; if (byte 127) { let highByteCount 0; for (let j i; j Math.min(i 10, checkLength); j) { const currentByte buffer[j]; if (currentByte ! undefined currentByte 127) highByteCount; } if (highByteCount 7) return false; } } return true; }因此读取 PNG、JPG、字体等二进制资源时会返回原始Uint8Array而读取.tsx、.json等文本文件时自动解码为 UTF-8 字符串配合writeFile同时接受string | Uint8Array可以实现无损的二进制复制copyFile正是读原文件再写目标路径。三、目录操作递归读取与排序readDirectory默认递归遍历整个目录树返回的FileEntry数组是按目录优先、再按名称字典序排序的树形结构export interface FileEntry { name: string; path: string; // 相对虚拟根已去掉 basePath 前缀 isDirectory: boolean; size?: number; // BigInt 已转 Number modifiedTime?: Date; children?: FileEntry[]; // 目录才有递归子项 }递归实现见 fs.ts 的readDirRecursive对每个条目调用stat构造FileEntry若为目录则递归挂到childrenpath通过path.relative(this.basePath, entryPath)计算保证返回的是调用方熟悉的逻辑路径目录不存在ENOENT时返回空数组而非抛错。除目录遍历外FileSystem还提供了完整的目录级操作且与文件操作语义一致方法说明createDirectory(path)递归创建目录deleteDirectory(path)递归删除目录rm recursivemoveDirectory(from, to)移动/重命名目录目标父目录自动创建copyDirectory(from, to)递归复制目录树目录mkdir、文件逐个读写deleteFile(path)智能删除stat判断目录走递归删除否则直接删stat 失败时仍尝试递归rm还有两个轻量的枚举辅助方法listFiles(pattern **/*)返回所有文件路径相对虚拟根支持*通配符内部转换为正则过滤注释明确说明不是完整 glob 语义listAll()返回{ path, type: file | directory }的扁平列表type字段便于过滤。四、文件监控watchFile 与 watchDirectoryFileSystem内置了基于 ZenFSwatch的文件变更监听回调收到统一的FileChangeEventexport interface FileChangeEvent { type: create | update | delete | rename; path: string; oldPath?: string; // rename 时可能携带旧路径 }4.1 防抖机制50ms 合并源码注释解释了防抖的必要性watcher 会在文件真正写完之前就触发导致读到损坏状态。因此每次事件到达后会先清掉该路径上挂起的旧setTimeout再重新安排 50ms 后执行回调见 fs.ts 中watcherTimeouts与timeoutKey的使用。这保证了连续写入一个文件时回调只在写入稳定后触发一次。4.2 rename 事件的语义归一ZenFS 的rename事件可能代表创建、删除或重命名FileSystem用文件是否仍存在来消歧watchFilerename后stat成功 → 归并为updatestat抛错 → 归并为deletechange事件 → 归并为updatewatchDirectoryrename后stat成功且路径从未被监听 →create若新路径是目录则自动递归挂载监听stat抛错 →delete并从监听集合移除已监听路径上再次出现 →update。4.3 返回值与清理watchFile/watchDirectory都返回一个清理函数() { watcher.close(); ... }用于关闭底层 watcher 并清掉未决的防抖定时器。实例级的cleanup()则会遍历关闭所有 watcher、清除所有 pending timeoutuseFS的 effect 清理阶段正是调用它来释放资源。五、面向代码编辑的扩展CodeFileSystemFileSystem之上还有一个重要的子类CodeFileSystemcode-fs.ts它按projectId/branchId划分虚拟根目录super(/${projectId}/${branchId})。这正是 Onlook 多项目、多分支可视化编辑的基础——每个项目分支组合拥有独立的文件空间。5.1 写入时的 JSX 加工管线CodeFileSystem覆写了writeFile当目标文件是 JSX/TSX/\.(jsx?|tsx?)$/i匹配且不是 preload 脚本时写入前会走processJsxFile管线getAstFromContent(content)解析为 AST若是根布局文件isRootLayoutFile取决于routerType默认RouterType.APP通过injectPreloadScript(ast)注入 Onlook preload 脚本读取该文件已存在的 OID见 5.2 索引调用addOidsToAst(ast, existingOids)为 JSX 元素打上稳定的 OID 标识getContentFromAst序列化回代码再经formatContent统一格式化后落盘。这使得编辑器可以对每个 JSX 元素做稳定追踪可视化修改源码时元素身份不因重写而漂移。CodeFileSystem还覆写了writeFiles改为逐个await串行写入避免并发写索引文件的竞态。5.2 元素索引OID → 元数据CodeFileSystem维护一份{ oid: JsxElementMetadata }索引落盘在ONLOOK_CACHE_DIRECTORY/index.json。索引读写走 index-cache.ts 的双层缓存内存缓存staticMemoryMap键为projectId/branchId优先命中避免重复读盘落盘缓存debouncedSaveIndexToFile用lodash.debounce延迟 1000ms 批量写盘降低频繁编辑时的 IO 压力并发保护loadingPromises对同一 cacheKey 的并发加载做去重防止竞态重复读文件。关键操作包括写入/删除/移动 JSX 文件时同步增删改索引updateMetadataForFile、deleteFile、moveFile覆写rebuildIndex()按每批 10 个文件并行重扫整个目录树重建索引并在日志中输出耗时统计[CodeEditorApi] Index built: N elements from M files in XmsgetJsxElementMetadata(oid)供上层按 OID 反查元素代码。cleanup()在卸载前会把内存中的最新索引先落盘再清缓存。六、React HooksuseFS / useFile / useDirectory针对 React 组件onlook/file-system/hooks子路径见 package.json 的exports字段./hooks: ./src/hooks/index.ts导出三个 Hooks。6.1 useFS文件系统生命周期管理const { fs, isInitializing, error } useFS(projectId, branchId);useFSuse-fs.tsx内部创建CodeFileSystem实例并initialize()通过 effect 的清理函数在projectId/branchId变化或组件卸载时调用cleanup()保证监听器与定时器不泄漏。返回值在初始化中、出错、就绪三种状态下用类型守卫保证类型安全。6.2 useFile单文件读写 自动刷新const { content, loading, error } useFile(projectId, branchId, path);useFileuse-file.ts首次加载调用fs.readFile(path)同时注册fs.watchFile(path, ...)——文件一旦变化就自动重新读取实现文件在别处被修改组件内容实时同步。content的类型是string | Uint8Array | null与readFile的自动检测一致。6.3 useDirectory目录树渲染 自动刷新const { entries, loading, error } useDirectory(projectId, branchId, path);useDirectoryuse-dir.ts加载fs.readDirectory(path)得到树形FileEntry[]并注册fs.watchDirectory(path, ...)实现目录变更创建/删除/更新时自动重新拉取。README 中的示例组件展示了典型用法import { useDirectory } from onlook/file-system/hooks; function FileExplorer() { const { entries, loading, error } useDirectory(projectId, branchId, /); if (loading) return divLoading.../div; if (error) return divError: {error.message}/div; return ( ul {entries.map(entry ( li key{entry.path} {entry.isDirectory ? : } {entry.name} /li ))} /ul ); }需要说明的是README 中的示例签名useDirectory(rootDir, /)是简化示意当前源码use-dir.ts中三个 Hooks 的统一签名均为(projectId, branchId, path)实际使用时以上述源码签名为准。七、安装与工程结构onlook/file-system是 monorepo 中的私有包private: true在 package.json 中声明了依赖zenfs/core2.4.0、zenfs/dom1.2.5、lodash.debounce^4.0.8并以react^18作为 peerDependency。子路径导出约定为onlook/file-system→ 主入口 src/index.ts聚合code-fs、hooks、typesonlook/file-system/hooks→ src/hooks/index.tsuseFS、useFile、useDirectory及类型导出。源码目录一览文件职责fs.tsFileSystem基类路径管理、读写删移、递归遍历、文件监听config.tsZenFS 配置IndexedDB 挂载、单例初始化code-fs.tsCodeFileSystemJSX 加工、OID 注入、元素索引index-cache.tsOID 索引的内存/落盘双层缓存types.tsFileEntry、FileInfo、FileChangeEvent类型定义hooksuseFS、useFile、useDirectory三个 React Hooks八、总结onlook/file-system用约 500 行的核心实现fs.ts把 ZenFS IndexedDB 封装成了开箱即用的浏览器文件系统FileSystem解决路径与递归的样板代码文本/二进制自动检测让读写透明watchFile/watchDirectory加上 50ms 防抖与 rename 事件归一为 UI 层提供了可靠的数据流而CodeFileSystem通过 OID 注入与索引缓存把通用文件系统进一步升级为面向 React 源码编辑的专用能力。配合useFS、useFile、useDirectory三个 Hooks前端可以非常自然地构建出文件树 文件内容 实时联动的编辑器界面——这套模式对任何需要浏览器内管理代码文件的产品低代码平台、在线 IDE、可视化编辑器都具备直接参考价值。【免费下载链接】onlookThe Cursor for Designers • An Open-Source AI-First Design tool • Visually build, style, and edit your React App with AI项目地址: https://gitcode.com/GitHub_Trending/on/onlook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →