如何在 Chrome 插件中访问任意网页中的 JavaScript 变量?

在折腾小工具的时候有产生了奇怪的需求,Chrome 插件需要监听任意网页中某个变量的变化,或是访问其中的值。但默认注入的 content.js 和原网页 Javascript 脚本并不运行在一个相同的环境中,无法相互访问。

搜索和自己尝试了如下的解决方法,现罗列如下。

访问到目标变量

这个很简单。虽然默认注入的content.js不能访问到原网页的变量,但可以访问和修改Dom,所以再注入一个脚本就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function loadScript(url) {
var elem = document.createElement('script');
elem.type = 'text/javascript';
elem.charset = 'utf-8';
elem.addEventListener('load', doCallback, false);
elem.src = url;
document.getElementsByTagName('head')[0].appendChild(elem);
}

function url(file) {
return chrome.extension.getURL(file);
}

loadScript(url('inject.js'));

```

在inject.js中便是和原网页一样的javascript环境了,但是问题来了,怎样才能将在此环境下的变量发送到后台插件中呢?

建立原网页作用域与插件后台页面的双工通信

目标很明确,在inject.js中建立与后台background.js的双工通信,这样我们可以实时将网页中值的变化发送到插件中进行分析……

不过阻碍也很明显,由于chrome的安全策略,inject.js和后台插件虽然能直接连通,但受域名限制,而content.js则可以很轻易连通。所以如果打通inject.js与content.js就好了。

外部服务器中转

inject.js和background.js连上相同的websocket服务器中转,问题解决。不过还要多加一个外部服务器。

inject.js直连后台插件

的确,Chrome提供了直连的方法。

首先在manifest.json中申明externally_connectable,需要申明域名限制。正当我兴高采烈地输入*://*/*全匹配后,发现出错,chrome文档如是说:

This will expose the messaging API to any page which matches the URL patterns you specify. The URL pattern must contain at least a second-level domain - that is, hostname patterns like ““, “.com”, “.co.uk”, and “.appspot.com” are prohibited.

没有办法做到让所有域名发起的连接都与manifest.json匹配,但对于认可的域名,只需要在inject.js中调用如下API,

1
2
3
4
5
var ExtensionID = 'nnhoaecbdmfokhcnldeiadllnjeebhcb';

// Make a simple request:
chrome.runtime.sendMessage(ExtensionID, {openUrlInEditor: url}, function(response) {});
```

inject.js注入Dom事件

这是网上一个比较通行的办法,可以约定一个Dom元素,再使用MutationObserver监控这个Dom元素即可。
不过修改Dom比较重量级,不是很喜欢这种做法。

轮询localStorage等公共可访问变量

两个脚本均可访问到localStorage,location等变量,所以一方修改一方轮询也是一种办法,不过如果变量发生了多次修改,每次的修改事件就很难及时发出。

SharedWorker转发

最终试验成功了一种还比较满意的方法,可由 content.js 创建一个 SharedWorker的函数,并将其转换为 Blob ,再使用URL.createObjectURL创建为共享链接存入localStorage , inject.js 读取后此链接后,两个脚本同时连上同一个 SharedWorker,由 worker 转发消息,可以很好双向通信。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// background.js
//----------------------------------------------------------------
// 简单监听来自content.js的转发的内容
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
console.log('bg', request);
}
);

// content.js
//----------------------------------------------------------------
// SharedWorker执行的逻辑,广播信息
var worker_function = function() {
var ports = []
onconnect = function(e) {
if (e.ports && e.ports.length > 0) {
for (var i = 0, j = e.ports.length; i < j; i++) {
e.ports[i].onmessage = function(e) {
for (var m = 0, n = ports.length; m < n; m++) {
ports[m].postMessage(e.data);
}
}
ports.push(e.ports[i]);
}
}
};
}

// 将上述函数转为可访问的URL, 并存入localStorage
localStorage.sharedWorkerURL = URL.createObjectURL(new Blob(["(" +
worker_function.toString() + ")()"
], {
type: 'text/javascript'
}));

var shared = new SharedWorker(localStorage.sharedWorkerURL);
shared.port.start();
shared.port.onmessage = function(e) {
console.log('content', e);
// 向background.js转发
chrome.extension.sendMessage({
greeting: e.data
}, function(response) {
console.log('content get bg', response);
});
}

// inject.js
//----------------------------------------------------------------
var shared = new SharedWorker(localStorage.sharedWorkerURL);
shared.port.start();
shared.port.onmessage = function(e) {
console.log('inject', e);
}

```

使用window.postMessage

经网友提醒,参考Chrome开发文档中针对content script通信的说明。使用window.postMessage可以很好进行双向通信。