使用内容脚本将代码插入页面上下文


480

我正在学习如何创建Chrome扩展程序。我刚刚开始开发一个捕捉YouTube事件的工具。我想将其与YouTube Flash Player结合使用(稍后,我将尝试使其与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视频时。

将此代码放入控制台后,它就可以工作了。我究竟做错了什么?


14
尝试删除函数名称周围的引号:player.addEventListener("onStateChange", state);
Eduardo 2012年

2
还值得注意的是,在编写匹配项时,不要忘记包含https://http://,这www.youtube.com/*不会让您打包扩展名,并且会引发缺少方案分隔符错误
Nilay Vishwakarma

Answers:


874

内容脚本在“隔离的世界”环境中执行。您必须将state()方法注入页面本身。

如果您想使用chrome.*脚本中的API之一,则必须实现特殊的事件处理程序,如以下答案中所述:Chrome扩展程序-检索Gmail的原始消息

否则,如果您不必使用chrome.*API,我强烈建议您通过添加<script>标签将所有JS代码注入页面中:

目录

  • 方法1:注入另一个文件
  • 方法2:注入嵌入式代码
  • 方法2b:使用一个函数
  • 方法3:使用一个内联事件
  • 注入代码中的动态值

方法1:注入另一个文件

当您有很多代码时,这是最简单/最佳的方法。将实际的JS代码包括在扩展名中的文件中,例如script.js。然后,让您的内容脚本如下所示(在此处进行说明:Google Chome“ Application Shortcut” Custom Javascript):

var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

注意:如果使用此方法,则必须将注入的script.js文件添加到该"web_accessible_resources"部分示例)。如果不这样做,Chrome会拒绝加载脚本并在控制台中显示以下错误:

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

方法2:注入嵌入式代码

当您想快速运行一小段代码时,此方法很有用。(另请参见:如何使用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('\n');

方法2b:使用一个函数

对于一大段代码,引用字符串是不可行的。除了使用数组,还可以使用函数并对其进行字符串化:

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:使用一个内联事件

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

注入代码中的动态值

有时,您需要将任意变量传递给注入的函数。例如:

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

81
这个答案应该是官方文档的一部分。官方文档应该以推荐的方式提供-> 3种做同一件事的方式...错了吗?
火星罗伯逊

7
@ Qantas94Heavy扩展的CSP也不会影响内容的脚本。仅页面的CSP是相关的。可以使用script-src不包含扩展名来源的指令来阻止方法1,可以使用不包含“ unsafe-inline”`的CSP来阻止方法2。
罗布W

3
有人问为什么我使用删除脚本标签script.parentNode.removeChild(script);。我这样做的原因是因为我喜欢清理我的烂摊子。将嵌入式脚本插入文档后,将立即执行该脚本,并且<script>可以安全地删除该标记。
罗布W

9
其他方法:location.href = "javascript: alert('yeah')";在内容脚本中的任何位置使用。简短的代码片段更容易,并且还可以访问页面的JS对象。
Métoule

3
@ChrisP使用时要小心javascript:。跨多行的代码可能无法按预期工作。行注释(//)将截断其余部分,因此将失败:location.href = 'javascript:// Do something <newline> alert(0);';。通过确保使用多行注释可以避免这种情况。要注意的另一件事是表达式的结果应该为空。javascript:window.x = 'some variable';将导致文档被卸载,并被替换为“某些变量”。如果使用得当,它确实是的有吸引力的替代品<script>
Rob W

61

唯一的事情 失踪 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),
    }));

我实际上已经将我答案的第二行中的代码和解释链接到stackoverflow.com/questions/9602022/…
2013年

1
您是否有更新方法的参考(例如,错误报告或测试用例?)CustomEvent构造函数取代了不赞成使用的document.createEventAPI。
罗布W

对我来说,“dispatchEvent(新的自定义事件...”工作,我有Chrome浏览器33.另外,因为我注入JS代码后写了以前的addEventListener没有工作。
jscripter

要特别注意将第二个参数传递给CustomEvent构造函数的内容。我遇到了2个非常令人困惑的挫折:1. null当Content Script的侦听器接收到“ detail”时,简单地将单引号引起困惑就使该值生效。2.更重要的是,由于某种原因,我不得不这样做JSON.parse(JSON.stringify(myData)),否则它也将变成null。鉴于此,在我看来,以下Chromium开发人员的主张(即自动使用“结构化克隆”算法)是不正确的。bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk

我认为官方方式是使用window.postMessage:developer.chrome.com/extensions/…–
Enrique

9

我还遇到了已加载脚本排序的问题,这是通过顺序加载脚本解决的。加载基于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的新手,所以随时向我推荐更好的方法。


3
这种插入脚本的方法不好,因为您正在污染网页的名称空间。如果网页使用名为formulaImageUrl或的变量codeImageUrl,那么您实际上是在破坏网页的功能。如果要将变量传递给网页,建议将数据附加到脚本元素(e.g. script.dataset.formulaImageUrl = formulaImageUrl;),并(function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();在脚本中使用例如访问数据。
Rob W

@RobW感谢您的来信,尽管它更多地与示例有关。您能否说明一下,为什么我应该使用IIFE而不是仅仅使用dataset
德米特里·金茨堡

4
document.currentScript在执行时仅指向script标记。如果您想访问脚本标签和/或其属性/属性(例如dataset),则需要将其存储在变量中。我们需要一个IIFE来获得一个闭包来存储此变量,而又不会污染全局名称空间。
罗布W

@RobW太好了!但是我们不能只使用一些几乎不与现有变量相交的变量名。是非惯用语还是我们可能会有其他问题?
德米特里·金茨堡

2
可以,但是使用IIFE的成本可以忽略不计,因此我看不出有理由比IIFE更喜欢名称空间污染。我很肯定我不会以某种方式破坏他人的网页,并拥有使用简短的变量名的能力。使用IIFE的另一个好处是,如果需要(return;),您可以更早退出脚本。
罗布W

6

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

这样,我就可以在CS与Real Dom之间进行2路通信。例如,如果您需要侦听webscoket事件或任何内存变量或事件,它非常有用。


1

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

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

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


0

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

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

您可以将参数(不幸的是,不能将对象和数组进行字符串化)传递给函数。将其添加到裸露的容器中,如下所示:

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


这很酷...但是带有颜色变量的第二个版本对我不起作用...我无法识别,代码抛出错误...不将其视为变量。
-18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.