File System Access API 浅析

前言

最近在用pythontkinter GUI库)做一个小工具时,选择文件后可以获得其真实路径。而前端(浏览器)出于安全和性能等方面的考虑,对文件的操作是非常局限的。

HTML5标准的File API之前,纯前端几乎都无法完成图片预览的功能。即使有了File API,我们也不能直接读取本地文件,创建文件也只能通过下载的方式,更别说修改文件内容了。

如果我们想实现下载的时候弹出文件框,让用户自行选择保存的位置,又该怎么处理?

借此,梳理一下前端文件相关的方式方法,开帖记录(本篇为公司内部分享文章,同步发布到相关平台)。

File API

可以让用户在网页中选择本地文件,并读取这些文件信息。

最常见的可能就是上传功能了,一般通过htmlinput标签实现,示例如下

<input type="file" multiple onchange="fileChange()">

function fileChange(){
	console.log(event.target.files);
}

input-file通常搭配下面两个参数使用

属性描述
acceptMIME_TYPE规定通过文件上传来提交的文件的类型
multiple-是否可多选

读取到的File信息一般包括

属性描述
name文件名
size文件大小
type文件MIME类型
lastModified文件上次修改时间(时间戳)
lastModifiedDate文件上次修改时间(Date对象)

FileReader

一般用于读取文件,参数为File对象或Blob对象,示例如下

<input type="file" onchange="fileChange()"/>

function fileChange(){
    const reader = new FileReader();
    reader.readAsDataURL(event.target.files[0]);
    reader.onload = function(e){
        console.log(e.target.result)
    }          
}

API有如下方法和事件

方法描述
abort()终止读取操作
readAsArrayBuffer(Blob|File)返回ArrayBuffer对象
readAsBinaryString(Blob|File)返回二进制字符串
readAsDataURL(Blob|File)返回Base64编码的对象
readAsText(Blob|File,encoding)返回文本字符串
onloadstart读取操作开始时调用
onprogress读取数据过程中周期性调用
onabort读取操作被中止时调用
onerror读取操作发生错误时调用
onload读取操作成功完成时调用
onloadend读取操作完成时调用,无论成功,失败或取消

简单示例如下
在这里插入图片描述

File System API

HTML5新增了文件系统API,可以创建一个独立的沙箱文件系统,让开发者在此系统中进行创建、读写、移动、删除、索引文件等操作。它基于文件写入 APIFile Writer API),可以用于缓存和处理大量数据。

当然,此特性是非标准的(目前只有Google Chrome支持),尽量不要在生产环境中使用。

应用场景

应用场景

  • 上传中断(网络问题或浏览器崩溃等)后重新上传
  • 应用后台下载资源,无需等待完成才能进行后续动作
  • 配合IndexedDb或其它缓存方案,实现高效读写
  • 其它

一些说明

  • 此沙箱是一个虚拟的文件系统,不能读写用户硬盘中的文件
  • 浏览器会给每一个应用限定配额并分配存储(防止应用占满磁盘,可使用配额管理API申请合理的空间)
  • 文件系统支持异步和同步方法
  • 可以配合XMLHttpRequestDrop APIWeb Workerinput-file等使用,传递文件对象
  • 同源策略

下面介绍相关API和简单使用示例

基本使用

FileEntry/DirectoryEntry

沙盒环境的文件通过FileEntry句柄(目录为DirectoryEntry)操作

属性/方法说明
name操作对象名称
isFile操作对象是否为文件
isDirectory操作对象是否为目录
fullPath完整路径,文件系统的绝对路径
filesystem文件系统对象(name/root),详见FileSystem
getMetadata获取文件/目录信息
moveTo移动文件/目录
copyTo拷贝文件/目录
toURL完整路径,文件系统的绝对路径
remove删除文件/目录
getParent获取父目录
file获取文件数据对象(FileEntry)
createWriter用于写入文件(FileEntry)
createReader用于读取目录(DirectoryEntry)
getDirectory创建目录(DirectoryEntry)
getFile创建文件(DirectoryEntry)
removeRecursively递归删除目录(DirectoryEntry)
查询配额

