André Alves
Estoy aprendiendo a crear extensiones de Chrome. Empecé a desarrollar uno para captar eventos de YouTube. Quiero usarlo con YouTube flash player (luego intentaré hacerlo compatible con HTML5).
manifiesto.json:
{
"name": "MyExtension",
"version": "1.0",
"description": "Gotta catch Youtube events!",
"permissions": ["tabs", "http://*/*"],
"content_scripts" : [{
"matches" : [ "www.youtube.com/*"],
"js" : ["myScript.js"]
}]
}
miScript.js:
function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");
El problema es que la consola me da la “¡Empezado!”pero no hay “¡Estado cambiado!” cuando reproduzco/pauso videos de YouTube.
Cuando este código se puso en la consola, funcionó. ¿Qué estoy haciendo mal?
robar w
Causa subyacente:
Los scripts de contenido se ejecutan en un “mundo aislado” ambiente.
Solución:
Inyecte el código en la página usando DOM; ese código podrá acceso funciones/variables del contexto de la página (“mundo principal”) o exponer funciones/variables al contexto de la página (en su caso es el state()
método).
-
Nota en caso de que se necesite comunicación con el script de la página:
Usar DOM
CustomEvent
manipulador. Ejemplos: uno, dos y tres. -
Nota en caso
chrome
Se necesita API en el script de la página:Ya que
chrome.*
Las API no se pueden usar en la secuencia de comandos de la página, debe usarlas en la secuencia de comandos de contenido y enviar los resultados a la secuencia de comandos de la página a través de mensajes DOM (consulte la nota anterior).
Advertencia de seguridad:
Una página puede redefinir o aumentar/enganchar un prototipo incorporado, por lo que su código expuesto puede fallar si la página lo hizo de manera incompatible. Si desea asegurarse de que su código expuesto se ejecute en un entorno seguro, debe a) declarar su secuencia de comandos de contenido con “run_at”: “document_start” y use los Métodos 2-3, no 1, o b) extraiga los integrados nativos originales a través de un iframe vacío, por ejemplo. Tenga en cuenta que con document_start
es posible que necesite usar DOMContentLoaded
evento dentro del código expuesto para esperar DOM.
Tabla de contenido
- Método 1: inyectar otro archivo – compatible con ManifestV3
- Método 2: Inyectar código incrustado – MV2
- Método 2b: Usando una función – MV2
- Método 3: Uso de un evento en línea: compatible con ManifestV3
- Método 4: usar el mundo de executeScript – solo ManifestV3
- Valores dinámicos en el código inyectado
Método 1: inyectar otro archivo (ManifestV3/MV2)
Particularmente bueno cuando tienes mucho código. Ponga el código en un archivo dentro de su extensión, digamos script.js
. Luego cárguelo en su script de contenido de esta manera:
var s = document.createElement('script');
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
El archivo js debe estar expuesto en web_accessible_resources
:
-
ejemplo de manifest.json para ManifestV2
"web_accessible_resources": ["script.js"],
-
ejemplo de manifest.json para ManifestV3
"web_accessible_resources": [{ "resources": ["script.js"], "matches": ["<all_urls>"] }]
De lo contrario, aparecerá el siguiente error en la consola:
Denegar la carga de chrome-extension://[EXTENSIONID]/script.js. Los recursos deben estar incluidos en la clave de manifiesto web_accessible_resources para que las páginas fuera de la extensión los carguen.
Método 2: Inyectar código incrustado (MV2)
Este método es útil cuando desea ejecutar rápidamente un pequeño fragmento de código. (Consulte también: ¿Cómo deshabilitar las teclas de acceso rápido de Facebook con la extensión de 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: literales de plantilla solo se admiten en Chrome 41 y superior. Si desea que la extensión funcione en Chrome 40-, use:
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étodo 2b: Usando una función (MV2)
Para una gran parte del código, citar la cadena no es factible. En lugar de usar una matriz, se puede usar una función y convertirla en cadenas:
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();
Este método funciona, porque el +
operador en cadenas y una función convierte todos los objetos en una cadena. Si tiene la intención de usar el código más de una vez, es aconsejable crear una función para evitar la repetición del código. Una implementación podría verse así:
function injectScript(func) {
var actualCode="(" + func + ')();'
...
}
injectScript(function() {
alert("Injected script");
});
Nota: Dado que la función está serializada, el alcance original y todas las propiedades vinculadas se pierden.
var scriptToInject = function() {
console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output: "undefined"
Método 3: Usar un evento en línea (ManifestV3/MV2)
A veces, desea ejecutar algún código inmediatamente, por ejemplo, para ejecutar algún código antes de la <head>
se crea el elemento. Esto se puede hacer insertando un <script>
etiqueta con textContent
(ver método 2/2b).
Una alternativa, pero no recomendado es usar eventos en línea. No se recomienda porque si la página define una política de seguridad de contenido que prohíbe los scripts en línea, se bloquean los detectores de eventos en línea. Los scripts en línea inyectados por la extensión, por otro lado, aún se ejecutan. Si aún desea usar eventos en línea, así es como:
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: este método asume que no hay otros detectores de eventos globales que manejen el reset
evento. Si lo hay, también puede elegir uno de los otros eventos globales. Simplemente abra la consola de JavaScript (F12), escriba document.documentElement.on
y seleccione uno de los eventos disponibles.
Método 4: Usar la API de chrome.scripting world
(Solo manifiesto V3)
- Chrome 95 o posterior,
chrome.scripting.executeScript
conworld: 'MAIN'
- Chrome 102 o posterior,
chrome.scripting.registerContentScripts
conworld: 'MAIN'
también permiterunAt: 'document_start'
para garantizar la ejecución temprana del script de la página.
A diferencia de los otros métodos, este es para la secuencia de comandos de fondo o la secuencia de comandos emergente, no para la secuencia de comandos de contenido. Ver el documentación y ejemplos
Valores dinámicos en el código inyectado (MV2)
Ocasionalmente, necesita pasar una variable arbitraria a la función inyectada. Por ejemplo:
var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
alert(GREETING + NAME);
};
Para inyectar este código, debe pasar las variables como argumentos a la función anónima. ¡Asegúrate de implementarlo correctamente! La siguiente voluntad no trabajar:
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 solución es usar JSON.stringify
antes de pasar el argumento. Ejemplo:
var actualCode="(" + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';
Si tiene muchas variables, vale la pena usar JSON.stringify
una vez, para mejorar la legibilidad, de la siguiente manera:
...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]).slice(1, -1) + ')';
Valores dinámicos en el código inyectado (ManifestV3)
-
El método 1 puede establecer la URL del elemento del script en el script de contenido:
s.src = chrome.runtime.getURL('script.js?') + new URLSearchParams({foo: 1});
Entonces script.js puede leerlo:
const params = new URLSearchParams(document.currentScript.src.split('?')[1]); console.log(params.get('foo'));
-
El método 4 executeScript tiene
args
parámetro, registerContentScripts actualmente no lo hace (esperemos que se agregue en el futuro).
-
Esta respuesta debería ser parte de los documentos oficiales. Los documentos oficiales deben enviarse de la forma recomendada –> 3 formas de hacer lo mismo… ¿Error?
– Marte Robertson
9 de junio de 2013 a las 11:17
-
Por lo general, el método 1 es mejor siempre que sea posible, debido a las restricciones de CSP (política de seguridad de contenido) de Chrome para algunas extensiones.
– Qantas 94 Pesado
2 de agosto de 2013 a las 2:33
-
@Qantas94Heavy El CSP de la extensión sí no afectar a los guiones de contenido. Solo el CSP de la página es relevante. El método 1 se puede bloquear usando un
script-src
directiva que excluye el origen de la extensión, el método 2 se puede bloquear usando un CSP que excluye “unsafe-inline”`.– Rob W.
2 de agosto de 2013 a las 7:24
-
Alguien preguntó por qué elimino la etiqueta del script usando
script.parentNode.removeChild(script);
. Mi razón para hacerlo es porque me gusta limpiar mi desorden. Cuando se inserta un script en línea en el documento, se ejecuta inmediatamente y el<script>
la etiqueta se puede quitar con seguridad.– Rob W.
29 de agosto de 2013 a las 13:35
-
Otro método: usar
location.href = "javascript: alert('yeah')";
en cualquier parte de su script de contenido. Es más fácil para fragmentos cortos de código y también puede acceder a los objetos JS de la página.– Métoule
26/09/2013 a las 21:08
laktak
La única cosa perdido oculto de la excelente respuesta de Rob W es cómo comunicarse entre el script de página inyectado y el script de contenido.
En el lado receptor (ya sea su secuencia de comandos de contenido o la secuencia de comandos de la página inyectada), agregue un detector de eventos:
document.addEventListener('yourCustomEvent', function (e) {
var data = e.detail;
console.log('received', data);
});
En el lado del iniciador (script de contenido o script de página inyectado) envíe el 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 }));
Notas:
- La mensajería DOM utiliza un algoritmo de clonación estructurado, que solo puede transferir algunos tipos de datos además de los valores primitivos. No puede enviar instancias de clase o funciones o elementos DOM.
-
En Firefox, para enviar un objeto (es decir, no un valor primitivo) desde el script de contenido al contexto de la página, debe clonarlo explícitamente en el objetivo usando
cloneInto
(una función integrada), de lo contrario fallará con un error de violación de seguridad.document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: cloneInto(data, document.defaultView), }));
-
De hecho, me he vinculado al código y la explicación en la segunda línea de mi respuesta, a stackoverflow.com/questions/9602022/….
– Rob W.
11/10/2013 a las 9:00
-
¿Tiene una referencia para su método actualizado (por ejemplo, un informe de error o un caso de prueba?) El
CustomEvent
constructor reemplaza el obsoletodocument.createEvent
API.– Rob W.
6 de noviembre de 2013 a las 16:23
-
Para mí, ‘dispatchEvent (new CustomEvent…’ funcionó. Tengo Chrome 33. Tampoco funcionó antes porque escribí addEventListener después de inyectar el código js.
– jscripter
10 de marzo de 2014 a las 9:15
-
Creo que la forma oficial es usar window.postMessage: desarrollador.chrome.com/extensiones/…
– Enrique
15 de diciembre de 2018 a las 13:08
-
cómo enviar una respuesta desde la secuencia de comandos de contenido a la secuencia de comandos del iniciador
– Vinay
16 de septiembre de 2019 a las 7:41
Dmitri Ginzburg
También me enfrenté al problema de ordenar los scripts cargados, que se resolvió mediante la carga secuencial de scripts. La carga se basa en la respuesta 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();
}
}
El ejemplo de uso sería:
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 realidad, soy un poco nuevo en JS, así que siéntete libre de enviarme un ping para encontrar mejores formas.
-
Esta forma de insertar secuencias de comandos no es agradable, porque está contaminando el espacio de nombres de la página web. Si la página web usa una variable llamada
formulaImageUrl
ocodeImageUrl
, entonces estás destruyendo efectivamente la funcionalidad de la página. Si desea pasar una variable a la página web, le sugiero que adjunte los datos al elemento del script (e.g. script.dataset.formulaImageUrl = formulaImageUrl;
) y usar, por ejemplo(function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();
en el script para acceder a los datos.– Rob W.
17 mayo 2015 a las 17:27
-
@RobW gracias por tu nota, aunque se trata más de la muestra. ¿Puede aclarar por qué debería usar IIFE en lugar de simplemente obtener
dataset
?– Dmitri Ginzburg
17 mayo 2015 a las 17:36
-
document.currentScript
solo apunta a la etiqueta del script mientras se está ejecutando. Si alguna vez desea acceder a la etiqueta del script y/o sus atributos/propiedades (por ejemplo,dataset
), entonces necesita almacenarlo en una variable. Necesitamos un IIFE para obtener un cierre para almacenar esta variable sin contaminar el espacio de nombres global.– Rob W.
17 mayo 2015 a las 17:38
-
@RobW excelente! Pero, ¿no podemos simplemente usar algún nombre de variable, que difícilmente se cruzaría con el existente? ¿Es simplemente no idiomático o podemos tener otros problemas con eso?
– Dmitri Ginzburg
17 mayo 2015 a las 17:51
-
Podría, pero el costo de usar un IIFE es insignificante, por lo que no veo una razón para preferir la contaminación del espacio de nombres sobre un IIFE. Valoro la certeza de que no romperé la página web. de otros de alguna manera, y la capacidad de usar nombres de variables cortos. Otra ventaja de usar un IIFE es que puede salir del script antes si lo desea (
return;
).– Rob W.
17 mayo 2015 a las 17:58
en el script de contenido, agrego una etiqueta de script al encabezado que vincula un controlador ‘onmessage’, dentro del controlador que uso, eval para ejecutar el código. En el script de contenido de la cabina, también uso el controlador de mensajes, por lo que obtengo una comunicación bidireccional.
Documentos de 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 es un detector de URL de mensaje posterior
//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 esta manera, puedo tener una comunicación bidireccional entre CS y Real Dom. Es muy útil, por ejemplo, si necesita escuchar eventos de webscoket, o cualquier variable o evento en memoria.
Arik
Puede usar una función de utilidad que he creado con el fin de ejecutar código en el contexto de la página y recuperar el valor devuelto.
Esto se hace serializando una función en una cadena e inyectándola en la página web.
la utilidad es disponible aquí en GitHub.
ejemplos de uso –
// 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'
Si desea inyectar una función pura, en lugar de texto, puede usar este método:
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');
Y puede pasar parámetros (lamentablemente, no se pueden clasificar objetos ni matrices) a las funciones. Añádelo a las baretheses, así:
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+")";
intente eliminar las comillas alrededor del nombre de su función:
player.addEventListener("onStateChange", state);
– Eduardo
1 de marzo de 2012 a las 17:13
También es notable que al escribir coincidencias, no olvide incluir
https://
ohttp://
estewww.youtube.com/*
no te dejaría empacar extensión y te tiraría Error de separador de esquema faltante– Nilay Vishwakarma
31 de enero de 2017 a las 19:34
Ver también bugs.chromium.org/p/chromium/issues/detail?id=478183
– Pacerier
15 de julio de 2017 a las 19:21