Bir içerik komut dosyası kullanarak sayfa içeriğine kod ekleme


480

Chrome uzantılarını nasıl oluşturacağımı öğreniyorum. YouTube etkinliklerini yakalamak için yeni bir tane geliştirmeye başladım. YouTube flash player ile kullanmak istiyorum (daha sonra HTML5 ile uyumlu hale getirmeye çalışacağım).

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

Sorun şu ki konsol bana "Başladı!" , ancak "Devlet Değişti!" YouTube videolarını oynattığımda / duraklattığımda.

Bu kod konsola konduğunda işe yaradı. Neyi yanlış yapıyorum?


14
İşlev adınızdaki tırnak işaretlerini kaldırmaya çalışın:player.addEventListener("onStateChange", state);
Eduardo

2
Ayrıca, maçlar yazarken, eklemeyi unutmayın https://veya http://bu www.youtube.com/*uzantıyı paketlemenize izin vermeyecektir ve Eksik şema ayırıcı hatası
atacaktır

Yanıtlar:


874

İçerik komut dosyaları "yalıtılmış bir dünya" ortamında yürütülür . state()Yönteminizi sayfanın kendisine enjekte etmeniz gerekir .

chrome.*Koddaki API'lardan birini kullanmak istediğinizde , şu cevapta açıklandığı gibi özel bir olay işleyici uygulamanız gerekir: Chrome uzantısı - Gmail'in orijinal iletisini alma .

Aksi takdirde, chrome.*API'ları kullanmak zorunda değilseniz , bir JS ekleyerek sayfadaki tüm JS kodunuzu eklemenizi önemle tavsiye ederim <script>:

İçindekiler

  • Yöntem 1: Başka bir dosya enjekte
  • Yöntem 2: Gömülü kod enjekte
  • Yöntem 2b: Bir işlev kullanma
  • Yöntem 3: Satır içi bir olay kullanma
  • Enjekte edilen koddaki dinamik değerler

Yöntem 1: Başka bir dosya enjekte

Bu, çok sayıda kodunuz olduğunda en kolay / en iyi yöntemdir. Örneğin, gerçek JS kodunuzu uzantınızdaki bir dosyaya ekleyin script.js. Ardından içerik komut dosyanızın aşağıdaki gibi olmasına izin verin (burada açıklanmıştır: Google Chome “Uygulama Kısayolu” Özel 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);

Not: Bu yöntemi kullanırsanız, enjekte edilen script.jsdosya "web_accessible_resources"bölüme eklenmelidir ( örnek ). Bunu yapmazsanız Chrome, komut dosyanızı yüklemeyi reddeder ve konsolda aşağıdaki hatayı görüntüler:

Krom uzantısının yüklenmesini reddetme: // [EXTENSIONID] /script.js. Uzantı dışındaki sayfalar tarafından yüklenebilmesi için kaynakların web_accessible_resources manifest anahtarında listelenmesi gerekir.

Yöntem 2: Gömülü kod enjekte

Bu yöntem, küçük bir kod parçasını hızlı bir şekilde çalıştırmak istediğinizde kullanışlıdır. (Ayrıca bkz: Chrome uzantısına sahip facebook kısayol tuşlarını nasıl devre dışı bırakabilirim? ).

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

Not: şablon değişmez değerleri yalnızca Chrome 41 ve sonraki sürümlerinde desteklenir. Uzantının Chrome 40- ile çalışmasını istiyorsanız şunu kullanın:

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

Yöntem 2b: Bir işlev kullanma

Büyük bir kod grubu için, dizeden alıntı yapmak mümkün değildir. Bir dizi kullanmak yerine, bir işlev kullanılabilir ve dizilebilir:

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

Bu yöntem çalışır, çünkü +dizelerdeki işleç ve bir işlev tüm nesneleri bir dizeye dönüştürür. Kodu birden çok kez kullanmayı düşünüyorsanız, kod tekrarını önlemek için bir işlev oluşturmak akıllıca olacaktır. Bir uygulama şöyle görünebilir:

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

Not: İşlev serileştirildiğinden, orijinal kapsam ve tüm bağlı özellikler kaybolur!

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

Yöntem 3: Satır içi bir olay kullanma

Bazen hemen bir kod çalıştırmak, örneğin <head>eleman oluşturulmadan önce bir kod çalıştırmak istersiniz . Bu, bir <script>etiket eklenerek yapılabilir textContent(bkz. Yöntem 2 / 2b).

Alternatif ancak önerilmeyen satır içi olayları kullanmaktır. Sayfa satır içi komut dosyalarını yasaklayan bir İçerik Güvenliği ilkesi tanımlarsa satır içi olay dinleyicileri engellenir. Öte yandan, uzantı tarafından enjekte edilen satır içi komut dosyaları hala çalışır. Hâlâ satır içi etkinlikleri kullanmak istiyorsanız, şu şekilde:

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

