Chromium 的文件系统访问 API(2019 年推出)
有一个相对较新的非标准File System Access API(不要与早期的File and Directory Entries API 或File System API 混淆)。看起来它是在 2019/2020 年在 Chromium/Chrome 中引入的,并且在 Firefox 或 Safari 中不支持。
使用此 API 时,本地打开的页面可以打开/保存其他本地文件并使用页面中文件的数据。它确实需要初始权限才能保存,但是当用户在页面上时,特定文件的后续保存会“静默”进行。用户还可以授予特定目录的权限,随后对该目录的读取和写入不需要批准。用户关闭网页的所有选项卡并重新打开页面后,需要再次批准。
您可以在https://web.dev/file-system-access/ 阅读有关此新 API 的更多信息。它旨在用于制作更强大的 Web 应用程序。
需要注意的几点:
-
默认情况下,它需要一个安全的上下文才能运行。在 https、localhost 或通过 file:// 运行它应该可以工作。
-
您可以通过使用DataTransferItem.getAsFileSystemHandle拖放文件来获取文件句柄
-
最初读取或保存文件需要用户批准,并且只能通过用户交互来启动。之后,后续的读取和保存都不需要批准,直到站点再次打开。
-
文件句柄可以保存在页面中(因此,如果您正在编辑 'path/to/file.html' 并重新加载页面,它将能够引用文件)。它们似乎不能被字符串化,因此通过 IndexedDB 之类的东西存储(有关更多信息,请参阅this answer)。使用存储的句柄进行读/写需要用户交互和用户批准。
这里有一些简单的例子。它们似乎没有在跨域 iframe 中运行,因此您可能需要将它们保存为 html 文件并在 Chrome/Chromium 中打开它们。
通过拖放打开和保存(无外部库):
<body>
<div><button id="open">Open</button><button id="save">Save</button></div>
<textarea id="editor" rows=10 cols=40></textarea>
<script>
let openButton = document.getElementById('open');
let saveButton = document.getElementById('save');
let editor = document.getElementById('editor');
let fileHandle;
async function openFile() {
try {
[fileHandle] = await window.showOpenFilePicker();
await restoreFromFile(fileHandle);
} catch (e) {
// might be user canceled
}
}
async function restoreFromFile() {
let file = await fileHandle.getFile();
let text = await file.text();
editor.value = text;
}
async function saveFile() {
var saveValue = editor.value;
if (!fileHandle) {
try {
fileHandle = await window.showSaveFilePicker();
} catch (e) {
// might be user canceled
}
}
if (!fileHandle || !await verifyPermissions(fileHandle)) {
return;
}
let writableStream = await fileHandle.createWritable();
await writableStream.write(saveValue);
await writableStream.close();
}
async function verifyPermissions(handle) {
if (await handle.queryPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
if (await handle.requestPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
return false;
}
document.body.addEventListener('dragover', function (e) {
e.preventDefault();
});
document.body.addEventListener('drop', async function (e) {
e.preventDefault();
for (const item of e.dataTransfer.items) {
if (item.kind === 'file') {
let entry = await item.getAsFileSystemHandle();
if (entry.kind === 'file') {
fileHandle = entry;
restoreFromFile();
} else if (entry.kind === 'directory') {
// handle directory
}
}
}
});
openButton.addEventListener('click', openFile);
saveButton.addEventListener('click', saveFile);
</script>
</body>
存储文件句柄可能很棘手,因为它们不能被取消字符串化,though apparently they can be used with IndexedDB and mostly with history.state。对于这个例子,我们将使用idb-keyval 来访问 IndexedDB 以存储文件句柄。要查看它是否正常工作,请打开或保存文件,然后重新加载页面并按“恢复”按钮。这个例子使用了来自https://stackoverflow.com/a/65938910/的一些代码。
<body>
<script src="https://unpkg.com/idb-keyval@6.1.0/dist/umd.js"></script>
<div><button id="restore" style="display:none">Restore</button><button id="open">Open</button><button id="save">Save</button></div>
<textarea id="editor" rows=10 cols=40></textarea>
<script>
let restoreButton = document.getElementById('restore');
let openButton = document.getElementById('open');
let saveButton = document.getElementById('save');
let editor = document.getElementById('editor');
let fileHandle;
async function openFile() {
try {
[fileHandle] = await window.showOpenFilePicker();
await restoreFromFile(fileHandle);
} catch (e) {
// might be user canceled
}
}
async function restoreFromFile() {
let file = await fileHandle.getFile();
let text = await file.text();
await idbKeyval.set('file', fileHandle);
editor.value = text;
restoreButton.style.display = 'none';
}
async function saveFile() {
var saveValue = editor.value;
if (!fileHandle) {
try {
fileHandle = await window.showSaveFilePicker();
await idbKeyval.set('file', fileHandle);
} catch (e) {
// might be user canceled
}
}
if (!fileHandle || !await verifyPermissions(fileHandle)) {
return;
}
let writableStream = await fileHandle.createWritable();
await writableStream.write(saveValue);
await writableStream.close();
restoreButton.style.display = 'none';
}
async function verifyPermissions(handle) {
if (await handle.queryPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
if (await handle.requestPermission({ mode: 'readwrite' }) === 'granted') {
return true;
}
return false;
}
async function init() {
var previousFileHandle = await idbKeyval.get('file');
if (previousFileHandle) {
restoreButton.style.display = 'inline-block';
restoreButton.addEventListener('click', async function (e) {
if (await verifyPermissions(previousFileHandle)) {
fileHandle = previousFileHandle;
await restoreFromFile();
}
});
}
document.body.addEventListener('dragover', function (e) {
e.preventDefault();
});
document.body.addEventListener('drop', async function (e) {
e.preventDefault();
for (const item of e.dataTransfer.items) {
console.log(item);
if (item.kind === 'file') {
let entry = await item.getAsFileSystemHandle();
if (entry.kind === 'file') {
fileHandle = entry;
restoreFromFile();
} else if (entry.kind === 'directory') {
// handle directory
}
}
}
});
openButton.addEventListener('click', openFile);
saveButton.addEventListener('click', saveFile);
}
init();
</script>
</body>
附加说明
Firefox 和 Safari 支持似乎不太可能,至少在短期内是这样。见https://github.com/mozilla/standards-positions/issues/154和https://lists.webkit.org/pipermail/webkit-dev/2020-August/031362.html