从扩展访问页面上下文中定义的变量和函数

Access variables and functions defined in page context from an extension

提问人:André Alves 提问时间:3/1/2012 最后编辑:wOxxOmAndré Alves 更新时间:11/15/2023 访问量:384784

问:

我想在我的扩展中控制 youtube.com 的播放器:

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视频时,没有“状态已更改!

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

javascript 谷歌浏览器 扩展 内容脚本

评论

22赞 Eduardo 3/2/2012
尝试删除函数名称周围的引号:player.addEventListener("onStateChange", state);
5赞 Nilay Vishwakarma 2/1/2017
同样值得注意的是,在编写匹配项时,不要忘记包含 or ,这不会让您打包扩展,并且会抛出 Missing scheme separator 错误https://http://www.youtube.com/*
2赞 Pacerier 7/16/2017
另请参阅 bugs.chromium.org/p/chromium/issues/detail?id=478183

答:

1249赞 Rob W 3/1/2012 #1

根本原因:
内容脚本在隔离的“世界”环境中执行,这意味着它无法访问“世界”(页面上下文)中的 JS 函数和变量,也无法公开自己的 JS 内容,就像你所采用的方法一样。
MAINstate()

解决方案:
使用如下所示的方法将代码注入页面(“world”)的 JS 上下文中。
MAIN

关于使用 chrome API:
自 Chrome 107 以来允许通过externally_connectable消息传递
• 通过使用正常内容脚本进行消息传递,请参阅下一段。
<all_urls>CustomEvent

使用普通内容脚本进行消息传递时:
按此处、此处或此处此处所示使用。简而言之,注入的脚本向普通内容脚本发送一条消息,该脚本调用 或,然后通过另一条 CustomEvent 消息将结果发送到注入的脚本。不要使用,因为您的数据可能会破坏具有侦听器期望某种格式的消息的网站。
CustomEventchrome.storagechrome.runtime.sendMessagewindow.postMessage

谨慎!
该页面可能会重新定义内置原型或全局,并从您的私人通信中泄露数据,或使注入的代码失败。防止这种情况很复杂(参见 Tampermonkey 或 Violentmonkey 的“保险库”),因此请确保验证所有接收到的数据。

目录

