【问题标题】:Can a site invoke a browser extension?网站可以调用浏览器扩展吗?
【发布时间】:2012-05-18 14:41:22
【问题描述】:

我是浏览器扩展开发的新手,我了解浏览器扩展更改页面并将代码注入其中的概念。

有没有办法可以扭转这个方向?我编写了一个提供一组 API 的扩展,想要使用我的扩展的网站可以检测到它的存在,如果它存在,网站可以调用我的 API 方法,如var extension = Extenion(foo, bar)。这在 Chrome、Firefox 和 Safari 中是否可行?

例子:

  1. Google 创建了一个名为 BeautifierExtension 的新扩展。它有一组作为 JS 对象的 API。

  2. 用户访问 reddit.com。 Reddit.com 检测到 BeautifierExtension 并通过调用 beautifer = Beautifier();

  3. 调用 API

参见 #2 - 通常它是检测匹配网站并更改页面的扩展程序。我有兴趣知道#2 是否可行。

【问题讨论】:

    标签: javascript google-chrome google-chrome-extension firefox-addon safari-extension


    【解决方案1】:

    自从 Chrome 引入了externally_connectable,这在 Chrome 中是很容易做到的。首先,在您的manifest.json 文件中指定允许的域:

    "externally_connectable": {
      "matches": ["*://*.example.com/*"]
    }
    

    使用chrome.runtime.sendMessage从页面发送消息:

    chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
      function(response) {
        // ...
      });
    

    最后,使用chrome.runtime.onMessageExternal 在后台页面收听:

    chrome.runtime.onMessageExternal.addListener(
      function(request, sender, sendResponse) {
        // verify `sender.url`, read `request` object, reply with `sednResponse(...)`...
      });
    

    如果您无权获得externally_connectable 支持,原始答案如下:

    我将从以 Chrome 为中心的角度来回答,尽管此处描述的原则(网页脚本注入、长时间运行的后台脚本、消息传递)几乎适用于所有浏览器扩展框架。

    从高层次来看,您要做的是将content script 注入每个网页,这会添加一个可供网页访问的 API。当站点调用 API 时,API 会触发内容脚本执行某些操作,例如通过异步回调向后台页面发送消息和/或将结果发送回内容脚本。

    这里的主要困难是“注入”到网页中的内容脚本不能直接改变页面的 JavaScript execution environment。它们共享 DOM,因此 事件DOM 结构的更改 在内容脚本和网页之间共享,但函数和变量不共享。例子:

    • DOM 操作:如果内容脚本将<div> 元素添加到页面,则将按预期工作。内容脚本和页面都会看到新的<div>

    • 事件:如果内容脚本设置了一个事件监听器,例如,点击一个元素,监听器将在事件发生时成功触发。如果页面为从内容脚本触发的自定义事件设置了侦听器,那么当内容脚本触发这些事件时,它们将被成功接收。

    • 函数: 如果内容脚本定义了一个新的全局函数 foo()(您可以在设置新 API 时尝试)。页面无法查看或执行foo,因为foo仅存在于内容脚本的执行环境中,而不存在于页面的环境中。

    那么,如何设置合适的 API?答案分为多个步骤:

    1. 在底层,将您的 API 设为event-based。网页使用dispatchEvent 触发自定义DOM 事件,内容脚本使用addEventListener 监听它们,并在收到它们时采取行动。这是一个简单的基于事件的存储 API,网页可以使用该 API 扩展来为其存储数据:

      content_script.js(在您的扩展程序中):

      // an object used to store things passed in from the API
      internalStorage = {};
      
      // listen for myStoreEvent fired from the page with key/value pair data
      document.addEventListener('myStoreEvent', function(event) {
          var dataFromPage = event.detail;
          internalStorage[dataFromPage.key] = dataFromPage.value
      });
      

      非扩展网页,使用基于事件的 API:

      function sendDataToExtension(key, value) {
          var dataObj = {"key":key, "value":value};
          var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
          document.dispatchEvent(storeEvent);
      }
      sendDataToExtension("hello", "world");
      

      如您所见,普通网页正在触发内容脚本可以看到并做出反应的事件,因为它们共享 DOM。这些事件附有数据,添加在CustomEvent constructor 中。我的示例非常简单——一旦内容脚本包含来自页面的数据(很可能是pass itbackground page 进行进一步处理),您显然可以在内容脚本中做更多的事情。

    2. 然而,这只是成功的一半。在我上面的例子中,普通网页必须自己创建sendDataToExtension。创建和触发自定义事件非常冗长(我的代码占用 3 行,并且相对简短)。您不想强迫网站编写神秘的事件触发代码只是为了使用您的 API。解决方案有点讨厌:将 <script> 标记附加到您的共享 DOM 中,这会将事件触发代码添加到主页的执行环境中。

      content_script.js 内部:

      // inject a script from the extension's files
      // into the execution environment of the main page
      var s = document.createElement('script');
      s.src = chrome.extension.getURL("myapi.js");
      document.documentElement.appendChild(s);
      

      myapi.js 中定义的任何功能都可以访问主页。 (如果您使用的是"manifest_version":2,则需要在清单的web_accessible_resources 列表中包含myapi.js)。

      myapi.js:

      function sendDataToExtension(key, value) {
          var dataObj = {"key":key, "value":value};
          var storeEvent = new CustomEvent('myStoreEvent', {"detail":dataObj});
          document.dispatchEvent(storeEvent);
      }
      

      现在普通网页可以简单地做:

      sendDataToExtension("hello", "world");
      
    3. 我们的 API 流程还有一个问题myapi.js 脚本在加载时将不完全可用。相反,它将在页面加载时间之后的某个时间加载。因此,普通网页需要知道何时可以安全地调用您的 API。您可以通过让myapi.js 触发“API 就绪”事件来解决此问题,您的页面会监听该事件。

      myapi.js:

      function sendDataToExtension(key, value) {
          // as above
      }
      
      // since this script is running, myapi.js has loaded, so let the page know
      var customAPILoaded = new CustomEvent('customAPILoaded');
      document.dispatchEvent(customAPILoaded);
      

      普通网页使用 API:

      document.addEventListener('customAPILoaded', function() {
          sendDataToExtension("hello", "world");
          // all API interaction goes in here, now that the API is loaded...
      });
      
    4. 另一个解决加载时脚本可用性问题的方法是将清单中内容脚本的run_at属性设置为"document_start",如下所示:

      ma​​nifest.json:

          "content_scripts": [
            {
              "matches": ["https://example.com/*"],
              "js": [
                "myapi.js"
              ],
              "run_at": "document_start"
            }
          ],
      

      摘自docs

      在“document_start”的情况下,文件是在任何来自 css 的文件之后注入的,但在构造任何其他 DOM 或运行任何其他脚本之前。

      对于某些可能比“加载 API”事件更合适且省力的内容脚本。

    5. 为了将结果返回到页面,你需要提供一个异步回调函数。无法从您的 API 同步返回结果,因为事件触发/侦听本质上是异步的(即,您的站点端 API 函数在内容脚本通过 API 请求获取事件之前终止)。

      myapi.js:

      function getDataFromExtension(key, callback) {
          var reqId = Math.random().toString(); // unique ID for this request
          var dataObj = {"key":key, "reqId":reqId};
          var fetchEvent = new CustomEvent('myFetchEvent', {"detail":dataObj});
          document.dispatchEvent(fetchEvent);
      
          // get ready for a reply from the content script
          document.addEventListener('fetchResponse', function respListener(event) {
              var data = event.detail;
      
              // check if this response is for this request
              if(data.reqId == reqId) {
                  callback(data.value);
                  document.removeEventListener('fetchResponse', respListener);
              }
          }
      }
      

      content_script.js(在您的扩展程序中):

      // listen for myFetchEvent fired from the page with key
      // then fire a fetchResponse event with the reply
      document.addEventListener('myStoreEvent', function(event) {
          var dataFromPage = event.detail;
          var responseData = {"value":internalStorage[dataFromPage.key], "reqId":data.reqId};
          var fetchResponse = new CustomEvent('fetchResponse', {"detail":responseData});
          document.dispatchEvent(fetchResponse);
      });
      

      普通网页:

      document.addEventListener('customAPILoaded', function() {
          getDataFromExtension("hello", function(val) {
              alert("extension says " + val);
          });
      });
      

      reqId 是必要的,以防您同时发出多个请求,这样他们就不会读取错误的响应。

    我认为这就是一切!因此,当您考虑到其他扩展也可以将侦听器绑定到您的事件以窃听页面如何使用您的 API 时,不适合胆小的人,而且可能不值得。我之所以知道这一切,是因为我为一个学校项目制作了一个概念验证密码学 API(并随后了解了与之相关的主要安全陷阱)。

    总而言之: 内容脚本可以侦听来自普通网页的自定义事件,并且该脚本还可以注入具有使网页更容易触发这些事件的函数的脚本文件。内容脚本可以将消息传递到后台页面,然后后台页面存储、转换或传输消息中的数据。

    【讨论】:

    • 感谢您的详细解释! +1 用于指出执行环境。
    • 考虑使用CustomEvent 构造函数更新您的答案。它的语法看起来比不推荐使用的 document.createEvent 方法好得多。
    • 在注入脚本之前有什么方法可以检查页面的安全细节/HSTS 状态吗?
    • @RobW 我终于搞定了。我个人从来没有CustomEvent构造函数,但我认为我在这里做得正确。
    • 如果有人在 2020 年发现这一点:这种方法似乎不再适用(在 FF 中)。 FF 不再允许将扩展程序中的 JS 加载到页面中。我收到“安全错误:xxxxxxxxx.net 的内容可能无法加载或链接到 moz-extension://46a55a72-0d87-4ca7-86e6-6d2bc890970e/myapi.js。”
    猜你喜欢
    • 1970-01-01
    • 2014-09-03
    • 2019-08-29
    • 1970-01-01
    • 2018-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多