【问题标题】:Use a content script to access the page context variables and functions使用内容脚本访问页面上下文变量和函数
【发布时间】:2021-08-29 03:00:57
【问题描述】:

我正在学习如何创建 Chrome 扩展程序。我刚开始开发一个来捕捉 YouTube 事件。我想将它与 YouTube flash player 一起使用(稍后我将尝试使其与 HTML5 兼容)。

ma​​nifest.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!");

问题是控制台给了我“Started!”,但是当我播放/暂停 YouTube 视频时没有“State Changed!”。 p>

将此代码放入控制台后,它就可以工作了。我做错了什么?

【问题讨论】:

标签: javascript google-chrome google-chrome-extension youtube-api content-script


【解决方案1】:

根本原因:
内容脚本在"isolated world" 环境中执行。

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

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

  • 注意页面脚本中是否需要chrome API:
    由于chrome.* API 不能在页面脚本中使用,您必须在内容脚本中使用它们,并通过 DOM 消息将结果发送到页面脚本(请参阅上面的注释)。

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

目录

  • 方法 1:注入另一个文件 - 与 ManifestV3 兼容
  • 方法二:注入嵌入式代码
  • 方法 2b:使用函数
  • 方法 3:使用内联事件
  • 注入代码中的动态值

方法一:注入另一个文件

目前唯一兼容 ManifestV3 的方法。 当您有大量代码时特别好。将代码放在扩展程序中的文件中,例如script.js。然后将其加载到您的content script 中,如下所示:

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 的manifest.json 示例

    "web_accessible_resources": ["script.js"],
    
  • ManifestV3 的manifest.json 示例

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

如果没有,控制台会出现如下错误:

拒绝加载 chrome-extension://[EXTENSIONID]/script.js。资源必须列在 web_accessible_resources 清单键中才能被扩展程序之外的页面加载。

方法二:注入嵌入式代码

当您想快速运行一小段代码时,此方法很有用。 (另见: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();

注意:template literals 仅在 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:使用内联事件

有时,您想立即运行一些代码,例如在创建 &lt;head&gt; 元素之前运行一些代码。这可以通过插入带有textContent&lt;script&gt; 标签来完成(参见方法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]) + ')';

【讨论】:

  • 这个答案应该是官方文档的一部分。官方文档应该以推荐的方式发布 --> 3 种方法来做同样的事情......错了吗?
  • @Qantas94Heavy 扩展程序的 CSP 确实影响内容脚本。只有 页面的 CSP 是相关的。方法 1 可以通过使用排除扩展源的 script-src 指令来阻止,方法 2 可以通过使用排除“unsafe-inline”`的 CSP 来阻止。
  • 有人问我为什么用script.parentNode.removeChild(script);删除脚本标签。我这样做的原因是因为我喜欢收拾我的烂摊子。当一个内联脚本被插入到文档中时,它会立即被执行并且&lt;script&gt;标签可以被安全地移除。
  • 其他方法:在内容脚本中的任何位置使用location.href = "javascript: alert('yeah')";。短sn-ps的代码更容易,也可以访问页面的JS对象。
  • @ChrisP 请小心使用javascript:。跨越多行的代码可能无法按预期工作。行注释 (//) 将截断其余部分,因此这将失败:location.href = 'javascript:// Do something &lt;newline&gt; alert(0);';。这可以通过确保使用多行 cmets 来规避。要注意的另一件事是表达式的结果应该是无效的。 javascript:window.x = 'some variable'; 将导致文档卸载,并替换为短语“一些变量”。如果使用得当,它确实是&lt;script&gt; 的一个有吸引力的替代品。
【解决方案2】:

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 消息传递使用结构化克隆算法,除了原始值之外,它只能传输some types of data。它不能发送类实例或函数或 DOM 元素。
  • 在 Firefox 中,要将内容脚本中的对象(即不是原始值)发送到页面上下文,您必须使用 cloneInto(内置函数)将其显式克隆到目标中,否则它将因安全违规错误而失败。

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
    

【讨论】:

  • 我实际上已经将答案第二行的代码和解释链接到stackoverflow.com/questions/9602022/…
  • 您是否有更新方法的参考(例如错误报告或测试用例?)CustomEvent 构造函数取代了已弃用的document.createEvent API。
  • 对我来说,'dispatchEvent(new CustomEvent...' 有效。我有 Chrome 33。以前也没有工作,因为我在注入 js 代码后编写了 addEventListener。
  • 我觉得官方的方式是使用window.postMessage:developer.chrome.com/extensions/…
  • 如何将响应从内容脚本发送回启动器脚本
【解决方案3】:

我也遇到了加载脚本的顺序问题,通过顺序加载脚本解决了这个问题。加载基于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();
    }
}

使用示例如下:

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 还很陌生,所以请随时向我推荐更好的方法。

【讨论】:

  • 这种插入脚本的方式不好,因为你污染了网页的命名空间。如果网页使用名为formulaImageUrlcodeImageUrl 的变量,那么您实际上是在破坏页面的功能。如果您想将变量传递给网页,我建议将数据附加到脚本元素(e.g. script.dataset.formulaImageUrl = formulaImageUrl;)并使用例如(function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })(); 在脚本中访问数据。
  • @RobW 感谢您的说明,尽管它更多地是关于示例。您能否澄清一下,为什么我应该使用 IIFE 而不是仅仅获取 dataset
  • document.currentScript 仅在执行时指向脚本标签。如果您想访问脚本标签和/或其属性/属性(例如dataset),则需要将其存储在变量中。我们需要一个 IIFE 来获得一个闭包来存储这个变量而不污染全局命名空间。
  • @RobW 太棒了!但是我们不能只使用一些变量名,它几乎不会与现有的相交。它只是非惯用语还是我们可能会遇到其他问题?
  • 你可以,但是使用 IIFE 的成本可以忽略不计,所以我认为没有理由更喜欢命名空间污染而不是 IIFE。我肯定不会以某种方式破坏其他人的网页,以及使用短变量名的能力。使用 IIFE 的另一个优点是您可以根据需要提前退出脚本 (return;)。
【解决方案4】:

在内容脚本中,我将脚本标记添加到绑定“onmessage”处理程序的头部,在我使用的处理程序内部,eval 以执行代码。 在展位内容脚本中,我也使用 onmessage 处理程序,所以我得到了两种方式的通信。 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 是一个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");

这样,我可以在 CS 和 Real Dom 之间进行 2 路通信。 例如,如果您需要收听 webscoket 事件,它非常有用, 或任何内存变量或事件。

【讨论】:

    【解决方案5】:

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

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

    您可以将参数(不幸的是没有对象和数组可以被字符串化)传递给函数。将其添加到 bartheses 中,如下所示:

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

    【讨论】:

    • 这很酷...但是带有颜色变量的第二个版本对我不起作用...我得到“无法识别”并且代码抛出错误...不将其视为变量。
    • 第一个例子就像一个魅力。非常感谢您的回答。即使内联脚本受到限制,这也有效,先生,我尊重你。
    • 很好的解决方法,不需要来回传递消息。
    【解决方案6】:

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

    这是通过将函数序列化为字符串并将其注入网页来完成的。

    实用程序是available here on 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'
    
    
    

    【讨论】:

      猜你喜欢
      相关资源
      最近更新 更多