MDN will be in maintenance mode on Wednesday September 20th, starting at 10 AM Pacific / 5 PM UTC, for about 1 hour.

We are planning to deprecate the use by Firefox add-ons of the techniques described in this document.

Don't use these techniques to develop new add-ons. Use WebExtensions instead.

If you maintain an add-on which uses the techniques described here, consider migrating it to use WebExtensions instead.

Add-ons developed using these techniques might not work with multiprocess Firefox (e10s), which is already the default in Firefox Nightly and Firefox Developer Edition, and will soon be the default in Beta and Release versions of Firefox. We have documentation on making your add-ons multiprocess-compatible, but it will be more future-proof for you to migrate to WebExtensions.

A wiki page containing resources, migration paths, office hours, and more, is available to help developers transition to the new technologies.

Многим дополнениям (add-on) необходим доступ к веб-страницам и возможность их изменения. Но основной код дополнения не имеет прямого доступа к веб-содержимому. Взамен, SDK-дополнений необходим способ в коде, который даст доступ к веб-содержимому в отдельных скриптах, которые называются content scripts (скрипты содержимого). Эта страница описывает как разрабатывать и реализовывать content scripts.

Скрипты content scripts, вероятно, один из наиболее сбивающих с толку аспектов при работе с SDK, но вам они скорее всего будут нужны. Существуют пять основных принципов:

  • расширения основного кода, включая "main.js" и другие модули в "lib", могут использовать SDK верхнего-уровня и нижнего-уровня API, но не имеют доступа к веб-содержимому напрямую;
  • скрипты content scripts не могут использовать API в SDK (нет доступа к глобальным exports, require) но есть доступ к веб-содержимому;
  • API в SDK которые используют content scripts, например page-mod и tabs, предоставляют функции, которые позволяют коду расширения загружать скрипты содержимого в веб-страницы;
  • скрипты content scripts могут быть загружены как строки, но чаще они хранятся как отдельные файлы в папке "data". jpm не создаёт каталог "data" по умолчанию, поэтому вы должны создать его и положить туда ваши скрипты;
  • API передачи сообщений позволяет основному коду и скриптам content scripts взаимодействовать друг с другом.

Следующее дополнение (полностью завершённое) показывает эти принципы. "main.js" прикрепляет content scripts к текущей вкладке, используя модуль tabs. В этом случае, content scripts передаётся, как строка. Скрипт content scripts просто заменяет содержимое страницы:

// main.js
var tabs = require("sdk/tabs");
var contentScriptString = 'document.body.innerHTML = "<h1>this page has been eaten</h1>";'

tabs.activeTab.attach({
  contentScript: contentScriptString
});

Следующие высокоуровневые SDK-модули, могут использовать скрипты content scripts для изменения веб-страниц:

  • page-mod: позволяет вам прикреплять content scripts к веб-страницам, которые соответствуют заданному URL шаблону.
  • tabs: экспортирует объект Tab для работы с вкладкой браузера. Tab-объект включает функцию attach(), которая позволяет прикрепить content scripts ко вкладке.
  • page-worker: позволяет вам получить страницу, без отображения её. Вы можете прикрепить content scripts к странице, чтобы иметь доступ и возможность изменять DOM страницы.
  • context-menu: использует content scripts для взаимодействия со страницей, в которой вызывается меню.

В дополнение к этому, некоторые SDK компоненты пользовательского интерфейса - panel, sidebar, frame - заданы в помощью HTML, и необходимо использовать отдельные скрипты для взаимодействия с их контентом. В большинстве случаев они похожи на скрипты content scripts, но в данной статье они не описываются. Для изучения способов взаимодействия с данными модулями пользовательского интерфейса обратитесь к документации: panel, sidebar, frame.

Почти все примеры дополнений, представленных в этом руководстве, доступны в полнофункциональном, но минимально необходимом, виде. На GitHub: addon-sdk-content-scripts repository.

Загрузка content scripts

Вы можете загрузить одиночный скрипт посредством задания строкового атрибута contentScript или contentScriptFile. Атрибут contentScript определяет строковое значение как сам скрипт:

// 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
});

Атрибут contentScriptFile определяет строковое значение как путь к ресурсу://URL-путь к скрипту, который находится в подкаталоге вашего дополнения. jpm не создаёт папку "data" по умолчанию, поэтому вы должны добавить её и положить внутрь файл content scripts.

Следующее дополнение использует URL для ссылки на файл "content-script.js", находящийся в папке data в корне дополнения.

