Fügen Sie mithilfe eines Inhaltsskripts Code in den Seitenkontext ein

480

Ich lerne, wie man Chrome-Erweiterungen erstellt. Ich habe gerade angefangen, eine zu entwickeln, um YouTube-Events zu verfolgen. Ich möchte es mit dem YouTube Flash Player verwenden (später werde ich versuchen, es mit HTML5 kompatibel zu machen).

manifest.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}

myScript.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

Das Problem ist, dass die Konsole mir das "Started!" , aber es gibt kein "State Changed!" wenn ich YouTube-Videos abspiele / pausiere.

Wenn dieser Code in die Konsole eingefügt wird, hat es funktioniert. Was mache ich falsch?

André Alves
quelle
14
Versuchen Sie, die Anführungszeichen um Ihren Funktionsnamen zu entfernen:player.addEventListener("onStateChange", state);
Eduardo
2
Es ist auch bemerkenswert, dass Sie beim Schreiben von Übereinstimmungen nicht vergessen sollten , das https://http://www.youtube.com/*
einzuschließen,

Antworten:

874

Inhaltsskripte werden in einer Umgebung mit "isolierter Welt" ausgeführt . Sie müssen Ihre state()Methode in die Seite selbst einfügen.

Wenn Sie eine der chrome.*APIs im Skript verwenden möchten , müssen Sie einen speziellen Ereignishandler implementieren, wie in dieser Antwort beschrieben: Chrome-Erweiterung - Abrufen der ursprünglichen Nachricht von Google Mail .

Wenn Sie keine chrome.*APIs verwenden müssen, empfehle ich dringend, den gesamten JS-Code durch Hinzufügen eines <script>Tags in die Seite einzufügen:

Inhaltsverzeichnis

  • Methode 1: Injizieren Sie eine andere Datei
  • Methode 2: Injizieren Sie eingebetteten Code
  • Methode 2b: Verwenden einer Funktion
  • Methode 3: Verwenden eines Inline-Ereignisses
  • Dynamische Werte im injizierten Code

Methode 1: Injizieren Sie eine andere Datei

Dies ist die einfachste / beste Methode, wenn Sie viel Code haben. Fügen Sie beispielsweise Ihren tatsächlichen JS-Code in eine Datei in Ihrer Erweiterung ein script.js. Lassen Sie dann Ihr Inhaltsskript wie folgt aussehen (hier erklärt: Google Chome "Application Shortcut" Custom Javascript ):

var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

Hinweis: Wenn Sie diese Methode verwenden, muss die injizierte script.jsDatei zum "web_accessible_resources"Abschnitt hinzugefügt werden ( Beispiel ). Wenn Sie dies nicht tun, wird Chrome verweigert Ihr Skript und zeigt den folgenden Fehler in der Konsole zu laden:

Verweigern des Ladens der Chrome-Erweiterung: // [EXTENSIONID] /script.js. Ressourcen müssen im Manifestschlüssel web_accessible_resources aufgeführt sein, damit sie von Seiten außerhalb der Erweiterung geladen werden können.

Methode 2: Injizieren Sie eingebetteten Code

Diese Methode ist nützlich, wenn Sie schnell einen kleinen Code ausführen möchten. (Siehe auch: So deaktivieren Sie Facebook-Hotkeys mit der Chrome-Erweiterung? )

var actualCode = `// Code here.
// If you want to use a variable, use $ and curly braces.
// For example, to use a fixed random number:
var someFixedRandomValue = ${ Math.random() };
// NOTE: Do not insert unsafe variables in this way, see below
// at "Dynamic values in the injected code"
`;

var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Hinweis: Vorlagenliterale werden nur in Chrome 41 und höher unterstützt. Wenn die Erweiterung in Chrome 40- funktionieren soll, verwenden Sie:

var actualCode = ['/* Code here. Example: */' + 'alert(0);',
                  '// Beware! This array have to be joined',
                  '// using a newline. Otherwise, missing semicolons',
                  '// or single-line comments (//) will mess up your',
                  '// code ----->'].join('\n');

