Вставте код у контекст сторінки за допомогою сценарію вмісту


480

Я дізнаюся, як створити розширення для 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.

Коли цей код поміщено в консоль, він спрацював. Що я роблю неправильно?


14
спробуйте видалити лапки навколо назви вашої функції:player.addEventListener("onStateChange", state);
Едуардо

2
Також примітно, що під час написання матчів не забудьте включити https://або http://, це www.youtube.com/*не дозволить вам упакувати розширення і викине помилку роздільника пропущеної схеми
Nilay Vishwakarma

Відповіді:


874

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

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

В іншому випадку, якщо вам не потрібно використовувати chrome.*API, я настійно рекомендую вставити весь свій JS-код на сторінку, додавши <script>тег:

Зміст

  • Спосіб 1: Вставте інший файл
  • Спосіб 2: Вставте вбудований код
  • Спосіб 2б: Використання функції
  • Спосіб 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 відмовиться завантажувати ваш сценарій та відображатиме таку помилку на консолі:

Заперечення завантаження хромованого розширення: // [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');

Спосіб 2б: Використання функції

Для великого фрагмента коду цитування рядка неможливо. Замість використання масиву можна використовувати функцію та строфікувати:

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

81
Ця відповідь має бути частиною офіційних документів. Офіційні документи повинні надсилатись рекомендованим способом -> 3 способи зробити те ж саме ... Неправильно?
Марс Робертсон

7
@ Qantas94Heavy CSP розширення не впливає на скрипти вмісту. Тільки в СНТ сторінки актуальна. Спосіб 1 може бути заблокований за допомогою script-srcдирективи, яка виключає походження розширення, метод 2 може бути заблокований за допомогою CSP, який виключає "небезпечний вбудований" ".
Rob W

3
Хтось запитав, чому я видаляю тег сценарію за допомогою script.parentNode.removeChild(script);. Моя причина для цього полягає в тому, що я люблю прибирати безлад. Коли в документ вставляється вбудований сценарій, він негайно виконується, і <script>тег можна безпечно видалити.
Роб Ш

9
Інший спосіб: використовуйте location.href = "javascript: alert('yeah')";будь-де в сценарії вмісту. Це простіше для коротких фрагментів коду, а також можна отримати доступ до JS-об'єктів сторінки.
Métoule

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

Я фактично пов’язаний з кодом та поясненнями у другому рядку моєї відповіді на stackoverflow.com/questions/9602022/… .
Роб Ш

1
Чи є у вас посилання на ваш оновлений метод (наприклад, звіт про помилку чи тестовий випадок?) CustomEventКонструктор замінює застарілий document.createEventAPI.
Роб Ш

Для мене "dispatchEvent (новий CustomEvent ..." спрацював. У мене Chrome 33. Також він не працював раніше, тому що я написав addEventListener після введення коду js.
jscripter

Будьте особливо уважні до того, що ви передаєте CustomEventконструктору як свій 2-й параметр . У мене виникли 2 дуже заплутаних невдачі: 1. просто розміщення одинарних цитат навколо "деталей" здивовано зробило значення, nullотримане слухачем мого сценарію вмісту. 2. Що ще важливіше, я чомусь повинен був, JSON.parse(JSON.stringify(myData))інакше це теж стане null. Зважаючи на це, мені здається, що наступне твердження розробника Chromium - про те, що алгоритм "структурованого клону" використовується автоматично - не відповідає дійсності. bugs.chromium.org/p/chromium/isissue/detail?id=260378#c18
jdunk

Я думаю, що офіційним способом є використання window.postMessage: developer.chrome.com/extensions/…
Enrique

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, ви ефективно знищуєте її функціональність. Якщо ви хочете передати змінну на веб-сторінку, я пропоную долучити дані до елемента скрипту ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) і використовувати, наприклад, (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();у скрипті для доступу до даних.
Роб Ш

@RobW дякую за вашу замітку, хоча мова йде більше про зразок. Чи можете ви уточнити, чому я повинен використовувати IIFE, а не просто отримувати dataset?
Дмитро Гінзбург

4
document.currentScriptлише вказує на тег сценарію під час його виконання. Якщо ви хочете будь-коли отримати доступ до тегу сценарію та / або його атрибутів / властивостей (наприклад dataset), вам потрібно зберегти його у змінній. Нам потрібен IIFE для закриття для зберігання цієї змінної, не забруднюючи глобальний простір імен.
Роб Ш

@RobW чудово! Але чи не можемо ми просто використати назву змінної, яка навряд чи перетинатиметься з існуючою. Це просто неідіоматично чи ми можемо мати з цим якісь інші проблеми?
Дмитро Гінзбург

2
Можна, але вартість використання IIFE незначна, тому я не бачу причин віддавати перевагу забрудненню простору імен над IIFE. Я, безумовно, ціную те, що я якось не зламаю веб-сторінку інших людей і можливість використовувати короткі імена змінних. Ще одна перевага використання IIFE полягає в тому, що ви можете вийти зі сценарію раніше, якщо захочете ( return;).
Роб Ш

6

У скрипті Content я додаю тег сценарію до голови, який пов'язує обробник "onmessage", всередині обробника, який я використовую, eval для виконання коду. У сценарії вмісту стендів я також використовую оброблювач повідомлення, тому я отримую двосторонню комунікацію. Документи 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 - це слухач 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'


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надцять
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.