// 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>";

Начиная с Firefox 34 и далее , вы можете использовать "./content-script.js" как синоним для self.data.url("content-script.js"). Поэтому можно переписать код main.js, указанный выше, следующим образом:

var pageMod = require("sdk/page-mod");

pageMod.PageMod({
  include: "*.mozilla.org",
  contentScriptFile: "./content-script.js"
});

Настоятельно рекоммендуется использовать  contentScript только для очень простых скриптов или статичных строк: если это не так, то могут возникнуть проблемы с принятием Вашего дополнения на AMO (addons.mozilla.org).

Содержите ваши скрипты в отдельном файле и загружайте их, используя contentScriptFile. Это сделает ваш код проще в поддержке, отладке, безопаснее, удобочитаемее.

Для любого из параметров contentScript или 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")]
});

Если так сделать, то скрипты смогут взаимодействовать друг с другом, как скрипты загружаемые на одной web-странице.

Можно использовать параметры contentScript and contentScriptFile одновременно. В таком случае скрипты, загружаемые contentScriptFile  загрузятся до contentScript. Это похволяет загружать библиотеки JavaScript, такие как jQuery по URL, а затем использвать их в простом скрипте, загруженном через contentScript:

// 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")
});

Настоятельно рекоммендуется использовать  contentScript только для очень простых скриптов или статичных строк: если это не так, то могут возникнуть проблемы с принятием Вашего дополнения на AMO (addons.mozilla.org).

Содержите ваши скрипты в отдельном файле и загружайте их, используя contentScriptFile. Это сделает ваш код проще в поддержке, отладке, безопаснее, удобочитаемее.

Определение момента (времени) подключения скрипта

Опция contentScriptWhen определяет момент, когда content script должен быть загружен. Возможные варианты:

  • "start": загрузить сразу после того, как элемент документа страницы вставляется в DOM. В таком случае DOM-контент ещё пока не загружен, поэтому скрипт не может работать с ним.
  • "ready": загрузить скрипт после того, как DOM страницы загружен: то есть в точке активации событий DOMContentLoaded. В этот момент content scripts уже могут взаимодействовать с DOM-контентом, но загрузка внешних CSS и картинок ещё могла не завершиться.
  • "end": загрузить скрипт после завершения загрузки всего контента (DOM, JS, CSS, картинки), в то время, как активируется событие window.onload event.

Значение по умолчанию "end".

Обратите внимание, что tab.attach() не имеет параметра contentScriptWhen, потому что он обычно вызывается после загрузки страницы.

Передача конфигурационных опций

Атрибут contentScriptOptions это JSON-объект, который используется скриптом как read-only значение доступное через свойство self.options:

// main.js

var tabs = require("sdk/tabs");

tabs.on('ready', function(tab) {
  tab.attach({
      contentScript: 'window.alert(self.options.message);',
      contentScriptOptions: {"message" : "hello world"}
  });
});

Могут быть использованы любые варианты JSON-объектов (object, array, string, etc.).

Доступ к DOM

Скрипты content scripts могут иметь доступ к DOM страницы, конечно, только те скрипты, которые уже загрузились на странице. При этом скрипты content scripts изолированы от скриптов web-страницы:

  • content scripts не видят объектов JavaScript, добавленных скриптами web-страницы.
  • Если скриты web-страницы переопределят поведения каких-либо DOM-объектов, то скрипты content script обнаружат исходное поведение.

То же происходит в обратную сторону: скрипты web-страницы не увидят объектов JavaScript, добавленных скриптами content scripts.

Например, рассмотрим страницу, где скрипты web-страницы создают переменную foo в объекте window:

<!DOCTYPE html">
<html>
  <head>
    <script>
    window.foo = "hello from page script"
    </script>
  </head>
</html>

Другой скрипт (но тоже page-script), загруженный на страницу после этого скрипта (указанного выше), будет иметь доступ к foo. Но скрипт content script нет:

// 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

Есть веские причины для изоляции. Во-первых, из content script не утекают объекты в web-страницу, что потенциально является дырой в безопасности. Во-вторых, content scripts могут не беспокоиться о пересечении объектов с объектами, созданных скриптами web-страницы.

Такая изоляция необходима, например, в случае, если web-страница загружает библиотеку jQuery, но content script не увидит объектов, созданных этой библиотекой. В этом случае content script может добавить свою собственный jQuery-объект, который не пересечётся со страничным объектом.

