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

2024-11-02 21:00:00
admin
原创
44
摘要:问题描述:我想在我的扩展中控制 youtube.com 的播放器:清单.json:{ "name": "MyExtension", "version": "1.0&am...

问题描述:

我想在我的扩展中控制 youtube.com 的播放器:

清单.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}

我的Script.js:

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

问题是,控制台给了我“已开始!” ,但是当我播放/暂停 YouTube 视频时没有“状态已改变!” 。

当将此代码放入控制台时,它起作用了。我做错了什么?


解决方案 1:

根本原因:内容脚本在“世界”
环境
中执行,这意味着它无法访问“世界”(页面上下文)中的 JS 函数和变量,也无法公开其自己的 JS 内容,例如您案例中的方法。ISOLATED`MAIN`state()

解决方案:使用下面显示的方法

将代码注入页面的 JS 上下文( “world”)。MAIN

关于使用chromeAPI:

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

使用普通内容脚本发送消息:按此处、或此处、或此处所示

使用。简而言之,注入的脚本向普通内容脚本发送一条消息,后者调用或,然后通过另一条 CustomEvent 消息将结果发送到注入的脚本。不要使用,因为您的数据可能会破坏具有监听器并期望消息具有特定格式的站点。CustomEvent`chrome.storagechrome.runtime.sendMessagewindow.postMessage`

小心!

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

目录

那么,什么是最好的?对于 ManifestV3,如果代码应始终运行,则使用声明性方法 (#5),或者使用chrome.scripting(#4) 从扩展脚本(如弹出窗口或服务工作线程)进行条件注入,否则使用基于内容脚本的方法 (#1 和 #3)。

  • 内容脚本控制注入:

+ 方法 1:注入另一个文件 - 兼容 ManifestV3
+ 方法 2:注入嵌入代码 - MV2
+ 方法 2b:使用函数 - MV2
+ 方法 3:使用内联事件 - 兼容 ManifestV3
  • 扩展脚本控制注入(例如后台服务工作者或弹出脚本):

+ 方法 4:使用 executeScript 的世界 - 仅限 ManifestV3
  • 声明式注入:

+ 方法 5:在 manifest.json 中使用`world`- 仅限 ManifestV3,Chrome 111+
  • 注入代码中的动态值

方法 1:注入另一个文件(ManifestV3/MV2)

当您有大量代码时,这种方法尤其有用。将代码放在扩展程序中的文件中,例如script.js。然后将其加载到内容脚本中,如下所示:

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

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

方法 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 
' + 
                 '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.executeScriptworld: 'MAIN'

  • Chrome 102 或更新版本,chrome.scripting.registerContentScripts带有world: 'MAIN',还允许runAt: 'document_start'保证页面脚本的尽早执行。

与其他方法不同,此方法适用于背景脚本或弹出脚本,而不适用于内容脚本。请参阅文档和示例。

方法 5:world在 manifest.json 中使用(仅限 ManifestV3)

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

注入代码中的动态值(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并添加以下行:

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 目前没有(希望将来会添加)。

解决方案 2:

在 JS“世界”之间交换数据

想要接收数据的脚本添加事件监听器:

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

笔记:

  • 这是同步的,比异步的 window.postMessage 更安全,异步的 window.postMessage 可能会破坏一些已经使用它来接收与您不同的特定格式的数据的站点。

  • CustomEvent 使用结构化克隆算法,除了原始值之外,它只能传输某些类型的数据。它不能发送类实例或函数或 DOM 元素(请参阅下面的“DOM 节点”部分)。

  • cloneInto在 Firefox 中,要将对象(即非原始值)从内容脚本发送到页面上下文,您必须使用(内置函数)明确将其克隆到目标中,否则它将因安全违规错误而失败。

document.dispatchEvent(new CustomEvent('yourCustomEvent', {
  detail: cloneInto(data, document.defaultView),
}));
  • 在 Firefox 中您也可以直接使用wrappedJSObject,无需任何消息。

发送 DOM 节点

使用MouseEvent

// 接收者

addEventListener('foo', e => {
  let data = e.detail, node;
  if (data.cmd === 'node') { // these messages are synchronous
    addEventListener('foo2', e2 => { node = e2.relatedTarget; }, { once: true });
  }
  console.log('received', data, node);
});

// 发送者

dispatchEvent(new CustomEvent('foo', { detail: { cmd: 'node' } }));
dispatchEvent(new MouseEvent('foo2', { relatedTarget: myElem }));

解决方案 3:

在内容脚本中,我将脚本标签添加到绑定“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 是一个帖子消息 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 事件或任何内存变量或事件,它就非常有用。

解决方案 4:

我也遇到过加载脚本的顺序问题,这个问题通过按顺序加载脚本解决了。加载基于Rob 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 还很陌生,所以请随时向我咨询更好的方法。

解决方案 5:

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

这是通过将函数序列化为字符串并将其注入到网页来完成的。

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


解决方案 6:

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

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

运行代码片段Hide results展开片段

您还可以将参数(不幸的是,对象和数组不能被字符串化)传递给函数。将其添加到 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+")"; 

运行代码片段Hide results展开片段

解决方案 7:

改进 Ali Zarei 的答案,但使用 ES6 语法:

// content-script.js
const scriptElement = document.createElement('script');
scriptElement.src = chrome.runtime.getURL(`injected-script.js?extensionId=${chrome.runtime.id}`);
scriptElement.type = 'module';
scriptElement.onload = () => scriptElement.remove();
document.head.append(scriptElement);
// injected-script.js
const extensionId = new URL(import.meta.url).searchParams.get("extensionId")

解决方案 8:

如果您想在注入的代码 (ManifestV3) 中使用动态值,并且注入的脚本的类型是模块,则不能使用document.currentScript.datasetRob 的出色回答中描述的方法,而是可以将参数作为 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")
相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用