Wstaw kod do kontekstu strony za pomocą skryptu zawartości

480

Uczę się, jak tworzyć rozszerzenia Chrome. Właśnie zacząłem opracowywać taki, aby rejestrować wydarzenia na YouTube. Chcę go używać z odtwarzaczem flash YouTube (później postaram się uczynić go kompatybilnym z 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!");

Problem polega na tym, że konsola daje mi komunikat „Rozpoczęto!” , ale nie ma „Zmienionego stanu!” kiedy odtwarzam / wstrzymuję filmy z YouTube.

Po umieszczeniu tego kodu w konsoli działało. Co ja robię źle?

André Alves
źródło
14
spróbuj usunąć cudzysłowy wokół nazwy funkcji:player.addEventListener("onStateChange", state);
Eduardo
2
Warto również zauważyć, że pisząc mecze, nie zapomnij dołączyć https://lub http://, www.youtube.com/*co nie pozwoli ci spakować rozszerzenia i wyrzuci błąd separatora brakującego schematu
Nilay Vishwakarma

Odpowiedzi:

874

Skrypty zawartości są wykonywane w środowisku „odizolowanego świata” . Musisz wstrzyknąć swoją state()metodę do samej strony.

Jeśli chcesz użyć jednego z chrome.*interfejsów API w skrypcie, musisz zaimplementować specjalną procedurę obsługi zdarzeń, zgodnie z opisem w odpowiedzi: Rozszerzenie Chrome - pobieranie oryginalnej wiadomości Gmaila .

W przeciwnym razie, jeśli nie musisz używać chrome.*interfejsów API, zdecydowanie zalecamy wstrzyknięcie całego kodu JS na stronę poprzez dodanie <script>tagu:

Spis treści

  • Metoda 1: Wstrzyknięcie innego pliku
  • Metoda 2: Wstrzyknięcie osadzonego kodu
  • Metoda 2b: Korzystanie z funkcji
  • Metoda 3: Wykorzystanie zdarzenia wbudowanego
  • Wartości dynamiczne we wstrzykiwanym kodzie

Metoda 1: Wstrzyknięcie innego pliku

Jest to najłatwiejsza / najlepsza metoda, gdy masz dużo kodu. Powiedzmy, że dołączasz swój rzeczywisty kod JS do pliku w swoim rozszerzeniu script.js. Następnie pozwól, aby Twój skrypt treści wyglądał następująco (wyjaśniono tutaj: Google Chome „Skrót aplikacji” Niestandardowy 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);

Uwaga: Jeśli używasz tej metody, wstrzyknięty script.jsplik należy dodać do "web_accessible_resources"sekcji ( przykład ). Jeśli tego nie zrobisz, Chrome odmówi załadowania skryptu i wyświetli następujący błąd w konsoli:

Odmowa obciążenia rozszerzenia chrome: // [EXTENSIONID] /script.js. Zasoby muszą być wymienione w kluczu manifestu web_accessible_resources, aby mogły zostać załadowane przez strony spoza rozszerzenia.

Metoda 2: Wstrzyknięcie osadzonego kodu

Ta metoda jest przydatna, gdy chcesz szybko uruchomić mały fragment kodu. (Zobacz też: Jak wyłączyć klawisze skrótu Facebooka z rozszerzeniem 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();

Uwaga: literały szablonów są obsługiwane tylko w Chrome 41 i nowszych wersjach. Jeśli chcesz, aby rozszerzenie działało w Chrome 40-, użyj:

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

Metoda 2b: Korzystanie z funkcji

W przypadku dużej części kodu cytowanie ciągu nie jest możliwe. Zamiast używać tablicy, można użyć funkcji i skreślić:

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

Ta metoda działa, ponieważ +operator ciągów i funkcja konwertuje wszystkie obiekty na ciąg. Jeśli zamierzasz używać kodu więcej niż jeden raz, dobrze jest stworzyć funkcję, aby uniknąć powtórzenia kodu. Implementacja może wyglądać następująco:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});

Uwaga: Ponieważ funkcja jest serializowana, oryginalny zakres i wszystkie powiązane właściwości zostają utracone!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"

Metoda 3: Wykorzystanie zdarzenia wbudowanego

Czasami chcesz uruchomić jakiś kod natychmiast, np. Aby uruchomić jakiś kod przed utworzeniem <head>elementu. Można to zrobić, wstawiając <script>znacznik za pomocą textContent(patrz metoda 2 / 2b).

Alternatywą, ale niezalecaną, jest użycie zdarzeń wbudowanych. Nie jest to zalecane, ponieważ jeśli strona definiuje zasadę Content Security, która zabrania wbudowanych skryptów, wówczas wbudowane detektory zdarzeń są blokowane. Z drugiej strony skrypty wbudowane wstrzykiwane przez rozszerzenie nadal działają. Jeśli nadal chcesz korzystać ze zdarzeń wbudowanych, wykonaj następujące czynności:

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

