news 2026/9/11 18:24:28

airi 项目实战:基于 VueUse useFileSystemAccess 在浏览器中创建、读取与写入本地文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
airi 项目实战:基于 VueUse useFileSystemAccess 在浏览器中创建、读取与写入本地文件

airi 项目实战:基于 VueUse useFileSystemAccess 在浏览器中创建、读取与写入本地文件

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

导读

useFileSystemAccess是 VueUse 提供的浏览器端(Browser 分类)组合式函数,它将现代浏览器原生的 File System Access API 封装为声明式的 Vue 响应式状态与操作函数,让开发者可以在 Web 应用中"打开、创建、读取、写入"用户本地文件。在 airi 这类同时拥有 Web、桌面端(Electron)与移动端(Capacitor)多端形态的 AI 陪伴应用中,该能力可以用于本地化地读取配置、导入模型素材、导出对话记录等场景。阅读本文后,你将掌握useFileSystemAccess的完整 API 用法、dataType数据形态选择、open / create / save / saveAs / updateData五个操作的调用方式,并通过 airi 仓库中同源的 File System Access / OPFS 缓存实现,理解其底层FileSystemFileHandleFileSystemWritableFileStream的真实工作方式。


一、为什么需要 useFileSystemAccess

传统 Web 应用读写本地文件时,通常只能依赖<input type="file">选择文件(只读),或借助a[download]触发下载(无法覆盖原文件)。File System Access API 打破了这一限制:通过window.showOpenFilePickerwindow.showSaveFilePicker,页面可以在用户授权下获得真实文件的句柄(File Handle),从而原地读取、改写用户本地文件,体验接近原生桌面应用。

但原生 API 使用起来较为繁琐:需要手动处理浏览器能力检测、句柄获取、文件读取、可写流(Writable Stream)创建与关闭、文件元信息提取等环节。VueUse 的useFileSystemAccess把这一切收敛为一个组合式函数,并在 Vue 的响应式系统中以ref/computed形式暴露文件内容与元数据,天然适配 airi 基于 Vue 3 + Vite + UnoCSS 的前端架构(本技能来源参见 .agents/skills/vueuse-functions/SKILL.md 中 Browser 分类的AUTO调用规则:适用时自动使用)。

二、快速上手:基础用法

在 Vue 3(或 Nuxt 3)项目中引入:

import { useFileSystemAccess } from '@vueuse/core' const { isSupported, data, file, fileName, fileMIME, fileSize, fileLastModified, create, open, save, saveAs, updateData } = useFileSystemAccess()

调用open()会弹出系统文件选择框,用户选中的文件内容会写入data,同时fileNamefileMIMEfileSizefileLastModified等元数据自动同步更新;调用create()/save()/saveAs()则走保存流程。整个使用过程中无需手动维护任何事件监听或文件句柄状态——这正是组合式函数相对原生 API 的核心价值。

返回值速查

