上一卷

改写本地存储对象实现多标签页事件分发

  • 22 阅读
  • 1117 字
下一章

全部代码

js
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 函数,并将接收的数据作为参数传入。

js
1
2
3
4
const channel = new BroadcastChannel("storage");
channel.addEventListener("message", (e) => {
    dispatch(e.data);
});

核心函数

dispatch 方法里,我们先将键值缓存到一个 cache 对象中。这是为了优化浏览器操作 localStorage 对象时读取数据的效率,减少反复调用造成的性能损耗。

js
1
2
3
const cache = {};

cache[key] = String(value);

然后自定义一个事件,并以 storage:${key} 的格式命名。这种做法的好处是,不用在单个监听事件中使用 if / elseswitch 语句进行冗长的判断,而是将其分割成多个监听事件。

js
1
2
3
4
const event = new Event(`storage:${key}`);
event.key = key;
event.value = value;
window.dispatchEvent(event);

之后要做的事情就非常简单了。

js
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 时,已经模拟浏览器的行为将转为字符串的键值写入了缓存,所以每次调用该方法获取的数据都是最新的。

js
1
2
3
4
const original = localStorage.getItem;
localStorage.getItem = function(key) {
    return cache[key] ??= original.call(this, key);
};

使用场景

对网站的一些设置,一般可以使用 localStorage 进行存储。而部分设置项只在特定的页面生效,将这些处理函数放在设置组件的 js 中显然是不合理的。根据以上步骤进行改写后,则能够在特定页面下的 js 中监听某一项键值的修改,优化 js 的结构;并且这种对键值变更的响应是多页面实时的。如:

js
1
2
// view/reader.js
window.addEventListener("storage:font-family", setFontFamily);

每当在设置中修改了字体,所有打开的阅读页都将同步运行 setFontFamily 函数。

或者,像是快捷键这种需要频繁进行读取的设置项,则不必考虑将首屏渲染时获取的值存入变量后造成的不同步等问题,因为 getItem 方法已经自带缓存了。

js
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

60FPS