根本原因:
内容脚本在一个“隔离”环境中执行,意味着它无法访问主页面上的JS函数和变量,也无法暴露自己的JS内容,比如你的情况中的state()方法。
解决方案:
使用下面所示的方法将代码注入到页面的JS上下文(主页面)中。
关于使用chrome API:
• 通过在
<all_urls>
上允许的
externally_connectable
消息传递,自Chrome 107版本开始可用。
• 通过与正常内容脚本使用
CustomEvent
消息传递,详见下一段落。
在与正常内容脚本进行消息传递时:
使用CustomEvent
,如这里,或这里,或这里。简而言之,注入的脚本向正常内容脚本发送消息,正常内容脚本调用chrome.storage
或chrome.runtime.sendMessage
,然后通过另一个CustomEvent消息将结果发送回注入的脚本。不要使用window.postMessage
,因为您的数据可能会破坏那些期望特定格式的消息的站点。
注意!
页面可能会重新定义内置原型或全局对象,并窃取您的私人通信数据或导致您的注入代码失败。防范这种情况很复杂(请参考Tampermonkey或Violentmonkey的“vault”),因此请确保验证所有接收到的数据。
目录
那么,什么是最好的选择呢?如果代码应该始终运行,那么使用ManifestV3的声明式方法(#5);如果需要从扩展脚本(如弹出窗口或服务工作者)进行条件性注入,则使用chrome.scripting(#4);否则,使用基于内容脚本的方法(#1和#3)。
- 内容脚本控制注入:
- 方法1:注入另一个文件 - ManifestV3兼容
- 方法2:注入嵌入代码 - MV2
- 方法2b:使用函数 - MV2
- 方法3:使用内联事件 - ManifestV3兼容
- 扩展脚本控制注入(例如后台服务工作者或弹出窗口脚本):
- 方法4:使用executeScript的world - 仅ManifestV3
- 声明式注入:
- 方法5:在manifest.json中使用world - 仅ManifestV3,Chrome 111+
- 注入代码中的动态值
方法一:注入另一个文件(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 清单键中列出,以便被扩展之外的页面加载。
方法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() {
var alert = null;
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);
方法三:使用内联事件(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”和“world:'MAIN'”
- Chrome 102或更新版本,使用“chrome.scripting.registerContentScripts”和“world:'MAIN'”,还允许使用“runAt:'document_start'”来确保页面脚本的早期执行。
与其他方法不同,此方法适用于后台脚本或弹出脚本,而不适用于内容脚本。请参阅
documentation和
examples获取更多信息。
方法五:在 manifest.json 中使用
world
(仅适用于 ManifestV3)
在 Chrome 111 或更高版本中,您可以在 manifest.json 的
content_scripts
声明中添加
"world": "MAIN"
,以覆盖默认值
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 + ')';
var actualCode = "(function(GREETING, NAME) {...})(Hi, I'm ,Rob)";
解决方案是在传递参数之前使用
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'});
然后注入的script.js可以读取它:
(() => {
const params = JSON.parse(document.currentScript.dataset.params);
console.log('injected params', params);
})();
为了隐藏页面脚本中的参数,您可以将脚本元素放在封闭的ShadowDOM中。
方法4 executeScript有args
参数,registerContentScripts目前没有(希望将来会添加)。
player.addEventListener("onStateChange", state);
- Eduardohttps://
或http://
,在www.youtube.com/*
中,如果没有包含协议前缀,就无法打包扩展,并且会抛出 Missing scheme separator error 错误。 - Nilay Vishwakarma