IndexedDB存储文件句柄后,文件修改刷新页面不更新的问题咨询
参考Stack Overflow方案实现了一个支持拖放文件展示内容的网页:拖放文件后,文件会被存储到IndexedDB,页面刷新时会重新加载最后拖放的文件。但遇到以下问题:修改本地文件后刷新页面,无法显示新内容;重新拖放修改后的文件则能正常展示;即使执行Chrome的“清空缓存并硬刷新”操作,仍显示旧内容。
需要解答:
- 问题原因是什么?
- 存储的文件是否是完整文件?
- 如何实现修改文件后刷新页面即可加载新内容?
原因分析
你存储在IndexedDB里的不是文件句柄,而是拖放时生成的File对象副本——这个对象是拖放那一刻文件内容的快照,并非指向本地文件的实时引用。当本地文件修改后,IndexedDB里的旧File对象不会自动同步更新,刷新页面时读取的还是旧的快照内容,自然看不到新修改。
文件是否存储了完整文件?
是的,你存储的是完整的文件内容快照。File对象本质是Blob的子类,通过item.getAsFile()获取的是拖放瞬间文件内容的完整副本,存储到IndexedDB后,它和本地原文件就不再有任何关联。
解决方案
要实现修改文件后刷新加载新内容,不能存储File对象,而是要存储文件持久化句柄(FileSystemFileHandle),通过这个句柄可以实时读取本地文件的最新内容。修改步骤如下:
1. 拖放时获取并存储文件句柄
修改dropHandler函数,不再获取File对象,改为获取FileSystemFileHandle:
function dropHandler(ev) { ev.preventDefault(); if (ev.dataTransfer.items) { [...ev.dataTransfer.items].forEach(async (item, i) => { if (item.kind === "file") { // 获取文件句柄而非File对象 const fileHandle = await item.getAsFileSystemHandle(); handleFile(ev.currentTarget, fileHandle); } }); } }
2. 修改handleFile函数,支持实时读取文件内容
更新函数逻辑,区分File对象和FileSystemFileHandle,从句柄中实时读取最新文件内容:
async function handleFile(domElement, fileOrHandle) { let file; if (fileOrHandle.kind === 'file') { // 如果是文件句柄,实时读取本地文件的最新内容 file = await fileOrHandle.getFile(); // 存储文件句柄到IndexedDB await idbStore(makeKey(domElement), fileOrHandle); } else { // 兼容旧的File对象(可选保留) file = fileOrHandle; await idbStore(makeKey(domElement), file); } const reader = new FileReader(); reader.onload = function (e) { domElement.dispatchEvent( new CustomEvent("handlefilecontent", { detail: e.target.result }) ); }; reader.readAsText(file); }
3. 权限说明
使用FileSystemFileHandle需要用户授权,首次拖放文件时浏览器会询问是否允许访问该文件,用户同意后才能持久化存储句柄。后续刷新页面时,浏览器会自动复用之前的授权,无需重复询问。
修改后的完整代码
<html> <head></head> <body> <div id="display"></div> <script> // 来自idb-keyval的工具函数 function promisifyRequest(request) { return new Promise((resolve, reject) => { request.oncomplete = request.onsuccess = () => resolve(request.result); request.onabort = request.onerror = () => reject(request.error); }); } function createStore(dbName, storeName) { const request = indexedDB.open(dbName); request.onupgradeneeded = () => request.result.createObjectStore(storeName); const dbp = promisifyRequest(request); return (txMode, callback) => dbp.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); } let defaultGetStoreFunc; function defaultGetStore() { if (!defaultGetStoreFunc) { defaultGetStoreFunc = createStore('keyval-store', 'keyval'); } return defaultGetStoreFunc; } function idbLoad(key, customStore = defaultGetStore()) { return customStore('readonly', (store) => promisifyRequest(store.get(key))); } function idbStore(key, value, customStore = defaultGetStore()) { return customStore('readwrite', (store) => { store.put(value, key); return promisifyRequest(store.transaction); }); } function makeKey(domElement) { return 'lastOpened-' + domElement.tagName + "-" + domElement.id; } async function handleFile(domElement, fileOrHandle) { let file; if (fileOrHandle.kind === 'file') { // 从文件句柄读取最新文件内容 file = await fileOrHandle.getFile(); await idbStore(makeKey(domElement), fileOrHandle); } else { file = fileOrHandle; await idbStore(makeKey(domElement), file); } const reader = new FileReader(); reader.onload = function (e) { domElement.dispatchEvent( new CustomEvent("handlefilecontent", { detail: e.target.result }) ); }; reader.readAsText(file); } function dropHandler(ev) { ev.preventDefault(); if (ev.dataTransfer.items) { [...ev.dataTransfer.items].forEach(async (item, i) => { if (item.kind === "file") { const fileHandle = await item.getAsFileSystemHandle(); handleFile(ev.currentTarget, fileHandle); } }); } } function dragOverHandler(ev) { ev.preventDefault(); } async function checkLastOpened(domElement) { try { let storedItem = await idbLoad(makeKey(domElement)); if (storedItem) { handleFile(domElement, storedItem) } } catch (error) { alert(error.name, error.message); } } function dragDropInitialize(domElement, callback) { domElement.addEventListener("drop", dropHandler) domElement.addEventListener("dragover", dragOverHandler) domElement.addEventListener("handlefilecontent", (ev) => { callback(ev.detail) }) checkLastOpened(domElement) } dragDropInitialize(document.body, (content) => { document.getElementById("display").innerHTML = content }) </script> </body> </html>
内容的提问来源于stack exchange,提问作者mqnc

