Content Scripts

Dieser Artikel benötigt eine technische Überprüfung. So können Sie helfen.

Dieser Artikel benötigt eine redaktionelle Überprüfung. So können Sie helfen.

Viele Add-ons müssen den Inhalt von Webseiten modifizieren oder auf diesen zugreifen können. Der Grundcode des Add-on  bekommt aber keinen direkten Zugriff auf Webinhalte. Stattdessen müssen SDK Add-ons den Code,  der Zugriff auf Webinhalte erhält in seperate Skripte auslagern, die sogenannten "Content Scripts". Diese Seite beschreibt wie man Content Scripts entwickelt und diese implementiert.

Content Scripts sind einer der verwirrenderen Aspekte beim Arbeiten mit der SDK, aber Sie werden mit Sicherheit mit ihnen arbeiten müssen. Es gibt fünf grundlegende Prinzipien:

  • Der Grundcode des Add-ons, wie zum Beispiel "main.js" und andere module im "lib" Verzeichnis, können die high-level und low-level APIs benutzen, aber nicht direkt auf Webinhalte zugreifen.
  • Content Skripts können keine SDK APIs ( kein Zugang zu exports, require), dafür aber auf Webinhalte zugreifen.
  • SDK APIs, die Content Skripts verwenden, wie zum Beispiel page-mod und tabs, bieten Funktionen die es dem Grundcode des Add-ons ermöglichen Content Skripts  in eine Weebseite zu laden.
  • Content Skripts können als Strings geladen werden, werden aber öfter in seperaten Dateien im Add-on Ordner "data" abgespeichert. Jpm erzeugt keinen "data" Ordner, daher muss dieser manuell hinzugefügt werden und ihr Content Scrikt dort abgespeichert werden.
  • Eine Nachricht übertragende API erlaubt es dem Grundcode und dem Content Skript miteinander zu kommunizieren.

Dieses komplette Add-on zeigt alle diese Prinzipien. Die "main.js" hängt ein Content Skript an den aktuellen Tab, mittels den tabs Modules, an. In diesem Fall wird der Content Skript in Form eines Strings übergeben. Das Content Skript ersetzt einfach nur den Inhalt der Seite:

// main.js
var tabs = require("sdk/tabs");
var contentScriptString = 'document.body.innerHTML = "<h1>Diese Seite wurde aufgegessen</h1>";'

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

Die folgenden high-level SDK Module können Content Skripts benutzen, um Webseiten zu bearbeiten:

  • page-mod: Erlaubt es Ihnen Content Skripts bei Webseiten, die mit einem bestimmten URL Muster übereinstimmen, einzusetzen.
  • tabs: Exportiert ein Tab Objekt um mit einem Browser Tab zu arbeiten.Das Tab Objekt beinhaltet eine Funktion attach() um dem Skript Content Skripte anzuhängen.
  • page-worker:  Lässt Sie eine Webseite erhalten ohne diese anzuzeigen. Sie können diesen Seiten Content Skripte anhängen, die DOM der Seite erreichen oder diese maniulieren.
  • context-menu: Hiermit können Sie ein Content Skript benutzen um mit einer Seite zu interagieren, in der das Menü aufgerufen ist.

Zusätzlich sind manche SDK Benutzeroberflächen Komponenten - Panel, Sidebar, frames - darauf ausgelegt HTML zu benutzen und haben deshalb seperate Skripte um mit ihrem Inhalt zu interagieren. In vielen Punkten sind diese Skripte wie Content Skripte, aber dies ist nicht Teil dieses Artikels. Um mehr über die Interaktion des Inhalts eines Benutzeroberflächenmoduls zu erfahren, schauen Sie sich die modulspezifischen Dokumentationen: panel, sidebar, frame an.

Fast alle diese Beispiele, die in dieser Anleitung präsentiert werden, sind als komplette, aber minimalistische, Add-ons in der addon-sdk-content-scripts repository auf Github vorhanden.

Content Skripts laden

Sie können ein einzelnes Skript laden, indem Sie einen String an die contentScript oder die contentScriptFile Option übergeben. Die contentScript Option behandelt den übergebenen String wie ein eigenes Skript:

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

