Inserisci il codice nel contesto della pagina usando uno script di contenuto


480

Sto imparando come creare estensioni di Chrome. Ho appena iniziato a svilupparne uno per catturare eventi su YouTube. Voglio usarlo con YouTube Flash Player (in seguito proverò a renderlo compatibile con HTML5).

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!");

Il problema è che la console mi dà il "Started!" , ma non esiste "Stato modificato!" quando riproduco / metto in pausa i video di YouTube.

Quando questo codice è stato inserito nella console, ha funzionato. Che cosa sto facendo di sbagliato?


14
prova a rimuovere le virgolette intorno al nome della tua funzione:player.addEventListener("onStateChange", state);
Eduardo

2
È anche da notare che quando si scrivono le partite, non dimenticare di includere https://o http://, questo www.youtube.com/*non ti consentirebbe di impacchettare l'estensione e genererebbe un errore di separatore dello schema mancante
Nilay Vishwakarma

Risposte:


875

Gli script di contenuto vengono eseguiti in un ambiente "mondo isolato" . Devi iniettare il tuo state()metodo nella pagina stessa.

Quando si desidera utilizzare una delle chrome.*API nello script, è necessario implementare un gestore eventi speciale, come descritto in questa risposta: Estensione di Chrome : recupero del messaggio originale di Gmail .

Altrimenti, se non devi usare le chrome.*API, ti consiglio vivamente di iniettare tutto il tuo codice JS nella pagina aggiungendo un <script>tag:

Sommario

  • Metodo 1: iniettare un altro file
  • Metodo 2: iniettare codice incorporato
  • Metodo 2b: utilizzo di una funzione
  • Metodo 3: utilizzo di un evento inline
  • Valori dinamici nel codice iniettato

Metodo 1: iniettare un altro file

Questo è il metodo più semplice / migliore quando hai un sacco di codice. Includi il tuo codice JS effettivo in un file all'interno della tua estensione, per esempio script.js. Quindi lascia che il tuo script dei contenuti sia il seguente (spiegato qui: Javascript personalizzato "Collegamento applicazione" di Google Chome ):

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

Nota: se si utilizza questo metodo, il script.jsfile iniettato deve essere aggiunto alla "web_accessible_resources"sezione ( esempio ). In caso contrario, Chrome rifiuterà di caricare lo script e visualizzerà il seguente errore nella console:

Negare il carico dell'estensione chrome: // [EXTENSIONID] /script.js. Le risorse devono essere elencate nella chiave manifest web_accessible_resources per essere caricate da pagine esterne all'estensione.

Metodo 2: iniettare codice incorporato

