• 开往
  • 设置
  • 用户
  • 回到顶部
  • 收起
  • 改写本地存储对象实现多标签页事件分发

    • ? 阅读
    • 1107 字
    下一章

    全部代码

    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
    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