Die contentScriptFile Option behandelt den String wie eine resource:// URL, die auf ein Skript zeigt, dass in ihrem Add-on Verzeichnis "data" gespeichert ist. jpm erstellt standardmäßig keinen "data" Ordner, also muss dieser erst erstellt werden, wenn Sie ihre Content Scripts verwenden wollen.

Das Add-on liefert eine URL, die auf die Datei "content-script.js" zeigt, welche im data Unterordner des Add-on Stammverzeichnisses enthalten ist:

// 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>Seite erfüllt die Regeln.</h1>";

Ab Firefox 34 , kann "./content-script.js" als Alias für self.data.url("content-script.js") verwendet werden. Die main.js kann also auch folgendermaßen geschrieben werden:

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

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

Wenn ihr Content Skript nicht sehr simpel ist oder aus einem statischen String besteht, sollten Sie contentScript:  nicht benutzen. Wenn Sie es doch tun, könnten Sie Probleme haben Ihr Add.on auf AMO verifiziert zu bekommmen.

Stattdessen sollten Sie ihr Skript in einer seperaten Datei schreiben und mit contentScriptFile laden. Das macht ihren Code übersichtlicher und er ist einfacher zu Warten, sichern und debuggen.

Sie können auch mehrere Skripte in contentScript oder contentScriptFile laden, indem Sie ein Array von Strings übergeben:

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

Wenn Sie das tuen, können die Skripte direkt miteinander kommunizieren, als wären es Skripte der gleichen Webseite.

Sie können auch contentScript und contentScriptFile zusammen benutzen. Wenn Sie das tun, werden die Skripte, die sie in contentScriptFile spezifizieren vor denen in contentScript geladen. Das ermöglicht es Ihnen javaScript Bibliotheken, wie JQuery über eine URL zu laden und dann ein simples Skript inline zu verwenden, dass diese Bibliothek benutzt:

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

Wenn ihr Content Skript nicht sehr simpel ist oder aus einem statischen String besteht, sollten Sie contentScript:  nicht benutzen. Wenn Sie es doch tun, könnten Sie Probleme haben Ihr Add.on auf AMO verifiziert zu bekommmen.

Stattdessen sollten Sie ihr Skript in einer seperaten Datei schreiben und mit contentScriptFile laden. Das macht ihren Code übersichtlicher und er ist einfacher zu Warten, sichern und debuggen.

Kontrollieren, wann das Skript angehängt werden soll.

Die contentScriptWhen Option spezifiziert, wann das/die Content Skript/e geladen werden sollen. Diese brauch eine dieser Parameter:

  • "start": Läd das Skript sofort, nach dem das Dokumentelement der Seite in den DOM eingefügt wird. ZU diesem Zeitpunkt wurde der DOM Inhalt noch nicht geladen,, deshalb kann das Skript nicht damit interagieren.
  • "ready": Läd das Skript nachdem der DOM der Seite geladen wurde: Dies ist der Fall, wenn das DOMContentLoaded Event  abgefeuert wird. Ab diesem Zeitpunkt können Content Skripts mit dem DOM interagieren, aber extern referenzierte Stylesheets und Bilder könnten noch nicht geladen sein.
  • "end": Läd das Skript nachdem der komplette Inhalt (DOM, JS, CSS, images) der Seute geladen wurde. Zu diesem Zeitpunkt wird das window.onload event abgefeuert.

Der Standardwert ist "end".

Die Funktion tab.attach() akzepiert contentScriptWhen nicht, da es generell aufgerufen wird wenn die Seite geladen wurde.

Übergabe von Konfigurationsoptionen

Das contentScriptOptions Objekt ist ein JSON Objekt, das den Content Skripts als "read-only" Wert als self.options Eigenschaft übergeben wird:

// main.js

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

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

Jeder Wert (Objekt, Feld, String, etc), dass in JSON dargestellt werden kann, kann hier benutzt werden.

Zugriff auf den DOM

Content Skripts können natürlich wie jedes andere Skript, dass die Seite geladen hat ( Page Skripts) auf den DOM zugreifen. Content Skripts und Page Skripts sind wie folgt von einander isoliert:

  • Content Skripts sehen keine JavaScript Objekte, die der Seite über Page Skripts hinzugefügt wurden.
  • Auch wenn ein Page Skript das Verhalten eines DOM Objekts verändert hat, sieht das Content Skript nur das Originalverhalten.