Questo metodo è utile quando si desidera eseguire rapidamente un piccolo pezzo di codice. (Vedi anche: Come disabilitare i tasti di scelta rapida di Facebook con l'estensione di Chrome? ).

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();

Nota: i letterali dei modelli sono supportati solo in Chrome 41 e versioni successive. Se vuoi che l'estensione funzioni in Chrome 40-, usa:

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');

Metodo 2b: utilizzo di una funzione

Per un grosso pezzo di codice, quotare la stringa non è fattibile. Invece di utilizzare un array, è possibile utilizzare una funzione e stringere:

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();

Questo metodo funziona perché l' +operatore su stringhe e una funzione converte tutti gli oggetti in una stringa. Se si intende utilizzare il codice più di una volta, è consigliabile creare una funzione per evitare la ripetizione del codice. Un'implementazione potrebbe apparire come:

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

Nota: poiché la funzione è serializzata, l'ambito originale e tutte le proprietà associate vengono perse!

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

Metodo 3: utilizzo di un evento inline

A volte, si desidera eseguire immediatamente un codice, ad esempio per eseguire un codice prima di <head>creare l' elemento. Questo può essere fatto inserendo un <script>tag con textContent(vedi metodo 2 / 2b).

Un'alternativa, ma non è consigliata, è utilizzare gli eventi in linea. Non è consigliabile perché se la pagina definisce una politica di sicurezza dei contenuti che proibisce gli script incorporati, i listener di eventi incorporati vengono bloccati. Gli script incorporati iniettati dall'estensione, invece, continuano a essere eseguiti. Se desideri comunque utilizzare eventi inline, ecco come:

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');

Nota: questo metodo presuppone che non vi siano altri listener di eventi globali che gestiscono l' resetevento. Se c'è, puoi anche scegliere uno degli altri eventi globali. Basta aprire la console JavaScript (F12), digitare document.documentElement.one selezionare gli eventi disponibili.

Valori dinamici nel codice iniettato

Occasionalmente, è necessario passare una variabile arbitraria alla funzione iniettata. Per esempio:

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

Per iniettare questo codice, è necessario passare le variabili come argomenti alla funzione anonima. Assicurati di implementarlo correttamente! Quanto segue non funzionerà:

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!

La soluzione è usare JSON.stringifyprima di passare l'argomento. Esempio:

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

Se hai molte variabili, vale la pena usarle JSON.stringifyuna volta, per migliorare la leggibilità, come segue:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';

83
Questa risposta dovrebbe far parte di documenti ufficiali. I documenti ufficiali devono essere spediti con le modalità consigliate -> 3 modi per fare la stessa cosa ... Sbagliato?
Mars Robertson,

7
@ Qantas94Heavy Il CSP dell'estensione non influisce sugli script di contenuto. È rilevante solo il CSP della pagina . Il metodo 1 può essere bloccato utilizzando ascript-src direttiva che esclude l'origine dell'estensione, il metodo 2 può essere bloccato usando un CSP che esclude "unsafe-inline" `.
Rob W

3
Qualcuno mi ha chiesto perché rimuovo il tag script usando script.parentNode.removeChild(script); . La mia ragione per farlo è perché mi piace pulire il mio casino. Quando uno script inline viene inserito nel documento, viene immediatamente eseguito e il <script>tag può essere rimosso in modo sicuro.
Rob W,

9
Altro metodo: utilizzare location.href = "javascript: alert('yeah')";ovunque nello script del contenuto. È più facile per brevi frammenti di codice e può anche accedere agli oggetti JS della pagina.
Métoule

3
@ChrisP Fare attenzione con l'utilizzo javascript:. L'estensione del codice su più righe potrebbe non funzionare come previsto. Una linea-comment ( //) troncherà il resto, quindi questo non sarà riuscito: location.href = 'javascript:// Do something <newline> alert(0);';. Questo può essere aggirato assicurandoti di utilizzare i commenti su più righe. Un'altra cosa da fare attenzione è che il risultato dell'espressione dovrebbe essere nullo. javascript:window.x = 'some variable';causerà lo scarico del documento e verrà sostituito con la frase "alcune variabili". Se usato correttamente, è davvero un'alternativa interessante a <script>.
Rob W,

61

L'unica cosa mancante nascosto dall'ottima risposta di Rob W è come comunicare tra lo script della pagina iniettato e lo script del contenuto.

Sul lato ricevente (lo script del contenuto o lo script della pagina iniettato) aggiungi un listener di eventi:

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

Sul lato dell'iniziatore (script del contenuto o script della pagina iniettata) invia l'evento:

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

Appunti:

  • La messaggistica DOM utilizza un algoritmo di clonazione strutturato, che può solo trasferire alcuni tipi di dati oltre ai valori primitivi. Non può inviare istanze di classe o funzioni o elementi DOM.
  • In Firefox, per inviare un oggetto (ovvero non un valore di base) dallo script del contenuto al contesto della pagina, è necessario clonarlo esplicitamente nella destinazione utilizzando cloneInto(una funzione incorporata), altrimenti fallirà con un errore di violazione della sicurezza .

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

In realtà ho collegato il codice e la spiegazione nella seconda riga della mia risposta, a stackoverflow.com/questions/9602022/… .
Rob W,

1
Hai un riferimento per il tuo metodo aggiornato (ad esempio una segnalazione di bug o un caso di test?) Il CustomEventcostruttore sostituisce l' document.createEventAPI obsoleta .
Rob W,

Per me 'dispatchEvent (nuovo CustomEvent ...' ha funzionato. Ho Chrome 33. Inoltre non ha funzionato prima perché ho scritto addEventListener dopo aver iniettato il codice js.
jscripter

Fai molta attenzione a ciò che passi come secondo parametro al CustomEventcostruttore. Ho sperimentato 2 battute d'arresto molto confuse: 1. mettere semplicemente virgolette singole intorno al 'dettaglio' ha reso perplesso il valore nullquando è stato ricevuto dall'ascoltatore del mio Content Script. 2. Ancora più importante, per qualche motivo dovevo farlo JSON.parse(JSON.stringify(myData))altrimenti sarebbe diventato null. Detto questo, mi sembra che la seguente affermazione dello sviluppatore di Chromium - che l'algoritmo del "clone strutturato" sia usato automaticamente - non sia vera. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk

Penso che il modo ufficiale sia usare window.postMessage: developer.chrome.com/extensions/…
Enrique il

9

Ho anche affrontato il problema di ordinare gli script caricati, che è stato risolto tramite il caricamento sequenziale degli script. Il carico si basa sulla risposta di Rob W .

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

L'esempio di utilizzo sarebbe:

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

In realtà, sono un po 'nuovo di JS, quindi sentitevi liberi di mandarmi in un modo migliore.


3
Questo modo di inserire script non è carino, perché stai inquinando lo spazio dei nomi della pagina web. Se la pagina Web utilizza una variabile chiamata formulaImageUrlo codeImageUrl, stai effettivamente distruggendo la funzionalità della pagina. Se si desidera passare una variabile alla pagina Web, suggerisco di allegare i dati all'elemento script ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) e utilizzare ad es. (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();Nello script per accedere ai dati.
Rob W,

@RobW grazie per la tua nota, sebbene riguardi più del campione. Potete per favore chiarire, perché dovrei usare IIFE invece di semplicemente ottenere dataset?
Dmitry Ginzburg,

4
document.currentScriptpunta solo al tag script mentre è in esecuzione. Se vuoi mai accedere al tag script e / o ai suoi attributi / proprietà (ad es. dataset), Devi memorizzarlo in una variabile. Abbiamo bisogno di un IIFE per ottenere una chiusura per archiviare questa variabile senza inquinare lo spazio dei nomi globale.
Rob W,

@RobW eccellente! Ma non possiamo semplicemente usare un nome di variabile, che difficilmente si interseca con quello esistente. È semplicemente non idiomatico o possiamo avere altri problemi con esso?
Dmitry Ginzburg,

2
Potresti, ma il costo dell'utilizzo di un IIFE è trascurabile, quindi non vedo un motivo per preferire l'inquinamento dello spazio dei nomi rispetto a un IIFE. Apprezzo certamente il fatto che non romperò in qualche modo la pagina web degli altri e la capacità di usare nomi di variabili brevi. Un altro vantaggio dell'utilizzo di un IIFE è che puoi uscire dallo script prima se lo desideri ( return;).
Rob W,

6

in Content script, aggiungo un tag di script alla testa che lega un gestore "onmessage", all'interno del gestore che utilizzo, eval per eseguire il codice. Nello script del contenuto dello stand utilizzo anche il gestore di messaggi, quindi ottengo una comunicazione bidirezionale. 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 è un listener di url post messaggio

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

In questo modo, posso avere una comunicazione a 2 vie tra CS e Real Dom. È molto utile, ad esempio, se è necessario ascoltare eventi di webscoket o qualsiasi variabile in memoria o eventi.


1

È possibile utilizzare una funzione di utilità che ho creato allo scopo di eseguire il codice nel contesto della pagina e recuperare il valore restituito.

Questo viene fatto serializzando una funzione in una stringa e iniettandola nella pagina Web.

L'utilità è disponibile qui su GitHub .

Esempi di utilizzo -



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


0

Se si desidera iniettare la funzione pura, anziché il testo, è possibile utilizzare questo metodo:

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');

E puoi passare i parametri (sfortunatamente nessun oggetto e array può essere stretto) alle funzioni. Aggiungilo alle baresesi, in questo modo:

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+")"; 


Questo è piuttosto interessante ... ma la seconda versione, con una variabile per il colore, non funziona per me ... Ottengo "non riconosciuto" e il codice genera un errore ... non lo vede come una variabile.
11
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.