返回值类型说明
isSupportedRef<boolean>当前浏览器环境是否支持 File System Access API,常用于渲染降级 UI 或禁用按钮
dataShallowRef<T \| undefined>文件内容数据,具体类型取决于dataType选项(string/ArrayBuffer/Blob
fileShallowRef<File \| undefined>当前打开的File对象本身
fileNameComputedRef<string>当前文件名(不含路径,出于安全考虑 API 不暴露完整路径)
fileMIMEComputedRef<string>文件的 MIME 类型
fileSizeComputedRef<number>文件大小(字节)
fileLastModifiedComputedRef<number>文件最后修改时间戳
create函数弹出保存对话框创建新文件,返回后可通过save写入
open函数弹出打开对话框选择本地文件并读取
save函数data写入已持有的文件句柄(原地覆盖)
saveAs函数弹出保存对话框另存为(获取新的文件句柄)
updateData函数从当前file句柄重新读取内容并刷新data

三、选项与类型声明详解

useFileSystemAccess提供了一套完整的 TypeScript 类型声明(完整定义见 .agents/skills/vueuse-functions/references/useFileSystemAccess.md),下面逐项拆解其语义。

3.1 dataType:决定 data 的数据形态

export type UseFileSystemAccessOptions = ConfigurableWindow & UseFileSystemAccessCommonOptions & { /** * file data type */ dataType?: MaybeRefOrGetter<"Text" | "ArrayBuffer" | "Blob"> }

dataType是核心选项,支持Text(默认,字符串)、ArrayBuffer(二进制原始字节)与Blob(二进制大对象)三种取值,且可以是ref或 getter(MaybeRefOrGetter),即可以运行时动态切换。该选项直接决定data的类型,并通过函数重载在编译期给出精确的类型推导:

export declare function useFileSystemAccess(): UseFileSystemAccessReturn< string | ArrayBuffer | Blob > export declare function useFileSystemAccess( options: UseFileSystemAccessOptions & { dataType: "Text" }, ): UseFileSystemAccessReturn<string> export declare function useFileSystemAccess( options: UseFileSystemAccessOptions & { dataType: "ArrayBuffer" }, ): UseFileSystemAccessReturn<ArrayBuffer> export declare function useFileSystemAccess( options: UseFileSystemAccessOptions & { dataType: "Blob" }, ): UseFileSystemAccessReturn<Blob>

三种形态的选型建议:

  • Text:适合.json.md.txt.yaml等文本类文件。airi 中涉及角色卡、对话记录、配置导出的场景可直接用字符串处理;
  • ArrayBuffer:适合需要按字节精确处理的二进制场景,例如分块读取、哈希计算、加密;
  • Blob:适合图片、音频、模型文件等大体积二进制资源,可直接用于URL.createObjectURLFormData上传或FileReader后续处理。

3.2 打开文件选项:showOpenFilePicker 参数

export interface FileSystemAccessShowOpenFileOptions { multiple?: boolean types?: Array<{ description?: string accept: Record<string, string[]> }> excludeAcceptAllOption?: boolean }
  • multiple:是否允许多选,默认为false
  • types:可接受的文件类型描述数组,每个条目包含description(下拉框显示文本)与accept(MIME 类型到扩展名数组的映射),例如{ description: '文本文件', accept: { 'text/plain': ['.txt'] } }
  • excludeAcceptAllOption:为true时在文件选择器中隐藏"所有文件"选项,强制用户选择指定类型。

3.3 保存文件选项:showSaveFilePicker 参数

export interface FileSystemAccessShowSaveFileOptions { suggestedName?: string types?: Array<{ description?: string accept: Record<string, string[]> }> excludeAcceptAllOption?: boolean }

相比打开选项,多出suggestedName(建议文件名),用于在保存对话框中预填文件名(如对话记录.txt)。

3.4 抽取后的组合选项

useFileSystemAccess从上述两个原生选项对象中抽取了自己所需的子集:

export type UseFileSystemAccessCommonOptions = Pick< FileSystemAccessShowOpenFileOptions, "types" | "excludeAcceptAllOption" > export type UseFileSystemAccessShowSaveFileOptions = Pick< FileSystemAccessShowSaveFileOptions, "suggestedName" >

即:open方法只透传typesexcludeAcceptAllOptioncreatesavesaveAs三个保存类方法只透传suggestedNamesave原地覆盖时通常无需传)。

四、完整实战示例:本地文本编辑器

将上述 API 组合起来,可以非常简洁地实现一个带"打开 / 保存 / 另存为 / 新建"能力的本地文件编辑器:

<script setup lang="ts"> import { computed, ref } from 'vue' import { useFileSystemAccess } from '@vueuse/core' const { isSupported, data, fileName, fileMIME, fileSize, fileLastModified, create, open, save, saveAs, updateData, } = useFileSystemAccess({ dataType: 'Text' }) const editorContent = ref('') const lastSaved = ref<number | null>(null) // 将响应式 data 同步到编辑器(可选用 watch 双向同步) async function handleOpen() { await open({ types: [{ description: '文本文件', accept: { 'text/plain': ['.txt', '.md'] } }] }) if (data.value != null) editorContent.value = data.value } async function handleSave() { // 直接覆盖当前文件句柄(若来自 open 则原地写回) await save() lastSaved.value = Date.now() } async function handleSaveAs() { await saveAs({ suggestedName: fileName.value ?? 'untitled.txt' }) lastSaved.value = Date.now() } async function handleCreate() { await create({ suggestedName: '新文件.txt' }) await save() } const metaSummary = computed(() => `文件:${fileName.value ?? '(未打开)'}|类型:${fileMIME.value ?? '-'}|大小:${fileSize.value ?? 0} 字节|修改时间:${fileLastModified.value ? new Date(fileLastModified.value).toLocaleString() : '-'}`) </script> <template> <div v-if="isSupported"> <button @click="handleOpen">打开</button> <button @click="handleSave">保存</button> <button @click="handleSaveAs">另存为</button> <button @click="handleCreate">新建</button> <button @click="updateData">重新读取</button> <p>{{ metaSummary }}</p> <textarea v-model="editorContent" rows="12" cols="80" /> </div> <div v-else> 当前浏览器不支持 File System Access API,请使用较新的 Chrome / Edge。 </div> </template>

要点说明:

  • 文件打开后updateData()可用于"重新读取磁盘上被外部修改后的内容";
  • save()对来自open()的文件句柄执行原地覆盖,这是传统<input type="file">方案做不到的能力;
  • create()saveAs()都会先弹出系统保存对话框,区别在于create语义上用于新建。

五、底层原理:句柄与可写流

useFileSystemAccess的类型声明同时揭示了其底层依赖的浏览器对象结构,理解它们有助于排查问题与扩展能力:

5.1 FileSystemFileHandle:文件句柄

export interface FileSystemFileHandle { getFile: () => Promise<File> createWritable: () => FileSystemWritableFileStream }
  • getFile():返回该文件对应的File对象(含 name、type、size、lastModified 属性),用于读取;
  • createWritable():创建可写流,用于写入内容。

5.2 FileSystemWritableFileStream:可写流

interface FileSystemWritableFileStream extends WritableStream { write: FileSystemWritableFileStreamWrite seek: (position: number) => Promise<void> truncate: (size: number) => Promise<void> }

write是一个支持多种重载的联合签名:

interface FileSystemWritableFileStreamWrite { (data: string | BufferSource | Blob): Promise<void> (options: { type: "write" position: number data: string | BufferSource | Blob }): Promise<void> (options: { type: "seek"; position: number }): Promise<void> (options: { type: "truncate"; size: number }): Promise<void> }

write既可以整体写入一段数据(字符串、二进制缓冲区或 Blob),也可以结合seek(移动写入位置)与truncate(截断到指定大小)实现随机访问写入——例如只修改大文件的某一段,而不必重写整个文件。

5.3 浏览器窗口类型声明

export type FileSystemAccessWindow = Window & { showSaveFilePicker: ( options: FileSystemAccessShowSaveFileOptions, ) => Promise<FileSystemFileHandle> showOpenFilePicker: ( options: FileSystemAccessShowOpenFileOptions, ) => Promise<FileSystemFileHandle[]> }

useFileSystemAccessisSupported正是基于对showOpenFilePicker/showSaveFilePicker是否存在进行能力检测(这也是 VueUseuseSupported模式在 Browser 场景的典型应用),因此不支持该 API 的浏览器(如部分 Safari / Firefox 版本)中isSupportedfalse,业务代码应据此提供降级路径。

六、仓库佐证:airi 中同源的本地文件读写实践

airi 仓库虽然没有直接调用useFileSystemAccess,但多个核心包都依赖@vueuse/core(包括 apps/stage-web/package.json、packages/stage-ui/package.json、packages/stage-ui-live2d/package.json 等),并且在 Live2D 与 MMD 模型加载链路中,对 File System Access API 的底层句柄与可写流接口做了真实的生产级运用——这与useFileSystemAccess封装的底层 API 完全同源。

6.1 Live2D 模型的 OPFS 缓存(packages/stage-ui-live2d/src/utils/opfs-loader.ts)