Methode 2b: Verwenden einer Funktion

Für einen großen Teil des Codes ist das Zitieren der Zeichenfolge nicht möglich. Anstatt ein Array zu verwenden, kann eine Funktion verwendet und stringifiziert werden:

var actualCode = '(' + function() {
    // All code is executed in a local scope.
    // For example, the following does NOT overwrite the global `alert` method
    var alert = null;
    // To overwrite a global variable, prefix `window`:
    window.alert = null;
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Diese Methode funktioniert, da der +Operator für Zeichenfolgen und eine Funktion alle Objekte in eine Zeichenfolge konvertiert. Wenn Sie den Code mehrmals verwenden möchten, sollten Sie eine Funktion erstellen, um eine Wiederholung des Codes zu vermeiden. Eine Implementierung könnte folgendermaßen aussehen:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});

Hinweis: Da die Funktion serialisiert ist, gehen der ursprüngliche Bereich und alle gebundenen Eigenschaften verloren!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"

Methode 3: Verwenden eines Inline-Ereignisses

Manchmal möchten Sie sofort Code ausführen, z. B. um Code auszuführen, bevor das <head>Element erstellt wird. Dies kann durch Einfügen eines <script>Tags mit erfolgen textContent(siehe Methode 2 / 2b).

Eine Alternative, die jedoch nicht empfohlen wird, ist die Verwendung von Inline-Ereignissen. Es wird nicht empfohlen, da Inline-Ereignis-Listener blockiert werden, wenn die Seite eine Inhaltssicherheitsrichtlinie definiert, die Inline-Skripts verbietet. Von der Erweiterung eingefügte Inline-Skripte werden dagegen weiterhin ausgeführt. Wenn Sie weiterhin Inline-Ereignisse verwenden möchten, gehen Sie wie folgt vor:

var actualCode = '// Some code example \n' + 
                 'console.log(document.documentElement.outerHTML);';

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Hinweis: Bei dieser Methode wird davon ausgegangen, dass keine anderen globalen Ereignis-Listener für das resetEreignis vorhanden sind. Wenn ja, können Sie auch eines der anderen globalen Ereignisse auswählen. Öffnen Sie einfach die JavaScript-Konsole (F12), geben Sie document.documentElement.ondie verfügbaren Ereignisse ein und wählen Sie sie aus.

Dynamische Werte im injizierten Code

Gelegentlich müssen Sie eine beliebige Variable an die injizierte Funktion übergeben. Zum Beispiel:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};

Um diesen Code einzufügen, müssen Sie die Variablen als Argumente an die anonyme Funktion übergeben. Stellen Sie sicher, dass Sie es richtig implementieren! Folgendes wird nicht funktionieren:

var scriptToInject = function (GREETING, NAME) { ... };
var actualCode = '(' + scriptToInject + ')(' + GREETING + ',' + NAME + ')';
// The previous will work for numbers and booleans, but not strings.
// To see why, have a look at the resulting string:
var actualCode = "(function(GREETING, NAME) {...})(Hi, I'm ,Rob)";
//                                                 ^^^^^^^^ ^^^ No string literals!

Die Lösung besteht darin, JSON.stringifyvor dem Übergeben des Arguments zu verwenden. Beispiel:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

