正在开发一个使用新的(ish)文件系统访问API的应用程序,我想保存最近加载的文件的文件句柄,以显示“最近的文件...”菜单选项并让用户加载其中之一这些文件无需打开系统文件选择窗口。
这篇文章有一段关于在 IndexedDB 中存储 fileHandles 的内容,其中提到从 API 返回的句柄是“可序列化的”,但它没有任何示例代码,并且 JSON.stringify 不会这样做。
文件句柄是可序列化的,这意味着您可以将文件句柄保存到 IndexedDB,或调用 postMessage() 在同一顶级源之间发送它们。
除了 JSON 之外还有其他方法可以序列化句柄吗?我以为 IndexedDB 可能会自动执行此操作,但这似乎也不起作用。
这是一个最小的示例,演示如何在 IndexedDB 中存储和检索文件句柄(准确地说是
FileSystemHandle
)(为简洁起见,代码使用 idb-keyval 库):
import { get, set } from 'https://unpkg.com/[email protected]/dist/esm/index.js';
const pre = document.querySelector('pre');
const button = document.querySelector('button');
button.addEventListener('click', async () => {
try {
const fileHandleOrUndefined = await get('file');
if (fileHandleOrUndefined) {
pre.textContent =
`Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
return;
}
// This always returns an array, but we just need the first entry.
const [fileHandle] = await window.showOpenFilePicker();
await set('file', fileHandle);
pre.textContent =
`Stored file handle for "${fileHandle.name}" in IndexedDB.`;
} catch (error) {
alert(error.name, error.message);
}
});
我创建了一个 demo,展示了上述代码的实际运行情况。
当平台接口为
[Serializable]
时,意味着它具有关联的内部序列化和反序列化规则,执行“结构化克隆”算法的API将使用这些规则来创建JS值的“副本”。如前所述,消息 API 使用结构化克隆。它也被 History API 使用,因此至少在理论上您可以将 FileHandle 对象与历史记录条目关联起来。
在撰写本文时,在 Chromium 中,通常与
FileHandle
一起使用时,history.state
对象似乎可以成功序列化和反序列化,例如跨越重新加载和向后导航。奇怪的是,当返回前向条目时,反序列化似乎可能会默默地失败:当向前遍历到其关联状态包括一个或多个文件句柄的条目时,popStateEvent.state 和 History.state 总是返回 null
。这似乎是一个错误。
历史条目是“会话”存储“架子”的一部分。这里的会话(大致)指的是“选项卡/窗口的生命周期”。有时这可能正是您想要的 FileHandle (例如,向后遍历时,重新打开在早期状态下打开的文件)。然而,它对跨多个会话持续存在的“原始架”生命周期存储没有帮助。据我所知,唯一可以序列化和反序列化 FileHandle 以进行原始级存储的 API 是 IndexedDB。
对于那些使用 Dexie 与 IndexedDB 交互的人,您将得到一个空对象,除非您未命名主键(“非入站”):
db.version(1).stores({
test: '++id'
});
const [fileHandle] = await window.showOpenFilePicker();
db.test.add({ fileHandle })
这会产生一条带有
{ fileHandle: {} }
(空对象)的记录
但是,如果您没有命名主键,它会正确序列化对象:
db.version(1).stores({
test: '++'
});
const [fileHandle] = await window.showOpenFilePicker();
db.test.add({ fileHandle })
结果:
{ fileHandle: FileSystemFileHandle... }
这可能是 Dexie 中的一个错误,如下所示:https://github.com/dfahlander/Dexie.js/issues/1236
我专门制作这些函数来存储和检索
FileSystemDirectoryHandle
/** @type {Promise<IDBDatabase>} */
const dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open('handle', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('handles', { keyPath: 'id' });
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
console.error(event);
reject(event.target.error);
};
});
/**
* Saves a directory handle to the database.
* @param {FileSystemDirectoryHandle} dirHandle - The directory handle to save.
* @returns {Promise<void>} A promise that resolves when the handle is saved.
*/
export const saveDirHandle = (dirHandle) => {
return new Promise((resolve, reject) => {
dbPromise.then((db) => {
const transaction = db.transaction(['handles'], 'readwrite');
const store = transaction.objectStore('handles');
const request = store.put({ id: 'lastFolder', handle: dirHandle });
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
});
};
/**
* Retrieves the last saved folder handle from the database.
* @returns {Promise<FileSystemDirectoryHandle|undefined>} A promise that resolves with the folder handle or undefined if not found.
*/
export const getLastFolderHandle = () => {
return new Promise((resolve, reject) => {
dbPromise.then((db) => {
const transaction = db.transaction(['handles'], 'readonly');
const store = transaction.objectStore('handles');
const request = store.get('lastFolder');
request.onsuccess = (event) => resolve(event.target.result?.handle);
request.onerror = () => reject(request.error);
});
});
};