那么,什么是最好的?对于 ManifestV3,如果代码应始终运行,请使用声明性方法 (#2),或者使用 (#1) 从扩展脚本(如 popup 或 Service Worker)进行条件注入,否则使用基于内容脚本的方法(#1 和 #3)。chrome.scripting

  • 内容脚本控件注入:

    • 方法 1:注入另一个文件 - 兼容 ManifestV3
    • 方法 2:注入嵌入代码 - MV2
    • 方法 2b:使用函数 - MV2
    • 方法 3:使用内联事件 - 兼容 ManifestV3
  • 扩展脚本控制注入(例如后台服务工作者或弹出脚本):

    • 方法 4:使用 executeScript 的世界 - 仅限 ManifestV3
  • 声明性注入:

    • 方法 5:在 manifest.json 中使用 - 仅限 ManifestV3,Chrome 111+world
  • 注入代码中的动态值

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

当你有很多代码时,特别好。将代码放在扩展名中的文件中,例如 .然后将其加载到您的内容脚本中,如下所示:script.js

var s = document.createElement('script');
s.src = chrome.runtime.getURL('script.js');
s.onload = function() { this.remove(); };
// see also "Dynamic values in the injected code" section in this answer
(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 清单键中列出,以便由扩展外部的页面加载。

方法 2:注入嵌入代码 (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)

有时,您希望立即运行一些代码,例如,在创建元素之前运行一些代码。这可以通过插入一个标签来完成(参见方法 2/2b)。<head><script>textContent

另一种(但不推荐)方法是使用内联事件。不建议这样做,因为如果页面定义了禁止内联脚本的内容安全策略,则会阻止内联事件侦听器。另一方面,扩展注入的内联脚本仍然运行。 如果您仍想使用内联事件,请按以下步骤操作:

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),键入 ,然后选择可用事件即可。resetdocument.documentElement.on

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

  • Chrome 95 或更高版本,具有chrome.scripting.executeScriptworld: 'MAIN'
  • Chrome 102 或更高版本,带有 ,还允许保证页面脚本的早期执行。chrome.scripting.registerContentScriptsworld: 'MAIN'runAt: 'document_start'

与其他方法不同,此方法适用于后台脚本或弹出脚本,而不是用于内容脚本。请参阅文档示例

方法 5:在 manifest.json 中使用(仅限 ManifestV3)world

在 Chrome 111 或更高版本中,您可以在 manifest.json 中添加声明以覆盖默认值,即 .脚本按列出的顺序运行。"world": "MAIN"content_scriptsISOLATED

  "content_scripts": [{
    "world": "MAIN",
    "js": ["page.js"],
    "matches": ["<all_urls>"],
    "run_at": "document_start"
  }, {
    "js": ["content.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 并添加以下行:

    s.dataset.params = JSON.stringify({foo: 'bar'});
    

    然后注入的脚本.js可以读取它:

    (() => {
      const params = JSON.parse(document.currentScript.dataset.params);
      console.log('injected params', params);
    })();
    

    若要从页面脚本中隐藏参数,可以将脚本元素放在已关闭的 ShadowDOM 中。

  • 方法 4 executeScript 有参数,registerContentScripts 目前没有(希望将来会添加)。args

评论

136赞 Mars Robertson 6/9/2013
这个答案应该是官方文档的一部分。官方文档应附带推荐的方式 - > 3 种方法来做同样的事情......错?
4赞 Qantas 94 Heavy 8/2/2013
通常,由于 Chrome 对某些扩展程序的 CSP(内容安全策略)限制,方法 1 尽可能更好。
15赞 Rob W 8/2/2013
@Qantas94Heavy 扩展的 CSP 不会影响内容脚本。只有页面的 CSP 是相关的。可以使用排除扩展源的指令来阻止方法 1,可以使用排除“unsafe-inline”的 CSP 来阻止方法 2。script-src
7赞 Rob W 8/29/2013
有人问我为什么使用 .我这样做的原因是因为我喜欢收拾我的烂摊子。在文档中插入内联脚本时,它会立即执行,并且可以安全地删除标记。script.parentNode.removeChild(script);<script>
9赞 Métoule 9/27/2013
其他方法:在内容脚本中的任意位置使用。它更容易处理短代码片段,还可以访问页面的 JS 对象。location.href = "javascript: alert('yeah')";
112赞 laktak 10/11/2013 #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 消息传递使用结构化克隆算法,除了原始值之外,该算法只能传输某些类型的数据。它不能发送类实例或函数或 DOM 元素。
  • 在Firefox中,要将对象(即不是原始值)从内容脚本发送到页面上下文,您必须使用(内置函数)将其显式克隆到目标中,否则它将失败并出现安全违规错误。cloneInto

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

评论

1赞 Rob W 10/11/2013
我实际上已经链接到我答案第二行的代码和解释,stackoverflow.com/questions/9602022/...
1赞 Rob W 11/7/2013
您是否有更新方法的参考(例如错误报告或测试用例?构造函数取代了已弃用的 API。CustomEventdocument.createEvent
1赞 jdunk 3/28/2017
要格外小心作为第二个参数传递给构造函数的内容。我经历了 2 个非常令人困惑的挫折:1.简单地在“细节”周围加上单引号令人困惑地使我的内容脚本的听众收到价值。2. 更重要的是,出于某种原因,我不得不这样做,否则它也会变成.鉴于此,在我看来,以下 Chromium 开发人员的说法——“结构化克隆”算法是自动使用的——是不正确的。bugs.chromium.org/p/chromium/issues/detail?id=260378#c18CustomEventnullJSON.parse(JSON.stringify(myData))null
4赞 Enrique 12/15/2018
我认为官方方法是使用 window.postMessage: developer.chrome.com/extensions/...
2赞 Vinay 9/16/2019
如何将响应从内容脚本发送回发起方脚本
9赞 Dmitry Ginzburg 5/18/2015 #3

我还遇到了加载脚本的排序问题,这是通过按顺序加载脚本来解决的。加载基于 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 有点陌生,所以请随时向我提供更好的方法。

评论

3赞 Rob W 5/18/2015
这种插入脚本的方式并不好,因为您正在污染网页的命名空间。如果网页使用名为 or 的变量,则实际上破坏了页面的功能。如果你想将一个变量传递给网页,我建议将数据附加到脚本元素()并使用例如 在脚本中访问数据。formulaImageUrlcodeImageUrle.g. script.dataset.formulaImageUrl = formulaImageUrl;(function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();
0赞 Dmitry Ginzburg 5/18/2015
@RobW感谢您的笔记,尽管它更多的是关于样本的。您能否澄清一下,为什么我应该使用 IIFE 而不是仅仅获得?dataset
4赞 Rob W 5/18/2015
document.currentScript 仅在执行时指向脚本标记。如果您想访问脚本标签和/或其属性/属性(例如),则需要将其存储在变量中。我们需要一个 IIFE 来获得一个闭包来存储这个变量,而不会污染全局命名空间。dataset
0赞 Dmitry Ginzburg 5/18/2015
@RobW太好了!但是我们不能只使用一些变量名称,它几乎不会与现有的变量相交。它只是非惯用语,还是我们可能遇到其他一些问题?
2赞 Rob W 5/18/2015
你可以,但使用 IIFE 的成本可以忽略不计,所以我认为没有理由更喜欢命名空间污染而不是 IIFE。我当然很看重我不会以某种方式破坏其他人的网页,以及使用短变量名称的能力。使用 IIFE 的另一个优点是,如果需要,可以提前退出脚本 ()。return;
8赞 doron aviguy 1/11/2016 #4

在内容脚本中,我将脚本标签添加到绑定“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 是一个帖子消息 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 之间进行 2 路通信。 它非常有用,例如,如果您需要收听 webscoket 事件, 或内存中的任何变量或事件。

4赞 Tarmo Saluste 4/5/2018 #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');

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

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

评论

0赞 11teenth 8/15/2018
这很酷......但是第二个版本,带有颜色变量,对我不起作用......我被“无法识别”,代码抛出错误......不将其视为变量。
2赞 John Yepthomi 11/3/2021
第一个示例就像一个魅力。非常感谢您的回答。即使内联脚本受到限制,这也有效,先生,我尊重您。
1赞 forgetso 12/30/2021
很好的解决方法,不需要来回传递消息。
1赞 stallingOne 10/25/2022
content-script.js:拒绝执行内联事件处理程序,因为它违反了以下内容安全策略指令:“script-src 'report-sample'
8赞 Arik 4/5/2020 #6

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

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

该实用程序可在 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'


1赞 Ali Zarei 11/1/2023 #7

如果要在注入的代码 (Manifest V3) 中使用动态值,并且注入的脚本类型为 module,则不能按照 Rob 的回答中所述使用。但是,您可以使用 传递 url 参数并在注入的代码中检索它们。请参阅示例:document.currentScript.datasetimport.meta.url

内容脚本:

const s = document.createElement('script');
s.src = chrome.runtime.getURL(`injected-script.js?extensionId=${chrome.runtime.id}`);
s.type = 'module';
s.onload = () => s.remove();
(document.head || document.documentElement).append(s);

注入的脚本:

const extensionId = new URL(import.meta.url).searchParams.get("extensionId")