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

Я изучаю, как создавать расширения 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!");

Проблема в том, что консоль дает мне "Started!", но нет "State Changed!". когда я проигрываю/приостанавливаю видеоролики YouTube.

Когда этот код помещается в консоль, он работал. Что я делаю неправильно?

Ответ 1

Сценарии содержимого выполняются в среде "изолированного мира". Вы должны внедрить свой метод state() в саму страницу.

Если вы хотите использовать в сценарии один из API chrome.*, Вам необходимо реализовать специальный обработчик событий, как описано в этом ответе: Расширение Chrome - получение исходного сообщения Gmail.

В противном случае, если вам не нужно использовать API-интерфейсы chrome.*, Я настоятельно рекомендую добавить весь код 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.runtime.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 ниже:

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

Ответ 2

Единственная вещь отсутствует скрыто от Роба В. Отличный ответ - как установить связь между введенным скриптом страницы и скриптом контента.

На принимающей стороне (ваш скрипт контента или скрипт страницы) добавьте прослушиватель событий:

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

На стороне инициатора (контент или скрипт страницы) отправьте событие:

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

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

Убедитесь, что переданные данные являются JSON-ifiable

Вы можете выполнить строковое преобразование/синтаксический анализ явным образом, чтобы удалить непередаваемые данные, в противном случае только null будет перенесен для всей detail в современном Chrome, см. Crbug.com/917703.

  • получатель:

    document.addEventListener('yourCustomEvent', function (e) {
      var data = JSON.parse(e.detail);
      console.log('received', data);
    });
    
  • инициатор:

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

Ответ 3

Я также столкнулся с проблемой упорядочения загруженных скриптов, которая была решена путем последовательной загрузки скриптов. Загрузка основана на Отвечать 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();
    }
}

Пример использования:

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, поэтому не стесняйтесь пинговать меня до лучших способов.

Ответ 4

в скрипте содержимого я добавляю тег сценария в голову, который связывает обработчик '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 является почтовым сообщением

//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. Это очень полезно, например, если вам нужно прослушивать события webscoket или любые переменные или события памяти.

Ответ 5

Если вы хотите ввести чистую функцию вместо текста, вы можете использовать этот метод:

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