首先我们需要查询系统可使用的临时/持久空间大小

// 持久磁盘配额
navigator.webkitPersistentStorage.queryUsageAndQuota(successCallback,errorCallback)
// 临时磁盘配额
navigator.webkitTemporaryStorage.queryUsageAndQuota(successCallback,errorCallback)
属性说明
successCallback成功回调
errorCallback失败回调
// 查询临时配额
navigator.webkitTemporaryStorage.queryUsageAndQuota ( 
    function(usedBytes, grantedBytes) {
        console.log('已使用:',usedBytes,' 总量:',grantedBytes);
    }, 
    function(e) { console.log('Error', e);  }
);
申请配额

查询成功后,可以根据实际需求申请合适的配额空间

navigator.webkitTemporaryStorage.queryUsageAndQuota(requestedBytes,successCallback,errorCallback)
属性说明
requestedBytes申请配额空间大小(字节)
successCallback成功回调
errorCallback失败回调
// 申请5M空间
var requestedBytes = 1024*1024*5;
navigator.webkitTemporaryStorage.requestQuota (
    requestedBytes,
    function(grantedBytes) {  
        console.log('请求成功的空间: ', gengerate(grantedBytes));
    }, 
    function(e) {
        console.log('Error', e);
    }
);
初始化文件系统

在请求配额成功后,可以请求访问文件系统

window.webkitRequestFileSystem(type,size,successCallback,errorCallback)
属性说明
typewindow.TEMPORARY:临时存储空间;window.PERSISTENT:永久存储空间
size需要用于存储的大小(字节)
successCallback成功回调
errorCallback失败或发生错误时回调
window.webkitRequestFileSystem(window.PERSISTENT, 1024*1024*5, initFsHanlder, errorHandler);
// 成功回调
function initFsHanlder(fs) {
    console.log(fs);
}
// 错误回调
function errorHandler(e) {
    console.log('错误:',e)
}
使用文件

请求文件系统成功后,我们可以创建文件

// 创建文件
fs.root.getFile(name,opts,successCallback,errorCallback)
属性说明
name文件名/目录名
options(create/exclusive)文件操作的参数
successCallback(fileEntry|dirEntry)成功回调
errorCallback失败或发生错误时回调
创建文件

使用getFile查找或创建文件,成功回调传递FileSystem对象(FileEntry)。下面示例为在根目录创建test.txt文本。

function initFsHanlder(fs) {
    // 直接创建文件
    // create:默认false,如果目标文件不存在是否创建
    // exclusive:默认false,需配合create:true使用,如果目标文件不存在则创建,存在则覆盖
    fs.root.getFile('test.txt', { create: true }, function (fileEntry) {
        console.log('创建文件成功:',fileEntry);
    }, errorHandler);
}

在这里插入图片描述

读写文件

这里我们创建test.txt文本,并向其中添加(追加)自定义内容。

// 创建文件
function createFile(fs, filename) {
    fs.root.getFile(filename, { create: true },function (fileEntry) {
        console.log(`创建文件${filename}成功`)
    }, errorHandler);
}
// 读取内容
function readFile(fileEntry){
    if (fileEntry.isFile) {
        fileEntry.file(function (file) {
            var reader = new FileReader();
            reader.onloadend = function () {
                console.log('读取文件成功:',reader)
            }
            reader.readAsText(file);
        });
    }
}
// 写入内容
function addContent(fs,filename) {
    fs.root.getFile(filename, { create: true },function (fileEntry) {
        if (fileEntry.isFile) {
            fileEntry.createWriter(function (fileWriter) {
                // 写入内容(File或Blob对象)
                var blob = new Blob(['hello world,this is my first try'], {
                    type: 'text/plain'
                });
                // 写入结束
                fileWriter.onwriteend = function (e) {
                    console.log('写入文件结束',e);
                }
                // 写入错误
                fileWriter.onerror = function (e) {
                    console.log('写入异常:',e);
                }
                // 移动(光标)到指定位置,追加内容
                // fileWriter.seek(fileWriter.length);
                // 执行写入
                fileWriter.write(blob);
            }, errorHandler);
        }
    }, errorHandler);
}

