【问题标题】:Chrome extension: Checking if content script has been injected or notChrome 扩展:检查内容脚本是否已被注入
【发布时间】:2015-12-30 11:36:03
【问题描述】:

我正在开发 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 是否足够安全?也就是说,这是幂等的吗?

问题#2chrome.tabs.insertCSS有类似的方法吗?

在后台脚本中检查内容脚本注入状态似乎是不可能的,因为它无法访问网页的 DOM。我尝试了一种 ping/pong 方法来检查任何内容脚本实例是否存在。但这会带来设计 ping 超时的开销和复杂性。

问题#3,有没有更好的方法让后台脚本检查内容脚本的注入状态,这样我就可以防止每次用户点击图标时调用chrome.tabs.executeScript

提前致谢!

【问题讨论】:

    标签: javascript google-chrome google-chrome-extension


    【解决方案1】:

    这是否足够安全,可以天真地打电话给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:不推荐,因为每个人都可以改变那个变量和Is there a spec that the id of elements should be made global variable?
    2. 使用 chrome.storage API:您可以与其他窗口共享 contentScript 的状态(您可以看到缺点,这根本不是缺点,是您需要请求 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 元素的自动创建的全局变量,请参阅Is there a spec that the id of elements should be made global variable?

    选项 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);
         chrome.storage.syn.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
    

    【讨论】:

    • 谢谢罗伯!我喜欢你对第二个问题的解决方案。对于#3,它将引入我在问题中所说的超时设计复杂性。例如,如果有响应,很好。但是如果没有响应,我无法确定是因为没有内容脚本正在运行还是只是时间或其他问题......如果我错了,请纠正我。再次感谢。
    • @KFLin 使用包含保护,多次注入并不重要。因此,在最坏的情况下,您只是在发生时间问题时不必要地注入脚本。当您使用 #3 时,理论上有两种选择:1)您得到响应并知道脚本存在或 2)在没有响应的情况下调用回调(并且在大多数情况下设置了 chrome.runtime.lastError)。 (待续)
    • @FKLin 实际上还有另一种情况:没有调用回调。当标签被杀死时(crbug.com/439780),或者在页面重定向到204(crbug.com/533863)时会发生这种情况。这些是边缘情况,在您的情况下不太可能发生(我已承诺在未来修复它们)。
    • Rob,很好的解释!我非常感谢它!现在我遇到了另一个问题,因为 browserify 在我的构建流程中。内容脚本由 browserify 构建到 pastebin.com/bQLyFVEq 中。所以包含保护的返回值只是错过了闭包中的某个地方。在后台脚本中,我从chrome.tabs.executeScript 得到了结果null。有点跑题了,但你会建议在这种情况下做什么?再次感谢。
    • @KFLin 修改您的构建过程,以便您可以更改返回值。例如,添加包含保护:window.hasRun ? true : (window.hasRun=true), (function(){ ... browserify output here that does not return true ... })();(还有许多其他变体,您只需更改最后一个表达式的值即可。
    【解决方案2】:

    Rob W 的选项 3 对我很有效。基本上,后台脚本会 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"});
        }
    });
    

    【讨论】:

    • 如果 content.js 不存在,则抛出 Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
    • 它确实抛出了@EssenceBlue 提到的异常,您无法尝试/捕获。但是添加“if(chrome.runtime.lastError) {}”确实会阻止 Chrome 说您的扩展程序存在错误。另见:stackoverflow.com/a/28432087/906308
    • 您还需要检查 chrome.tabs.onUpdated,因为用户可能会重新加载包含脚本的页面,并且在这种情况下不会调用 onActivated。
    【解决方案3】:

    只是对 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()
        }
        )
    }
    

    【讨论】:

      【解决方案4】:

      对于 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 does NOT return a Promise that waits for sendResponse although the documentation says so

      内容脚本中:

      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 响应将导致后台脚本不再执行。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-07
        • 2012-04-12
        • 2012-03-07
        • 1970-01-01
        • 1970-01-01
        • 2013-10-21
        相关资源
        最近更新 更多