javascript 网站可以调用浏览器扩展吗?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/10526995/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Can a site invoke a browser extension?
提问by keewooi
I am a newbie to the browser extension development and I understand the concept of browser extensions altering the page and injecting codes into it.
我是浏览器扩展开发的新手,我了解浏览器扩展更改页面并向其中注入代码的概念。
Is there a way this direction can be turned around? I write an extension that provides a set of APIs, and web sites that want to use my extension can detect its presence and if it is present, the website can call my API methods like var extension = Extenion(foo, bar)
. Is this possible in Chrome, Firefox and Safari?
有没有办法扭转这个方向?我编写了一个提供一组 API 的扩展,想要使用我的扩展的网站可以检测到它的存在,如果它存在,网站可以调用我的 API 方法,如var extension = Extenion(foo, bar)
. 这在 Chrome、Firefox 和 Safari 中可能吗?
Example:
例子:
Google created a new extension called BeautifierExtension. It has a set of APIs as JS objects.
User goes to reddit.com. Reddit.com detects BeautifierExtension and invoke the API by calling
beautifer = Beautifier();
Google 创建了一个名为 BeautifierExtension 的新扩展。它有一组 API 作为 JS 对象。
用户访问 reddit.com。Reddit.com 检测 BeautifierExtension 并通过调用调用 API
beautifer = Beautifier();
See #2 - normally it's the extension that detects the matching sites and alter the pages. What I am interested to know is whether #2 is possible.
参见#2 - 通常它是检测匹配站点并更改页面的扩展程序。我有兴趣知道#2 是否可行。
回答by apsillers
Since Chrome introduced externally_connectable
, this is quite easy to do in Chrome. First, specify the allowed domain in your manifest.json
file:
由于 Chrome 引入了externally_connectable
,这在 Chrome 中很容易做到。首先,在您的manifest.json
文件中指定允许的域:
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}
Use chrome.runtime.sendMessage
to send a message from the page:
用于chrome.runtime.sendMessage
从页面发送消息:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
// ...
});
Finally, listen in your background page with chrome.runtime.onMessageExternal
:
最后,在您的背景页面中使用chrome.runtime.onMessageExternal
以下内容收听:
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
// verify `sender.url`, read `request` object, reply with `sednResponse(...)`...
});
If you don't have access to externally_connectable
support, the original answer follows:
如果您无法获得externally_connectable
支持,原始答案如下:
I'll answer from a Chrome-centric perspective, although the principles described here (webpage script injections, long-running background scripts, message passing) are applicable to virtually all browser extension frameworks.
我将从以 Chrome 为中心的角度来回答,尽管这里描述的原则(网页脚本注入、长时间运行的后台脚本、消息传递)几乎适用于所有浏览器扩展框架。
From a high level, what you want to do is inject a content scriptinto every web page, which adds an API, accessible to the web page. When the site calls the API, the API triggers the content script to do something, like sending messages to the background page and/or send a result back to the content script, via asynchronous callback.
从高层次来看,您想要做的是将内容脚本注入到每个网页中,这会添加一个可供网页访问的 API。当站点调用 API 时,API 会触发内容脚本执行某些操作,例如通过异步回调向后台页面发送消息和/或将结果发送回内容脚本。
The main difficulty here is that content scripts which are "injected" into a web page cannot directly alter the JavaScript execution environmentof a page. They share the DOM, so eventsand changes to DOM structureare shared between the content script and the web page, but functions and variables are not shared. Examples:
这里的主要困难是“注入”到网页中的内容脚本不能直接改变页面的 JavaScript执行环境。它们共享 DOM,因此在内容脚本和网页之间共享事件和对 DOM 结构的更改,但不共享函数和变量。例子:
DOM manipulation:If a content script adds a
<div>
element to a page, that will work as expected. Both content script and page will see the new<div>
.Events:If a content script sets up an event listener, e.g., for clicks on an element, the listener will successfully fire when the event occurs. If the page sets up a listener for custom events fired from the content script, they will be successfully received when the content script fires those events.
Functions:If the content script defines a new global function
foo()
(as you might try when setting up a new API). The page cannotsee or executefoo
, becausefoo
exists only in the content script's execution environment, not in the page's environment.
DOM 操作:如果内容脚本
<div>
向页面添加元素,这将按预期工作。内容脚本和页面都将看到新的<div>
.事件:如果一个内容脚本设置了一个事件监听器,例如,点击一个元素,当事件发生时监听器将成功触发。如果页面为从内容脚本触发的自定义事件设置了侦听器,则当内容脚本触发这些事件时,它们将被成功接收。
函数:如果内容脚本定义了一个新的全局函数
foo()
(就像您在设置新 API 时可能尝试的那样)。该页面无法查看或执行foo
,因为foo
仅存在于内容脚本的执行环境中,而不存在于页面环境中。
So, how can you set up a proper API? The answer comes in many steps:
那么,如何设置合适的 API?答案有很多步骤:
At a low-level, make your API event-based. The web page fires custom DOM events with
dispatchEvent
, and the content scripts listens for them withaddEventListener
, taking action when they are received. Here's a simple event-based storage API which a web page can use to have the extension to store data for it:content_script.js(in your extension):
// 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 });
Non-extension web page, using your event-based API:
function sendDataToExtension(key, value) { var dataObj = {"key":key, "value":value}; var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj}); document.dispatchEvent(storeEvent); } sendDataToExtension("hello", "world");
As you can see, the ordinary web page is firing events that the content script can see and react to, because they share the DOM. The events have data attached, added in the
CustomEvent
constructor. My example here is pitifully simple -- you can obviously do much more in your content script once it has the data from the page (most likely pass itto the background pagefor further processing).However, this is only half the battle. In my example above, the ordinary web page had to create
sendDataToExtension
itself. Creating and firing custom events is quite verbose (my code takes up 3 lines and is relatively brief). You don't want to force a site to write arcane event-firing code just to use your API. The solution is a bit of a nasty hack: append a<script>
tag to your shared DOM which adds the event-firing code to the main page's execution environment.Inside 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);
Any functions that are defined in
myapi.js
will become accessible to the main page. (If you are using"manifest_version":2
, you'll need to includemyapi.js
in your manifest's list ofweb_accessible_resources
).myapi.js:
function sendDataToExtension(key, value) { var dataObj = {"key":key, "value":value}; var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj}); document.dispatchEvent(storeEvent); }
Now the plain web pagecan simply do:
sendDataToExtension("hello", "world");
There is one further wrinkleto our API process: the
myapi.js
script will not be available exactly at load time. Instead, it will be loaded some time after page-load time. Therefore, the plain web page needs to know when it can safely call your API. You can solve this by havingmyapi.js
fire an "API ready" event, which your page listens for.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);
Plain web pageusing API:
document.addEventListener('customAPILoaded', function() { sendDataToExtension("hello", "world"); // all API interaction goes in here, now that the API is loaded... });
Another solution to the problem of script availability at load time is setting
run_at
property of content script in manifest to"document_start"
like this:manifest.json:
"content_scripts": [ { "matches": ["https://example.com/*"], "js": [ "myapi.js" ], "run_at": "document_start" } ],
Excerpt from docs:
In the case of "document_start", the files are injected after any files from css, but before any other DOM is constructed or any other script is run.
For some contentscripts that could be more appropriate and of less effort than having "API loaded" event.
In order to send results backto the page, you need to provide an asynchronous callback function. There is no way to synchronously return a result from your API, because event firing/listening is inherently asynchronous (i.e., your site-side API function terminates before the content script ever gets the event with the API request).
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(in your extension):
// 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); });
ordinary web page:
document.addEventListener('customAPILoaded', function() { getDataFromExtension("hello", function(val) { alert("extension says " + val); }); });
The
reqId
is necessary in case you have multiple requests out at once, so that they don't read the wrong responses.
在低级别,使您的 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
如果您一次有多个请求,这是必要的,这样他们就不会读取错误的响应。
And I think that's everything! So, not for the faint of heart, and possibly not worth it, when you consider that other extensions can also bind listeners to your events to eavesdrop on how a page is using your API. I only know all this because I made made a proof-of-concept cryptography API for a school project (and subsequently learned the major security pitfalls associated with it).
我认为这就是一切!因此,当您考虑到其他扩展也可以将侦听器绑定到您的事件以窃听页面如何使用您的 API 时,不适合胆小的人,而且可能不值得。我之所以知道这一切,是因为我为学校项目制作了一个概念验证密码学 API(随后了解了与之相关的主要安全陷阱)。
In sum:A content script can listen for custom events from an ordinary web page, and the script can also inject a script file with functions that makes it easier for web pages to fire those events. The content script can pass messages to a background page, which then stores, transforms, or transmits data from the message.
总而言之:内容脚本可以侦听来自普通网页的自定义事件,该脚本还可以注入带有函数的脚本文件,使网页更容易触发这些事件。内容脚本可以将消息传递到后台页面,然后后台页面存储、转换或传输来自消息的数据。