全部代码
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