Wenn Sie viele Variablen haben, lohnt es sich, diese JSON.stringifyeinmal zu verwenden , um die Lesbarkeit zu verbessern:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Rob W.
quelle
81
Diese Antwort sollte Teil der offiziellen Dokumente sein. Offizielle Dokumente sollten mit der empfohlenen Methode geliefert werden -> 3 Methoden, um dasselbe zu tun ... Falsch?
Mars Robertson
7
@ Qantas94Heavy Der CSP der Erweiterung wirkt sich nicht auf Inhaltsskripte aus. Nur der CSP der Seite ist relevant. Methode 1 kann mithilfe einer script-srcDirektive blockiert werden , die den Ursprung der Erweiterung ausschließt. Methode 2 kann mithilfe eines CSP blockiert werden, der "unsafe-inline" ausschließt.
Rob W
3
Jemand fragte, warum ich das Skript-Tag mit entferne script.parentNode.removeChild(script);. Mein Grund dafür ist, dass ich mein Chaos gerne aufräumte. Wenn ein Inline-Skript in das Dokument eingefügt wird, wird es sofort ausgeführt und das <script>Tag kann sicher entfernt werden.
Rob W
9
Andere Methode: Verwenden Sie eine location.href = "javascript: alert('yeah')";beliebige Stelle in Ihrem Inhaltsskript. Es ist einfacher für kurze Codeausschnitte und kann auch auf die JS-Objekte der Seite zugreifen.
Métoule
3
@ChrisP Seien Sie vorsichtig mit javascript:. Code, der sich über mehrere Zeilen erstreckt, funktioniert möglicherweise nicht wie erwartet. Ein Zeilenkommentar ( //) schneidet den Rest ab, sodass dies fehlschlägt : location.href = 'javascript:// Do something <newline> alert(0);';. Dies kann umgangen werden, indem sichergestellt wird, dass Sie mehrzeilige Kommentare verwenden. Eine andere Sache, auf die Sie achten sollten, ist, dass das Ergebnis des Ausdrucks nichtig sein sollte. javascript:window.x = 'some variable';Das Dokument wird entladen und durch den Ausdruck "eine Variable" ersetzt. Bei richtiger Anwendung ist es in der Tat eine attraktive Alternative zu <script>.
Rob W
61

Die einzige Sache fehlt Vor Rob Ws hervorragender Antwort verbirgt sich die Kommunikation zwischen dem injizierten Seitenskript und dem Inhaltsskript.

Fügen Sie auf der Empfangsseite (entweder Ihr Inhaltsskript oder das Skript für die injizierte Seite) einen Ereignis-Listener hinzu:

document.addEventListener('yourCustomEvent', function (e) {
  var data = e.detail;
  console.log('received', data);
});

Auf der Initiatorseite (Inhaltsskript oder injiziertes Seitenskript) senden Sie das Ereignis:

var data = {
  allowedTypes: 'those supported by structured cloning, see the list below',
  inShort: 'no DOM elements or classes/functions',
};

document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));

Anmerkungen:

  • DOM-Messaging verwendet einen strukturierten Klonalgorithmus, der neben primitiven Werten nur einige Datentypen übertragen kann . Es können keine Klasseninstanzen oder -funktionen oder DOM-Elemente gesendet werden.
  • Um in Firefox ein Objekt (dh keinen primitiven Wert) aus dem Inhaltsskript an den Seitenkontext zu senden, müssen Sie es explizit mit cloneInto(einer integrierten Funktion) in das Ziel klonen , da es sonst mit einem Sicherheitsverletzungsfehler fehlschlägt .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
Laktak
quelle
Ich habe tatsächlich auf den Code und die Erklärung in der zweiten Zeile meiner Antwort auf stackoverflow.com/questions/9602022/… verlinkt .
Rob W
1
Haben Sie eine Referenz für Ihre aktualisierte Methode (z. B. einen Fehlerbericht oder einen Testfall?) Der CustomEventKonstruktor ersetzt die veraltete document.createEventAPI.
Rob W
Für mich hat 'dispatchEvent (neues CustomEvent ...' funktioniert. Ich habe Chrome 33. Außerdem hat es vorher nicht funktioniert, weil ich den addEventListener nach dem Einfügen des js-Codes geschrieben habe.
jscripter
Seien Sie besonders vorsichtig, was Sie als zweiten Parameter an den CustomEventKonstruktor übergeben. Ich habe zwei sehr verwirrende Rückschläge erlebt: 1. Einfach einfache Anführungszeichen um 'Detail' zu setzen, machte den Wert verwirrend, nullwenn er vom Hörer meines Inhaltsskripts empfangen wurde. 2. Noch wichtiger ist, aus irgendeinem Grund musste ich JSON.parse(JSON.stringify(myData))oder es würde auch werden null. Angesichts dessen scheint mir die Behauptung des folgenden Chromium-Entwicklers, dass der Algorithmus "strukturierter Klon" automatisch verwendet wird, nicht wahr zu sein. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk
Ich denke, der offizielle Weg ist die Verwendung von window.postMessage: developer.chrome.com/extensions/…
Enrique
9

