web-dev-qa-db-fra.com

Insérer du code dans le contexte de la page à l'aide d'un script de contenu

J'apprends à créer Chrome extensions. Je viens de commencer à en développer un pour capturer les événements YouTube. Je souhaite l’utiliser avec YouTube Flash Player (plus tard, je tenterai de le rendre compatible avec 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!");

Le problème est que la console me donne le "Démarré!" , mais il n'y a pas "État modifié!" lorsque je lis/mets en pause des vidéos YouTube.

Lorsque ce code est mis dans la console, cela a fonctionné. Qu'est-ce que je fais mal?

443
André Alves

Les scripts de contenu sont exécutés dans un environnement "monde isolé" . Vous devez injecter votre méthode state() dans la page elle-même.

Lorsque vous souhaitez utiliser l'une des API _chrome.*_ dans le script, vous devez implémenter un gestionnaire d'événements spécial, comme décrit dans cette réponse: extension Chrome - récupération du message d'origine de Gmail .

Sinon, si vous n'avez pas besoin d'utiliser les API _chrome.*_, je vous recommande fortement d'injecter tout votre code JS dans la page en ajoutant une balise _<script>_:

Table des matières

  • Méthode 1: injecter un autre fichier
  • Méthode 2: injecter du code incorporé
  • Méthode 2b: Utiliser une fonction
  • Méthode 3: Utiliser un événement en ligne
  • Valeurs dynamiques dans le code injecté

Méthode 1: injecter un autre fichier

C'est la méthode la plus simple/la meilleure solution lorsque vous avez beaucoup de code. Incluez votre code JS dans un fichier de votre extension, par exemple _script.js_. Ensuite, laissez votre script de contenu être comme suit (expliqué ici: 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);
_

Remarque: Si vous utilisez cette méthode, le fichier injecté _script.js_ doit être ajouté à la section "web_accessible_resources" ( exemple ). Si vous ne le faites pas, Chrome refusera de charger votre script et affichera l'erreur suivante dans la console:

Refuser la charge de chrome-extension: // [EXTENSIONID] /script.js. Les ressources doivent être répertoriées dans la clé de manifeste web_accessible_resources pour pouvoir être chargées par des pages extérieures à l'extension.

Méthode 2: injecter du code incorporé

Cette méthode est utile lorsque vous souhaitez exécuter rapidement un petit morceau de code. (Voir aussi: Comment désactiver les touches de raccourci facebook avec l'extension 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();
_

Remarque: les littéraux de modèle ne sont pris en charge que dans Chrome 41 et les versions ultérieures. Si vous souhaitez que l'extension fonctionne sous Chrome 40-, utilisez:

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

Méthode 2b: Utiliser une fonction

Pour un gros morceau de code, citer la chaîne n'est pas faisable. Au lieu d'utiliser un tableau, une fonction peut être utilisée et stringifiée:

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

Cette méthode fonctionne, car l'opérateur _+_ sur les chaînes et une fonction convertit tous les objets en chaîne. Si vous avez l'intention d'utiliser le code plus d'une fois, il est recommandé de créer une fonction pour éviter la répétition du code. Une implémentation pourrait ressembler à:

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

Remarque: Puisque la fonction est sérialisée, l'étendue d'origine et toutes les propriétés liées sont perdues!

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

Méthode 3: Utiliser un événement en ligne

Parfois, vous souhaitez exécuter du code immédiatement, par exemple. exécuter du code avant la création de l'élément _<head>_. Cela peut être fait en insérant une balise _<script>_ avec textContent (voir méthode 2/2b).

Une alternative mais non recommandée consiste à utiliser des événements en ligne. Cela n'est pas recommandé car si la page définit une stratégie de sécurité du contenu interdisant les scripts en ligne, les écouteurs d'événements en ligne sont bloqués. Les scripts en ligne injectés par l'extension, par contre, sont toujours exécutés. Si vous souhaitez toujours utiliser des événements en ligne, procédez comme suit:

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

Remarque: Cette méthode suppose qu'il n'y a pas d'autres écouteurs d'événements globaux qui gèrent l'événement reset. Si c'est le cas, vous pouvez également choisir l'un des autres événements globaux. Ouvrez simplement la console JavaScript (F12), tapez _document.documentElement.on_ et choisissez parmi les événements disponibles.

Valeurs dynamiques dans le code injecté

Parfois, vous devez passer une variable arbitraire à la fonction injectée. Par exemple:

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

Pour injecter ce code, vous devez passer les variables comme arguments à la fonction anonyme. Assurez-vous de le mettre en œuvre correctement! Ce qui suit ne fonctionnera pas :

_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 solution consiste à utiliser JSON.stringify avant de passer l'argument. Exemple:

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

Si vous avez plusieurs variables, il vaut la peine d'utiliser _JSON.stringify_ une fois pour améliorer la lisibilité, comme suit:

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

La seule chose manquant La meilleure réponse de Rob W est de savoir comment communiquer entre le script de page injecté et le script de contenu.

Du côté du destinataire (soit votre script de contenu, soit le script de page), ajoutez un écouteur d'événement:

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

Du côté de l'initiateur (contenu ou script de page), envoyez l'événement:

var data = {
  any: 'JSON-ifiable data',
  meaning: 'no DOM elements or classes/functions',
};

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

Assurez-vous que les données transférées sont compatibles JSON

Vous pouvez effectuer explicitement la stringification/l'analyse pour effacer les données non transférables, sinon, seul null sera transféré pour l'ensemble detail dans Chrome moderne, voir crbug.com/9177 = .

  • receveur:

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

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

J'ai également rencontré le problème de la commande de scripts chargés, qui a été résolu par le chargement séquentiel de scripts. Le chargement est basé sur réponse de 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'exemple d'utilisation serait:

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

En fait, je suis un peu novice chez JS, alors n'hésitez pas à me contacter par les meilleurs moyens.

8
Dmitry Ginzburg

dans le script de contenu, j'ajoute une balise de script à la tête qui lie un gestionnaire 'onmessage', dans le gestionnaire que j'utilise, eval pour exécuter du code. Dans le script de contenu du stand, j’utilise également le gestionnaire de message, de sorte que la communication est bidirectionnelle. Docs Chrome

//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 est un écouteur post message url

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

De cette façon, je peux avoir une communication bidirectionnelle entre CS et Real Dom. C'est très utile par exemple si vous avez besoin d'écouter des événements webscoket, ou de n'importe quelle variable ou événement en mémoire.

6
doron aviguy

Si vous souhaitez injecter une fonction pure, au lieu de texte, vous pouvez utiliser cette méthode:

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

Et vous pouvez transmettre des paramètres (malheureusement, aucun objet ni tableau ne peut être stratifié) aux fonctions. Ajoutez-le dans les baréthèses, comme ceci:

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