opfs-loader.ts 中的OPFSCache类把已加载的 Live2D zip 模型持久化到浏览器 OPFS(Origin Private File System),避免重复网络拉取:

  • 写入(writeFile)通过getFileHandle(name, { create: true })获取句柄,再createWritable()writable.write(content)writable.close()完成落盘,与useFileSystemAccess内部写文件的流程一致;
  • 读取(readDirectoryRecursive)通过dir.values()递归遍历目录、fileHandle.getFile()还原File对象;
  • 采用__meta.json记录sourceUrl与 schemaversion(当前为live2DOpfsCacheVersion = 3),版本不匹配或来源 URL 变化时自动清除旧缓存重建——与useFileSystemAccess场景中"文件元信息随内容一同管理"的思路一脉相承。

6.2 MMD 源文件缓存(packages/stage-ui-mmd/src/utils/opfs-loader.ts)

MMD 版 opfs-loader.ts 的writeFile采用了完全相同的三步写入模式(getFileHandlecreateWritablewrite/close),并明确指出"OPFS 是可选的加速层,缓存 miss 时回退到 fetch",这正是 File System Access 类能力在真实产品中应具备的优雅降级态度

6.3 可写流接口的单元测试(packages/stage-ui-live2d/src/utils/opfs-loader.test.ts)

opfs-loader.test.ts 用内存模拟实现了MemoryFileHandle.createWritable()(返回{ write, close }),完整验证了"句柄 → 可写流 → 写入 → 关闭"链路。这也从侧面说明:FileSystemWritableFileStreamwrite接口同时接受stringBlob,与 useFileSystemAccess.md 中声明的write(data: string | BufferSource | Blob)完全吻合。

如果你需要在 airi 的 Web 端实现"本地导入角色配置 / 导出对话记录 / 保存 Live2D 模型 zip"等能力,建议遵循两条路线:面向用户交互的文件选择与编辑使用useFileSystemAccess声明式完成;面向程序化的持久化缓存借鉴上述 OPFS 实现,二者共享同一套 File System Access 底层协议。

七、浏览器兼容性与降级策略

  • File System Access API 目前主要在 Chromium 系浏览器(Chrome、Edge)中可用,因此必须优先检查isSupported
  • 不支持时可按场景降级:读取可用<input type="file">+FileReader;保存可用URL.createObjectURL+a[download](但无法原地覆盖原文件);
  • 页面运行环境要求安全上下文(HTTPS 或 localhost),非安全环境下相关 API 不可用;
  • 涉及敏感文件操作时,浏览器会在打开、保存时分别向用户请求授权,且用户可随时撤销授权,业务代码需对句柄失效(如抛错)做容错处理。

八、小结

useFileSystemAccess用一个组合式函数覆盖了浏览器本地文件读写的完整生命周期:open读取、create/save/saveAs写入、updateData刷新、dataType控制数据形态、元数据以 computed 形式即时响应。结合 airi 仓库中 opfs-loader.ts 与 opfs-loader.test.ts 对底层句柄、可写流接口的生产级运用,你可以放心地把该能力引入 Web 端功能开发,让 AI 陪伴应用拥有"读写用户本地文件"的桌面级体验。

【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 18:24:01

UmiJS 4 打包优化:把 2.6MB 的 umi.js 砍到 860KB 的四步清单

UmiJS 4 打包优化&#xff1a;把 2.6MB 的 umi.js 砍到 860KB 的四步清单 【免费下载链接】umi A framework in react community ✨ 项目地址: https://gitcode.com/GitHub_Trending/um/umi 生产环境 build 出来的 dist 里躺着一个 2.6MB 的 umi.js&#xff0c;gzip 后传…

作者头像 李华
网站建设 2026/9/11 18:23:07

HDFS核心机制:NameNode与Secondary NameNode协作解析

1. HDFS核心机制概述&#xff1a;分布式文件系统的基石HDFS&#xff08;Hadoop Distributed File System&#xff09;作为大数据生态的存储基石&#xff0c;其设计哲学与单机文件系统有着本质区别。我在实际生产环境中部署过多个PB级HDFS集群&#xff0c;最深刻的体会是&#x…

作者头像 李华