Das gleiche gilt auch umgekehrt: Page Skripts sehen keine JavaScript Objekte, die von Content Skripts hinzugefügt wurden.

Stellen Sie sich eine Seite vor, die zum Beispiel eine Variable foo über ein Page Skript zum window Objekt hinzufügt:

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

Ein anderes Skript, dass nach diesem Skript in die Seite geladen wird, kann auf foo zugreifen. Ein Content Skript kann dies nicht:

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

Es gibt gute Gründe für diese Isolation. Erstens können Content Skripts so keine Objekte an Webseiten übermitteln und somit Sicherheitslücken schaffen. Zweitens können Content Skripts so Objekte erzeugen, ohne sich Sorgen machen zu müssen, dass diese mit Objekten kollidieren, die in Page Skripts erzeugt wurden.

Die Isulation bedeutet, dass wenn zum Beispiel eine Webseite die JQuery Bibliothek läd, das Content Skript nicht in der Lage ist dieses zu sehen, aber eine eigene JQuery Bibliothek laden kann ohne das es ein Problem mit der Version gibt, die über das Page Skript hinzugefügt wurde.

Interaktion mit Page Skripts

Normalerweise möchte man Content Skripts und Page Skripts voneinander isolieren. Wenn dies nicht der Fall ist, da Sie zum Beispiel Objekte zwischen beiden Skripten austauschen wollen, oder Nachrichten zwischen ihnen schicken wollen können Sie mehr zum Thema unter  Interaktion mit Page Skripts finden.

Event Listeners

Man kann in Content Skripts genau wie in einem Page Skript auf DOM Events warten. Es gibt nur zwei wichtige Unterschieden:

Erstens: Falls Sie einen Event Listener als String an setAttribute() übergeben, wird der Listener im Seitenkontext ausgeführt und hat somit keinen Zugriff auf Variablen, die im Content Skript definiert wurden.

Zum Beispiel, wird dieses Content Skript mit der Fehlermeldung "theMessage is not defined" ausgeben:

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

Zweitens: Falls Sie einen Event Listener per direkter Zuweisung einem globalen Event Handler  wie onclick zuweisen, könnten Zuweisungen, die die Seite vornimmt überschrieben werden. Zur Veranschaulichung ist hier ein Add-on, das versucht einen click Handler per Zuweisung an window.onclick anzufügen:

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

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

Das wird auf den meisten Seiten funktionieren, bis auf denen, die ebenfalls ein onclick zuweisen:

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

Aus diesen Gründen ist es besser Event Listeners per addEventListener() hinzuzufügen. So definieren Sie einen Listener als Funktion:

var theMessage = "Hello from content script!";

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

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

Kommunikation mit dem Add-on

Damit Add-On Sktipts und Content Skripts miteinander kommunizieren können, haben beide Seiten der Konversation Zugriff auf ein port Objekt.

  • Um eine Nachricht von einer Seite zur anderen zu schicken nutzen Sie port.emit()
  • Um eine Nachricht von der anderen Seite zu empfangen nutzen port.on()

Nachrichten sind asyncron, was bedeutet, dass der Sender nicht wartet, bis er eine Antwort des Empfängers erhält, sondern die Nachricht schickt und das weiter arbeitet.

Hier ist ein simples Beispieladd-on, das eine Nachricht an ein Content Skript per port schickt:

// 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("http://www.mozilla.org");
// content-script.js

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

Das context-menu Modul benutzt das Kommunikationsmodul, das hier beschrieben wird nicht. Um etwas über die Kommunikation mit geladenen Content Skripts im context-menu zu erfahren, schauen Sie in die context-menu Dokumentation.

Zugriff auf port im Content Skript

Im Content Skript ist das port Objekt als Eigenschaft im globalen Objekt self verfügbar. So versenden Sie eine Nachricht vom Content Skript:

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

Um eine Nachricht vom Add-on Code zu bekommen:

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

