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

vlf7wbxs  于 2022-12-06  发布在  Go
关注(0)|答案(6)|浏览(293)

我正在学习如何创建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"]
    }]
}

我的脚本.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

问题是,当我播放/暂停YouTube视频时,控制台给我 “已启动!",但没有 “状态已更改!"
当这段代码放进控制台时,它工作了。我做错了什么?

tuwxkamq

tuwxkamq1#

    • 根本原因:**

内容脚本在"隔离世界"环境中执行。

    • 解决方案:**

使用DOM将代码注入到页面中--该代码将能够 * 访问 * 页面上下文("主世界")的函数/变量,或者 * 向页面上下文公开 * 函数/变量(在您的示例中是state()方法)。

      • 如果需要与页面脚本通信,请注意:**

使用DOM CustomEvent行程常式。范例:onetwothree中的一个或多个。

      • 如果页面脚本中需要chrome API,请注意:**

由于chrome.* API不能在页面脚本中使用,您必须在内容脚本中使用它们,并通过DOM消息传递将结果发送到页面脚本(请参阅上面的注解)。

    • 安全警告**:

网页可能会重新定义或增加/挂钩内置原型,因此如果网页以不兼容的方式执行代码,您的公开代码可能会失败。如果您想确保公开代码在安全的环境中运行,那么您应该a)使用"run_at"声明内容脚本:"document_start"并使用方法2 - 3而不是方法1,或者b)通过空iframe example提取原始原生内置。请注意,对于document_start,您可能需要在公开的代码中使用DOMContentLoaded事件来等待DOM。

目录

  • 方法1:插入另一个文件-ManifestV3兼容
  • 方法2:注入嵌入式代码-MV2
  • 方法2b:使用函数-MV2
  • 方法3:使用内联事件-ManifestV3兼容
  • 方法4:使用executeScript的环境-仅限清单V3
  • 注入代码中的动态值

方法1:插入另一个文件(清单V3/MV2)

当你有很多代码的时候特别好。把代码放在扩展名中的一个文件里,比如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);
"web_accessible_resources": ["script.js"],
  • 清单V3的manifest.json示例
"web_accessible_resources": [{
  "resources": ["script.js"],
  "matches": ["<all_urls>"]
}]

否则,控制台中将出现以下错误:
拒绝加载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"

方法三:使用内嵌事件(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,然后选择可用的事件。

方法四:使用chrome.脚本API world(仅限清单V3)

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

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

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

rlcwz9us2#

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

e5nqia273#

我还遇到了加载脚本的顺序问题,这个问题通过顺序加载脚本来解决,加载基于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,所以随时为我提供更好的方式。

7gs2gvoe

7gs2gvoe4#

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

sc4hvdpw5#

在内容脚本中,我将script标签添加到绑定了“onmessage”处理程序的head中,在我使用的处理程序中,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");

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

qq24tv8q

qq24tv8q6#

如果希望注入纯函数而不是文本,可以使用以下方法:

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

你还可以给函数传递参数(不幸的是,对象和数组都不能被字符串化),把它添加到barethesis中,如下所示:

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

相关问题