Javascript 使用内容脚本访问页面上下文变量和函数

Javascript 使用内容脚本访问页面上下文变量和函数,javascript,google-chrome,google-chrome-extension,youtube-api,content-script,Javascript,Google Chrome,Google Chrome Extension,Youtube Api,Content Script,我正在学习如何创建Chrome扩展。我刚开始开发一个用来捕捉YouTube事件的。我想将它与YouTube flash player一起使用(稍后我将尝试使其与HTML5兼容) manifest.json: { "name": "MyExtension", "version": "1.0", "description": "Gotta catch Youtube events!", "permissions": ["tabs", "http://*/*"],

我正在学习如何创建Chrome扩展。我刚开始开发一个用来捕捉YouTube事件的。我想将它与YouTube flash player一起使用(稍后我将尝试使其与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"]
    }]
}
function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");
myScript.js:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}
function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");
问题是控制台给了我“已启动!”,但当我播放/暂停YouTube视频时没有“状态已更改!”


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

根本原因:
内容脚本在环境中执行

解决方案:
要访问页面上下文(“主世界”)的函数/变量,必须使用DOM将代码注入页面本身。如果您想将函数/变量公开到页面上下文中(在您的例子中,它是
state()
方法),也是一样的

  • 如果需要与页面脚本通信,请注意:
    使用DOM
    CustomEvent
    处理程序。示例:、和

  • 注意,如果页面脚本中需要
    chrome
    API:

    由于
    chrome.*
    api不能在页面脚本中使用,因此必须在内容脚本中使用它们,并通过DOM消息将结果发送到页面脚本(请参见上面的注释)

安全警告
一个页面可能会重新定义或扩充/钩住一个内置原型,因此如果该页面以不兼容的方式进行操作,那么暴露的代码可能会失败。如果要确保公开的代码在安全环境中运行,则应a)使用方法2-3而不是1声明内容脚本,或b)通过空iframe提取原始本机内置。请注意,使用
document\u start
时,您可能需要在公开的代码中使用
DOMContentLoaded
事件来等待DOM

目录
  • 方法1:注入另一个文件-与ManifestV3兼容
  • 方法2:注入嵌入式代码
  • 方法2b:使用函数
  • 方法3:使用内联事件
  • 注入代码中的动态值
方法1:注入另一个文件 目前唯一与ManifestV3兼容的方法。当您有大量代码时,特别好。将代码放在扩展名内的文件中,比如
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文件必须在中公开:

  • ManifestV2的manifest.json示例

    “web可访问资源”:[“script.js”],
    
  • ManifestV3的manifest.json示例

    “网络可访问资源”:[{
    “资源”:[“script.js”],
    “匹配项”:[“”

    注意:仅在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:使用内联事件 有时,您希望立即运行一些代码,例如在创建
    元素之前运行一些代码。这可以通过插入带有
    文本内容的
    标记来完成(请参见方法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.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!
    
    解决方案是在传递参数之前使用。示例:

    var actualCode = '(' + function(greeting, name) { ...
    } + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';
    
    如果您有许多变量,则值得使用一次
    JSON.stringify
    ,以提高可读性,如下所示:

    ...
    } + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
    
    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),
      }));
      

    我还面临着加载脚本的排序问题,这是通过顺序加载脚本来解决的。加载基于

    用法示例如下:

    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有点陌生,所以请随意选择更好的方式。

    在内容脚本中,我将脚本标记添加到he
    //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
    
    //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");