Вставьте код в контекст страницы, используя скрипт содержимого

Я учусь создавать расширения 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")
Другие вопросы по тегам