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

x7yiwoj4  于 2023-11-15  发布在  Java
关注(0)|答案(7)|浏览(83)

我想在我的扩展中控制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视频时,控制台给我 “已开始!",但没有 “状态已更改!"
当这段代码被放到控制台时,它工作了。我做错了什么?

huwehgph

huwehgph1#

根本原因:

内容脚本是在ISOLATED“world”环境中执行的,这意味着它不能访问MAIN“world”(页面上下文)中的JS函数和变量,也不能公开自己的JS内容,就像你的例子中的state()方法一样。

解决方案:

使用以下方法将代码注入到页面的JS上下文(MAIN“world”)中。

使用chrome API时:

·通过<all_urls>上的externally_connectable消息传递允许since Chrome 107
·使用普通内容脚本通过CustomEvent消息传递,请参阅下一段。

使用普通内容脚本发送消息时:

使用CustomEvent,如here,或here,或here所示。简而言之,注入的脚本向正常内容脚本发送消息,该脚本调用chrome.storagechrome.runtime.sendMessage,然后通过另一个CustomEvent消息将结果发送到注入的脚本。不要使用window.postMessage,因为您的数据可能会破坏具有期望特定格式的侦听器的站点的信息。

注意!

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

目录

所以,什么是最好的?对于ManifestV 3,如果代码应该始终运行,则使用声明性方法(#5),或者使用chrome.scripting(#4)从扩展脚本(如popup或service worker)进行条件注入,否则使用基于内容脚本的方法(#1和#3)。

*内容脚本控制注入:

  • 方法1:注入另一个文件-ManifestV 3兼容
  • 方法2:注入嵌入式代码-MV 2
  • 方法2b:使用函数-MV 2
  • 方法3:使用内联事件-ManifestV 3兼容
    *扩展脚本控制注入(如后台服务worker或popup脚本):
  • 方法4:使用executeScript的世界-仅限ManifestV 3
    *声明性注入:
  • 方法5:在manifest.json中使用world-仅限ManifestV 3,Chrome 111+
  • 注入代码中的动态值

方法一:注入另一个文件(ManifestV 3/MV 2)

当你有很多代码的时候特别好。把代码放在你扩展名的文件中,比如script.js。然后像这样把它加载到你的content script中:

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中暴露

  • ManifestV 2的manifest.json示例
"web_accessible_resources": ["script.js"],

  • ManifestV 3的manifest.json示例
"web_accessible_resources": [{
  "resources": ["script.js"],
  "matches": ["<all_urls>"]
}]


否则,控制台中会出现以下错误:
拒绝加载chrome-extension://[EXTENSIONID]/script. js。资源必须在web_accessible_resources清单键中列出,以便由扩展外部的页面加载。

方法二:插入嵌入代码(MV 2)

当你想快速运行一小段代码时,这个方法很有用。(参见: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:使用函数(MV 2)

对于一大块代码,引用字符串是不可行的。代替使用数组,可以使用一个函数,并进行字符串化:

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"

方法三:使用内联事件(ManifestV 3/MV 2)

有时,你想立即运行一些代码,例如在<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,然后选择可用的事件。

方法四:使用Chrome.scripting API world(仅限ManifestV 3)

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

与其他方法不同,这个方法用于后台脚本或弹出脚本,而不是内容脚本。参见documentationexamples

方法五:在manifest.json中使用world(仅限ManifestV 3)

在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"
  }],

注入代码中的动态值(MV 2)

有时候,你需要传递一个任意的变量给注入的函数。例如:

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

注入代码中的动态值(ManifestV 3)

  • 使用方法1并添加以下行:
s.dataset.params = JSON.stringify({foo: 'bar'});

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

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

要隐藏页面脚本中的参数,您可以将script元素放在closed ShadowDOM中。

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

kqqjbcuj2#

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消息传递使用结构化克隆算法,除了原始值外,只能传递some types of data,不能发送类示例或函数或DOM元素。
  • 在Firefox中,要从内容脚本发送对象(即不是原始值)到页面上下文,您必须使用cloneInto(内置函数)显式地将其克隆到目标中,否则它将失败并出现安全违规错误。
document.dispatchEvent(new CustomEvent('yourCustomEvent', {
  detail: cloneInto(data, document.defaultView),
}));

8dtrkrch

8dtrkrch3#

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

dfty9e19

dfty9e194#

在Content脚本中,我将脚本标记添加到头部,它绑定了一个'onmessage'处理程序,在处理程序中,我使用eval来执行代码。在booth内容脚本中,我也使用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是一个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");


这样,我可以有2种方式之间的通信CS到真实的DOM。它非常有用的,例如,如果你需要监听webscoket事件,或任何内存中的变量或事件。

yftpprvb

yftpprvb5#

您可以使用我创建的一个实用函数来在页面上下文中运行代码并获取返回值。
这是通过将函数序列化为字符串并将其注入到Web页面来完成的。
该实用程序是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'

字符串

5ktev3wc

5ktev3wc6#

如果你想注入纯函数,而不是文本,你可以使用这个方法:

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

2exbekwf

2exbekwf7#

如果你想在注入的代码(ManifestV 3)中使用动态值,并且你注入的脚本的类型是模块,你不能像Rob的回答中描述的那样使用document.currentScript.dataset,相反,你可以将参数作为url参数传递,并在注入的代码中检索它们。下面是一个例子:
内容脚本:

var s = document.createElement('script');
s.src = chrome.runtime.getURL('../override-script.js?extensionId=' + chrome.runtime.id);
s.type = 'module';
s.onload = function () {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

字符串
注入的代码(在我的例子中是override-script.js):

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

相关问题