Uwaga: Ta metoda zakłada, że ​​nie ma innych globalnych detektorów zdarzeń, które obsługiwałyby resetzdarzenie. Jeśli tak, możesz wybrać jedno z innych wydarzeń globalnych. Wystarczy otworzyć konsolę JavaScript (F12), wpiszdocument.documentElement.on i wybrać dostępne zdarzenia.

Wartości dynamiczne we wstrzykiwanym kodzie

Czasami musisz przekazać dowolną zmienną do wstrzykiwanej funkcji. Na przykład:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};

Aby wstrzyknąć ten kod, musisz przekazać zmienne jako argumenty do funkcji anonimowej. Pamiętaj, aby poprawnie go wdrożyć! Następujące elementy nie będą działać:

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!

Rozwiązaniem jest użycie JSON.stringifyprzed przekazaniem argumentu. Przykład:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

Jeśli masz wiele zmiennych, warto JSON.stringifyraz użyć , aby poprawić czytelność, jak następuje:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Rob W.
źródło
81
Ta odpowiedź powinna być częścią oficjalnych dokumentów. Oficjalne dokumenty powinny być wysyłane w zalecany sposób -> 3 sposoby, aby zrobić to samo ... Źle?
Mars Robertson,
7
@ Qantas94Heavy CSP rozszerzenia nie wpływa na skrypty treści. Tylko CSP Page'a jest istotne. Metodę 1 można zablokować za pomocą script-srcdyrektywy, która wyklucza pochodzenie rozszerzenia, metodę 2 można zablokować za pomocą CSP, który wyklucza „niebezpieczne wstawianie”.
Rob W
3
Ktoś zapytał, dlaczego usuwam tag skryptu za pomocą script.parentNode.removeChild(script);. Moim powodem jest to, że lubię sprzątać bałagan. Po wstawieniu skryptu wbudowanego do dokumentu jest on natychmiast wykonywany, a <script>znacznik można bezpiecznie usunąć.
Rob W
9
Inna metoda: użyj w location.href = "javascript: alert('yeah')";dowolnym miejscu w skrypcie zawartości. Jest to łatwiejsze w przypadku krótkich fragmentów kodu, a także może uzyskać dostęp do obiektów JS strony.
Métoule,
3
@ChrisP Bądź ostrożny z użyciem javascript:. Kod obejmujący wiele wierszy może nie działać zgodnie z oczekiwaniami. Liniowego komentarz ( //) spowoduje obcinania pozostałej więc będzie ustała location.href = 'javascript:// Do something <newline> alert(0);';. Można to obejść, zapewniając stosowanie komentarzy wieloliniowych. Kolejną rzeczą, na którą należy uważać, jest to, że wynik wyrażenia powinien być nieważny. javascript:window.x = 'some variable';spowoduje rozładowanie dokumentu i zastąpienie go zwrotem „jakaś zmienna”. Przy właściwym zastosowaniu jest to rzeczywiście atrakcyjna alternatywa dla <script>.
Rob W.
61

Jedyną rzeczą brakujący doskonałą odpowiedzią Roba W jest to, jak komunikować się między skryptem wstrzykiwanej strony a skryptem treści.

Po stronie odbierającej (skrypt treści lub skrypt strony wstrzykniętej) dodaj detektor zdarzeń:

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

Po stronie inicjatora (skrypt treści lub skrypt wstrzykiwanej strony) wyślij zdarzenie:

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

Uwagi:

  • Przesyłanie komunikatów DOM wykorzystuje algorytm ustrukturyzowanego klonowania, który może być przesyłany tylko oprócz prymitywnych wartości niektóre typy danych . Nie może wysyłać instancji klas, funkcji ani elementów DOM.
  • W Firefoksie, aby wysłać obiekt (tj. Nie prymitywną wartość) ze skryptu zawartości do kontekstu strony, musisz jawnie sklonować go w celu za pomocą cloneInto(funkcji wbudowanej), w przeciwnym razie zakończy się niepowodzeniem z błędem naruszenia zabezpieczeń .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
laktak
źródło
Ja faktycznie związana z kodem i wyjaśnienia w drugim wierszu moją odpowiedź, aby stackoverflow.com/questions/9602022/... .
Rob W
1
Czy masz odniesienie do zaktualizowanej metody (np. Raport o błędzie lub przypadek testowy?) CustomEventKonstruktor zastępuje przestarzały document.createEventinterfejs API.
Rob W
Dla mnie zadziałało „dispatchEvent (nowe CustomEvent ...”). Mam Chrome 33. Również to nie działało wcześniej, ponieważ napisałem addEventListener po wstrzyknięciu kodu js.
jscripter
Zachowaj szczególną ostrożność w kwestii tego, co przekazujesz jako drugi parametr do CustomEventkonstruktora. Doświadczyłem dwóch bardzo mylących niepowodzeń: 1. po prostu umieszczenie pojedynczych cudzysłowów wokół „szczegółów” wprawiło w zakłopotanie wartość, nullgdy otrzymałem je od słuchacza mojego skryptu treści. 2. Co ważniejsze, z jakiegoś powodu musiałem, bo JSON.parse(JSON.stringify(myData))inaczej też się stanie null. Biorąc to pod uwagę, wydaje mi się, że następujące twierdzenie twórcy Chromium - że algorytm „klonowania strukturalnego” jest używany automatycznie - nie jest prawdziwe. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk
Myślę, że oficjalnym sposobem jest użycie window.postMessage: developer.chrome.com/extensions/…
Enrique
9

Stałem także przed problemem porządkowania załadowanych skryptów, który został rozwiązany poprzez sekwencyjne ładowanie skryptów. Obciążenie jest na podstawie odpowiedzi Rob W 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();
    }
}