在这里插入图片描述

使用目录

请求文件统成功后,我们可以创建文件(方法与文件一致,可参考使用文件部分)

// 创建目录
fs.root.getDirectory(name,opts,successCallback,errorCallback)
创建目录

使用getDirectory查找或创建目录,成功回调传递FileSystem对象(DirectoryEntry )。下面示例为在根目录创建testDir目录,并在testDir目录下创建dir.txt文本

function initFsHanlder(fs) {
    // 直接创建目录
    // create:默认false,如果目录不存在是否创建
    // exclusive:默认false,需配合create:true使用,如果目录不存在则创建,存在则覆盖
    fs.root.getDirectory('testDir', { create: true }, function (dirEntry) {
        console.log('创建目录成功:', dirEntry)
    }, errorHandler)
}

在这里插入图片描述

创建子目录

需要注意的是不能直接创建其直接父目录不存在的目录,一般通过递归依次创建各级目录,示例如下。

// 错误示例如下
fs.root.getDirectory('父文件夹/子文件夹', { create: true }, function (dirEntry) {
    console.log('创建目录成功:', dirEntry)
}, errorHandler)

// 正确示例如下
function createDir(rootDirEntry, folders) {  
    rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) {  
        if (folders.length) {  
            createDir(dirEntry, folders.slice(1));  
        }  
    }, errorHandler);  
};
var folderPath = '我的文档/资料/web前端';
createDir(fs.root, folderPath.split('/'));

在这里插入图片描述

读取目录

需要注意的是只能直接读取当前目录的直属子目录,一般通过递归依次读取各级目录,示例如下(示例中提前创建了一些目录和文件)。

// 读取文件夹和文件
function readFolder(foldername){
    fs.root.getDirectory(foldername, { create: true }, function (dirEntry) {
        if(dirEntry.isDirectory){
            var reader = dirEntry.createReader();
            reader.readEntries(function(e) {  
                if (e.length) { 
                    let list = Array.prototype.slice.call(e, 0); 
                    list.map(v=>{
                        if(v.isDirectory){
                            readFolder(v.fullPath,v.name);
                        }
                    })  
                }
            }, errorHandler);  
        }
    }, errorHandler)
}

在这里插入图片描述

其它操作
文件拷贝、删除、移动
fs.root.getDirectory('testDir', { create: true }, function (dirEntry) {
    console.log('创建目录testDir成功,路径:', dirEntry.fullPath)
    // 拷贝文件并且重命名
    fs.root.getFile('test.txt', { create: true }, function (fileEntry) {
        console.log('创建文件test.txt成功,路径:', fileEntry.fullPath);
        fileEntry.copyTo(
            dirEntry,
            'test_copy.txt',
            (e) => {  console.log('拷贝文件test.txt成功,新路径:', e.fullPath) },
            (e) => { console.log('拷贝文件test.txt失败:', e) }
        );
    }, errorHandler);
    // 移动文件并且重命名
    fs.root.getFile('test1.txt', { create: true }, function (fileEntry) {
        console.log('创建文件test1.txt成功,路径:', fileEntry.fullPath);
        fileEntry.moveTo(
            dirEntry,
            'test1_move.txt',
            (e) => {  console.log('移动test1.txt成功,新路径:', e.fullPath) },
            (e) => { console.log('移动test1.txt失败:', e) }
        );
    }, errorHandler);
    // 删除文件
    fs.root.getFile('test2.txt', { create: true }, function (fileEntry) {
        console.log('创建文件test2.txt成功,路径:', fileEntry.fullPath);
        fileEntry.remove(
            (e) => { console.log('删除test2.txt成功:', e) },
            (e) => { console.log('删除test2.txt失败:', e) }
        );
    }, errorHandler);
}, errorHandler)

