file://协议下跨本地HTML文件共享localStorage的实现问题咨询
背景
- 我司将数千个.htm格式的离线文档部署在用户本地设备中,用户通过浏览器的file:///协议离线访问这些文档内容。
- 每个.htm文件的页脚都有一个横幅,询问用户是否同意我们通过Application Insights采集用户行为分析数据,用户作出选择后横幅不再展示。
目标与遇到的问题
我的目标是将用户的选择存储在浏览器的localStorage中,单个页面的存储功能可正常运行,但存在如下问题:
这些都是离线.htm文件,没有网站域名,因此localStorage的键值对仅在用户作出选择时打开的.htm文件下生效。用户回到该页面时可正常读取选择结果,但导航到文档库的其他.htm文件时,localStorage键值对无法跨页面共享,脚本无法读取用户之前的选择,横幅会再次弹出。
我的替代方案思路
我不确定这是最优方案,但由于相关经验不足暂时没有其他思路,目前尝试的方案如下:
- 所有localStorage读写请求都通过一个名为storage.htm的单独文件处理,借助该固定文件作为localStorage的唯一操作入口来解决上述跨页面存储问题。
- storage.htm通过空白iframe加载,嵌入到每个.htm文档页面中。
- 当文档页面加载时,iframe同步加载,理想状态下iframe内的函数可被主页面脚本调用。
- 用户点击横幅作出选择时,主页面脚本将选择结果作为查询参数传给iframe的src属性。
- storage.htm内包含
parseQuery()函数,该函数负责解析传入的查询参数,执行实际的localStorage.getValue(key)和localStorage.setValue(key,value)操作。 - 我希望能在iframe携带参数刷新后,从主页面脚本调用iframe内的
parseQuery()函数完成存储操作。
我的代码与遇到的报错
主页面脚本代码如下:
尝试1:我参照了Stack Overflow帖子的最高赞答案,但代码卡在checkIfFrameLoaded()函数中无限循环检测iframe加载状态,始终判定未加载。我推测是离线文件场景下contentWindow和/或contentDocument无法正常工作,因此不展开相关代码。
尝试2:这个方案逻辑更简洁,是我更希望实现的方案:
function doIframeStorage(type,key,value){ // We get a handle on the iframe's id of 'storage'. let storage = document.querySelector('#storage'); const src = storage.getAttribute('src'); let query = src; if (type==='get'){ query = src + `?type=${type}&key=${key}`; } else if (type==='set'){ query = src + `?type=${type}&key=${key}&value=${value}`; } storage.src = query; storage.addEventListener('load', (e) => parseQuery()); }
但运行时出现报错,storage.htm中定义的parseQuery()函数始终无法被主页面识别:Uncaught ReferenceError: parseQuery is not defined
请问我当前的实现方式是否可以正常调用parseQuery()函数?如果可行应该如何调整?是否存在更优的方案可以实现我的需求?
解决方案
现有方案调整方法
你的调用逻辑存在上下文错误:parseQuery()定义在iframe的全局作用域中,直接调用会从父页面的全局作用域查找函数,自然会报未定义错误。同时你每次调用存储函数都会重复绑定load事件,会导致函数多次触发,修正后的代码如下:
// 父页面代码:提前绑定单次load事件 const storageIframe = document.querySelector('#storage'); let storageLoadCallback = null; storageIframe.addEventListener('load', () => { if (storageLoadCallback) { storageLoadCallback(); storageLoadCallback = null; } }); function doIframeStorage(type, key, value) { const baseSrc = storageIframe.getAttribute('src').split('?')[0]; let query = baseSrc; if (type === 'get') { query = `${baseSrc}?type=${encodeURIComponent(type)}&key=${encodeURIComponent(key)}`; } else if (type === 'set') { query = `${baseSrc}?type=${encodeURIComponent(type)}&key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`; } storageLoadCallback = () => { // 从iframe的window对象调用函数 const res = storageIframe.contentWindow.parseQuery(); // get请求可在此处拿到返回的存储值 if (type === 'get') { console.log('读取结果:', res); } }; storageIframe.src = query; }
同时要确保storage.htm中的parseQuery挂载到全局window对象:
// storage.htm代码 window.parseQuery = function() { const params = new URLSearchParams(window.location.search); const type = params.get('type'); const key = params.get('key'); if (type === 'get') return localStorage.getItem(key); if (type === 'set') { localStorage.setItem(key, params.get('value')); return true; } }
更优方案推荐
不需要修改iframe的src触发刷新,直接用postMessage跨上下文通信性能更高,也不会有加载时序问题:
// 父页面代码 const storageIframe = document.querySelector('#storage'); // 监听iframe返回的操作结果 window.addEventListener('message', (e) => { if (e.data.type === 'storageRes') { console.log('存储操作结果:', e.data.payload); } }); function doIframeStorage(type, key, value) { storageIframe.contentWindow.postMessage({ type: 'storageOp', payload: {type, key, value} }, '*'); }
// storage.htm代码 window.addEventListener('message', (e) => { if (e.data.type !== 'storageOp') return; const {type, key, value} = e.data.payload; let res; if (type === 'get') res = localStorage.getItem(key); if (type === 'set') { localStorage.setItem(key, value); res = true; } // 结果回传给父页面 e.source.postMessage({type: 'storageRes', payload: res}, '*'); });
内容的提问来源于stack exchange,提问作者Jared

