Chrome扩展程序:检查内容脚本是否已注入

26

我正在开发一个Chrome扩展程序。与其使用manifest.json来匹配所有URL的内容脚本,我会懒惰地通过调用chrome.tabs.executeScript在用户点击扩展图标时注入内容脚本。

我的目标是避免多次执行脚本。因此,在我的内容脚本中有以下代码:

if (!window.ALREADY_INJECTED_FLAG) {
    window.ALREADY_INJECTED_FLAG = true
    init() // <- All side effects go here
}

问题 #1,每次单击扩展程序图标时调用chrome.tabs.executeScript是否足够安全?换句话说,这是否具有幂等性?

问题 #2,是否有类似的方法可以使用 chrome.tabs.insertCSS

由于后台脚本无法访问 Web 页面的 DOM,因此似乎不可能检查内容脚本的注入状态。我已经尝试了一种 ping/pong 方法来检查任何内容脚本实例是否处于活动状态。但是这会增加开销并增加设计 ping-timeout 的复杂性。

问题 #3,是否有更好的后台脚本方法来检查内容脚本的注入状态,以便我可以防止在用户单击图标时每次调用 chrome.tabs.executeScript

提前致谢!

4个回答

22

每次单击扩展图标时,调用chrome.tabs.executeScript是否足够安全?换句话说,这是幂等的吗?

  1. 是的,除非您的内容脚本修改了页面的DOM并且重新加载了扩展程序(通过设置页面重新加载,更新等)。在这种情况下,旧的内容脚本将不再在扩展程序的上下文中运行,因此无法使用扩展API,也不能直接与您的扩展通信。

chrome.tabs.insertCSS是否有类似的方法?

  1. 没有chrome.tabs.insertCSS的包含保护。但是重新插入相同的样式表不会改变页面的外观,因为所有规则具有相同的CSS特异性,并且在这种情况下,最后一个样式表优先。但是,如果样式表与您的扩展紧密耦合,则可以使用executeScript注入脚本,检查是否第一次注入,如果是,则插入样式表(请参见下面的示例)。

有更好的后台脚本检查内容脚本注入状态的方法吗,以便我可以防止用户每次单击图标时都调用chrome.tabs.executeScript

  1. 您可以向标签发送消息(chrome.tabs.sendMessage),如果没有收到回复,则假定该标签中没有内容脚本并插入内容脚本。

第2个问题的代码示例

在您的弹出窗口/后台脚本中:

chrome.tabs.executeScript(tabId, {
    file: 'contentscript.js',
}, function(results) {
    if (chrome.runtime.lastError || !results || !results.length) {
        return;  // Permission error, tab closed, etc.
    }
    if (results[0] !== true) {
        // Not already inserted before, do your thing, e.g. add your CSS:
        chrome.tabs.insertCSS(tabId, { file: 'yourstylesheet.css' });
    }
});

