自从 Chrome 推出以来externally_connectable
,这在 Chrome 中很容易做到。首先,在文件中指定允许的域manifest.json
:
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
用于chrome.runtime.sendMessage
从页面发送消息:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
// ...
});
最后,在您的背景页面中收听chrome.runtime.onMessageExternal
:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
// verify `sender.url`, read `request` object, reply with `sednResponse(...)`...
});
如果您无法获得externally_connectable
支持,原始答案如下:
我将从以 Chrome 为中心的角度来回答,尽管此处描述的原则(网页脚本注入、长时间运行的后台脚本、消息传递)几乎适用于所有浏览器扩展框架。
从高层次来看,您要做的是将内容脚本注入每个网页,这会添加一个可供网页访问的 API。当站点调用 API 时,API 会触发内容脚本执行某些操作,例如通过异步回调将消息发送到后台页面和/或将结果发送回内容脚本。
这里的主要困难是“注入”到网页中的内容脚本不能直接改变页面的 JavaScript执行环境。它们共享 DOM,因此内容脚本和网页之间共享事件和对 DOM 结构的更改,但不共享函数和变量。例子:
DOM 操作:如果内容脚本将<div>
元素添加到页面,则将按预期工作。内容脚本和页面都将看到新的<div>
.
事件:如果内容脚本设置了一个事件监听器,例如,点击一个元素,监听器将在事件发生时成功触发。如果页面为从内容脚本触发的自定义事件设置了侦听器,那么当内容脚本触发这些事件时,它们将被成功接收。
函数:如果内容脚本定义了一个新的全局函数foo()
(就像您在设置新 API 时可能会尝试的那样)。页面无法查看或执行foo
,因为foo
只存在于内容脚本的执行环境中,而不存在于页面的环境中。
那么,如何设置合适的 API 呢?答案分为多个步骤:
在低级别,使您的 API基于事件。网页使用 触发自定义 DOM 事件dispatchEvent
,内容脚本使用 监听它们,并addEventListener
在收到它们时采取行动。这是一个简单的基于事件的存储 API,网页可以使用它来扩展来存储数据:
content_script.js(在您的扩展中):
// an object used to store things passed in from the API
internalStorage = {};
// listen for myStoreEvent fired from the page with key/value pair data
document.addEventListener('myStoreEvent', function(event) {
var dataFromPage = event.detail;
internalStorage[dataFromPage.key] = dataFromPage.value
});
使用基于事件的 API 的非扩展网页:
function sendDataToExtension(key, value) {
var dataObj = {"key":key, "value":value};
var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
document.dispatchEvent(storeEvent);
}
sendDataToExtension("hello", "world");
如您所见,普通网页正在触发内容脚本可以看到并做出反应的事件,因为它们共享 DOM。事件附加了数据,添加到CustomEvent
构造函数中。我的示例非常简单——一旦内容脚本拥有来自页面的数据(很可能将其传递到后台页面进行进一步处理),您显然可以在内容脚本中做更多的事情。
然而,这只是成功的一半。在我上面的例子中,普通网页必须自己创建sendDataToExtension
。创建和触发自定义事件非常冗长(我的代码占用 3 行,并且相对简短)。您不想强迫网站编写神秘的事件触发代码只是为了使用您的 API。解决方案有点令人讨厌:将<script>
标签附加到您的共享 DOM 中,这会将事件触发代码添加到主页的执行环境中。
在content_script.js 中:
// inject a script from the extension's files
// into the execution environment of the main page
var s = document.createElement('script');
s.src = chrome.extension.getURL("myapi.js");
document.documentElement.appendChild(s);
中定义的任何功能都myapi.js
将可供主页访问。(如果您使用的是,则"manifest_version":2
需要myapi.js
在清单的列表中包含web_accessible_resources
)。
myapi.js:
function sendDataToExtension(key, value) {
var dataObj = {"key":key, "value":value};
var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
document.dispatchEvent(storeEvent);
}
现在普通网页可以简单地做:
sendDataToExtension("hello", "world");
我们的 API 流程还有一个问题myapi.js
:脚本在加载时不完全可用。相反,它将在页面加载时间之后的某个时间加载。因此,普通网页需要知道何时可以安全地调用您的 API。您可以通过myapi.js
触发您的页面侦听的“API 就绪”事件来解决此问题。
myapi.js:
function sendDataToExtension(key, value) {
// as above
}
// since this script is running, myapi.js has loaded, so let the page know
var customAPILoaded = new CustomEvent('customAPILoaded');
document.dispatchEvent(customAPILoaded);
使用 API的普通网页:
document.addEventListener('customAPILoaded', function() {
sendDataToExtension("hello", "world");
// all API interaction goes in here, now that the API is loaded...
});
加载时脚本可用性问题的另一个解决方案是将run_at
清单中内容脚本的属性设置为"document_start"
如下所示:
清单.json:
"content_scripts": [
{
"matches": ["https://example.com/*"],
"js": [
"myapi.js"
],
"run_at": "document_start"
}
],
文档摘录:
在“document_start”的情况下,文件是在任何来自 css 的文件之后注入的,但在构造任何其他 DOM 或运行任何其他脚本之前。
对于某些可能比“加载 API”事件更合适且省力的内容脚本。
为了将结果发送回页面,您需要提供一个异步回调函数。无法从您的 API 同步返回结果,因为事件触发/侦听本质上是异步的(即,您的站点端 API 函数在内容脚本通过 API 请求获取事件之前终止)。
myapi.js:
function getDataFromExtension(key, callback) {
var reqId = Math.random().toString(); // unique ID for this request
var dataObj = {"key":key, "reqId":reqId};
var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj});
document.dispatchEvent(fetchEvent);
// get ready for a reply from the content script
document.addEventListener('fetchResponse', function respListener(event) {
var data = event.detail;
// check if this response is for this request
if(data.reqId == reqId) {
callback(data.value);
document.removeEventListener('fetchResponse', respListener);
}
}
}
content_script.js(在您的扩展中):
// listen for myFetchEvent fired from the page with key
// then fire a fetchResponse event with the reply
document.addEventListener('myStoreEvent', function(event) {
var dataFromPage = event.detail;
var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId};
var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData});
document.dispatchEvent(fetchResponse);
});
普通网页:
document.addEventListener('customAPILoaded', function() {
getDataFromExtension("hello", function(val) {
alert("extension says " + val);
});
});
如果您同时发出多个请求,这reqId
是必要的,这样他们就不会读取错误的响应。
我认为这就是一切!因此,当您考虑到其他扩展也可以将侦听器绑定到您的事件以窃听页面如何使用您的 API 时,不适合胆小的人,而且可能不值得。我之所以知道这一切,是因为我为一个学校项目制作了一个概念验证密码学 API(并随后了解了与之相关的主要安全陷阱)。
总而言之:内容脚本可以侦听来自普通网页的自定义事件,并且该脚本还可以注入具有使网页更容易触发这些事件的功能的脚本文件。内容脚本可以将消息传递到后台页面,然后后台页面存储、转换或传输来自消息的数据。