admin管理员组

文章数量:1126447

I want to control youtube's player in my extension:

manifest.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube/*"],
        "js" : ["myScript.js"]
    }]
}

myScript.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

The problem is that the console gives me the "Started!", but there is no "State Changed!" when I play/pause YouTube videos.

When this code is put in the console, it worked. What am I doing wrong?

I want to control youtube.com's player in my extension:

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

The problem is that the console gives me the "Started!", but there is no "State Changed!" when I play/pause YouTube videos.

When this code is put in the console, it worked. What am I doing wrong?

Share Improve this question edited Nov 14, 2023 at 16:44 woxxom 73.4k14 gold badges155 silver badges160 bronze badges asked Mar 1, 2012 at 11:53 André AlvesAndré Alves 6,7653 gold badges18 silver badges24 bronze badges 3
  • 23 try to remove the quotes around your function name: player.addEventListener("onStateChange", state); – Eduardo Commented Mar 1, 2012 at 17:13
  • 5 It is also notable that when writing matches, do not forget to include https:// or http://, this www.youtube.com/* would not let you pack extension and would throw Missing scheme separator error – Nilay Vishwakarma Commented Jan 31, 2017 at 19:34
  • 2 Also see bugs.chromium.org/p/chromium/issues/detail?id=478183 – Pacerier Commented Jul 15, 2017 at 19:21
Add a comment  | 

8 Answers 8

Reset to default 1312

Underlying cause:
A content script is executed in an ISOLATED "world" environment, meaning it can't access JS functions and variables in the MAIN "world" (the page context), and can't expose its own JS stuff, like the state() method in your case.

Solution:
Inject the code into the JS context of the page (MAIN "world") using methods shown below.

On using chrome API:
 • via externally_connectable messaging on <all_urls> allowed since Chrome 107.
 • via CustomEvent messaging with the normal content script, see the next paragraph.

On messaging with the normal content script:
Use CustomEvent as shown here, or here, or here. In short, the injected script sends a message to the normal content script, which calls chrome.storage or chrome.runtime.sendMessage, then sends the result to the injected script via another CustomEvent message. Don't use window.postMessage as your data may break sites that have a listener expecting a certain format of the message.

Caution!
The page may redefine a built-in prototype or a global and exfiltrate data from your private communication or make your injected code fail. Guarding against this is complicated (see Tampermonkey's or Violentmonkey's "vault"), so make sure to verify all the received data.

Table of contents

So, what's best? For ManifestV3 use the declarative method (#5) if the code should always run, or use chrome.scripting (#4) for conditional injection from an extension script like the popup or service worker, or use the content script-based methods (#1 and #3) otherwise.

  • Content script controls injection:

    • Method 1: Inject another file - ManifestV3 compatible
    • Method 2: Inject embedded code - MV2
    • Method 2b: Using a function - MV2
    • Method 3: Using an inline event - ManifestV3 compatible
  • Extension script controls injection (e.g. background service worker or the popup script):

    • Method 4: Using executeScript's world - ManifestV3 only
  • Declarative injection:

    • Method 5: Using world in manifest.json - ManifestV3 only, Chrome 111+
  • Dynamic values in the injected code

Method 1: Inject another file (ManifestV3/MV2)

Particularly good when you have lots of code. Put the code in a file within your extension, say script.js. Then load it in your content script like this:

var s = document.createElement('script');
s.src = chrome.runtime.getURL('script.js');
s.onload = function() { this.remove(); };
// see also "Dynamic values in the injected code" section in this answer
(document.head || document.documentElement).appendChild(s);

The js file must be exposed in web_accessible_resources:

  • manifest.json example for ManifestV2

    "web_accessible_resources": ["script.js"],
    
  • manifest.json example for ManifestV3

    "web_accessible_resources": [{
      "resources": ["script.js"],
      "matches": ["<all_urls>"]
    }]
    

If not, the following error will appear in the console:

Denying load of chrome-extension://[EXTENSIONID]/script.js. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

Method 2: Inject embedded code (MV2)

This method is useful when you want to quickly run a small piece of code. (See also: How to disable facebook hotkeys with Chrome extension?).

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

Note: template literals are only supported in Chrome 41 and above. If you want the extension to work in Chrome 40-, use:

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

Method 2b: Using a function (MV2)

For a big chunk of code, quoting the string is not feasible. Instead of using an array, a function can be used, and stringified:

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

This method works, because the + operator on strings and a function converts all objects to a string. If you intend on using the code more than once, it's wise to create a function to avoid code repetition. An implementation might look like:

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

Note: Since the function is serialized, the original scope, and all bound properties are lost!

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

Method 3: Using an inline event (ManifestV3/MV2)

Sometimes, you want to run some code immediately, e.g. to run some code before the <head> element is created. This can be done by inserting a <script> tag with textContent (see method 2/2b).

An alternative, but not recommended is to use inline events. It is not recommended because if the page defines a Content Security policy that forbids inline scripts, then inline event listeners are blocked. Inline scripts injected by the extension, on the other hand, still run. If you still want to use inline events, this is how:

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

Note: This method assumes that there are no other global event listeners that handle the reset event. If there is, you can also pick one of the other global events. Just open the JavaScript console (F12), type document.documentElement.on, and pick on of the available events.

Method 4: Using chrome.scripting API world (ManifestV3 only)

  • Chrome 95 or newer, chrome.scripting.executeScript with world: 'MAIN'
  • Chrome 102 or newer, chrome.scripting.registerContentScripts with world: 'MAIN', also allows runAt: 'document_start' to guarantee early execution of the page script.

Unlike the other methods, this one is for the background script or the popup script, not for the content script. See the documentation and examples.

Method 5: Using world in manifest.json (ManifestV3 only)

In Chrome 111 or newer you can add "world": "MAIN" to content_scripts declaration in manifest.json to override the default value which is ISOLATED. The scripts run in the listed order.

  "content_scripts": [{
    "world": "MAIN",
    "js": ["page.js"],
    "matches": ["<all_urls>"],
    "run_at": "document_start"
  }, {
    "js": ["content.js"],
    "matches": ["<all_urls>"],
    "run_at": "document_start"
  }],

Dynamic values in the injected code (MV2)

Occasionally, you need to pass an arbitrary variable to the injected function. For example:

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

To inject this code, you need to pass the variables as arguments to the anonymous function. Be sure to implement it correctly! The following will not 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!

The solution is to use JSON.stringify before passing the argument. Example:

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

If you have many variables, it's worthwhile to use JSON.stringify once, to improve readability, as follows:

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

Dynamic values in the injected code (ManifestV3)

  • Use method 1 and add the following line:

    s.dataset.params = JSON.stringify({foo: 'bar'});
    

    Then the injected script.js can read it:

    (() => {
      const params = JSON.parse(document.currentScript.dataset.params);
      console.log('injected params', params);
    })();
    

    To hide the params from the page scripts you can put the script element inside a closed ShadowDOM.

  • Method 4 executeScript has args parameter, registerContentScripts currently doesn't (hopefully it'll be added in the future).

Exchanging data between the JS "worlds"

The script that wants to receive the data adds an event listener:

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

The other script sends the event:

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

Notes:

  • This is synchronous and safer than the asynchronous window.postMessage, which can break some sites that already use it to receive data in a specific format that differs from yours.

  • CustomEvent uses structured cloning algorithm, which can transfer only some types of data in addition to primitive values. It can't send class instances or functions or DOM elements (see "DOM nodes" section below).

  • In Firefox, to send an object (i.e. not a primitive value) from the content script to the page context you have to explicitly clone it into the target using cloneInto (a built-in function), otherwise it'll fail with a security violation error.

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
    
  • In Firefox you can also use wrappedJSObject directly without messages.

Sending DOM nodes

Use MouseEvent:

// receiver

addEventListener('foo', e => {
  let data = e.detail, node;
  if (data.cmd === 'node') { // these messages are synchronous
    addEventListener('foo2', e2 => { node = e2.relatedTarget; }, { once: true });
  }
  console.log('received', data, node);
});

// sender

dispatchEvent(new CustomEvent('foo', { detail: { cmd: 'node' } }));
dispatchEvent(new MouseEvent('foo2', { relatedTarget: myElem }));

in Content script , i add script tag to the head which binds a 'onmessage' handler, inside the handler i use , eval to execute code. In booth content script i use onmessage handler as well , so i get two way communication. Chrome Docs

//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 is a post message url listener

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

This way , I can have 2 way communication between CS to Real Dom. Its very usefull for example if you need to listen webscoket events , or to any in memory variables or events.

I've also faced the problem of ordering of loaded scripts, which was solved through sequential loading of scripts. The loading is based on Rob W's answer.

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

The example of usage would be:

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

Actually, I'm kinda new to JS, so feel free to ping me to the better ways.

You can use a utility function I've created for the purpose of running code in the page context and getting back the returned value.

This is done by serializing a function to a string and injecting it to the web page.

The utility is available here on GitHub.

Usage examples -



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


If you wish to inject pure function, instead of text, you can use this method:

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

And you can pass parameters (unfortunatelly no objects and arrays can be stringifyed) to the functions. Add it into the baretheses, like so:

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

Improving on the answer from Ali Zarei but using ES6 syntax instead:

// content-script.js
const scriptElement = document.createElement('script');
scriptElement.src = chrome.runtime.getURL(`injected-script.js?extensionId=${chrome.runtime.id}`);
scriptElement.type = 'module';
scriptElement.onload = () => scriptElement.remove();
document.head.append(scriptElement);
// injected-script.js
const extensionId = new URL(import.meta.url).searchParams.get("extensionId")

If you want to use dynamic values in the injected code (ManifestV3) and your injected script's type is module, you can't use document.currentScript.dataset as described in Rob's great answer, instead you can pass params as url param and retrieve them in injected code. Here is in an example:

Content Script:

var s = document.createElement('script');
s.src = chrome.runtime.getURL('../override-script.js?extensionId=' + chrome.runtime.id);
s.type = 'module';
s.onload = function () {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

Injected code (override-script.js in my case):

let extensionId = new URL(import.meta.url).searchParams.get("extensionId")

本文标签: javascriptAccess variables and functions defined in page context from an extensionStack Overflow