Ich hatte auch das Problem der Bestellung geladener Skripte, das durch sequentielles Laden von Skripten gelöst wurde. Die Beladung basiert auf Rob W Antwort .

function scriptFromFile(file) {
    var script = document.createElement("script");
    script.src = chrome.extension.getURL(file);
    return script;
}

function scriptFromSource(source) {
    var script = document.createElement("script");
    script.textContent = source;
    return script;
}

function inject(scripts) {
    if (scripts.length === 0)
        return;
    var otherScripts = scripts.slice(1);
    var script = scripts[0];
    var onload = function() {
        script.parentNode.removeChild(script);
        inject(otherScripts);
    };
    if (script.src != "") {
        script.onload = onload;
        document.head.appendChild(script);
    } else {
        document.head.appendChild(script);
        onload();
    }
}

Das Anwendungsbeispiel wäre:

var formulaImageUrl = chrome.extension.getURL("formula.png");
var codeImageUrl = chrome.extension.getURL("code.png");

inject([
    scriptFromSource("var formulaImageUrl = '" + formulaImageUrl + "';"),
    scriptFromSource("var codeImageUrl = '" + codeImageUrl + "';"),
    scriptFromFile("EqEditor/eq_editor-lite-17.js"),
    scriptFromFile("EqEditor/eq_config.js"),
    scriptFromFile("highlight/highlight.pack.js"),
    scriptFromFile("injected.js")
]);

Eigentlich bin ich ein bisschen neu bei JS, also zögern Sie nicht, mich auf die besseren Wege zu schicken.

