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


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 2017

回答:


874

コンテンツスクリプトは、「孤立した世界」環境で実行されますstate()メソッドをページ自体に挿入する必要があります。

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

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

目次

  • 方法1:別のファイルを挿入する
  • 方法2:埋め込みコードを挿入する
  • 方法2b:関数を使用する
  • 方法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はスクリプトの読み込みを拒否し、コンソールに次のエラーを表示します。

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

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

このメソッドは、小さなコードをすばやく実行したい場合に役立ちます。(参照:Chrome拡張機能でFacebookのホットキーを無効にする方法は?

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>要素を作成する前にコードを実行するなど、コードをすぐに実行したい場合があります。これは、<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]) + ')';

82
この回答は公式ドキュメントの一部である必要があります。公式ドキュメントは推奨される方法で出荷する必要があります->同じことを行う3つの方法...間違っていますか?
Mars Robertson

7
@ Qantas94Heavy拡張機能のCSPはコンテンツスクリプトに影響しません。ページのCSPのみが関連します。方法1 script-srcは、拡張機能の生成元を除外するディレクティブを使用してブロックでき、方法2は、 "unsafe-inline" `を除外するCSPを使用してブロックできます。
Rob W

3
誰かがなぜを使用してスクリプトタグを削除するのかと尋ねましたscript.parentNode.removeChild(script);。私がそれをする理由は、私が混乱を片付けるのが好きだからです。インラインスクリプトがドキュメントに挿入されると、すぐに実行され、<script>タグを安全に削除できます。
Rob W

9
その他の方法:location.href = "javascript: alert('yeah')";コンテンツスクリプトの任意の場所で使用します。コードの短いスニペットの方が簡単で、ページのJSオブジェクトにもアクセスできます。
Métoule

3
@ChrisPの使用には注意してくださいjavascript:。複数行にまたがるコードは、期待どおりに機能しない可能性があります。行コメント(//)は残りを切り捨てるため、失敗します:location.href = 'javascript:// Do something <newline> alert(0);';。これは、複数行のコメントを使用することで回避できます。もう1つ注意すべきことは、式の結果が無効であることです。javascript:window.x = 'some variable';ドキュメントがアンロードされ、「some variable」という語句に置き換えられます。適切に使用すれば、それは確かにの魅力的な代替手段<script>です。
Rob W

61

唯一のもの 行方不明 Rob Wの優れた答えから隠されているのは、挿入されたページスクリプトとコンテンツスクリプトの間の通信方法です。

受信側(コンテンツスクリプトまたは挿入されたページスクリプト)で、イベントリスナーを追加します。

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

実際には、私の答えの2行目のコードと説明に、stackoverflow.com / questions / 9602022 / …にリンクしています。
Rob W

1
更新されたメソッドへの参照(バグレポートやテストケースなど)はありますか?CustomEventコンストラクターは廃止されたdocument.createEventAPIに取って代わります。
Rob W

私にとって「は、dispatchEvent(新しいCustomEvent ...」働いていた。また、それは私はJSコードを注入した後には、addEventListenerを書いたので、前に仕事をしませんでした私はクローム33持っている。。
jscripter

2番目のパラメーターとしてCustomEventコンストラクターに渡すものについては、特に注意してください。私は2つの非常に紛らわしい後退を経験nullしました。2.さらに重要なことに、何らかの理由で私がしなければならなかったJSON.parse(JSON.stringify(myData))か、そうでなければなりましたnull。これを考えると、次のChromium開発者の主張(「構造化クローン」アルゴリズムが自動的に使用される)は真実ではないと思われます。bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk

公式な方法はwindow.postMessage:developer.chrome.com/extensions/…
Enrique

9

また、読み込まれたスクリプトの順序の問題にも直面しました。これは、スクリプトの順次読み込みによって解決されました。読み込みは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してください。


3
このようにスクリプトを挿入する方法は、Webページの名前空間を汚染しているため、適切ではありません。WebページでformulaImageUrlor と呼ばれる変数を使用している場合codeImageUrlは、ページの機能を効果的に破壊しています。変数をWebページに渡したい場合は、データをスクリプト要素(e.g. script.dataset.formulaImageUrl = formulaImageUrl;)に添付し、スクリプトなどで(function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();データにアクセスすることをお勧めします。
Rob W

@RobWはサンプルについての詳細ですが、メモをありがとうございます。明確にすることはできますdatasetか?なぜ私は単に取得する代わりにIIFEを使用する必要があるのですか?
ドミトリーギン

4
document.currentScript実行中はスクリプトタグのみを指します。スクリプトタグやその属性/プロパティ(例:)にアクセスしたい場合datasetは、変数に保存する必要があります。グローバルネームスペースを汚染せずにこの変数を格納するクロージャーを取得するには、IIFEが必要です。
Rob W

@RobW素晴らしい!しかし、既存のものとほとんど交差しない変数名を使用することはできません。それは単なる非慣用的なものですか、それとも他の問題が発生する可能性がありますか?
ドミトリーギン

2
可能ですが、IIFEを使用するコストはごくわずかであるため、IIFEよりも名前空間の汚染を好む理由はわかりません。私は確かに私がウェブページを壊さないことを大切にしていますは他の人を何らかの方法でと、短い変数名を使用する機能を高く評価しています。IIFEを使用するもう1つの利点は、必要に応じてスクリプトを早く終了できることです(return;)。
Rob W

6

コンテンツスクリプトでは、「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イベント、またはメモリ内の変数やイベントをリッスンする必要がある場合に、非常に便利です。


1

ページコンテキストでコードを実行し、戻り値を取得するために作成したユーティリティ関数を使用できます。

これは、関数を文字列にシリアル化し、それをWebページに挿入することによって行われます。

このユーティリティは、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+")"; 


これはかなりクールです...しかし、色の変数を持つ2番目のバージョンは私には機能しません...「認識されません」と表示され、コードがエラーをスローします...それを変数として認識しません。
18
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.