我正在学习如何创建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视频时,控制台显示“已启动!”,但没有“状态已改变!”

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


当前回答

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

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


其他回答

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

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


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

函数注入(){ 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+")";

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

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

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