Deze vertaling is niet volledig. Help dit artikel te vertalen vanuit het Engels.
Many add-ons need to access and modify the content of web pages. But the main add-on code doesn't get direct access to web content. Instead, SDK add-ons need to factor the code that gets access to web content into separate scripts that are called content scripts.
Content scripts can be one of the more confusing aspects of working with the SDK, but you're very likely to have to use them. There are five basic principles:
- the add-on's main code, including "main.js" and other modules in "lib", can use the SDK high-level and low-level APIs, but can't access web content directly
- content scripts can't use the SDK's APIs but can access web content
- SDK APIs that use content scripts, like page-mod and tabs, provide functions that enable the add-on's main code to load content scripts into web pages
- content scripts can be loaded in as strings, but are more often stored as separate files under the add-on's "data" directory
- a message-passing API allows the main code and content scripts to communicate with each other
This complete add-on illustrates all of these principles. Its "main.js" attaches a content script to the current tab using the tabs module. In this case the content script is passed in as a string. The content script simply replaces the content of the page:
// main.js var tabs = require("sdk/tabs"); var contentScriptString = 'document.body.innerHTML = "<h1>this page has been eaten</h1>";' tabs.activeTab.attach({ contentScript: contentScriptString });
The following high-level SDK modules can use content scripts to modify web pages:
- page-mod: enables you to attach content scripts to web pages that match a specific URL pattern.
- tabs: exports a
Tab
object for working with a browser tab. TheTab
object includes anattach()
function to attach a content script to the tab. - page-worker: lets you retrieve a web page without displaying it. You can attach content scripts to the page, to access and manipulate the page's DOM.
- context-menu: use a content script to interact with the page in which the menu is invoked.
Additionally, some SDK user interface components - panel, sidebar, frame - are specified using HTML, and use separate scripts to interact with this content. In many ways these are like content scripts, but they're not the focus of this article. To learn about how to interact with the content for a given user interface module, please see the module-specific documentation: panel, sidebar, frame.
Almost all the examples presented in this guide are available as complete, but minimal, add-ons in the addon-sdk-content-scripts repository on GitHub.
Loading content scripts
You can load a single script by assigning a string to either the contentScript
or the contentScriptFile
option. The contentScript
option treats the string itself as a script:
// main.js var pageMod = require("sdk/page-mod"); var contentScriptValue = 'document.body.innerHTML = ' + ' "<h1>Page matches ruleset</h1>";'; pageMod.PageMod({ include: "*.mozilla.org", contentScript: contentScriptValue });
The contentScriptFile
option treats the string as a resource:// URL pointing to a script file stored in your add-on's data
directory.
This add-on supplies a URL pointing to the file "content-script.js", located in the data
subdirectory under the add-on's root directory:
// main.js var data = require("sdk/self").data; var pageMod = require("sdk/page-mod"); pageMod.PageMod({ include: "*.mozilla.org", contentScriptFile: data.url("content-script.js") });
// content-script.js document.body.innerHTML = "<h1>Page matches ruleset</h1>";
Unless your content script is extremely simple and consists only of a static string, don't use contentScript
: if you do, you may have problems getting your add-on approved on AMO.
Instead, keep the script in a separate file and load it using contentScriptFile
. This makes your code easier to maintain, secure, debug and review.
You can load multiple scripts by passing an array of strings to either contentScript
or contentScriptFile
:
// main.js var tabs = require("sdk/tabs"); tabs.on('ready', function(tab) { tab.attach({ contentScript: ['document.body.style.border = "5px solid red";', 'window.alert("hi");'] }); });
// main.js var data = require("sdk/self").data; var pageMod = require("sdk/page-mod"); pageMod.PageMod({ include: "*.mozilla.org", contentScriptFile: [data.url("jquery.min.js"), data.url("my-content-script.js")] });
If you do this, the scripts can interact directly with each other, just like scripts loaded by the same web page.
You can also use contentScript
and contentScriptFile
together. If you do this, scripts specified using contentScriptFile
are loaded before those specified using contentScript
. This enables you to load a JavaScript library like jQuery by URL, then pass in a simple script inline that can use jQuery:
// main.js var data = require("sdk/self").data; var pageMod = require("sdk/page-mod"); var contentScriptString = '$("body").html("<h1>Page matches ruleset</h1>");'; pageMod.PageMod({ include: "*.mozilla.org", contentScript: contentScriptString, contentScriptFile: data.url("jquery.js") });
Unless your content script is extremely simple and consists only of a static string, don't use contentScript
: if you do, you may have problems getting your add-on approved on AMO.
Instead, keep the script in a separate file and load it using contentScriptFile
. This makes your code easier to maintain, secure, debug and review.
Controlling when to attach the script
The contentScriptWhen
option specifies when the content script(s) should be loaded. It takes one of:
"start"
: load the scripts immediately after the document element for the page is inserted into the DOM. At this point the DOM content hasn't been loaded yet, so the script won't be able to interact with it."ready"
: load the scripts after the DOM for the page has been loaded: that is, at the point the DOMContentLoaded event fires. At this point, content scripts are able to interact with the DOM content, but externally-referenced stylesheets and images may not have finished loading."end"
: load the scripts after all content (DOM, JS, CSS, images) for the page has been loaded, at the time the window.onload event fires.
The default value is "end"
.
Note that tab.attach()
doesn't accept contentScriptWhen, because it's generally called after the page has loaded.
Passing configuration options
The contentScriptOptions
is a JSON object that is exposed to content scripts as a read-only value under the self.options
property:
// main.js var tabs = require("sdk/tabs"); tabs.on('ready', function(tab) { tab.attach({ contentScript: 'window.alert(self.options.message);', contentScriptOptions: {"message" : "hello world"} }); });
Any kind of jsonable value (object, array, string, etc.) can be used here.
Accessing the DOM
Content scripts can access the DOM of a page, of course, just like any scripts that the page has loaded (page scripts). But content scripts are insulated from page scripts:
- content scripts don't see any JavaScript objects added to the page by page scripts
- if a page script has redefined the behavior of some DOM object, the content script sees the original behavior.
The same applies in reverse: page scripts can't see JavaScript objects added by content scripts.
For example, consider a page that adds a variable foo
to the window
object using a page script:
<!DOCTYPE html"> <html> <head> <script> window.foo = "hello from page script" </script> </head> </html>
Another script loaded into the page after this script will be able to access foo
. But a content script will not:
// main.js var tabs = require("sdk/tabs"); var mod = require("sdk/page-mod"); var self = require("sdk/self"); var pageUrl = self.data.url("page.html") var pageMod = mod.PageMod({ include: pageUrl, contentScript: "console.log(window.foo);" }) tabs.open(pageUrl);
console.log: my-addon: null
There are good reasons for this insulation. First, it means that content scripts don't leak objects to web pages, potentially opening up security holes. Second, it means that content scripts can create objects without worrying about whether they might clash with objects added by page scripts.
This insulation means that, for example, if a web page loads the jQuery library, then the content script won't be able to see the jQuery
object added by the library - but the content script can add its own jQuery
object, and it won't clash with the page script's version.
Interacting with page scripts
Usually the insulation between content scripts and page scripts is what you want. But sometimes you might want to interact with page scripts: you might want to share objects between content scripts and page scripts or to send messages between them. If you need to do this, read about interacting with page scripts.
DOM access quirks
Most of the time DOM access for content scripts is just like DOM access for page scripts, but there are a few differences. If you're having trouble working with the DOM in a content script it's worthwhile reading about the differences.
Communicating with the add-on
To enable add-on scripts and content scripts to communicate with each other, each end of the conversation has access to a port
object.
- to send messages from one side to the other, use
port.emit()
- to receive messages sent from the other side, use
port.on()
Messages are asynchronous: that is, the sender does not wait for a reply from the recipient but just emits the message and continues processing.
Here's a simple add-on that sends a message to a content script using port
:
// main.js var tabs = require("sdk/tabs"); var self = require("sdk/self"); tabs.on("ready", function(tab) { worker = tab.attach({ contentScriptFile: self.data.url("content-script.js") }); worker.port.emit("alert", "Message from the add-on"); }); tabs.open("https://www.mozilla.org");
// content-script.js self.port.on('alert', function(message) { window.alert(message); });
The context-menu module doesn't use the communication model described here. To learn about communicating with content scripts loaded using context-menu, see the context-menu documentation.
Accessing port
in the content script
In the content script the port
object is available as a property of the global self
object. So to emit a message from a content script:
self.port.emit("myContentScriptMessage", myContentScriptMessagePayload);
To receive a message from the add-on code:
self.port.on("myAddonMessage", function(myAddonMessagePayload) { // Handle the message });
Note that the global self
object is completely different from the self
module, which provides an API for an add-on to access its data files and ID.
Accessing port
in the add-on script
In the add-on code, the channel of communication between the add-on and a particular content script context is encapsulated by the worker
object. So the port
object for communicating with a content script is a property of the corresponding worker
object.
However, the worker is not exposed to add-on code in quite the same way in all modules.
From page-worker
The page-worker
object integrates the worker API directly. So to receive messages from a content script associated with a page-worker
you use pageWorker.port.on()
:
// main.js var pageWorkers = require("sdk/page-worker"); var self = require("sdk/self"); var pageWorker = require("sdk/page-worker").Page({ contentScriptFile: self.data.url("content-script.js"), contentURL: "https://en.wikipedia.org/wiki/Internet" }); pageWorker.port.on("first-para", function(firstPara) { console.log(firstPara); });
To emit user-defined messages from your add-on you can just call pageWorker.port.emit()
:
// main.js var pageWorkers = require("sdk/page-worker"); var self = require("sdk/self"); pageWorker = require("sdk/page-worker").Page({ contentScriptFile: self.data.url("content-script.js"), contentURL: "https://en.wikipedia.org/wiki/Internet" }); pageWorker.port.on("first-para", function(firstPara) { console.log(firstPara); }); pageWorker.port.emit("get-first-para");
// content-script.js self.port.on("get-first-para", getFirstPara); function getFirstPara() { var paras = document.getElementsByTagName("p"); if (paras.length > 0) { var firstPara = paras[0].textContent; self.port.emit("first-para", firstPara); } }
From page-mod
A single page-mod
object might attach its scripts to multiple pages, each with its own context in which the content scripts are executing, so it needs a separate channel (worker) for each page.
So page-mod
does not integrate the worker API directly. Instead, each time a content script is attached to a page, the page-mod emits an attach
event, whose listener is passed the worker for that context. By supplying a listener to attach
you can access the port
object for content scripts attached to that page by this page-mod:
// main.js var pageMods = require("sdk/page-mod"); var self = require("sdk/self"); var pageMod = pageMods.PageMod({ include: ['*'], contentScriptFile: self.data.url("content-script.js"), onAttach: startListening }); function startListening(worker) { worker.port.on('click', function(html) { worker.port.emit('warning', 'Do not click this again'); }); }
// content-script.js window.addEventListener('click', function(event) { self.port.emit('click', event.target.toString()); event.stopPropagation(); event.preventDefault(); }, false); self.port.on('warning', function(message) { window.alert(message); });
In the add-on above there are two messages:
click
is sent from the page-mod to the add-on, when the user clicks an element in the pagewarning
sends a silly string back to the page-mod
From Tab.attach()
The Tab.attach()
method returns the worker you can use to communicate with the content script(s) you attached.
This add-on adds a button to Firefox: when the user clicks the button, the add-on attaches a content script to the active tab, sends the content script a message called "my-addon-message", and listens for a response called "my-script-response":
//main.js var tabs = require("sdk/tabs"); var buttons = require("sdk/ui/button/action"); var self = require("sdk/self"); buttons.ActionButton({ id: "attach-script", label: "Attach the script", icon: "./icon-16.png", onClick: attachScript }); function attachScript() { var worker = tabs.activeTab.attach({ contentScriptFile: self.data.url("content-script.js") }); worker.port.on("my-script-response", function(response) { console.log(response); }); worker.port.emit("my-addon-message", "Message from the add-on"); }
// content-script.js self.port.on("my-addon-message", handleMessage); function handleMessage(message) { alert(message); self.port.emit("my-script-response", "Response from content script"); }
The port API
port.emit()
The port.emit()
function sends a message from one side to the other.
It may be called with any number of parameters, but is most likely to be called with a name for the message and an optional payload. The payload can be any value that is serializable to JSON.
From the content script to the main add-on code:
// content-script.js var myMessagePayload = "some data"; self.port.emit("myMessage", myMessagePayload);
From the main add-on code to the content script:
// main.js var myMessagePayload = "some data"; worker.port.emit("myMessage", myMessagePayload);
port.on()
The port.on()
function registers a function as a listener for a particular message sent from the other side using port.emit()
.
It takes two parameters: the name of the message and a function to handle it.
In a content script, to listen for "myMessage" sent from the main add-on code:
// content-script.js self.port.on("myMessage", function handleMyMessage(myMessagePayload) { // Handle the message });
In the main add-on code, to listen for "myMessage" sent from a a content script:
// main.js worker.port.on("myMessage", function handleMyMessage(myMessagePayload) { // Handle the message });
port.removeListener()
You can use port.on()
to listen for messages. To stop listening for a particular message, use port.removeListener()
. This takes the same two parameters as port.on()
: the name of the message, and the name of the listener function.
For example, here's an add-on that creates a page-worker and a button. The page-worker loads https://en.wikipedia.org/wiki/Chalk alongside a content script. The button sends the content script a message called "get-first-para" when it is clicked:
// main.js pageWorker = require("sdk/page-worker").Page({ contentScriptFile: require("sdk/self").data.url("listener.js"), contentURL: "https://en.wikipedia.org/wiki/Chalk" }); require("sdk/ui/button/action").ActionButton({ id: "get-first-para", label: "get-first-para", icon: "./icon-16.png", onClick: function() { console.log("sending 'get-first-para'"); pageWorker.port.emit("get-first-para"); } });
The content script listens for "get-first-para". When it receives this message, the script logs the first paragraph of the document and then calls removeListener()
to stop listening.
// content-script.js function getFirstParagraph() { var paras = document.getElementsByTagName('p'); console.log(paras[0].textContent); self.port.removeListener("get-first-para", getFirstParagraph); } self.port.on("get-first-para", getFirstParagraph);
The result is that the paragraph is only logged the first time the button is clicked.
Due to bug 816272 the page-mod
's removeListener()
function does not prevent the listener from receiving messages that are already queued. This means that if "main.js" sends the message twice in successive lines, and the listener stops listening as soon as it receives the first message, then the listener will still receive the second message.
port.once()
Often you'll want to receive a message just once, then stop listening. The port
object offers a shortcut to do this: the once()
method.
This example rewrites the content script in the port.removeListener()
example so that it uses once()
:
// content-script.js function getFirstParagraph() { var paras = document.getElementsByTagName('p'); console.log(paras[0].textContent); } self.port.once("get-first-para", getFirstParagraph);
JSON-serializable values
The payload for a message can be any JSON-serializable value. When messages are sent their payloads are automatically serialized, and when messages are received their payloads are automatically deserialized, so you don't need to worry about serialization.
However, you do have to ensure that the payload can be serialized to JSON. This means that it needs to be a string, number, boolean, null, array of JSON-serializable values, or an object whose property values are themselves JSON-serializable. This means you can't send functions, and if the object contains methods they won't be encoded.
For example, to include an array of strings in the payload:
// main.js var pageMods = require("sdk/page-mod"); var self = require("sdk/self"); var pageMod = pageMods.PageMod({ include: ['*'], contentScriptFile: self.data.url("content-script.js"), onAttach: setupListener }); function setupListener(worker) { worker.port.on('loaded', function(pageInfo) { console.log(pageInfo[0]); console.log(pageInfo[1]); }); }
//content-script.js self.port.emit('loaded', [ document.location.toString(), document.title ]);
The postMessage API
Before the port
object was added, add-on code and content scripts communicated using a different API:
- the content script called
self.postMessage()
to send andself.on()
to receive - the add-on script called
worker.postMessage()
to send andworker.on()
to receive
The API is still available and documented, but there's no reason to use it instead of the port
API described here. The exception is the context-menu module, which still uses postMessage.
Content script to content script
Content scripts can only communicate with each other directly if they have been loaded into the same context. For example, if a single call to Tab.attach()
attaches two content scripts, then they can see each other directly, just as page scripts loaded by the same page can. But if you call Tab.attach()
twice, attaching a content script each time, then these content scripts can't communicate with each other. You must then relay messages through the main add-on code using the port API.
Cross-domain content scripts
By default, content scripts don't have any cross-domain privileges. In particular, they can't access content hosted in an iframe
, if that content is served from a different domain, or make cross-domain XMLHttpRequests.
However, you can enable these features for specific domains by adding them to your add-on's package.json under the "cross-domain-content"
key, which itself lives under the "permissions"
key. See the article on cross-domain content scripts.