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

And*_*ves 452 javascript google-chrome youtube-api google-chrome-extension content-script

我正在学习如何创建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"]
    }]
}
Run Code Online (Sandbox Code Playgroud)

myScript.js:

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

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

将此代码放入控制台时,它可以正常工作.我究竟做错了什么?

Rob*_*b W 828

内容脚本在"孤立的世界"环境中执行.您必须将您的state()方法注入页面本身.

如果要chrome.*在脚本中使用其中一个API,则必须实现一个特殊的事件处理程序,如本答案中所述:Chrome扩展程序 - 检索Gmail的原始邮件.

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

目录

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

方法1:注入另一个文件

当你有很多代码时,这是最简单/最好的方法.请将您的实际JS代码包含在扩展名中的文件中script.js.然后让您的内容脚本如下(在此处解释:Google Chome"应用程序快捷方式"自定义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);
Run Code Online (Sandbox Code Playgroud)

注意:如果使用此方法,则必须将注入的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();
Run Code Online (Sandbox Code Playgroud)

注意: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');
Run Code Online (Sandbox Code Playgroud)

方法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();
Run Code Online (Sandbox Code Playgroud)

此方法有效,因为+字符串和函数上的运算符将所有对象转换为字符串.如果您打算不止一次使用代码,那么创建一个函数以避免代码重复是明智的.实现可能如下所示:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});
Run Code Online (Sandbox Code Playgroud)

注意:由于函数是序列化的,原始范围和所有绑定属性都将丢失!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"
Run Code Online (Sandbox Code Playgroud)

方法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');
Run Code Online (Sandbox Code Playgroud)

注意:此方法假定没有其他全局事件侦听器处理该reset事件.如果有,您还可以选择其他全球事件之一.只需打开JavaScript控制台(F12),输入document.documentElement.on并选择可用事件即可.

注入代码中的动态值

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

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};
Run Code Online (Sandbox Code Playgroud)

要注入此代码,您需要将变量作为参数传递给匿名函数.一定要正确实施!下面将工作:

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!
Run Code Online (Sandbox Code Playgroud)

解决方案是JSON.stringify在传递参数之前使用.例:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';
Run Code Online (Sandbox Code Playgroud)

如果你有很多变量,那么值得使用JSON.stringify一次,以提高可读性,如下所示:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Run Code Online (Sandbox Code Playgroud)

  • 这个答案应该是官方文档的一部分.官方文档应该以推荐的方式发布 - > 3种方法来做同样的事情......错了吗? (74认同)
  • 其他方法:在你的内容脚本中的任何地方使用`location.href ="javascript:alert('yeah')";` 简短的代码片段更容易,也可以访问页面的JS对象. (9认同)
  • @ Qantas94Heavy扩展程序的CSP不会影响内容脚本.只有**页面的CSP**是相关的.方法1可以通过使用排除扩展名来源的`script-src`指令来阻止,方法2可以通过使用排除"unsafe-inline"的CSP来阻止. (6认同)
  • 有人问为什么我使用`script.parentNode.removeChild(script);`删除脚本标记.我这样做的原因是因为我喜欢清理我的烂摊子.当在文档中插入内联脚本时,它会立即执行,并且可以安全地删除`<script>`标记. (3认同)
  • @ChrisP小心使用`javascript:`.跨越多行的代码可能无法按预期工作.行注释(`//`)将截断余数,因此这将失败:`location.href ='javascript://执行某些操作<newline> alert(0);';`.确保使用多行注释可以避免这种情况.另外要注意的是表达式的结果应该是无效的.`javascript:window.x ='some variable';`将导致文档卸载,并替换为短语'some variable'.如果使用得当,它确实是"<script>"的有吸引力的替代品. (3认同)
  • 由于 Chrome 对某些扩展程序的 CSP(内容安全策略)限制,通常方法 1 在任何可能的情况下都更好。 (2认同)
  • @Lai32290使用事件或HTML属性,例如使用`document.currentScript.dataset.foo ='只能在注入的脚本中共享字符串';`,然后使用`script.dataset.foo`读取它(假设`script`是对你注入的`<script>`标签的引用. (2认同)

lak*_*tak 54

唯一的事情 失踪 从Row W中隐藏的优秀答案是如何从注入的脚本调用内容脚本,反之亦然(特别是如果你有无法字符串化的对象).

在inject或您的内容脚本中添加一个事件监听器:

document.addEventListener('yourCustomEvent', function (e) {
  var data = e.detail;
  console.log('received', data);
});
Run Code Online (Sandbox Code Playgroud)

另一方面(内容或注入脚本)调用事件:

var data = {
  any: 'JSON-ifiable data',
  meaning: 'no DOM elements or classes/functions',
};

document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));
Run Code Online (Sandbox Code Playgroud)

  • 我认为官方方法是使用 window.postMessage:https://developer.chrome.com/extensions/content_scripts#host-page-communication (4认同)
  • 如何将响应从内容脚本发送回启动器脚本 (2认同)

Dmi*_*urg 8

我还遇到了加载脚本的排序问题,这是通过顺序加载脚本来解决的.装载是基于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();
    }
}
Run Code Online (Sandbox Code Playgroud)

使用的例子是:

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")
]);
Run Code Online (Sandbox Code Playgroud)

实际上,我是JS的新手,所以请随意向我发出更好的方法.

  • [`document.currentScript`](https://developer.mozilla.org/en-US/docs/Web/API/Document/currentScript)仅在脚本标记执行时指向它.如果您想访问脚本标记和/或其属性/属性(例如`dataset`),则需要将其存储在变量中.我们需要一个IIFE来获取一个闭包来存储这个变量而不会污染全局命名空间. (4认同)
  • 这种插入脚本的方式并不好,因为您正在污染网页的命名空间.如果网页使用名为`formulaImageUrl`或`codeImageUrl`的变量,那么您实际上正在破坏页面的功能.如果你想将一个变量传递给网页,我建议将数据附加到脚本元素(例如script.dataset.formulaImageUrl = formulaImageUrl;`)并使用例如`(function(){var dataset = document.currentScript .dataset; alert(dataset.formulaImageUrl;)})();`在脚本中访问数据. (3认同)
  • 你可以,但使用IIFE的成本可以忽略不计,所以我没有理由比IIFE更喜欢命名空间污染.我非常重视我不会以某种方式打破其他人的网页*,以及使用短变量名称的能力.使用IIFE的另一个好处是,如果需要,可以提前退出脚本(`return;`). (2认同)

dor*_*guy 6

在内容脚本中,我将脚本标签添加到头部,它绑定了一个'onmessage'处理程序,在我使用的处理程序中,eval执行代码.在booth内容脚本中我也使用onmessage handler,所以我得到双向通信. 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);
 }
});
Run Code Online (Sandbox Code Playgroud)

pmListener.js是一个post message url listener

//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");
Run Code Online (Sandbox Code Playgroud)

这样,我可以在CS和Real Dom之间进行双向通信.例如,如果您需要监听webscoket事件或内存变量或事件中的任何事件,它非常有用.


Ari*_*rik 6

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

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

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


Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

236543 次

最近记录:

6 年,5 月 前