Взаимодействие со скриптами web-страницы

Обычно изоляция content scripts и page scripts (скрипты web-страницы) необходима. Но иногда вы захотите наладить такое взаимодействие: вы можете захотеть иметь общие объекты между content scripts и page scripts или передевать между ними сообщения. Если появится такая необходимость, то прочтите о взаимодействии со скриптами web-страницы (interacting with page scripts).

Прослушивание событий

Вы можете прослушивать события DOM в скриптах content scripts также, как в обычных скриптах web-страницы. Но есть два важных отличия:

Первое. Если вы определите слушатель через передачу строки в функцию setAttribute(), то слушатель будет вызываться в контексте web-страницы, поэтому он не будет иметь доступа ни к каким переменным, определённым в content script.

Например, при выполнении в данном content script появится ошибка "theMessage is not defined":

var theMessage = "Hello from content script!";
anElement.setAttribute("onclick", "alert(theMessage);");

Второе. Если вы определите слушатель напрямую через GlobalEventHandlers, например на onclick, то такое определение может быть переопределено на web-странице. Например, здесь представлен add-on, который пытается добавить обработчик click-события при помощи присвоения window.onclick:

var myScript = "window.onclick = function() {" +
               "  console.log('unsafewindow.onclick: ' + window.document.title);" +
               "}";

require("sdk/page-mod").PageMod({
  include: "*",
  contentScript: myScript,
  contentScriptWhen: "start"
});

Это всё будет прекрасно работать на многих страницах, но не сработает там, где также присваивается onclick:

<html>
  <head>
  </head>
  <body>
    <script>
    window.onclick = function() {
      window.alert("it's my click now!");
    }
    </script>
  </body>
</html>

По этим причинам, лучший вариант для добавления слушалелей это использование addEventListener(), определяющем функцию:

var theMessage = "Hello from content script!";

anElement.onclick = function() {
  alert(theMessage);
};

anotherElement.addEventListener("click", function() {
  alert(theMessage);
});

Взаимодействие с скриптом дополнения (add-on)

Для организации взаимодействия друг с другом скрипта дополнения (add-on script) и скрипта content script нужно обоим дать доступ к объекту port.

  • для отправки сообщений используется port.emit()
  • для получения сообщений - port.on()

Сообщения асинхронны: то есть, отправитель не ждёт ответа от получателя, а только отправляет сообщение и продолжает работать дальше.

Вот пример простого дополнения, которое отправляет сообщение скрипту content script, используя port:

// main.js

var tabs = require("sdk/tabs");
var self = require("sdk/self");

tabs.on("ready", function(tab) {
  var worker = tab.attach({
    contentScriptFile: self.data.url("content-script.js")
  });
  worker.port.emit("alert", "Message from the add-on");
});

tabs.open("http://www.mozilla.org");
// content-script.js

self.port.on("alert", function(message) {
  window.alert(message);
});

Модуль context-menu не использует данную модель коммуникации. Для изучения варианта взаимодействия скриптов content scripts, загруженных с использованием context-menu, смотрите context-menu documentation

Доступ к порту в content script

В скрипте content script объект port доступен через свойство глобального объекта self.  Чтобы послать сообщение из content script:

self.port.emit("myContentScriptMessage", myContentScriptMessagePayload);

Чтобы получить сообщение из кода дополнения:

self.port.on("myAddonMessage", function(myAddonMessagePayload) {
  // Handle the message
});

Учтите, что глобальный объект self совершенно отличается от модуля self module, предоставляющим API дополнению для доступа к его файлам и ID.

Доступ к порту в скрипте дополнения (add-on script)

В коде дополнения канал взаимодействия между дополнением и конкретным content script инкапсулируется посредством объекта worker. Поэтому объект port для для связи со скриптом content script это свойство связанного worker.

Тем не менее, объект worker не расширен на код дополнения так же, как в других модулях.

Сообщения из page-worker

Объект page-worker интегрирует в себе worker API. Поэтому для получения сообщений от скрипта content script, ассоциированного с page-worker нужно использовать pageWorker.port.on():

// main.js

var self = require("sdk/self");

var pageWorker = require("sdk/page-worker").Page({
  contentScriptFile: self.data.url("content-script.js"),
  contentURL: "http://en.wikipedia.org/wiki/Internet"
});

pageWorker.port.on("first-para", function(firstPara) {
  console.log(firstPara);
});

Для отправки пользовательских сообщений их дополнения нужно вызвать pageWorker.port.emit():

