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

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

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

当将这段代码放入控制台中时,它工作了。我做错了什么?

354715 次浏览
< p > # EYZ0
内容脚本在“孤立world"环境中执行 < p > # EYZ0
使用DOM将代码注入页面-该代码将能够将页面上下文的访问函数/变量("main world")或暴露函数/变量添加到页面上下文(在您的情况下是state()方法)
    <李> < p > # EYZ0
    使用DOM CustomEvent处理器。例如:一个两个三个 <李> < p > # EYZ1
    因为chrome.* api不能在页面脚本中使用,你必须在内容脚本中使用它们,并通过DOM消息传递将结果发送到页面脚本(参见上面的注释)
< p > # EYZ0:
页面可能会重新定义或增强/挂钩内置原型,因此如果页面以不兼容的方式执行此操作,则暴露的代码可能会失败。如果你想确保你暴露的代码在一个安全的环境中运行,那么你应该a)使用“run_at":“document_start"声明你的内容脚本,并使用方法2-3而不是1,或者b)通过空iframe 例子提取原始的本地内置程序。注意,对于document_start,你可能需要在公开的代码中使用DOMContentLoaded事件来等待DOM

目录

  • 方法1:注入另一个兼容ManifestV3的文件
  • 方法二:注入嵌入式代码- MV2
  • 方法2b:使用MV2函数
  • 方法3:使用内联事件- ManifestV3兼容
  • 方法四:只使用executeScript的world - 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);

# EYZ1:

  • < p >清单。ManifestV2的json示例

    "web_accessible_resources": ["script.js"],
    
  • < p >清单。ManifestV3的json示例

    "web_accessible_resources": [{
    "resources": ["script.js"],
    "matches": ["<all_urls>"]
    }]
    

如果不是,控制台中会出现以下错误:

拒绝加载chrome-extension://[EXTENSIONID]/script.js。资源必须列在web_accessible_resources清单键中,以便由扩展之外的页面加载。

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

当您希望快速运行一小段代码时,此方法非常有用。(参见:如何禁用facebook热键与Chrome扩展?)。

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>元素创建之前运行一些代码。这可以通过插入<script>标签和textContent来完成(参见方法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,并选择可用的事件。

方法四:使用铬。脚本API world(仅适用于ManifestV3)

  • Chrome 95或更新版本,chrome.scripting.executeScriptworld: 'MAIN'
  • Chrome 102或更新版本,chrome.scripting.registerContentScriptsworld: '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)

  • 方法1可以设置内容脚本中script元素的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'));
    
  • Method 4 executeScript有args参数,registerContentScripts目前没有(希望将来会添加)。

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),
    }));
    

我还遇到过加载脚本的排序问题,这个问题是通过顺序加载脚本解决的。加载基于罗伯·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有点陌生,所以请随意ping我以更好的方式。

在内容脚本,我添加脚本标签的头绑定一个'onmessage'处理程序,在我使用的处理程序,eval执行代码。 在展台内容脚本我使用onmessage处理程序以及,所以我得到双向通信。 # EYZ0 < / p >

//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是一个post消息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之间进行双向通信。 它非常有用,例如如果你需要监听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');

并且可以将参数(不幸的是没有对象和数组可以被字符串化)传递给函数。把它加到baretheses中,像这样:

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

您可以使用我为在页面上下文中运行代码并返回返回值而创建的实用程序函数。

这是通过将函数序列化为字符串并将其注入到web页面来实现的。

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