Dmitry Ginzburg
quelle
3
Diese Art des Einfügens von Skripten ist nicht sinnvoll, da Sie den Namespace der Webseite verschmutzen. Wenn die Webseite eine Variable namens formulaImageUrloder verwendet codeImageUrl, wird die Funktionalität der Seite effektiv zerstört. Wenn Sie eine Variable an die Webseite übergeben möchten, empfehle ich, die Daten an das Skriptelement ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) anzuhängen und z. B. (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();im Skript zu verwenden, um auf die Daten zuzugreifen.
Rob W
@RobW danke für deinen Hinweis, obwohl es mehr um das Beispiel geht. Können Sie bitte klarstellen, warum ich IIFE verwenden sollte, anstatt nur zu bekommen dataset?
Dmitry Ginzburg
4
document.currentScriptzeigt nur auf das Skript-Tag, während es ausgeführt wird. Wenn Sie jemals auf das Skript-Tag und / oder seine Attribute / Eigenschaften (z. B. dataset) zugreifen möchten, müssen Sie es in einer Variablen speichern. Wir benötigen ein IIFE, um einen Abschluss zum Speichern dieser Variablen zu erhalten, ohne den globalen Namespace zu verschmutzen.
Rob W
@RobW ausgezeichnet! Aber können wir nicht einfach einen Variablennamen verwenden, der sich kaum mit dem vorhandenen überschneiden würde? Ist es nur nicht idiomatisch oder können wir andere Probleme damit haben?
Dmitry Ginzburg
2
Sie könnten, aber die Kosten für die Verwendung eines IIFE sind vernachlässigbar, sodass ich keinen Grund sehe, die Verschmutzung von Namespaces einem IIFE vorzuziehen. Ich schätze die Gewissheit, dass ich die Webseite anderer nicht auf irgendeine Weise beschädigen werde, und die Fähigkeit, kurze Variablennamen zu verwenden. Ein weiterer Vorteil der Verwendung eines IIFE besteht darin, dass Sie das Skript bei Bedarf früher beenden können ( return;).
Rob W
6

Im Inhaltsskript füge ich dem Kopf ein Skript-Tag hinzu, das einen 'onmessage'-Handler innerhalb des von mir verwendeten Handlers bindet, um Code auszuführen. Im Standinhaltsskript verwende ich auch den onmessage-Handler, sodass ich eine bidirektionale Kommunikation bekomme. Chrome Docs

//Content Script

var pmsgUrl = chrome.extension.getURL('pmListener.js');
$("head").first().append("<script src='"+pmsgUrl+"' type='text/javascript'></script>");


//Listening to messages from DOM
window.addEventListener("message", function(event) {
  console.log('CS :: message in from DOM', event);
  if(event.data.hasOwnProperty('cmdClient')) {
    var obj = JSON.parse(event.data.cmdClient);
    DoSomthingInContentScript(obj);
 }
});

pmListener.js ist ein URL-Listener für Post-Nachrichten

//pmListener.js

//Listen to messages from Content Script and Execute Them
window.addEventListener("message", function (msg) {
  console.log("im in REAL DOM");
  if (msg.data.cmnd) {
    eval(msg.data.cmnd);
  }
});

console.log("injected To Real Dom");

Auf diese Weise kann ich eine bidirektionale Kommunikation zwischen CS und Real Dom herstellen. Dies ist zum Beispiel sehr nützlich, wenn Sie Webscoket-Ereignisse oder beliebige Variablen oder Ereignisse im Speicher abhören müssen.

doron aviguy
quelle
1

Sie können eine von mir erstellte Dienstprogrammfunktion verwenden, um Code im Seitenkontext auszuführen und den zurückgegebenen Wert zurückzugewinnen.

Dazu wird eine Funktion in eine Zeichenfolge serialisiert und in die Webseite eingefügt.

Das Dienstprogramm ist hier auf GitHub verfügbar .

Anwendungsbeispiele -



// Some code that exists only in the page context -
window.someProperty = 'property';
function someFunction(name = 'test') {
    return new Promise(res => setTimeout(()=>res('resolved ' + name), 1200));
}
/////////////////

// Content script examples -

await runInPageContext(() => someProperty); // returns 'property'

await runInPageContext(() => someFunction()); // returns 'resolved test'

await runInPageContext(async (name) => someFunction(name), 'with name' ); // 'resolved with name'

await runInPageContext(async (...args) => someFunction(...args), 'with spread operator and rest parameters' ); // returns 'resolved with spread operator and rest parameters'

await runInPageContext({
    func: (name) => someFunction(name),
    args: ['with params object'],
    doc: document,
    timeout: 10000
} ); // returns 'resolved with params object'

Arik
quelle
0

Wenn Sie anstelle von Text eine reine Funktion einfügen möchten, können Sie folgende Methode verwenden:

function inject(){
    document.body.style.backgroundColor = 'blue';
}

// this includes the function as text and the barentheses make it run itself.
var actualCode = "("+inject+")()"; 

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Und Sie können Parameter an die Funktionen übergeben (leider können keine Objekte und Arrays stringifiziert werden). Fügen Sie es wie folgt in die Barethesen ein:

function inject(color){
    document.body.style.backgroundColor = color;
}

// this includes the function as text and the barentheses make it run itself.
var color = 'yellow';
var actualCode = "("+inject+")("+color+")"; 

Tarmo Saluste
quelle
Das ist ziemlich cool ... aber die zweite Version mit einer Variablen für Farbe funktioniert bei mir nicht ... Ich werde "nicht erkannt" und der Code gibt einen Fehler aus ... sieht sie nicht als Variable.
11.