在这里插入图片描述

目录拷贝、删除、移动
// 普通删除:若文件夹下存在文件,则无法删除
fs.root.getDirectory('testDir/childDir', { create: true }, function (dirEntry) {
	dirEntry.remove(function () {
        console.log(`删除目录${dirEntry.fullPath}成功`);
    }, errorHandler);
}, errorHandler)

// 递归删除:会删除所有子文件和子文件夹
fs.root.getDirectory('testDir/childDir', { create: true }, function (dirEntry) {
	dirEntry.removeRecursively(function () {
        console.log(`删除目录${dirEntry.fullPath}成功`);
    }, errorHandler);
}, errorHandler)

// 移动、拷贝目录
fs.root.getDirectory('testDir/childDir1', { create: true }, function (dirEntry) {
    console.log('创建目录成功,路径:',dirEntry.fullPath)
    fs.root.getDirectory('testDir/childDir2', { create: true }, function (dirEntry2) {
        console.log('创建目录成功,路径:',dirEntry2.fullPath)
        // 拷贝到根目录,并重命名
        dirEntry.copyTo(fs.root, 'childDir1_copy', function (dirEntiry3) {
            console.log('复制目录成功,新路径:',dirEntiry3.fullPath);
        }, errorHandler);
        // 将childDir1移动到childDir2下,并重命名
        setTimeout(()=>{
            dirEntry.moveTo(dirEntry2, 'testDir_move', function (dirEntry4) {
                console.log('移动目录成功,新路径:', dirEntry4.fullPath);
            }, errorHandler);
        },2000)
    }, errorHandler)
}, errorHandler)

在这里插入图片描述

到这里我们就使用File System API完成了创建、写入、读取、拷贝、删除文件(夹)等基本操作。关于上面的应用场景的具体实现,这里不做赘述,感兴趣的可以自行查阅相关资料。

回到一开始我们说的问题:此沙箱是一个虚拟的文件系统,不能读写用户硬盘中的文件,也无法做到一些自定义交互效果。接下来我们来看一种更加稳妥且安全的交互API

File System Access API

文件系统访问 APIFile System Access API)允许与用户本地设备或用户可访问的网络文件系统上的文件进行交互。此API的核心功能包括读取文件、写入或保存文件以及对目录结构的访问。

相比于File System API,它提供了更强大的文件读写能力。

应用场景

  • 将文件从本地文件系统上传到Web应用程序
  • Web应用程序中的数据写入到本地文件系统中
  • 在用户的本地文件系统上创建、重命名和删除文件
  • 读取本地文件系统上的文件内容
  • 其它…

一些说明

  • 需用户授权访问相应的文件或目录
  • 文件系统支持异步方法
  • 同源策略

下面介绍相关API和简单使用示例

基本使用

此API提供3个基本(异步)方法,可配合async/await/then使用。

FileSystemFileHandle/FileSystemDirectoryHandle