Not: Bu yöntem, resetolayı işleyen başka küresel olay dinleyicisi olmadığını varsayar . Varsa, diğer küresel olaylardan birini de seçebilirsiniz. JavaScript konsolunu (F12) açın, yazın document.documentElement.onve mevcut etkinlikleri seçin.

Enjekte edilen koddaki dinamik değerler

Bazen, enjekte edilen işleve rasgele bir değişken iletmeniz gerekir. Örneğin:

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

Bu kodu enjekte etmek için değişkenleri anonim işleve bağımsız değişken olarak geçirmeniz gerekir. Doğru şekilde uyguladığınızdan emin olun! Aşağıdaki olacak değil çalışır:

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!

Çözüm, JSON.stringifyargümanı geçmeden önce kullanmaktır . Misal:

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

Çok sayıda değişkeniniz varsa JSON.stringify, okunabilirliği artırmak için aşağıdaki gibi bir kez kullanılmaya değer :

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

81
Bu cevap resmi belgelerin bir parçası olmalıdır. Resmi dokümanlar önerilen şekilde gönderilmelidir -> aynı şeyi yapmanın 3 yolu ... Yanlış mı?
Mars Robertson

7
Qantas94Heavy @ uzantının CSP yok değil içerik komut dosyaları etkiler. Yalnızca sayfanın CSP'si alakalı. Yöntem 1 script-src, uzantının kökenini hariç tutan bir yönerge kullanılarak engellenebilir , yöntem 2, "güvenli olmayan satır içi" hariç bir CSP kullanılarak engellenebilir.
Rob W

3
Birisi neden komut dosyası etiketini kullanarak kaldırdığımı sordu script.parentNode.removeChild(script);. Bunu yapmamın sebebi, dağınıklığımı temizlemeyi sevmem. Belgeye bir satır içi komut dosyası eklendiğinde, hemen yürütülür ve <script>etiket güvenle kaldırılabilir.
Rob W

9
Diğer yöntem: location.href = "javascript: alert('yeah')";içerik komut dosyanızda herhangi bir yerde kullanın . Kısa kod parçacıkları için daha kolaydır ve sayfanın JS nesnelerine de erişebilir.
Métoule

