我正在学习如何创建Chrome扩展。我刚开始开发一个用来捕捉YouTube上的事件。我想使用它与YouTube flash播放器(稍后我会尝试使它与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视频时,控制台显示“已启动!”,但没有“状态已改变!”

当将这段代码放入控制台中时,它工作了。我做错了什么?


根本原因: 内容脚本在一个“孤立的世界”环境中执行。

解决方案: 使用DOM将代码注入到页面中——这些代码将能够访问页面上下文(“main world”)的函数/变量,或者向页面上下文公开函数/变量(在您的情况下是state()方法)。

如果需要与页面脚本通信,请注意: 使用DOM CustomEvent处理程序。例子:1、2和3。 注意,在页面脚本中需要chrome API: 因为chrome。* api不能在页面脚本中使用,你必须在内容脚本中使用它们,并通过DOM消息传递将结果发送到页面脚本(参见上面的注释)。

Safety warning: A page may redefine or augment/hook a built-in prototype so your exposed code may fail if the page did it in an incompatible fashion. If you want to make sure your exposed code runs in a safe environment then you should either a) declare your content script with "run_at": "document_start" and use Methods 2-3 not 1, or b) extract the original native built-ins via an empty iframe, example. Note that with document_start you may need to use DOMContentLoaded event inside the exposed code to wait for DOM.

目录

方法1:注入另一个兼容ManifestV3的文件 方法二:注入嵌入式代码- MV2 方法2b:使用MV2函数 方法3:使用内联事件- ManifestV3兼容 方法四:只使用executeScript的world - ManifestV3 方法5:在manifest中使用world。仅支持ManifestV3, Chrome 111+ 注入代码中的动态值

方法一:注入另一个文件(ManifestV3/MV2)

当您有大量代码时尤其如此。将代码放在扩展名中的一个文件中,比如script.js。然后像这样在你的内容脚本中加载它:

var s = document.createElement('script');
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

js文件必须在web_accessible_resources中公开:

清单。ManifestV2的json示例 “web_accessible_resources”:“script.js”, 清单。ManifestV3的json示例 “web_accessible_resources”:[{ “资源”:“script.js”, “匹配”:[" < all_urls >”) })

如果不是,控制台中会出现以下错误:

拒绝加载chrome-extension://[EXTENSIONID]/script.js。资源必须列在web_accessible_resources清单键中,以便由扩展之外的页面加载。

方法二:注入嵌入式代码(MV2)

当您希望快速运行一小段代码时,此方法非常有用。(参见:如何禁用facebook热键与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:使用函数(MV2)

对于一大块代码,引用字符串是不可行的。不使用数组,可以使用函数,并进行字符串化:

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:使用内联事件(ManifestV3/MV2)

有时,你想要立即运行一些代码,例如在<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');

注意:此方法假设没有其他处理重置事件的全局事件侦听器。如果有,你也可以选择其他全球性事件之一。打开JavaScript控制台(F12),输入document.documentElement。打开,并选择可用的事件。

方法四:使用铬。脚本API世界(仅适用于ManifestV3)

Chrome 95或更新版本,Chrome .scripting. executescript with world: 'MAIN' Chrome 102或更新版本的Chrome .scripting. registercontentscripts带有world: 'MAIN',也允许runAt: 'document_start'来保证页面脚本的早期执行。

与其他方法不同的是,这个方法是用于背景脚本或弹出脚本,而不是用于内容脚本。请参阅文档和示例。

方法5:在manifest中使用world。json(仅适用于ManifestV3)

在Chrome 111或更新版本中,您可以在manifest中的content_scripts声明中添加“world”:“MAIN”。json覆盖默认值isolate。脚本按照列出的顺序运行。

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

注入代码中的动态值(MV2)

有时,需要将任意变量传递给注入的函数。例如:

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]).slice(1, -1) + ')';

注入代码中的动态值(ManifestV3)

方法1可以在内容脚本中设置脚本元素的URL: s.c src = chrome.runtime.getURL('script.js?') + new URLSearchParams({foo: 1}); 然后script.js可以读取它: const params = new URLSearchParams(document.currentScript.src.split('?')[1]); console.log (params.get(“foo”)); 方法4 executeScript有args参数,registerContentScripts目前没有(希望将来会添加)。

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

我还遇到过加载脚本的排序问题,这个问题是通过顺序加载脚本解决的。加载是基于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我以更好的方式。

在内容脚本中,我添加了脚本标签的头部,它绑定了一个“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是一个post消息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事件, 或内存中的任何变量或事件。

如果你想注入纯函数,而不是文本,你可以使用这个方法:

函数注入(){ document.body.style.backgroundColor = 'blue'; } //这包括函数作为文本和barentheses使它自己运行。 var actualCode = "("+inject+")()"; document.documentElement。setAttribute (onreset, actualCode); document.documentElement。dispatchEvent(新CustomEvent(“重置”)); document.documentElement.removeAttribute(“onreset”);

并且可以将参数(不幸的是没有对象和数组可以被字符串化)传递给函数。把它加到baretheses中,像这样:

函数注入(颜色){ document.body.style.backgroundColor =颜色; } //这包括函数作为文本和barentheses使它自己运行。 Var color = 'yellow'; var actualCode = "("+inject+")("+color+")";

您可以使用我为在页面上下文中运行代码并返回返回值而创建的实用程序函数。

这是通过将函数序列化为字符串并将其注入到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'