使用 contentScript.js 有两种解决方案:

  1. 直接使用 windows:不建议,因为每个人都可以更改那些变量,而且 是否有规范要求元素的 ID 应该成为全局变量?
  2. 使用 Chrome.storage API:您可以与其他窗口共享内容脚本的状态(缺点是您需要在 Manifest.json 上请求权限,但这是可以接受的,因为这是正确的方法。

选项1:contentscript.js:

// Wrapping in a function to not leak/modify variables if the script
// was already inserted before.
(function() {
    if (window.hasRun === true)
        return true;  // Will ultimately be passed back to executeScript
    window.hasRun = true;
    // rest of code ... 
    // No return value here, so the return value is "undefined" (without quotes).
})(); // <-- Invoke function. The return value is passed back to executeScript

注意,重要的是要明确检查window.hasRun的值(在上面的示例中为true),否则它可能会成为具有id =“hasRun”属性的DOM元素的自动创建的全局变量,请参见是否有规范使元素的ID应该成为全局变量?

选项2:contentscript.js(使用chrome.storage.sync,也可以使用chrome.storage.local

    // Wrapping in a function to not leak/modify variables if the script
    // was already inserted before.
    (chrome.storage.sync.get(['hasRun'], (hasRun)=>{
      const updatedHasRun = checkHasRun(hasRun); // returns boolean
      chrome.storage.sync.set({'hasRun': updatedHasRun});
    ))()

function checkHasRun(hasRun) {
        if (hasRun === true)
            return true;  // Will ultimately be passed back to executeScript
        hasRun = true;
        // rest of code ... 
        // No return value here, so the return value is "undefined" (without quotes).
    }; // <-- Invoke function. The return value is passed back to executeScript

谢谢Rob!我喜欢你对第二个问题的解决方案。对于第三个问题,它会引入超时设计复杂性,就像我在问题中所说的那样。例如,如果有响应,那很好。但是如果没有响应,我无法确定是因为没有内容脚本正在运行还是仅仅是时间或其他问题...如果我错了,请纠正我。再次感谢。 - KF Lin
在实践中还有另一种情况:回调没有被调用。这种情况发生在标签页被关闭时(https://crbug.com/439780),或者页面重定向到204时(https://crbug.com/533863)。这些都是边缘情况,不太可能在您的情况下发生(我已经承诺将来修复它们)。 - Rob W
@KFLin 修改您的构建过程,以便您可以更改返回值。例如,在包含保护之前添加:window.hasRun ? true : (window.hasRun=true), (function(){ ... browserify output here that does not return true ... })();(还有数十种其他变体,您只需要更改最后一个表达式的值即可)。 - Rob W
再次感谢。我不认为我理解你在'更改最后一个表达式的值'上的意思。我尝试了在函数调用中既添加头部也添加尾部的包含保护,但仍然从chrome.tabs.executeScript获得了空返回值。这似乎与主题无关,我已经在http://stackoverflow.com/questions/34540906上发布了一个新的问题帖子。 - KF Lin
@RobW,你能否修改一下你的Manifest V3解决方案? - SkyRar
显示剩余3条评论

11

Rob W的第三个选项对我非常有效。基本上,后台脚本会ping内容脚本,如果没有响应,它将添加所有必要的文件。我只在选项卡激活时执行此操作,以避免在后台添加到每个打开的选项卡中产生复杂性:

background.js

chrome.tabs.onActivated.addListener(function(activeInfo){
  tabId = activeInfo.tabId

  chrome.tabs.sendMessage(tabId, {text: "are_you_there_content_script?"}, function(msg) {
    msg = msg || {};
    if (msg.status != 'yes') {
      chrome.tabs.insertCSS(tabId, {file: "css/mystyle.css"});
      chrome.tabs.executeScript(tabId, {file: "js/content.js"});
    }
  });
});

content.js

chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
    if (msg.text === 'are_you_there_content_script?') {
      sendResponse({status: "yes"});
    }
});

5
如果content.js不存在,则会抛出“Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.”的错误。 - EssenceBlue
1
它确实会抛出@EssenceBlue提到的异常,你无法尝试/捕获。但是添加“if(chrome.runtime.lastError) {}”可以防止Chrome报告您的扩展存在错误。另请参见:https://dev59.com/ol4c5IYBdhLWcg3wAWJE#28432087 - BigJ
您还需要检查chrome.tabs.onUpdated,因为用户可能会重新加载具有脚本的页面,在这种情况下不会调用onActivated。 - bvs

1

在Rob给出的精彩答案旁边,我想补充一点。

我发现Pocket的Chrome扩展程序也使用了类似的方法。在他们动态注入的脚本中:

if (window.thePKT_BM)
    window.thePKT_BM.save();
else {
    var PKT_BM_OVERLAY = function(a) {
        // ... tons of code
    },
    $(document).ready(function() {
        if (!window.thePKT_BM) {
            var a = new PKT_BM;
            window.thePKT_BM = a,
            a.init()
        }
        window.thePKT_BM.save()
    }
    )
}

0
对于MV3 Chrome扩展,我使用以下代码,没有chrome.runtime.lastError“泄漏”:
背景/扩展页面(例如弹出窗口)中:
    private async injectIfNotAsync(tabId: number) {
        let injected = false;
        try {
            injected = await new Promise((r, rej) => {
                chrome.tabs.sendMessage(tabId, { op: "confirm" }, (res: boolean) => {
                    const err = chrome.runtime.lastError;
                    if (err) {
                        rej(err);
                    }

                    r(res);
                });
            });
        } catch {
            injected = false;
        }
        if (injected) { return tabId; }

        await chrome.scripting.executeScript({
            target: {
                tabId
            },
            files: ["/js/InjectScript.js"]
        });
        return tabId;
    }

请注意,目前在 Chrome/Edge 96 中,chrome.tabs.sendMessage 不会 返回等待 sendResponse 的 Promise,尽管文档中如此说明

内容脚本中:

const extId = chrome.runtime.id;
class InjectionScript{

    init() {
        chrome.runtime.onMessage.addListener((...params) => this.onMessage(...params));
    }

    onMessage(msg: any, sender: ChrSender, sendRes: SendRes) {
        if (sender.id != extId || !msg?.op) { return; }

        switch (msg.op) {
            case "confirm":
                console.debug("Already injected");
                return void sendRes(true);
            // Other ops
            default:
                console.error("Unknown OP: " + msg.op);
        }

    }

}
new InjectionScript().init();

它的功能:

  • 当用户打开扩展弹出窗口时,尝试要求当前标签页“确认”。

  • 如果脚本尚未注入,则找不到响应,并且chrome.runtime.lastError将具有值,拒绝该承诺。

  • 如果脚本已经注入,则true响应将导致后台脚本不再执行它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接