3
@ChrisP Kullanırken dikkatli olun javascript:. Birden çok satıra yayılan kod beklendiği gibi çalışmayabilir. Bir hat yorum ( //) kalan keser, bu nedenle bu başarısız olur: location.href = 'javascript:// Do something <newline> alert(0);';. Bu, çok satırlı yorumların kullanılmasını sağlayarak atlatılabilir. Dikkat edilmesi gereken bir diğer şey, ifadenin sonucunun geçersiz olması gerektiğidir. javascript:window.x = 'some variable';belgenin kaldırılmasına neden olur ve yerine "bazı değişken" ifadesi gelir. Düzgün kullanılırsa, gerçekten çekici bir alternatiftir <script>.
Rob W

61

Sadece bir şey eksik Rob W'ın mükemmel cevabından gizlenen, enjekte edilen sayfa komut dosyası ile içerik komut dosyası arasında nasıl iletişim kurulacağıdır.

Alıcı tarafa (içerik komut dosyanız veya enjekte edilen sayfa komut dosyanız) bir etkinlik dinleyicisi ekleyin:

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

Başlatıcı tarafında (içerik komut dosyası veya enjekte edilmiş sayfa komut dosyası) etkinliği gönderin:

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

Notlar:

  • DOM mesajlarında, ilkel değerlere ek olarak yalnızca bazı veri türlerini aktarabilen yapılandırılmış klonlama algoritması kullanılır . Sınıf örnekleri veya işlevleri veya DOM öğeleri gönderemez.
  • Firefox'ta, bir nesneyi (yani ilkel bir değer değil) içerik komut dosyasından sayfa içeriğine göndermek için cloneInto(yerleşik bir işlev) kullanarak açıkça hedefe klonlamanız gerekir , aksi takdirde bir güvenlik ihlali hatasıyla başarısız olur. .

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

Aslında cevabımın ikinci satırındaki kod ve açıklama ile stackoverflow.com/questions/9602022/… adresine bağlandım .
Rob W

1
Güncellenmiş yönteminiz için bir referansınız var mı (örn. Bir hata raporu veya bir test durumu?) CustomEventYapıcı, kullanımdan kaldırılmış document.createEventAPI'nın yerini alıyor .
Rob W

Benim için 'dispatchEvent (yeni CustomEvent ...' çalıştı Ayrıca ben js kodu enjekte sonra addEventListener yazdığı için önce işi değil mi Krom 33. var..
jscripter

Yapıcıya 2. parametreniz olarak neler ilettiğiniz konusunda çok dikkatli olun CustomEvent. 2 çok kafa karıştırıcı aksilik yaşadım: 1. sadece 'ayrıntı' etrafına tek tırnak koymak şaşırtıcı bir şekilde nullİçerik Betiğimin dinleyicisi tarafından alındığında değer yarattı. 2. Daha da önemlisi, bir nedenden ötürü yapmak zorunda kaldım, JSON.parse(JSON.stringify(myData))aksi halde de olurdum null. Bu göz önüne alındığında, aşağıdaki Chromium geliştiricisinin "yapılandırılmış klon" algoritmasının otomatik olarak kullanıldığı iddiasının doğru olmadığı anlaşılıyor. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk

Resmi yolun window.post kullanmak olduğunu düşünüyorumMessage: developer.chrome.com/extensions/…
Enrique

9

Ayrıca, komut dosyalarının sıralı yüklenmesi ile çözülen yüklü komut dosyalarının sipariş edilmesi sorunuyla da karşılaştım. Yükleme Rob W'nin cevabına dayanmaktadır .

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

Kullanım örneği şöyle olacaktır:

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

Aslında, JS'de biraz yeniyim, bu yüzden beni daha iyi yollara pinglemekten çekinmeyin.


3
Komut dosyası eklemenin bu yolu hoş değildir, çünkü web sayfasının ad alanını kirletiyorsunuz. Web sayfası formulaImageUrlveya adlı bir değişken kullanıyorsa codeImageUrl, sayfanın işlevselliğini etkili bir şekilde yok edersiniz. Web sayfasına bir değişken iletmek istiyorsanız, verileri script öğesine ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) eklemenizi ve verilere (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();erişmek için komut dosyasında örneğin kullanmanızı öneririm .
Rob W

@ Numune hakkında daha fazla bilgi olmasına rağmen notunuz için teşekkür ederiz. Lütfen neden IIFE'yi sadece almak yerine kullanmalıyım diye açıklığa kavuşturabilir misiniz dataset?
Dmitry Ginzburg

4
document.currentScriptyalnızca kod etiketini yürütürken işaret eder. Komut dosyası etiketine ve / veya özelliklerine / özelliklerine (örneğin dataset) erişmek istediğinizde , bunu bir değişkente saklamanız gerekir. Global ad alanını kirletmeden bu değişkeni saklamak için bir IIFE'ye ihtiyacımız var.
Rob W

@RobW mükemmel! Ancak, varolanla çok az kesişecek bazı değişken adları kullanamaz mıyız? Sadece deyimsel değil mi, yoksa başka problemlerimiz olabilir mi?
Dmitry Ginzburg

2
Yapabilirsiniz, ancak bir IIFE kullanmanın maliyeti ihmal edilebilir, bu yüzden bir IIFE yerine ad alanı kirliliğini tercih etmek için bir neden göremiyorum. Kesinlikle başkalarının web sayfasını bir şekilde kırmayacağım ve kısa değişken adları kullanma yeteneğine değer veriyorum. IIFE kullanmanın bir diğer avantajı, istenirse koddan daha önce çıkabilmenizdir ( return;).
Rob W

6

İçerik komut dosyasında, kullandığım işleyicinin içinde kod yürütmek için bir 'onmessage' işleyicisini bağlayan başlığa komut dosyası etiketi eklerim. Booth içerik komut dosyasında ben de onmessage işleyicisi kullanın, bu yüzden iki yönlü iletişim olsun. Chrome Belgeleri

//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 bir mesaj mesajı url dinleyicisidir

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

Bu şekilde CS ile Real Dom arasında 2 yönlü iletişim kurabilirim. Örneğin, webscoket olaylarını veya bellek değişkenleri veya olaylarındaki herhangi birini dinlemeniz gerekiyorsa çok kullanışlıdır.


1

Kodunu sayfa bağlamında çalıştırmak ve döndürülen değeri geri almak için oluşturduğum bir yardımcı program işlevini kullanabilirsiniz.

Bu, bir işlevi bir dizeye serileştirerek ve web sayfasına enjekte ederek yapılır.

Yardımcı programı GitHub'da bulabilirsiniz .

Kullanım örnekleri -



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

Metin yerine saf işlevi enjekte etmek isterseniz, bu yöntemi kullanabilirsiniz:

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

Ve fonksiyonlara parametreleri aktarabilirsiniz (ne yazık ki hiçbir nesne ve diziler dizgi oluşturulamaz). Bunu barbekülere ekleyin, şöyle:

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


Bu çok güzel ... ama renk için bir değişken olan ikinci versiyon benim için çalışmıyor ... 'Tanınmayan' alıyorum ve kod bir hata atıyor ... bir değişken olarak görmüyor.
11
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.