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 缓存实现,理解其底层FileSystemFileHandle与FileSystemWritableFileStream的真实工作方式。
一、为什么需要 useFileSystemAccess
传统 Web 应用读写本地文件时,通常只能依赖<input type="file">选择文件(只读),或借助a[download]触发下载(无法覆盖原文件)。File System Access API 打破了这一限制:通过window.showOpenFilePicker与window.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,同时fileName、fileMIME、fileSize、fileLastModified等元数据自动同步更新;调用create()/save()/saveAs()则走保存流程。整个使用过程中无需手动维护任何事件监听或文件句柄状态——这正是组合式函数相对原生 API 的核心价值。
返回值速查
| 返回值 | 类型 | 说明 |
|---|---|---|
isSupported | Ref<boolean> | 当前浏览器环境是否支持 File System Access API,常用于渲染降级 UI 或禁用按钮 |
data | ShallowRef<T \| undefined> | 文件内容数据,具体类型取决于dataType选项(string/ArrayBuffer/Blob) |
file | ShallowRef<File \| undefined> | 当前打开的File对象本身 |
fileName | ComputedRef<string> | 当前文件名(不含路径,出于安全考虑 API 不暴露完整路径) |
fileMIME | ComputedRef<string> | 文件的 MIME 类型 |
fileSize | ComputedRef<number> | 文件大小(字节) |
fileLastModified | ComputedRef<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.createObjectURL、FormData上传或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方法只透传types与excludeAcceptAllOption;create、save、saveAs三个保存类方法只透传suggestedName(save原地覆盖时通常无需传)。
四、完整实战示例:本地文本编辑器
将上述 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[]> }useFileSystemAccess的isSupported正是基于对showOpenFilePicker/showSaveFilePicker是否存在进行能力检测(这也是 VueUseuseSupported模式在 Browser 场景的典型应用),因此不支持该 API 的浏览器(如部分 Safari / Firefox 版本)中isSupported为false,业务代码应据此提供降级路径。
六、仓库佐证: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采用了完全相同的三步写入模式(getFileHandle→createWritable→write/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 }),完整验证了"句柄 → 可写流 → 写入 → 关闭"链路。这也从侧面说明:FileSystemWritableFileStream的write接口同时接受string与Blob,与 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),仅供参考