Das globale self Objekt ist etwas komplett anderes als das self Modul, das einer API in einem Add-on die Möglichkeit bietet auf Daten und die ID des Add-ons zuzugreifen.

Zugriff auf port im Add-on Skript

Im Add-on Code ist das Bindeglied zur Kommunikation zwischen Add-on und einem spezifischen Content Skript das  worker Objekt. Das port Objekt ist also eine Eigenschaft des  worker Objekts.

Der worker wird aber im Add-on Code nicht von allen Modulen gleich verwendet.

Vom page-worker

Das page-worker Objekt integriert die worker API direkt. Um also eine Nachricht von einem Content Skript zu erhalten, das mit dem page-worker assoziiert wird benutzt man 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: "http://en.wikipedia.org/wiki/Internet"
});

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

Um eine benutzerdefinierte Nachricht vom Add-on zu schicken, nutz man 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: "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);
  }
}

Vom page-mod

Ein einziges  page-mod Objekt kann ein Skript an mehrere Seiten anhängen. Jede dieser Seiten hat ihren eigenen Context, in dem sie dieses Skript aufrufen. Daher benötigt es seperate Kanäle(worker) für jede Seite.

page-mod integriert also die worker API nicht direkt, sondern es wird jedes Mal wenn ein Content Skript an eine Seite angehängt wird das attach Event aufgerufen,  dessen listener einen worker für den Kontext übergeben bekommt. Durch das bereit stellen eines listeners bei attach kann man das port Objekt für das Content Skript, dass dieser Seite angefügt wurde über diesen page-mod verwenden:

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

Im oben gezeigten Add-on gibt es zwei Nachrichten:

  • click wird vom page-mod an das Add-on geschickt, wenn der Nutzer auf ein Element auf der Seite klickt
  • warning schickt einen String zurück an den page-mod

Von Tab.attach()

Die Tab.attach() methode liefert einen worker zurück, den man zur Kommunikation mit dem/den Content Skript/Content Skripts, die angehängt wurden, verwenden kann.

Das Add-on fügt einen Button zu Firefox hinzu: Wenn der Benutzer den Button drückt, fügt das Add-on ein Content Skript an den aktuellen Tab an. Das Skript sendet eine Nachricht namens "my-addon-message" und wartet auf eine Antwort namens "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");
}

Die port API

Schaue unter der Referenzseite für das port Objekt.

Die postMessage API

bevor das port Objekt hinzugefügt wurde, kommunizierten Add-on Code und Content Skripts über eine andere API:

  • Das Content Skript rief self.postMessage() auf, um zu senden und self.on() um zu empfangen.
  • Das Add-on Skript rief worker.postMessage() auf, um zu senden und  worker.on() um zu empfangen

Die API ist immer noch verfügbar und dokumentiert, aber es gibt keinen Grund sie statt der port API zu verwenden, die hier beschrieben wird. Die Ausnahme bildet das context-menu Modul, welches immer noch postMessage verwendet.

Content Skript zu Content Skript

Content Skripts können nur direkt miteinander kommunizieren, wenn diese im gleichen Kontext geladen wurden. Beispiel: Wenn ein einziger Aufruf von  Tab.attach()  zwei Content Skripts anhängt, können diese sich gegenseitig sehen. Wenn aber Tab.attach() zweimal aufgerufen wird, und die Content Skripts einzeln anhängt,können die Content Skripte nicht miteinander kommunizieren. Dann müssen die Nachrichten über die  port API über den Add-on code gesendet werden.

Cross-domain Content Skripts

Grundsätzlich hat ein Content Skript keine cross-domain Privilegien. Sie können also keinen Inhalt eines iframes auslesen, wenn dieser Inhalt von einer anderen Domain stammt, oder XMLHttpRequests an eine andere Domain schicken.

Um dieses Feature für spezielle Domains hinzuzufügen fügen Sie dem package.json ihres Add-ons den Schlüssel "cross-domain-content" unter dem "permissions" Schlüssel hinzu. Siehe dafür den Artikel  cross-domain content scripts.

Schlagwörter des Dokuments und Mitwirkende

 Mitwirkende an dieser Seite: Bugfi5h
 Zuletzt aktualisiert von: Bugfi5h,