// main.js

var self = require("sdk/self");

var pageWorker = require("sdk/page-worker").Page({
  contentScriptFile: self.data.url("content-script.js"),
  contentURL: "http://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);
  }
}

Сообщения из page-mod

Один объект page-mod может привязать свои скрипты к нескольким страницам, каждая из них со своим контекстом, в котором запускаются content scripts. Поэтому для каждой страницы необходим отдельный канал (worker) связи.

page-mod не интегрирует в себе worker API напрямую. Вместо этого, когда скрипт content script привязывается к странице, page-mod бросает событие attach тому слушателю, который связан с worker. Создавая слушатель для события attach, вы можете получить доступ через объект port к тому скрипту content scripts, который связан с нужной страницей (через 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);
});

В дополнении, описанном выше, есть два сообщения:

  • click отправляется из page-mod в дополнение, когда пользователь кликает на элемент на web-странице
  • warning отправляет прикольную строчку обратно в объект page-mod

Сообщения из Tab.attach()

Функция Tab.attach() возвращает worker, который можно использовать для связи со скриптом content script(s).

Следующее дополнение добавляет кнопку в Firefox: когда пользователь надимает её, то дополнение привязывает скрипт content script к активной вкладке, отправляет этому скрипту сообщение "my-addon-message" и ждёт ответ "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");
}

Описание port API

Смотрите reference page for the port object.

Описание postMessage API

До того, как был введён объект port, дополнения и content scripts общались следующим образом, используя различные API:

  • скрипт content script вызывал self.postMessage() для отправки и self.on() для получения
  • дополнение (add-on) вызывал worker.postMessage() для отправки и worker.on() для получения

Данный API до сих пор доступно и документировано, но желательно использовать port API, описанный здесь выше. Исключением является модуль context-menu, который ещё использует postMessage.

Взаимодействие скриптов content script со скриптами content script

Скрипты content scripts могут взаимодействовать друг с другом напрямую если они загружены в одном контексте. Например, если один вызов Tab.attach() привязывает два скрипта content scripts, то они видят друг друга напрямую, как если два скрипта загружены на одну страницу. Но если вызвать Tab.attach() дважды, привязывая content scripts каждый раз, то они уже не будут загружены в одном контексте, и дожны взаимодействовать способами как скрипты из разных контекстов. Один из вариантом это пересылать сообщения через основной код дополнения, используя port API с передачей сообщения другим скриптам context script. Этои вариант будет работать независимо от контекста, в котором загружен скрипт content script.

В отдельном случае, когда два скрипта загружены на одной странице, существует возможность для обоих скриптов content scripts взаимодействовать друг с другом, используя DOM postMessage() API или CustomEvent. Следующее дополнение показывает как скрипт content script, добавленный через page-mod, получает событие CustomEvent, отправленное из context-menu, когда элемент меню был кликнут. Скрипт page-mod будет отображать алерт с URL той ссылки, по которой было отображено контекстное меню. URL передан в скрипт page-mod с использованием CustomEvent.

var pageMod = require("sdk/page-mod");
pageMod.PageMod({
  include: "*.mozilla.org",
  contentScript: 'function contextMenuAlert(href) {'
               + '    window.alert("The context menu was clicked on URL:\\n" + href);'
               + '};'
               + 'window.addEventListener("myAddonId-contextMenu-clicked",'
               + '    function(event){contextMenuAlert(event.detail);});'
});

let cm = require("sdk/context-menu");
cm.Item({
    label: "Alert URL",
    context: [
        cm.URLContext(["*.mozilla.org"]),
        cm.SelectorContext("a[href]")
    ],
    contentScript: 'self.on("click", function (node, data) {'
                 + '    var event = new CustomEvent("myAddonId-contextMenu-clicked",'
                 + '                                {detail:node.href});'
                 + '    window.dispatchEvent(event);'
                 + '});'
});

Междоменные скрипты content script

По умолчанию скрипты content script не имеют никаких междоменных привилегий. В частности, они не имеют доступа к содержимому в iframe, если содержимое получено из другого домена, или выполняются междоменные XMLHttpRequests.

Однако, вы можете разрешить эти функции для заданных доменов, путём добавления их в package.json дополнения в ключе "cross-domain-content", который расположен в ключе "permissions". Смотрите статью междоменные скрипты содержимого.

Метки документа и участники

 Внесли вклад в эту страницу: pk.prog, ldone
 Обновлялась последний раз: pk.prog,