选择文件或目录后,可以获取到FileSystemFileHandle(目录为FileSystemDirectoryHandle),后续操作由这个句柄进行(继承自FileSystemHandle

属性/方法说明
kind类型(file/directory)
name文件名/文件夹名
isSameEntry比较两个句柄关联的文件或目录是否匹配
queryPermission查询当前句柄的权限状态
requestPermission请求文件句柄的读/写权限
remove从文件系统中删除该句柄对应的文件或目录
move移动文件(FileHandle)
getFile获取File对象(FileHandle)
createWritable创建写入File对象(FileHandle)
entries返回[key,value]形式的异步迭代器(DirectoryHandle)
values返回键值异步迭代器(DirectoryHandle)
getDirectoryHandle返回指定名称的目录的句柄(DirectoryHandle)
getFileHandle返回指定名称的文件的句柄(DirectoryHandle)
removeEntry删除指定名称的文件或目录(DirectoryHandle)
resolve返回由从父句柄到指定子项的目录名数组,子项的名称作为最后一个数组项(DirectoryHandle)
选择文件

window.showOpenFilePicker():显示一个文件选择器,允许用户选择一个或多个文件并返回这些文件的句柄。

属性说明
multiple是否允许多选(默认false)
excludeAcceptAllOption是否排除“接受所有”选项(默认 false)
types允许选择的文件类型(description:描述/accept:MIME类型)
async function openFile() {
    let fileHandles = await window.showOpenFilePicker({
        multiple: true,
        excludeAcceptAllOption: true,
        types: [
            {
                description: "选择图片",
                accept: {
                    "image/*": [".png", ".gif", ".jpeg", ".jpg"],
                },
            },
        ],
    });
    console.log(fileHandles);
};

在这里插入图片描述

选择文件回调中可以获取File对象(后续可以按照之前的方式处理即可)

async function openFileAndRead() {
    let [fileHandle] = await window.showOpenFilePicker({
        multiple: false, // 取消多选
        excludeAcceptAllOption: true,
        types: [
            {
                description: "选择文本文件",
                accept: {
                    "text/plain": [".txt"],
                },
            },
        ],
    });
    let file = await fileHandle.getFile();
    console.log(file);
    let content = await file.text();
    console.log(`打开文件: ${fileHandle.name}\n文件内容:\n${content}`);
};

在这里插入图片描述

选择目录

window.showDirectoryPicker():显示一个目录选择器,允许用户选择一个目录。

属性说明
id通过指定 ID,浏览器能够记住不同 ID 所对应的目录。当使用相同的 ID 打开另一个目录选择器时,选择器会打开相同的目录
mode默认 read,可对目录进行只读访问。 readwrite 可对目录进行读写访问
startIn用于指定选择器的起始目录。可以是FileSystemHandle对象或者常见目录(如:“desktop”、“documents”、“downloads”、“music”、“pictures”、“videos”)
// 默认从“下载”目录打开
async function openDir() {
    let dirHandle = await window.showDirectoryPicker({
        mode: "read",
        startIn: "downloads",
    });
    console.log(dirHandle);
}

在这里插入图片描述

保存文件

window.showSaveFilePicker():显示一个文件选择器,允许用户保存一个文件。可以选择一个已有文件覆盖保存,也可以新建一个文件。

属性说明
excludeAcceptAllOption是否排除“接受所有”选项(默认 false)
suggestedName建议的文件名称
types允许保存文件类型的数组(description:描述/accept:MIME类型)
// 写入文本到下载目录
async function writeFile() {
    // 写入文本到下载目录
    let writeHandle = await window.showSaveFilePicker({
        suggestedName: "写入测试.txt", // 待写入的文件名
        startIn: "downloads", //默认打开下载目录
    });
    const writableStream = await writeHandle.createWritable();
    // 写入文件
    await writableStream.write({
        type: 'write',
        position: 4,
        data: "这里是通过API写入的文本内容,从第4位开始"
    });
    // 关闭流
    await writableStream.close();
    console.log(`保存成功: ${writeHandle.name}`);
}

在这里插入图片描述

当然,也可以配合Web WorkercreateSyncAccessHandle来实现高性能的文件读写操作,这里不做示例说明。

其它示例

API有很多有意思的方法,下面给出几个简单示例。

1、强制选择指定类型文件

一般在使用input-file时,即使我们指定上传文件类型,还会有一个默认的“所有文件”的选项,可以选择其它类型的文件

在这里插入图片描述

通过API调用,设置excludeAcceptAllOption则可手动控制隐藏这个选项

在这里插入图片描述

2、操作本地文件

我们可以模拟以下功能场景:

  • 选择一个目录,自动写入文件(夹)
  • 选择一个文件,再选择一个目录,自动将文件移动到所选目录中

首先在桌面创建两个文件夹testDirtestDir2备用。

选择testDir文件夹,在其中自动创建一个文件夹和文本文件(create:true表示不存在则创建)

async function createDir() {
    let dirHandle = await window.showDirectoryPicker({
        mode: "read",
        startIn: 'desktop'
    });
    console.log(dirHandle);
    // 创建默认文件夹
    const dirName = "默认文件夹";
    const subDir = await dirHandle.getDirectoryHandle(dirName, { create: true });
    // 创建默认文件
    const fileName = "默认文本.txt";
    const subFile = await dirHandle.getFileHandle(fileName, { create: true })
    }

在这里插入图片描述

选择testDir文件夹,并读取其内容句柄(刚才创建的文件和文件夹)

async function readDir() {
    let dirHandle = await window.showDirectoryPicker({
        mode: "read"
    });
    for await (const item of dirHandle.values()) {
        console.log(item)
    }
}

在这里插入图片描述

移动testDirtxt文本到testDir2文件夹中

async function moveTxt() {
    // 选择目标文件
    let [fileHandle] = await window.showOpenFilePicker({
        multiple: false,
        excludeAcceptAllOption: true,
        types: [
            {
                description: "选择文本",
                accept: {
                    "text/plain": [".txt"],
                },
            },
        ],
    });
	// 选择目标目录
    let dirHandle = await window.showDirectoryPicker({
        mode: "readwrite"
    });
    fileHandle.move(dirHandle)
}

在这里插入图片描述

选择testDir,删除刚才创建的文件和文件夹

async function deleteDir() {
    let dirHandle = await window.showDirectoryPicker({
        mode: "read"
    });
    for await (const item of dirHandle.values()) {
        console.log(item)
        if(item.kind == 'file'){
            // 删除文件
            dirHandle.removeEntry(item.name);
            // 也可以使用如下方法=>fileHandler.remove(fileHandler)
            // item.remove(item)
        }else{
            // recursive:文件夹递归删除所有子目录和文件
            dirHandle.removeEntry(item.name, { recursive: true });
        }
    }
}
3、自定义换肤

一般多主题方案是固定的(由最终产品提供固定的几种配色或布局),灵活一点的可能会允许用户针对基础配色进行调整,这种需要依赖前后端配置。

如果我们不依赖后台,而是考虑通过纯前端方式,在本地生成相应的“皮肤文件”,允许用户选择这些文件从而动态完成应用多主题,是不是显得(闲的)高大上呢?

这里不做赘述,有兴趣的可以继续深究。

兼容性

可以看到现代浏览器是部分支持(或支持部分API)。

在这里插入图片描述

后记

  • 浏览器对于本地文件的操作一直都是很局限的,其实也限制了一部分前端的发展,很多功能需要依赖后台或者其他插件
  • File System API只有Chrome支持实现,其最大贡献在于突破原有定式,赋予浏览器直接操作文件和目录的能力,这一步如果成功,可极大提升浏览器在应用系统中的地位(然而步子太大,GG了)
  • File System Access API真正意义上让浏览器可以操作本地文件。但出于安全考虑,浏览器不允许我们访问一些敏感目录(如包含系统文件的目录或者组策略不允许访问的目录)
  • File System Access API提供了更加强大的Web应用程序功能。我们可以利用它完成如文本/图形在线编辑器、导入导出优化、加载用户本地脚本、大文件切片缓存等功能
  • File System Access API功能很强大,但兼容性是当下比较现实的问题。基于安全和性能考量,各浏览器厂商并未完全纳入自有规范中,但可以看到主流浏览器都在朝着这个方向去发展(兼容了部分操作)
  • 为了方便理解,以上给出的示例都是比较简单的,实际上还有很多复杂的API和功能,感兴趣的话可自行查阅相关资料(有封装好的三方库,如browser-fs-access等)
  • 当然我们还是期待此标准能够尽快沉淀和完善起来,可以给前端(浏览器)的生态带来一些变化,也许我们可以摆脱inputa标签的束缚,做出更灵活的交互

参考资料

File_System_Access_API

file-system-access

HTML5 本地文件操作之FileSystemAPI实例

浏览器读写本地文件 File System Access API

FileSystemHandle

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值