Chrome 插件中更简单的通信方式

chrome 73 版本,增加了 chrome.storage.onChanged 可以实现全局监听 storage 改变,我们可以借助这个方法实现一种非常简单的通信方式。
下面代码是封装成 npm 包后的使用方式

1
2
3
// popup.js
const emitter = require('chrome-emitter');
emitter.emit('reload');
1
2
3
4
5
// content.js
const emitter = require('chrome-emitter');
emitter.on('reload', (title) => {
window.location.reload();
});

是不是非常熟悉的调用方式!是不是非常简单!在任何脚本中都可以调用。

那么如何实现请见下文。

初学者的感到困难的通信方式

在写 chrome 插件过程中,经常会遇到一个问题,就是各个脚本需要通信,我们共有如下脚本

  1. popup.js
  2. options.js
  3. background.js
  4. content.js
  5. injected.js

injected.js 是指 content.js 通过 script 脚本插入的一段脚本,所以它和页面自己加载的 js 脚本拥有相同功能,最主要的是能读取挂载到 window 上的变量,而 content.js 不行。

大部分介绍怎么通信的博客文章都是介绍脚本两两之间怎么通信,如 runtime.sendMessagetabs.sendMessage 。但它们的通信方式不是完全相同的,这就导致了需要考虑谁向谁通信该调用什么方法。

最重要的是,如果想跨脚本通信也很麻烦,如 injected.jspopup.js 发送消息,或者反过来 popup.jsinjected.js 发送消息都需要通过 content.js 中转。

optionscontent 发消息也非常麻烦,需要在页面加载时保存当前页面的引用,然后在 options 页面中找到这个引用并发送消息。

还有一些问题奇奇怪怪的问题,如经常遇到一些通信相关的报错

  1. Unchecked runtime.lastError: The message port closed before a response was received.
  2. Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.

刚刚接触 chrome 插件开发的同学(比如我)肯定是一脸懵逼。

那就没有一种简单的通信方式吗?

使用 storage.onChanged 全局监听

有,chrome 有提供一个存储的 api ,叫 storage ,我们可以用它来存储、读取一些持久化数据,在除 injected.js 外的脚本中都能直接调用,它在 73 版本支持 onChanged 监听方法,这就让 storageredux store 有类似的用法

1
2
3
4
5
6
7
// redux
store.dispatch({
type: 'reload',
});
store.subscribe(() => {
console.log(store);
});

每次 dispatch 改变数据后,都会调用监听函数。与之类似

1
2
3
4
chrome.storage.local.set({ key: 1, type: 'reload' });
chrome.storage.onChanged.addListener((changes) => {
console.log(changes);
});

在每次 set 后,也会调用监听函数。

所有脚本中只有 injected.js 不能调用 chrome.storage,所以只能通过 CustomEventcontent.js 通信,再由 content.js 和其他脚本通信。

我们需要做的,就是提供统一 API,让调用方无感知地完成通信。

简单的调用方式

如开头所示,我们只有两个方法,emiton

假设现在 popup 上有一个按钮,点击后刷新页面,这么一个需求,使用我们的 emitter 来写是这样的

1
2
3
4
// injected.js
emitter.on('reload', () => {
window.location.reload();
});
1
2
3
4
// popup.js
document.getElementById('btn').onclick = () => {
emitter.emit('reload');
};

就是这么简单!

而如果用官方推荐的通信方法 sendMessage

1
2
3
4
5
6
// popup.js
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { type: 'reload' }, (data) => {
console.log('reload success');
});
});
1
2
3
4
5
6
// content.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
window.dispatchEvent(new CustomEvent('page-event'), { detail: msg });
// 不调用就会报错
sendResponse();
});
1
2
3
4
5
6
7
// injected.js
window.addEventListener('page-event', (event) => {
const { detail } = event;
if (detail.type === 'reload') {
window.location.reload();
}
});

就问你懵不懵,两者的区别显而易见。

下载使用

那么这么容易使用的库在哪里能找到呢?现在只需要简单两步即可拥有

1
yarn add chrome-emitter
1
2
3
import emitter from 'chrome-emitter';

emitter.emit('reload');

原理实现

最核心的原理就是 chrome.storage.onChanged.addListener ,当然还需要对其进行一些封装。如 set 数据相同时,不会触发事件处理器;响应事件后移除 storage 中无用数据;

除此之外就是需要额外处理 injected 和其他脚本的通信,在 injected 内监听的事件,content 也需要监听,这样 popup 或者其他脚本,发送消息时先到 content.js,再从 content.js 使用 CustomEventinjected.js,对用户来说,感觉就像从 popup 直接到了 injected

具体可以查看 代码

示例

chrome-emitter-example

clone 代码后,在插件管理页面,加载 build 文件夹,即可安装该插件,体验 chrome-emitter 的效果。

emitter 存在的问题

讲完优点,缺点也不能忽略,emitter 和原生通信方式相比

  1. 实际代码量上,肯定前者更多因为多加载了一个 chrome-emitter.js 。并且如果不使用打包工具则需要手动把 emitter 代码拷贝到各个脚本中。
  2. emitter.emit 第二个参数用来给监听方法提供参数,只能使用 JSON 支持的类型(可能是会序列化保存到 storage 导致的)。
  3. 如果其他插件也调用了 onchanged.addListener 方法,会导致其也收到事件。
  4. 传递的数据量不能太大,由于需要存储到 storage 所以如果几百 kb 的数据量可能还是会有问题。

所以如果是简单的项目可以尝试使用下。

脚本间有区别的原因

前面提到脚本间有区别,其实本质上是 window.chrome 的值不同。

第一个是 background.js。可以看到 background.jspopup.jsoptions.js 都有完整的 api ,而 content.js 只有 runtimestorageinjected.js 更是什么都没有。

变量对比