Masukkan kode ke dalam konteks halaman menggunakan skrip konten

480

Saya sedang belajar cara membuat ekstensi Chrome. Saya baru saja mulai mengembangkan satu untuk menangkap acara YouTube. Saya ingin menggunakannya dengan pemutar flash YouTube (nanti saya akan mencoba membuatnya kompatibel dengan 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!");

Masalahnya adalah konsol memberi saya "Memulai!" , tetapi tidak ada "Negara Berubah!" ketika saya memutar / menghentikan video YouTube.

Ketika kode ini diletakkan di konsol, itu berhasil. Apa yang saya lakukan salah?

André Alves
sumber
14
coba hapus tanda kutip di sekitar nama fungsi Anda:player.addEventListener("onStateChange", state);
Eduardo
2
Juga penting bahwa ketika menulis pertandingan, jangan lupa untuk memasukkan https://atau http://, ini www.youtube.com/*tidak akan membiarkan Anda mengemas ekstensi dan akan membuat kesalahan pemisah skema Hilang
Nilay Vishwakarma

Jawaban:

875

Skrip konten dieksekusi di lingkungan "dunia terisolasi" . Anda harus menyuntikkan state()metode Anda ke halaman itu sendiri.

Saat Anda ingin menggunakan salah satu chrome.*API dalam skrip, Anda harus menerapkan pengendali acara khusus, seperti yang dijelaskan dalam jawaban ini: Ekstensi Chrome - mengambil pesan asli Gmail .

Jika tidak, jika Anda tidak harus menggunakan chrome.*API, saya sangat menyarankan untuk menyuntikkan semua kode JS Anda di halaman melalui menambahkan <script>tag:

Daftar Isi

  • Metode 1: Suntikkan file lain
  • Metode 2: Suntikkan kode tertanam
  • Metode 2b: Menggunakan fungsi
  • Metode 3: Menggunakan acara inline
  • Nilai dinamis dalam kode yang disuntikkan

Metode 1: Suntikkan file lain

Ini adalah metode termudah / terbaik ketika Anda memiliki banyak kode. Sertakan kode JS Anda yang sebenarnya dalam file dalam ekstensi Anda, katakanlah script.js. Kemudian biarkan skrip konten Anda sebagai berikut (dijelaskan di sini: Google Chome "Pintasan Aplikasi" Javascript Khusus ):

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

Catatan: Jika Anda menggunakan metode ini, script.jsfile yang disuntikkan harus ditambahkan ke "web_accessible_resources"bagian ( contoh ). Jika tidak, Chrome akan menolak memuat skrip Anda dan menampilkan kesalahan berikut di konsol:

Menolak memuat ekstensi chrome: // [EXTENSIONID] /script.js. Sumber daya harus dicantumkan dalam kunci manifes web_accessible_resources agar dapat dimuat oleh halaman di luar ekstensi.

Metode 2: Suntikkan kode tertanam

Metode ini berguna ketika Anda ingin menjalankan sepotong kode dengan cepat. (Lihat juga: Cara menonaktifkan hotkey facebook dengan ekstensi 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();

Catatan: literal templat hanya didukung di Chrome 41 dan di atasnya. Jika Anda ingin ekstensi berfungsi di Chrome 40-, gunakan:

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

Metode 2b: Menggunakan fungsi

Untuk sejumlah besar kode, mengutip string tidak layak. Alih-alih menggunakan array, sebuah fungsi dapat digunakan, dan dirender:

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

Metode ini berfungsi, karena +operator pada string dan fungsi mengubah semua objek menjadi string. Jika Anda berniat menggunakan kode lebih dari sekali, sebaiknya buat fungsi untuk menghindari pengulangan kode. Suatu implementasi mungkin terlihat seperti:

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

Catatan: Karena fungsi ini diserialisasi, ruang lingkup asli, dan semua properti terikat hilang!

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

Metode 3: Menggunakan acara inline

Terkadang, Anda ingin menjalankan beberapa kode segera, misalnya untuk menjalankan beberapa kode sebelum <head>elemen dibuat. Ini dapat dilakukan dengan memasukkan <script>tag dengan textContent(lihat metode 2 / 2b).

Alternatif, tetapi tidak disarankan adalah menggunakan acara inline. Tidak disarankan karena jika halaman menetapkan kebijakan Keamanan Konten yang melarang skrip inline, maka acara inline pendengar diblokir. Skrip sebaris yang disuntikkan oleh ekstensi, di sisi lain, masih berjalan. Jika Anda masih ingin menggunakan acara sebaris, ini caranya:

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

Catatan: Metode ini mengasumsikan bahwa tidak ada pendengar acara global lain yang menangani resetacara tersebut. Jika ada, Anda juga dapat memilih salah satu acara global lainnya. Cukup buka konsol JavaScript (F12), ketik document.documentElement.on, dan pilih dari acara yang tersedia.

Nilai dinamis dalam kode yang disuntikkan

Kadang-kadang, Anda harus meneruskan variabel arbitrer ke fungsi yang disuntikkan. Sebagai contoh:

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

Untuk menyuntikkan kode ini, Anda harus meneruskan variabel sebagai argumen ke fungsi anonim. Pastikan untuk menerapkannya dengan benar! Berikut ini tidak akan berfungsi:

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!

Solusinya adalah menggunakan JSON.stringifysebelum melewati argumen. Contoh:

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

Jika Anda memiliki banyak variabel, sebaiknya gunakan JSON.stringifysekali, untuk meningkatkan keterbacaan, sebagai berikut:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Rob W
sumber
83
Jawaban ini harus menjadi bagian dari dokumen resmi. Dokumen resmi harus dikirimkan dengan cara yang disarankan -> 3 cara untuk melakukan hal yang sama ... Salah?
Mars Robertson
7
@ Qantas94Heavy CSP ekstensi tidak memengaruhi skrip konten. Hanya CSP halaman yang relevan. Metode 1 dapat diblokir dengan menggunakan script-srcarahan yang mengecualikan asal ekstensi, metode 2 dapat diblokir dengan menggunakan CSP yang mengecualikan "tidak aman-sebaris" `.
Rob W
3
Seseorang bertanya mengapa saya menghapus tag skrip menggunakan script.parentNode.removeChild(script);. Alasan saya melakukannya adalah karena saya suka membersihkan kekacauan saya. Ketika skrip inline dimasukkan ke dalam dokumen, skrip segera dieksekusi dan <script>tag dapat dengan aman dihapus.
Rob W
9
Metode lain: gunakan location.href = "javascript: alert('yeah')";di mana saja dalam skrip konten Anda. Lebih mudah untuk cuplikan kode pendek, dan juga dapat mengakses objek JS halaman.
Métoule
3
@ ChrisP Hati-hati dengan menggunakan javascript:. Kode yang mencakup beberapa baris mungkin tidak berfungsi seperti yang diharapkan. Sebuah garis-komentar ( //) akan memotong sisanya, jadi ini akan gagal: location.href = 'javascript:// Do something <newline> alert(0);';. Ini dapat dielakkan dengan memastikan bahwa Anda menggunakan komentar multi-baris. Hal lain yang harus diperhatikan adalah bahwa hasil ekspresi harus batal. javascript:window.x = 'some variable';akan menyebabkan dokumen diturunkan, dan diganti dengan frasa 'some variable'. Jika digunakan dengan benar, itu memang alternatif yang menarik <script>.
Rob W
61

Satu-satunya hilang tersembunyi dari jawaban sempurna Rob W adalah bagaimana berkomunikasi antara skrip halaman yang disuntikkan dan skrip konten.

Di sisi penerima (skrip konten Anda atau skrip halaman yang disuntikkan) tambahkan pendengar acara:

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

Di sisi inisiator (skrip konten atau skrip halaman yang disuntikkan) kirim acara:

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

Catatan:

  • Olahpesan DOM menggunakan algoritma kloning terstruktur, yang dapat mentransfer hanya beberapa jenis data selain nilai-nilai primitif. Itu tidak bisa mengirim instance kelas atau fungsi atau elemen DOM.
  • Di Firefox, untuk mengirim objek (yaitu bukan nilai primitif) dari skrip konten ke konteks halaman Anda harus mengkloningnya secara eksplisit ke dalam target menggunakan cloneInto(fungsi bawaan), jika tidak maka akan gagal dengan kesalahan pelanggaran keamanan .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
laktak
sumber
Saya sebenarnya telah menautkan ke kode dan penjelasan di baris kedua dari jawaban saya, ke stackoverflow.com/questions/9602022/… .
Rob W
1
Apakah Anda memiliki referensi untuk metode Anda yang diperbarui (misalnya laporan bug atau uji kasus?) CustomEventKonstruktor menggantikan document.createEventAPI yang sudah usang .
Rob W
Bagi saya 'dispatchEvent (CustomEvent baru ...' berfungsi. Saya punya Chrome 33. Juga tidak berfungsi sebelumnya karena saya menulis addEventListener setelah menyuntikkan kode js.
jscripter
Berhati-hatilah dengan apa yang Anda berikan sebagai parameter ke-2 Anda kepada CustomEventkonstruktor. Saya mengalami 2 kemunduran yang sangat membingungkan: 1. hanya dengan menempatkan tanda kutip tunggal di sekitar 'detail' secara membingungkan membuat nilai nullketika diterima oleh pendengar Script Konten saya. 2. Lebih penting lagi, untuk beberapa alasan saya harus JSON.parse(JSON.stringify(myData))atau kalau tidak, itu juga akan menjadi null. Mengingat hal ini, bagi saya tampaknya klaim pengembang Chromium berikut - bahwa algoritma "terstruktur klon" digunakan secara otomatis - tidak benar. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk
Saya pikir cara resmi adalah dengan menggunakan window.postMessage: developer.chrome.com/extensions/…
Enrique
9

Saya juga menghadapi masalah pemesanan skrip yang dimuat, yang diselesaikan melalui pemuatan skrip berurutan. Loading didasarkan pada jawaban 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();
    }
}

Contoh penggunaannya adalah:

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

Sebenarnya, saya agak baru di JS, jadi silakan ping saya ke cara yang lebih baik.

Dmitry Ginzburg
sumber
3
Cara menyisipkan skrip ini tidak bagus, karena Anda mencemari namespace halaman web. Jika halaman web menggunakan variabel yang disebut formulaImageUrlatau codeImageUrl, maka Anda secara efektif merusak fungsionalitas halaman. Jika Anda ingin meneruskan variabel ke halaman web, saya sarankan untuk melampirkan data ke elemen skrip ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) dan menggunakan misalnya (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();dalam skrip untuk mengakses data.
Rob W
@RobW terima kasih atas catatan Anda, meskipun ini lebih tentang sampel. Bisakah Anda menjelaskan, mengapa saya harus menggunakan IIFE daripada hanya mendapatkan dataset?
Dmitry Ginzburg
4
document.currentScripthanya menunjuk ke tag skrip saat dijalankan. Jika Anda pernah ingin mengakses tag skrip dan / atau atribut / propertinya (mis. dataset), Maka Anda perlu menyimpannya dalam variabel. Kita membutuhkan IIFE untuk mendapatkan penutupan untuk menyimpan variabel ini tanpa mencemari namespace global.
Rob W
@ Bob sangat bagus! Tapi tidak bisakah kita hanya menggunakan beberapa nama variabel, yang tidak akan bersinggungan dengan yang ada. Apakah itu hanya non-idiomatis atau kita dapat memiliki masalah lain dengannya?
Dmitry Ginzburg
2
Anda bisa, tetapi biaya menggunakan IIFE dapat diabaikan, jadi saya tidak melihat alasan untuk lebih memilih polusi namespace daripada IIFE. Saya menghargai pasti bahwa saya tidak akan merusak halaman web orang lain dalam beberapa cara, dan kemampuan untuk menggunakan nama variabel pendek. Keuntungan lain menggunakan IIFE adalah Anda dapat keluar dari skrip lebih awal jika diinginkan ( return;).
Rob W
6

dalam skrip Konten, saya menambahkan tag skrip ke kepala yang mengikat penangan 'onmessage', di dalam penangan yang saya gunakan, eval untuk mengeksekusi kode. Dalam skrip konten stan saya menggunakan onmessage handler juga, jadi saya mendapatkan komunikasi dua arah. Chrome Documents

//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 adalah pendengar url pesan posting

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

Dengan cara ini, saya dapat memiliki komunikasi 2 arah antara CS ke Real Dom. Ini sangat berguna misalnya jika Anda perlu mendengarkan acara webscoket, atau variabel memori atau acara.

doron aviguy
sumber
1

Anda dapat menggunakan fungsi utilitas yang saya buat untuk menjalankan kode dalam konteks halaman dan mendapatkan kembali nilai yang dikembalikan.

Ini dilakukan dengan membuat serial suatu fungsi ke string dan menyuntikkannya ke halaman web.

Utilitas tersedia di sini di GitHub .

Contoh penggunaan -



// 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
sumber
0

Jika Anda ingin menyuntikkan fungsi murni, alih-alih teks, Anda dapat menggunakan metode ini:

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

Dan Anda dapat melewatkan parameter (sayangnya tidak ada objek dan array yang dapat dikompresi) ke fungsi. Tambahkan ke dalam baretese, seperti:

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
sumber
Ini cukup keren ... tetapi versi kedua, dengan variabel untuk warna, tidak bekerja untuk saya ... Saya mendapatkan 'tidak dikenal' dan kode tersebut melontarkan kesalahan ... tidak melihatnya sebagai variabel.
11