Вставьте код в контекст страницы, используя скрипт содержимого
Я учусь создавать расширения Chrome. Я только начал разрабатывать один, чтобы ловить события YouTube. Я хочу использовать его с флеш-плеером YouTube (позже я постараюсь сделать его совместимым с 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!");
Проблема в том, что консоль дает мне "Начато!", но нет "Государство изменилось!" когда я играю / приостанавливаю видео на YouTube.
Когда этот код помещен в консоль, это сработало. Что я делаю неправильно?
7 ответов
Сценарии содержимого выполняются в среде "изолированного мира". Вы должны ввести свой state()
метод в самой странице.
Когда вы хотите использовать один из chrome.*
API-интерфейсы в сценарии, вы должны реализовать специальный обработчик событий, как описано в этом ответе: Расширение Chrome - получение исходного сообщения Gmail.
В противном случае, если вам не нужно использовать chrome.*
API, я настоятельно рекомендую внедрить весь код JS на странице, добавив <script>
тег:
Оглавление
- Способ 1: добавить другой файл
- Способ 2: внедрить встроенный код
- Способ 2b: использование функции
- Способ 3: использование встроенного события
- Динамические значения в введенном коде
Способ 1: добавить другой файл
Это самый простой / лучший метод, когда у вас много кода. Включите ваш фактический код JS в файл в вашем расширении, скажем, script.js
, Затем пусть ваш контент-скрипт будет следующим (объяснено здесь: Google Chome "Ярлык приложения", пользовательский Javascript):
var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.extension.getURL('script.js');
s.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
Примечание: если вы используете этот метод, введенный script.js
файл должен быть добавлен в "web_accessible_resources"
раздел( пример). Если вы этого не сделаете, Chromeоткажется загружать ваш скрипт и отобразит в консоли следующую ошибку:
Запрещение загрузки chrome-extension://[EXTENSIONID]/script.js. Ресурсы должны быть перечислены в ключе манифеста web_accessible_resources, чтобы их могли загружать страницы за пределами расширения.
Способ 2: внедрить встроенный код
Этот метод полезен, когда вы хотите быстро запустить небольшой фрагмент кода. (См. Также: Как отключить горячие клавиши facebook с расширением 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();
Примечание: литералы шаблона поддерживаются только в Chrome 41 и выше. Если вы хотите, чтобы расширение работало в Chrome 40-, используйте:
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');
Способ 2b: использование функции
Для большой части кода цитирование строки неосуществимо. Вместо использования массива можно использовать функцию и строку:
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();
Этот метод работает, потому что+
оператор на строки и функция преобразует все объекты в строку. Если вы собираетесь использовать код более одного раза, целесообразно создать функцию, чтобы избежать повторения кода. Реализация может выглядеть так:
function injectScript(func) {
var actualCode = '(' + func + ')();'
...
}
injectScript(function() {
alert("Injected script");
});
Примечание. Так как функция сериализована, исходная область действия и все связанные свойства теряются!
var scriptToInject = function() {
console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output: "undefined"
Способ 3: использование встроенного события
Иногда вы хотите запустить какой-то код немедленно, например, запустить некоторый код до <head>
элемент создан. Это можно сделать, вставив<script>
пометить сtextContent
(см. метод 2/2b).
Альтернативой,но не рекомендуется, является использование встроенных событий. Это не рекомендуется, потому что если на странице определена политика безопасности содержимого, запрещающая встроенные сценарии, то прослушиватели встроенных событий блокируются. С другой стороны, встроенные сценарии, введенные расширением, все еще работают. Если вы все еще хотите использовать встроенные события, вот как:
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');
Примечание. Этот метод предполагает, что нет других глобальных прослушивателей событий, которые обрабатываютreset
событие. Если есть, вы также можете выбрать одно из других глобальных событий. Просто откройте консоль JavaScript (F12), введитеdocument.documentElement.on
и выберите из доступных событий.
Динамические значения в введенном коде
Иногда вам нужно передать произвольную переменную введенной функции. Например:
var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
alert(GREETING + NAME);
};
Чтобы внедрить этот код, вам нужно передать переменные в качестве аргументов анонимной функции. Будьте уверены, чтобы осуществить это правильно! Следующеене будет работать:
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!
Решение заключается в использованииJSON.stringify
прежде чем передать аргумент. Пример:
var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';
Если у вас много переменных, стоит использовать JSON.stringify
один раз, чтобы улучшить читаемость, следующим образом:
...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Единственная вещь отсутствует отличный от Row W отличный ответ - как вызывать внедренный сценарий к сценарию содержимого и наоборот (особенно, если у вас есть объекты, которые нельзя преобразовать в строку).
В сценарии ввода или в свой контент добавьте прослушиватель событий:
document.addEventListener('yourCustomEvent', function (e)
{
var data=e.detail;
console.log("received "+data);
});
С другой стороны (контент или внедренный скрипт) вызвать событие:
var data="anything";
// updated: this works with Chrome 30:
var evt=document.createEvent("CustomEvent");
evt.initCustomEvent("yourCustomEvent", true, true, data);
document.dispatchEvent(evt);
// the following stopped working in Chrome 30 (Windows), detail was
// not received in the listener:
// document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));
Вы можете использовать созданную мной служебную функцию для запуска кода в контексте страницы и получения возвращенного значения.
Это делается путем сериализации функции в строку и внедрения ее на веб-страницу.
Утилита доступна здесь, на GitHub.
Примеры использования -
// 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'
Я также столкнулся с проблемой упорядочения загруженных скриптов, которая была решена путем последовательной загрузки скриптов. Загрузка основана на ответе Роба В.
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();
}
}
Пример использования будет:
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")
]);
На самом деле, я немного новичок в JS, так что не стесняйтесь пинговать меня в лучшую сторону.
В Content script я добавляю скрипт script к заголовку, который связывает обработчик onmessage, внутри обработчика, который я использую, eval для выполнения кода. В скрипте содержимого стенда я также использую обработчик onmessage, поэтому я получаю двустороннюю связь. 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 - это сообщение прослушивателя 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");
Таким образом, я могу иметь двухстороннюю связь между CS и Real Dom. Это очень полезно, например, если вам нужно прослушать события веб-скокета или какие-либо переменные или события в памяти.
Если вы хотите ввести чистую функцию вместо текста, вы можете использовать этот метод:
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');
И вы можете передавать параметры (к сожалению, никакие объекты и массивы не могут быть преобразованы в строку) в функции. Добавьте это в голые вещи, вот так:
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+")";
Если вы хотите использовать динамические значения во внедренном коде (ManifestV3), а тип внедренного сценария — модуль, вы не можете использоватьdocument.currentScript.dataset
как описано в замечательном ответе Роба, вместо этого вы можете передавать параметры в качестве параметра URL-адреса и получать их во внедренном коде. Вот в примере:
Сценарий контента:
var s = document.createElement('script');
s.src = chrome.runtime.getURL('../override-script.js?extensionId=' + chrome.runtime.id);
s.type = 'module';
s.onload = function () {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
Внедренный код (в моем случае override-script.js):
let extensionId = new URL(import.meta.url).searchParams.get("extensionId")