web-dev-qa-db-ja.com

コンテンツスクリプトを使用してページコンテキストにコードを挿入する

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

問題は、コンソールに "Started!"と表示されることです。 しかし、何もありません 「状態が変更されました」 YouTubeの動画を再生/一時停止したとき。

このコードをコンソールに入れるとうまくいきました。何がおかしいのですか?

431
André Alves

コンテンツスクリプトは "孤立した世界"の環境 で実行されます。あなたはあなた自身のstate()メソッドをページ自体に注入しなければなりません。

スクリプトでchrome.* APIのいずれかを使用する場合は、次の回答で説明されているように、特別なイベントハンドラを実装する必要があります。 Chrome拡張機能 - Gmailの元のメッセージを取得する

それ以外の場合は、chrome.* APIを使用する必要がない場合は、<script>タグを追加して、すべてのJSコードをページに挿入することを強くお勧めします。

目次

  • 方法1:他のファイルを挿入する
  • 方法2:埋め込みコードを挿入する
  • 方法2b:関数を使う
  • 方法3:インラインイベントを使用する
  • 挿入されたコード内の動的値

方法1:他のファイルを挿入する

これは、たくさんのコードがある場合に最も簡単な方法です。実際のJSコードを拡張子内のファイルに含めます(script.jsなど)。次に、コンテンツスクリプトを次のようにします(ここで説明されている: Google Chomeの "Application Shortcut" Custom 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" section に追加する必要があります( example )。そうしないと、Chromeはスクリプトをロードしてコンソールに次のエラーを表示するために 拒否 します。

Chrome-extension:// [EXTENSIONID] /script.jsの読み込みを拒否しています。拡張機能の外側のページでリソースをロードするには、リソースをweb_accessible_resourcesマニフェストキーにリストする必要があります。

方法2:埋め込みコードを挿入する

このメソッドは、小さなコードをすばやく実行したい場合に便利です。 (参照: 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>要素が作成される前にコードを実行するため。これは、textContentを使って<script>タグを挿入することで実行できます(方法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);
};

このコードを注入するには、変数を引数として無名関数に渡す必要があります。正しく実装してください。以下は ではなく work:

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

唯一のもの 行方不明 Row Wの優れた答えから隠されているのは、注入されたスクリプトからコンテンツスクリプトへ、およびその逆の呼び出し方法です(特に、文字列化できないオブジェクトがある場合)。

注入されたスクリプトかコンテンツスクリプトのどちらかに、イベントリスナーを追加します。

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

反対側(コンテンツまたは挿入されたスクリプト)でイベントを呼び出します。

var data="anything";

// updated: this works with Chrome 30:
var evt=document.createEvent("CustomEvent");
evt.initCustomEvent("yourCustomEvent", true, true, data);
document.dispatchEvent(evt);

// the following stopped working in Chrome 30 (Windows), detail was 
// not received in the listener:
// document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));
49
laktak

私はまた、ロードされたスクリプトの順序付けの問題にも直面しました。これはスクリプトのシーケンシャルロードによって解決されました。ローディングは 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();
    }
}

使用例は次のようになります。

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に慣れていないので、もっと良い方法を私にpingしてください。

8
Dmitry Ginzburg

コンテンツスクリプトでは、頭にscriptタグを追加して、onmessageハンドラをバインドします。これは、使用するハンドラ内で、evalを使用してコードを実行します。ブースのコンテンツスクリプトではonmessageハンドラも使用しているので、双方向通信が可能です。 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への双方向通信が可能になります。たとえば、Webscoketイベントを聴く必要がある場合や、メモリ内の変数やイベントを聴く必要がある場合に非常に便利です。

6
doron aviguy

テキストではなく純粋な関数を注入したい場合は、このメソッドを使用できます。

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

そして、パラメータにパラメータを渡すことができます(残念ながら、オブジェクトや配列を文字列化することはできません)。それをbarethesesに追加してください。

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