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

480

Я учусь создавать расширения Chrome. Я только начал разрабатывать один, чтобы ловить события YouTube. Я хочу использовать его с YouTube Flash Player (позже я постараюсь сделать его совместимым с 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.

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

Андре Алвес
источник
14
попробуйте удалить кавычки вокруг названия вашей функции:player.addEventListener("onStateChange", state);
Эдуардо
2
Следует также отметить , что при написании матчей, не забудьте включить https://или http://, это www.youtube.com/*не позволит вам пакет расширения и будет бросать недостающее ошибку схема сепаратора
Nilay Vishwakarma

Ответы:

875

Сценарии содержимого выполняются в среде «изолированного мира» . Вы должны внедрить свой 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.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([arg1, arg2, arg3, arg4]) + ')';
Роб W
источник
83
Этот ответ должен быть частью официальных документов. Официальные документы должны быть отправлены рекомендованным способом -> 3 способа сделать то же самое ... Не так?
Марс Робертсон
7
@ Qantas94Heavy CSP расширения не влияет на скрипты содержимого. Релевантен только CSP страницы . Метод 1 может быть заблокирован с помощью script-srcдирективы, которая исключает источник расширения, метод 2 может быть заблокирован с помощью CSP, который исключает «unsafe-inline» `.
Роб W
3
Кто-то спросил, почему я удаляю тег script с помощью script.parentNode.removeChild(script);. Моя причина в том, что я люблю убирать беспорядок. Когда встроенный скрипт вставляется в документ, он немедленно исполняется, и <script>тег можно безопасно удалить.
Роб W
9
Другой метод: используйте в location.href = "javascript: alert('yeah')";любом месте вашего скрипта контента. Это проще для коротких фрагментов кода, а также для доступа к JS-объектам страницы.
Метул
3
@ChrisP Будьте осторожны с использованием javascript:. Код, охватывающий несколько строк, может работать не так, как ожидалось. Линия-комментарий ( //) будет укоротить остаток, так что это будет не в состоянии : location.href = 'javascript:// Do something <newline> alert(0);';. Это можно обойти, если вы используете многострочные комментарии. Другая вещь, о которой следует быть осторожным, заключается в том, что результат выражения должен быть пустым. javascript:window.x = 'some variable';приведет к выгрузке документа и замене фразой «некоторая переменная». При правильном использовании это действительно привлекательная альтернатива <script>.
Роб Вт
61

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

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

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

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

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

Ноты:

  • В DOM-сообщениях используется алгоритм структурированного клонирования, который может передавать только некоторые типы данных в дополнение к примитивным значениям. Он не может отправлять экземпляры классов или функции или элементы DOM.
  • В Firefox, чтобы отправить объект (то есть не примитивное значение) из скрипта содержимого в контекст страницы, вы должны явно клонировать его в цель, используя cloneInto(встроенную функцию), иначе он потерпит неудачу с ошибкой нарушения безопасности ,

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
laktak
источник
На самом деле я ссылался на код и объяснение во второй строке моего ответа на stackoverflow.com/questions/9602022/… .
Роб W
1
У вас есть ссылка на ваш обновленный метод (например, отчет об ошибке или контрольный пример?) CustomEventКонструктор заменяет устаревший document.createEventAPI.
Роб W
Для меня 'dispatchEvent (new CustomEvent ...' работал). У меня Chrome 33. Также он не работал раньше, потому что я написал addEventListener после введения кода js.
jscripter
Будьте особенно осторожны с тем, что вы передаете в качестве второго параметра CustomEventконструктору. Я испытал 2 очень запутанных неудачи: 1. просто помещая одинарные кавычки вокруг «детализации», сбивало с толку значение, nullполученное слушателем моего Content Script. 2. Что еще более важно, по какой-то причине я должен был JSON.parse(JSON.stringify(myData))или иначе это также стало бы null. Учитывая это, мне кажется, что следующее утверждение разработчика Chromium - что алгоритм «структурированного клона» используется автоматически - не соответствует действительности. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk
Я думаю, что официальным способом является использование window.postMessage: developer.chrome.com/extensions/…
Энрике
9

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

Дмитрий Гинзбург
источник
3
Этот способ вставки скриптов не очень хорош, потому что вы загрязняете пространство имен веб-страницы. Если веб-страница использует переменную с именем formulaImageUrlили codeImageUrl, то вы фактически разрушаете функциональность страницы. Если вы хотите передать переменную на веб-страницу, я предлагаю присоединить данные к элементу script ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) и использовать, например, (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();в сценарии для доступа к данным.
Роб W
@RobW спасибо за вашу заметку, хотя это больше про образец. Не могли бы вы уточнить, почему я должен использовать IIFE вместо того, чтобы просто получать dataset?
Дмитрий Гинзбург
4
document.currentScriptуказывает только на тег сценария во время его выполнения. Если вы когда-нибудь захотите получить доступ к тегу скрипта и / или его атрибутам / свойствам (например dataset), вам нужно сохранить его в переменной. Нам нужен IIFE, чтобы получить замыкание для хранения этой переменной без загрязнения глобального пространства имен.
Роб W
@RobW отлично! Но мы не можем просто использовать имя переменной, которая вряд ли пересекается с существующей. Это просто не идиоматично или у нас могут быть другие проблемы с этим?
Дмитрий Гинзбург
2
Вы могли бы, но стоимость использования IIFE незначительна, поэтому я не вижу причин предпочитать загрязнение пространства имен по сравнению с IIFE. Я ценю то, что я не буду каким-то образом ломать веб-страницу других , и способность использовать короткие имена переменных. Еще одним преимуществом использования IIFE является то, что вы можете выйти из сценария раньше, если хотите ( return;).
Роб W
6

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

Дорон Авигуй
источник
1

Вы можете использовать созданную мной служебную функцию для запуска кода в контексте страницы и получения возвращенного значения.

Это делается путем сериализации функции в строку и внедрения ее на веб-страницу.

Утилита доступна здесь, на 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'

Arik
источник
0

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

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

Тармо Салусте
источник
Это довольно круто ... но вторая версия, с переменной для цвета, не работает для меня ... Я получаю "нераспознанный", и код выдает ошибку ... не видит ее как переменную.
11