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

新手上路,请多包涵

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

清单.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 视频时。

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

原文由 André Alves 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

根本原因:

内容脚本在 “孤立世界” 环境中执行。

解决方案:

使用 DOM 将代码注入页面 - 该代码将能够 访问 页面上下文(“主世界”)的函数/变量或 函数/变量公开给页面上下文(在您的情况下是 state() 方法)。

  • 如果需要与页面脚本通信,请注意:

使用 DOM CustomEvent 处理程序。示例:

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

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

安全警告

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

目录

  • 方法 1:注入另一个文件 - ManifestV3 兼容

  • 方法二:注入嵌入式代码——MV2

  • 方法 2b:使用函数 - MV2

  • 方法 3:使用内联事件 - ManifestV3 兼容

  • 方法 4:使用 executeScript 的世界 - 仅限 ManifestV3

  • 注入代码中的动态值

方法一:注入另一个文件(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 的 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 清单键中,才能被扩展之外的页面加载。

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

当您想快速运行一小段代码时,此方法很有用。 (另请参阅: 如何使用 Chrome 扩展程序禁用 facebook 热键? )。

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

注意:此方法假定没有其他全局事件侦听器处理 reset 事件。如果有,您还可以选择其他全球事件之一。只需打开 JavaScript 控制台 (F12),键入 document.documentElement.on ,然后选择可用事件。

方法 4:使用 chrome.scripting API world (仅限 ManifestV3)

  • Chrome 95 或更新版本, chrome.scripting.executeScript with world: 'MAIN'

  • Chrome 102 或更新版本 chrome.scripting.registerContentScripts with world: 'MAIN' 也允许 runAt: '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)

  • 方法一可以在内容脚本中设置脚本元素的URL:
 s.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 发布,翻译遵循 CC BY-SA 4.0 许可协议

唯一的事情丢失的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),
  }));

原文由 laktak 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题