提问人:André Alves 提问时间:3/1/2012 最后编辑:wOxxOmAndré Alves 更新时间:11/15/2023 访问量:384784
从扩展访问页面上下文中定义的变量和函数
Access variables and functions defined in page context from an extension
问:
我想在我的扩展中控制 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视频时,没有“状态已更改!
当这段代码放在控制台中时,它起作用了。我做错了什么?
答:
根本原因:
内容脚本在隔离的“世界”环境中执行,这意味着它无法访问“世界”(页面上下文)中的 JS 函数和变量,也无法公开自己的 JS 内容,就像你所采用的方法一样。
MAIN
state()
解决方案:
使用如下所示的方法将代码注入页面(“world”)的 JS 上下文中。MAIN
关于使用 chrome
API:
• 自 Chrome 107 以来允许通过externally_connectable
消息传递。
• 通过使用正常内容脚本进行消息传递,请参阅下一段。<all_urls>
CustomEvent
使用普通内容脚本进行消息传递时:
按此处、此处或此处或此处所示使用。简而言之,注入的脚本向普通内容脚本发送一条消息,该脚本调用 或,然后通过另一条 CustomEvent 消息将结果发送到注入的脚本。不要使用,因为您的数据可能会破坏具有侦听器期望某种格式的消息的网站。CustomEvent
chrome.storage
chrome.runtime.sendMessage
window.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
- 方法 5:在 manifest.json 中使用 - 仅限 ManifestV3,Chrome 111+
注入代码中的动态值
方法 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),键入 ,然后选择可用事件即可。reset
document.documentElement.on
方法 4:使用 chrome.scripting API(仅限 ManifestV3)world
- Chrome 95 或更高版本,具有
chrome.scripting.executeScript
world: 'MAIN'
- Chrome 102 或更高版本,带有 ,还允许保证页面脚本的早期执行。
chrome.scripting.registerContentScripts
world: 'MAIN'
runAt: 'document_start'
与其他方法不同,此方法适用于后台脚本或弹出脚本,而不是用于内容脚本。请参阅文档和示例。
方法 5:在 manifest.json 中使用(仅限 ManifestV3)world
在 Chrome 111 或更高版本中,您可以在 manifest.json 中添加声明以覆盖默认值,即 .脚本按列出的顺序运行。"world": "MAIN"
content_scripts
ISOLATED
"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
评论
script-src
script.parentNode.removeChild(script);
<script>
location.href = "javascript: alert('yeah')";
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), }));
评论
CustomEvent
document.createEvent
CustomEvent
null
JSON.parse(JSON.stringify(myData))
null
我还遇到了加载脚本的排序问题,这是通过按顺序加载脚本来解决的。加载基于 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 有点陌生,所以请随时向我提供更好的方法。
评论
formulaImageUrl
codeImageUrl
e.g. script.dataset.formulaImageUrl = formulaImageUrl;
(function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();
dataset
document.currentScript
仅在执行时指向脚本标记。如果您想访问脚本标签和/或其属性/属性(例如),则需要将其存储在变量中。我们需要一个 IIFE 来获得一个闭包来存储这个变量,而不会污染全局命名空间。dataset
return;
在内容脚本中,我将脚本标签添加到绑定“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 事件, 或内存中的任何变量或事件。
如果你想注入纯函数,而不是文本,你可以使用这个方法:
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+")";
评论
您可以使用我创建的实用工具函数,以便在页面上下文中运行代码并获取返回值。
这是通过将函数序列化为字符串并将其注入网页来完成的。
该实用程序可在 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'
如果要在注入的代码 (Manifest V3) 中使用动态值,并且注入的脚本类型为 module,则不能按照 Rob 的回答中所述使用。但是,您可以使用 传递 url 参数并在注入的代码中检索它们。请参阅示例:document.currentScript.dataset
import.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")
评论
player.addEventListener("onStateChange", state);
https://
http://
www.youtube.com/*