我正在学习如何创建 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 视频时。
将此代码放入控制台时,它可以工作。我究竟做错了什么?
player.addEventListener("onStateChange", state);
https://
或 http://
,此 www.youtube.com/*
不会让您打包扩展名并会抛出 Missing scheme separator error
根本原因:
内容脚本在 "isolated world" 环境中执行。
解决方案:
使用 DOM 将代码注入页面 - 该代码将能够访问页面上下文(“主世界”)的函数/变量或 < em>向页面上下文公开函数/变量(在您的情况下,它是 state()
方法)。
如果需要与页面脚本通信,请注意:使用 DOM CustomEvent 处理程序。例如:一、二、三。
注意如果页面脚本中需要 chrome API:由于 chrome.* API 不能在页面脚本中使用,您必须在内容脚本中使用它们并通过 DOM 消息将结果发送到页面脚本(请参阅注意上面)。
安全警告:
页面可能会重新定义或扩充/挂钩内置原型,因此如果页面以不兼容的方式执行此操作,您公开的代码可能会失败。如果您想确保您的公开代码在安全的环境中运行,那么您应该 a) 使用 "run_at": "document_start" 声明您的内容脚本并使用方法 2-3 而不是 1,或者 b) 通过一个空的iframe,example。请注意,对于 document_start
,您可能需要在公开代码中使用 DOMContentLoaded
事件来等待 DOM。
目录
方法 1:注入另一个文件 - ManifestV3 兼容
方法二:注入嵌入式代码——MV2
方法 2b:使用函数 - MV2
方法 3:使用内联事件 - ManifestV3 兼容
方法 4:使用 executeScript 的世界 - 仅限 ManifestV3
注入代码中的动态值
方法 1:注入另一个文件 - 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“web_accessible_resources”的 manifest.json 示例:[“script.js”],
ManifestV3 "web_accessible_resources" 的 manifest.json 示例:[{ "resources": ["script.js"], "matches": ["
如果没有,控制台中会出现以下错误:
拒绝加载 chrome-extension://[EXTENSIONID]/script.js。资源必须列在 web_accessible_resources 清单键中,才能被扩展之外的页面加载。
方法二:注入嵌入式代码——MV2
当您想快速运行一小段代码时,此方法很有用。 (另见: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:使用函数 - 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 兼容
有时,您希望立即运行一些代码,例如在创建 <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 世界 - 仅限 ManifestV3
Chrome 95 或更新版本,chrome.scripting.executeScript with world: 'MAIN'
Chrome 102 或更新版本,带有 world: 'MAIN' 的 chrome.scripting.registerContentScripts,还允许 runAt: 'document_start' 保证页面脚本的早期执行。
与其他方法不同,此方法适用于后台脚本或弹出脚本,而不适用于内容脚本。请参阅 documentation 和 examples。
注入代码中的动态值
有时,您需要将任意变量传递给注入函数。例如:
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]) + ')';
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
API。
我还遇到了加载脚本的排序问题,通过顺序加载脚本解决了这个问题。加载基于 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 有点陌生,所以请随时向我提出更好的方法。
formulaImageUrl
或 codeImageUrl
的变量,那么您实际上是在破坏网页的功能。如果您想将变量传递给网页,我建议将数据附加到脚本元素 (e.g. script.dataset.formulaImageUrl = formulaImageUrl;
) 并在脚本中使用例如 (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();
来访问数据。
dataset
?
document.currentScript
仅在执行时指向脚本标记。如果您想访问脚本标签和/或其属性/属性(例如 dataset
),则需要将其存储在变量中。我们需要一个 IIFE 来获得一个闭包来存储这个变量而不污染全局命名空间。
return;
)。
在内容脚本中,我将脚本标记添加到绑定“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 是一个帖子消息 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 事件,或者任何内存变量或事件,它非常有用。
您可以使用我为在页面上下文中运行代码并取回返回值而创建的实用程序函数。
这是通过将函数序列化为字符串并将其注入网页来完成的。
实用程序是 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'
如果你想注入纯函数,而不是文本,你可以使用这个方法:
函数注入(){ document.body.style.backgroundColor = 'blue'; } // 这包括作为文本的函数,并且括号使它自己运行。 var actualCode = "("+inject+")()"; document.documentElement.setAttribute('onreset', actualCode); document.documentElement.dispatchEvent(new CustomEvent('reset')); document.documentElement.removeAttribute('onreset');
您可以将参数(不幸的是没有对象和数组可以被字符串化)传递给函数。将其添加到 baretheses 中,如下所示:
函数注入(颜色){ document.body.style.backgroundColor = color; } // 这包括作为文本的函数,并且括号使它自己运行。 var color = '黄色'; var actualCode = "("+inject+")("+color+")";
不定期副业成功案例分享
script-src
指令来阻止,方法 2 可以通过使用排除“unsafe-inline”`的 CSP 来阻止。script.parentNode.removeChild(script);
删除脚本标签。我这样做的原因是因为我喜欢收拾我的烂摊子。在文档中插入内联脚本后,它会立即执行,并且可以安全地删除<script>
标记。location.href = "javascript: alert('yeah')";
。更短的代码片段更容易,并且还可以访问页面的 JS 对象。