chrome运行脚本
A while ago, I set out to build my first Chrome extension. Having recently gotten into the world of web development and getting my hands dirty by building a React project, I felt I had the tools necessary to take on this new challenge.
前一段时间,我开始构建我的第一个Chrome扩展程序。 最近进入Web开发领域,并通过构建React项目使自己变得肮脏,我感到自己拥有应对这一新挑战所必需的工具。
While I wasn’t completely wrong to think this, it wouldn’t be the whole truth to say I didn’t have to rethink the way I approached designing my project.
虽然我并没有完全错误的想法,但是说我不必重新思考设计项目的方式并不是全部事实。
I realized this pretty early on into development. You see, when developing any sort of app, as our projects get bigger, we’ll tend to inevitably break it up into separate classes, functions, and eventually scripts. Nothing forces us to do this, but unless you want to end up with a remake of 1958's The Blob, it would be smart to do so.
我很早就意识到这一点。 您会看到,在开发任何类型的应用程序时,随着项目的扩大,我们将不可避免地将其分解为单独的类,函数和最终脚本。 没什么能迫使我们这样做的,但是除非您想对1958年的The Blob进行翻拍,否则这样做会很明智。
脚本互操作性的幼稚方法 (The Naive Approach to Script Interoperability)
In my case, the extension needed to do the following: Whenever a user makes any changes in the text field, its content needs to be parsed and displayed accordingly as entries in the table to its right. Then, as soon as the “Log entries!” button is clicked, these parsed entries are to be used to invoke changes on the web page.
以我为例,该扩展程序需要执行以下操作:每当用户在文本字段中进行任何更改时,都需要对其内容进行解析并作为相应项显示在右侧表中。 然后,只要“日志条目!” 单击按钮,这些解析的条目将用于调用网页上的更改。
To that end, I broke up my code’s functionality into the following scripts:
为此,我将代码的功能分解为以下脚本:
popup.js
: Contains the behavior of the pop-up and its components. For example, what happens when text is inserted into the text field or when a button is pressed.popup.js
:包含弹出窗口及其组件的行为。 例如,将文本插入文本字段或按下按钮时会发生什么。parser.js
: Contains functionality to parse text following certain rules and returns the parsed result in a specific format.parser.js
:包含用于按照某些规则解析文本并以特定格式返回解析结果的功能。crawler.js
: Contains functionality that utilizes data to crawl a web page in search of specific elements and make certain modifications.crawler.js
:包含利用数据对网页进行爬网以搜索特定元素并进行某些修改的功能。
There’s an obvious interdependence here. crawler.js
needs data presented to it in a certain format in order to be able to successfully crawl and modify the web page. This data is provided by parser.js
, which in turn receives its input from the pop-up’s text field, managed by popup.js
.
这里有一个明显的相互依存关系。 crawler.js
需要以某种格式提供给它的数据,以便能够成功抓取和修改网页。 此数据由parser.js
提供,而parser.js
则从由popup.js
管理的弹出窗口的文本字段中接收其输入。
If, like me, you were spoiled by the simplicity of using ES6 modules in React, your first notion might be to say, “Well, no problem. I’ll just export the relevant functions in parser.js
and crawler.js
and import them in popup.js
.”
如果像我一样,您因在React中使用ES6模块的简单性而被宠坏了,您的第一个想法可能是说:“好,没问题。 我将在parser.js
导出相关功能 和crawler.js
并将它们导入popup.js
。”
However, my then-vanilla ES5 JavaScript codebase had other ideas, and by the time I emerged bruised and bloodied from my attempt to integrate ES6 features into my project, I had already discovered the proper way of getting my extension’s scripts to talk to each other.
但是,那时我原始的ES5 JavaScript代码库还有其他想法,当我尝试将ES6功能集成到我的项目中时,受到挫折和流血打击时,我已经发现了使扩展脚本相互对话的正确方法。
Fun fact: On the road to ES6 integration, I did eventually make the leap to Parcel (which I can highly recommend to anyone getting started using bundlers after a brief incident with Webpack left me questioning my life choices). The use of a bundler was motivated partly by the need to easily reference external libraries.
有趣的事实:在进行ES6集成的过程中,我最终实现了Parcel的飞跃(我强烈建议在Webpack发生短暂事件后开始使用捆绑器的任何人都对我的生活选择提出疑问)。 使用捆绑程序的部分原因是需要轻松引用外部库。
Since Parcel comes preconfigured with Babel, I was then also able to use ES6 features such as import/export, which did enable that more familiar way of working with different files. Nevertheless, that isn’t the way communication is intended in Chrome extensions, as we’ll see shortly.
由于Parcel预先配置了Babel ,因此我还能够使用ES6功能(例如导入/导出),这确实启用了更熟悉的处理不同文件的方式。 但是,这不是我们在Chrome扩展程序中进行通讯的方式,我们很快就会看到。
内容和背景脚本 (Content and Background Scripts)
A Chrome extension will typically consist of various cohesive parts or components, each with a different set of responsibilities. In order for all these components to work together, they communicate via messaging.
Chrome扩展程序通常由各种具有凝聚力的部分或组件组成,每个部分都有不同的职责。 为了使所有这些组件一起工作,它们通过消息传递进行通信。
In our example, crawler.js
needs to interact with the web page and is thus declared as a so-called content script. Content scripts are those that need to be able to perform actions on web pages, such as DOM manipulations.
在我们的示例中, crawler.js
需要与网页进行交互,因此被声明为所谓的内容脚本。 内容脚本是那些需要能够在网页上执行操作的脚本,例如DOM操作。
On the other hand, parser.js
doesn’t need this, but it still needs to receive data from popup.js
and send it back. Thus, we’ll declare it as a background script.
另一方面, parser.js
不需要此,但仍需要从popup.js
接收数据 再寄回去因此,我们将其声明为后台脚本。
A background script, as the name implies, runs in the background. Its roles include listening and reacting to browser events (e.g. closing a tab, perform actions when the extension is (un-)installed), as well as sending and receiving messages.
顾名思义,后台脚本在后台运行。 它的作用包括侦听和响应浏览器事件(例如,关闭选项卡,(未安装)扩展程序时执行操作),以及发送和接收消息。
The declaration of content and background scripts is done in the extension’s manifest.json
.
内容和后台脚本的声明在扩展的manifest.json
。
{
...
"background": {
"scripts": [
"src/parser.js"
],
"persistent": false
},
"content_scripts": [
{
"matches": [
"*://*.sparkpeople.com/myspark/nutrition.asp"
],
"js": [
"src/crawler.js"
]
}
],
...
}
讯息传递101(Message Passing 101)
Now we know enough to finally get to the nitty-gritty.
现在我们知道了足够的知识,可以最终了解细节。
popup.js
, being the communication initiator here, will need to send out two messages. One whenever the text field is changed and another when the button is clicked. Depending on who the recipient is, it does this using one of two ways. If the recipient is a content script, chrome.tabs.sendMessage()
is used. Otherwise, it’s chrome.runtime.sendMessage()
.
popup.js
是这里的通信发起方,需要发送两条消息。 一种是在更改文本字段时,另一种是在单击按钮时。 根据接收者是谁,它使用两种方法之一来执行此操作。 如果收件人是内容脚本,则使用chrome.tabs.sendMessage()
。 否则为chrome.runtime.sendMessage()
。
非内容脚本通信 (Non-content script communication)
Let’s start with the second case. Here’s an example of what that might look like in popup.js
:
让我们从第二种情况开始。 这是popup.js
:
chrome.runtime.sendMessage({ msg: "Text field changed", data: textFieldContent }, (response) => {
// If this message's recipient sends a response it will be handled here
if (response) {
// do cool things with the response
// ...
}
});
Here, we’re assuming this piece of code gets executed in popup.js
whenever a change happens in the text field. As you can see, we’ve passed runtime.sendMessage()
two parameters: a required object and an optional callback. What the object should contain is entirely up to you, but in my case, I’ve included two properties. The first, msg
, contains a string identifier that is checked by the receiving end to determine how to handle the request. The second property,data
, simply contains the new content of the text field following the change.
在这里,我们假设这段代码在popup.js
执行 每当文本字段发生更改时。 如您所见,我们已经传递了runtime.sendMessage()
两个参数:一个必需的对象和一个可选的回调。 对象应包含的内容完全取决于您,但就我而言,我包括了两个属性。 第一个msg
包含一个字符串标识符,接收端检查该字符串标识符以确定如何处理该请求。 第二个属性data
只是包含更改后文本字段的新内容。
The callback function passed as the second argument to runtime.sendMessage()
must have a single parameter. This function handles the response sent by the recipient of this message.
作为第二个参数传递给runtime.sendMessage()
的回调函数必须具有一个参数。 此功能处理此消息的收件人发送的响应。
Note: The intended recipient of this message is parser.js
. However, as we’ll see shortly, any background script listening for onMessage
events will receive it. This is another reason why it’s useful to have a property such as msg
in the passed object. It acts as an identifier so that recipients can determine whether a message is intended for them.
注意:此消息的预期收件人是parser.js
。 但是,很快就会看到,任何监听onMessage
事件的后台脚本都会收到它。 这也是为什么在传递的对象中具有msg
的属性很有用的另一个原因。 它充当标识符,以便收件人可以确定邮件是否适合他们。
内容脚本交流 (Content script communication)
As mentioned before, when the recipient is a content script, we use tabs.sendMessage()
. Here’s what that could look like in popup.js
:
如前所述,当收件人是内容脚本时,我们使用tabs.sendMessage()
。 这就是popup.js
:
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.tabs.sendMessage(tabs[0].id, { msg: "Button clicked", data: parsedTexFieldContent }, (response) => {
if (response) {
// do cool things with the response
// ...
}
});
You’ll notice this time around that we don’t send the message straight away. With tabs.sendMessage()
, we need to know which tab to send the message to. To do that, we first call tabs.query()
, which retrieves all tabs that match the properties specified in the first argument. Since my extension pop-up only activates when I’m on a specific URL, I can simply get the active tab in the current window and be sure that it’s the one I need.
您会发现这次我们不会立即发送邮件。 使用tabs.sendMessage()
,我们需要知道将消息发送到哪个选项卡。 为此,我们首先调用tabs.query ()
,它检索与第一个参数中指定的属性匹配的所有选项卡。 由于我的扩展程序弹出窗口仅在使用特定URL时才激活,因此我可以简单地在当前窗口中获取活动标签,并确保它是我所需的标签。
Hint: To retrieve all tabs, pass an empty object as the first argument.
提示:要检索所有选项卡,请传递一个空对象作为第一个参数。
The retrieved tabs are passed to the callback specified in the second argument. This is where we send our actual message, which should now look familiar. The only difference is that with tabs.sendMessage()
, we need to pass the ID of the relevant tab. The rest follows the same structure as before.
检索到的选项卡将传递到第二个参数中指定的回调。 这是我们发送实际消息的地方,现在看起来应该很熟悉。 唯一的区别是,使用tabs.sendMessage()
,我们需要传递相关标签的ID。 其余部分采用与以前相同的结构。
接收和回复消息 (Receiving and responding to messages)
On the receiving end, it’s quite straightforward. There, we use chrome.runtime.onMessage.addListener()
. Essentially, what it does is add a listener to the onMessage
event, which gets fired whenever a message is sent using either of the sendMessage()
variations we’ve seen.
在接收端,这非常简单。 在那里,我们使用chrome.runtime.onMessage.addListener()
。 本质上,它的作用是在onMessage
事件中添加一个侦听器,该事件在使用我们所见过的sendMessage()
变体发送消息时都会被触发。
This method takes a callback function as its single argument, which gets called when the event is fired (i.e. a message is received). That callback, in turn, takes three arguments: the content of the message, its sender, and a function that is called if a response is to be sent back. This function takes a single argument of type object. That was verbose. Let’s look at some code.
该方法将回调函数作为其单个参数,该事件在触发事件(即收到消息)时被调用。 反过来,该回调函数接受三个参数:消息的内容,消息的发送者以及如果要发送回响应则调用的函数。 此函数采用类型为object的单个参数。 太冗长了。 让我们看一些代码。
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if(request) {
if (request.msg == "Text field changed") {
// do cool things with the request then send response
// ...
sendResponse({ sender: "parser.js", data: parsedTextFieldContent }); // This response is sent to the message's sender
}
}
});
奖励:内容脚本之间的通信(Bonus: Communication Between Content Scripts)
So far, so good. But what if we had not just one content script, as was the case here with crawler.js
, but two that wanted to communicate? To continue with our running example, say we broke up crawler.js
into two separate content scripts: finder.js
and filler.js
. As the names imply, the former searches for certain elements on the webpage, while the latter fills those elements with content.
到现在为止还挺好。 但是,如果我们不仅有一个内容脚本(如crawler.js
,又有两个想要交流的脚本该怎么办? 为了继续我们的运行示例,假设我们将crawler.js
分为两个单独的内容脚本: finder.js
和filler.js
。 顾名思义,前者在网页上搜索某些元素,而后者则用内容填充这些元素。
finder.js
wants to be able to send the elements it finds to filler.js
. “Well, no big deal,” I hear you saying. We’ll just use tabs.sendMessage()
and onMessage.addListener()
like before. As much as I hate to be the bearer of bad news, not quite. As it turns out, content scripts can’t communicate directly. This actually had me scratching my head for a while. Fortunately, the solution is simple.
finder.js
希望能够将找到的元素发送到filler.js
“嗯,没什么大不了的,”我听到你说。 我们将像以前一样使用tabs.sendMessage()
和onMessage.addListener()
。 尽管我不愿成为坏消息的承担者,但事实并非如此。 事实证明,内容脚本无法直接通信。 这实际上让我挠了一下头。 幸运的是,解决方案很简单。
Fun fact: In case you’re wondering why I even ran into this problem since I only have one content script, at some point, I unnecessarily had popup.js
registered as a content script too and consequently its messages weren’t reaching crawler.js
using the direct path of communication. I’ve since removed this error, but the lesson learned remains.
有趣的事实:如果您想知道为什么我什至遇到这个问题,因为我只有一个内容脚本,在某个时候,我不必要地也将popup.js
注册为内容脚本,因此它的消息没有到达crawler.js
使用直接通信路径。 此后,我已删除了此错误,但所汲取的教训仍然存在。
All we need to do is have a background script act as a middleman in this exchange. This then looks as follows. Don’t be intimidated by the size. I’ve essentially jammed code from three scripts into one gist for display purposes.
我们需要做的就是让后台脚本在此交换中充当中间人。 然后如下所示。 不要被大小吓倒。 实际上,我出于显示目的将三个脚本中的代码塞进了一个要点。
// In finder.js, the initiating content script. Message will be received by the background script facilitating the exchange.
chrome.runtime.sendMessage({ msg: "Fill elements", data: DOMElements }, (response) => {
// response will be received from the background script, but originally sent by filler.js
if (response) {
// do cool things with the response
// ...
}
});
// In the background script. Message being relayed to filler.js, the receiving content script.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.msg == "Fill elements") {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
// relay finder.js's message to filler.js
chrome.tabs.sendMessage(tabs[0].id, request, (response) => {
if (response) {
if (response.data) {
// relay filler.js's response to finder.js
sendResponse({ data: response.data });
}
}
});
});
}
return true; // Required to keep message port open
});
// In filler.js, the receiving content script. Message will be received from the background script facilitating the exchange.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if(request) {
if (request.msg == "Fill elements") {
// do cool things with the request then send response
// ...
sendResponse({ data: result }); // This response is sent to the message's sender, here the background script
}
}
});
Essentially, there’s nothing new here except a slight logistical change. Instead of direct point-to-point communication, we’re using a background script to relay messages between the communicating parties (i.e. the content scripts).
本质上,这里除了物流方面的细微变化外,没有其他新内容。 代替直接的点对点通信,我们使用后台脚本在通信方之间中继消息(即,内容脚本)。
One thing to note here is that we’re returning true
in the background script’s addListener()
. Without going too much into detail, this keeps the communication channel at the background script open to allow for filler.js
’s response to make it through to finder.js
. For more on that, take a look at the description provided in Chrome’s documentation for the sendResponse
parameter of runtime.onMessage.addListener()
.
这里要注意的一件事是,我们在后台脚本的addListener()
返回true
。 无需过多讨论,这将使后台脚本的通信通道保持打开状态,以允许filler.js
的响应将其传递到finder.js
。 有关更多信息,请查看Chrome文档中提供的runtime.onMessage.addListener()
的sendResponse
参数的描述。
结论 (Conclusion)
Thanks for sticking around! Chrome extensions can be quite idiosyncratic and there’s often not much to go on on the internet when you’re stuck. So I hope you found some of this useful.
感谢您的支持! Chrome扩展程序可能非常特殊,当您陷入困境时,Internet上通常没有太多活动。 因此,我希望您发现其中一些有用的东西。
As always, I’d be happy to hear your thoughts and answer any questions you may have.
与往常一样,我很高兴听到您的想法并回答您可能遇到的任何问题。
chrome运行脚本