全部代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
const cache = {}; //监听本地存储的修改 (() => { const channel = new BroadcastChannel("storage"); //跨页监听 channel.addEventListener("message", (e) => { dispatch(e.data); }); const original = localStorage.setItem; localStorage.setItem = function(key, value) { //本页 dispatch({ key, value }); //跨页 channel.postMessage({ key, value }); //执行原生行为 original.call(this, key, value); }; function dispatch({ key, value }) { //缓存 cache[key] = String(value); //触发全局事件 const event = new Event(`storage:${key}`); event.key = key; event.value = value; window.dispatchEvent(event); } })(); //缓存本地存储的读取 (() => { const original = localStorage.getItem; localStorage.getItem = function(key) { return cache[key] ??= original.call(this, key); }; })();
为什么需要重写
浏览器原生的 localStorage
对象只能监听同源下其他标签页的变化,而简单地重写使它能够将事件分发到当前标签页后,它又失去了前者的功能;将对 localStorage
的修改与响应其变化的 handler
函数分离,有利于代码的维护与功能的扩展。
同源通信
MDN:BroadcastChannel 接口代理了一个命名频道,可以让指定 origin 下的任意 browsing context 来订阅它。它允许同源的不同浏览器窗口,Tab 页,frame 或者 iframe 下的不同文档之间相互通信。通过触发一个 message 事件,消息可以广播到所有监听了该频道的 BroadcastChannel 对象。
我们首先新建一个 BroadcastChannel
对象,并使用 storage
为该通道命名。此时,所有同源标签页的文档之间建立了一个能够相互通信的频道;随后监听该频道的 message
事件,每当在该对象上调用 postMessage
方法时,触发一个 dispatch
函数,并将接收的数据作为参数传入。
1 2 3 4
const channel = new BroadcastChannel("storage"); channel.addEventListener("message", (e) => { dispatch(e.data); });
核心函数
在 dispatch
方法里,我们先将键值缓存到一个 cache
对象中。这是为了优化浏览器操作 localStorage
对象时读取数据的效率,减少反复调用造成的性能损耗。
1 2 3
const cache = {}; cache[key] = String(value);
然后自定义一个事件,并以 storage:${key}
的格式命名。这种做法的好处是,不用在单个监听事件中使用 if / else
或 switch
语句进行冗长的判断,而是将其分割成多个监听事件。
1 2 3 4
const event = new Event(`storage:${key}`); event.key = key; event.value = value; window.dispatchEvent(event);
之后要做的事情就非常简单了。
1 2 3 4 5 6
const original = localStorage.setItem; localStorage.setItem = function(key, value) { dispatch({ key, value }); channel.postMessage({ key, value }); original.call(this, key, value); };
为了配合此前定义的 cache
对象,对 localStorage.getItem
也进行简单的重写。当 cache
对象中不存在该 key
时,先进行一次读取,并将其写入缓存;之后再次调用时,则不再运行原生的 getItem
方法,而是直接从缓存中读取。当然,由于每次调用 setItem
时,已经模拟浏览器的行为将转为字符串的键值写入了缓存,所以每次调用该方法获取的数据都是最新的。
1 2 3 4
const original = localStorage.getItem; localStorage.getItem = function(key) { return cache[key] ??= original.call(this, key); };
使用场景
对网站的一些设置,一般可以使用 localStorage
进行存储。而部分设置项只在特定的页面生效,将这些处理函数放在设置组件的代码中显然是不合理的。根据以上步骤进行改写后,则能够在特定页面下监听某一项键值的修改,优化代码的结构;并且这种对键值变更的响应是多页面实时的。如:
1 2
// view/reader.js window.addEventListener("storage:font-family", setFontFamily);
每当在设置中修改了字体,所有打开的阅读页都将同步运行 setFontFamily
函数。
或者,像是快捷键这种需要频繁进行读取的设置项,则不必考虑将首屏渲染时获取的值存入变量后造成的不同步等问题,因为 getItem
方法已经自带缓存了。
1 2 3 4 5 6 7 8
window.addEventListener("keydown", (event) => { if (!isFirst && event.key === localStorage.getItem("shortcut-last")) { /* 前往上一章节 */ } if (!isLast && event.key === localStorage.getItem("shortcut-next")) { /* 前往下一章节 */ } });
评论0