Przykładem użycia byłoby:

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

Właściwie jestem trochę nowy w JS, więc zachęcaj mnie do pingowania na lepsze sposoby.

Dmitry Ginzburg
źródło
3
Ten sposób wstawiania skryptów nie jest przyjemny, ponieważ zanieczyszczasz przestrzeń nazw strony internetowej. Jeśli strona korzysta ze zmiennej o nazwie formulaImageUrllub codeImageUrl, to skutecznie niszczysz funkcjonalność strony. Jeśli chcesz przekazać zmienną do strony internetowej, proponuję dołączyć dane do elementu skryptu ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) i użyć np. (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();W skrypcie, aby uzyskać dostęp do danych.
Rob W
@RobW dziękuję za notatkę, chociaż chodzi raczej o próbkę. Czy możesz wyjaśnić, dlaczego powinienem używać IIFE zamiast po prostu otrzymywać dataset?
Dmitry Ginzburg,
4
document.currentScriptwskazuje tylko na znacznik skryptu podczas jego wykonywania. Jeśli kiedykolwiek chcesz uzyskać dostęp do znacznika skryptu i / lub jego atrybutów / właściwości (np. dataset), Musisz zapisać go w zmiennej. Potrzebujemy IIFE, aby uzyskać zamknięcie do przechowywania tej zmiennej bez zanieczyszczania globalnej przestrzeni nazw.
Rob W
@RobW doskonale! Ale czy nie możemy po prostu użyć nazwy zmiennej, która prawie nie przecinałaby się z istniejącą. Czy to tylko nie idiomatyczne, czy możemy mieć z tym jeszcze jakieś problemy?
Dmitry Ginzburg,
2
Możesz, ale koszt korzystania z IIFE jest znikomy, więc nie widzę powodu, aby preferować zanieczyszczenie przestrzeni nazw nad IIFE. Cenię sobie z pewnością to, że nie będę w jakiś sposób łamać stron internetowych innych osób , a także możliwość używania krótkich nazw zmiennych. Kolejną zaletą korzystania z IIFE jest to, że możesz wyjść ze skryptu wcześniej, jeśli chcesz ( return;).
Rob W
6

w skrypcie Content dodaję znacznik script do nagłówka, który wiąże moduł obsługi „onmessage”, wewnątrz modułu obsługi, którego używam, eval do wykonania kodu. W skrypcie zawartości stoiska używam również modułu obsługi onmessage, więc otrzymuję dwukierunkową komunikację. Dokumenty 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 to odbiornik adresu URL wiadomości

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

W ten sposób mogę mieć dwukierunkową komunikację między CS a Real Dom. Jest to bardzo przydatne na przykład, jeśli chcesz odsłuchiwać zdarzenia Webscoket lub dowolne zmienne lub zdarzenia pamięci.

doron aviguy
źródło
1

Możesz użyć funkcji narzędzia, którą utworzyłem, w celu uruchomienia kodu w kontekście strony i odzyskania zwróconej wartości.

Odbywa się to poprzez serializację funkcji do ciągu i wstrzykiwanie jej do strony internetowej.

Narzędzie jest dostępne tutaj na GitHub .

Przykłady użycia -



// 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
źródło
0

Jeśli chcesz wstawić czystą funkcję zamiast tekstu, możesz użyć tej metody:

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

Do funkcji można przekazywać parametry (niestety nie można strugować żadnych obiektów i tablic). Dodaj go do baretheses w następujący sposób:

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

Tarmo Saluste
źródło
To całkiem fajne ... ale druga wersja, ze zmienną dla koloru, nie działa dla mnie ... Dostaję się „nierozpoznany”, a kod zgłasza błąd ... nie widzi go jako zmiennej.
11