标签: background script

  • Chrome Extension MV3 在 background script 里扫描二维码并传出结果

    Chrome Extension MV3 在 background script 里扫描二维码并传出结果

    升级浏览器扩展到 MV3 时,因为 MV3 不再允许使用 background 页面,必须使用 service worker 类型的 background script。这样一来,就没有 window 对象,会遇到很多问题。

    今天分享下如何在 service worker 类型的 background script 里完成扫描二维码并传出结果。比如我帮朋友升级 二维码生成器 (Quick QR) 这个扩展,它有个功能是通过右键菜单扫描页面中的二维码,并返回扫描结果。这个过程只能在 background script 里完成,于是升级时就给我带来了不小的麻烦。

    首先,右键菜单的响应事件只能得到图片 URL。其次,扫描功能需要用到 jsqr,它接受 imageData 作为参数,可以返回扫描结果。所以我们就需要把 URL 转换成 imageData,过程如下:

    // service worker 里可以使用 `fetch()` 请求服务器
    const response = await fetch(src);
    const blob = await response.blob();
    // 使用 `createImageBitmap` 可以将支持的图片格式转换成位图
    const bitmap = await createImageBitmap(blob);
    const {width, height} = bitmap;
    // 在 service worker 里,需要使用 OffscreenCanvas
    const canvas = new OffscreenCanvas(width, height);
    // 注意,这里只能是 `2d`,`bitmaprenderer` 无法使用 `getImageData` 方法
    const context = canvas.getContext('2d');
    context.drawImage(bitmap, 0, 0);
    const imageData = context.getImageData(0, 0, width, height);
    // 接下来识别就好
    const data = jsQR(imageData.data, width, height);
    const [tab] = await getActiveTab();
    // 因为没有 `window`,`alert()`、`prompt()` 都不能用,只能通过在目标页面执行脚本的方式输出结果。注意,`tab.executeScript` 也没有了,必须用下面这个新 API,并且要提前申请权限
    await chrome.scripting.executeScript({
      args: [i18n(SCAN_RESULT), data.data],
      target: {
        tabId: tab.id,
      },
      func: (message, result) => {
        result = prompt(message, result);
        if (result) {
          navigator.clipboard.writeText(result)
            .catch(() => void 0);
        }
      },
    });

    大概过程如上面代码所示,需要注意的东西我都放在注释里了。

    关于 MV3 的其它升级经验,我会更新在 Chrome Extension Manifest V3 升级笔记,欢迎常来看看。

    (更多…)
  • Chrome 扩展大升级 Manifest V3:变化

    Chrome 扩展大升级 Manifest V3:变化

    上一篇博客 聊了聊 Manifest V3 的设计思路,接下来就该详细介绍下这个版本的变化。按照 官方介绍,V3 的变化主要有以下五点:

    1. 需使用 Service worker 替换 background scripts/pages。这个变化对我影响很大,未来开发新扩展的时候,需要使用全新的代码架构;老扩展要升级,多半也要彻底重构。
    2. 修改网络请求的 API 换作 chrome.declarativeNetRequest。这个变化我暂时没用到,将来有机会再说。
    3. 禁止运行远程代码,所有将会运行的代码都必须事先打包进扩展,接受商店的审查。这点对我厂的影响非常大,很可能我厂的 Showman 要彻底改变实现方式——最大的可能就是停留在 Manifest V2,如果 Manifest V2 被废弃,那就使用新技术栈开发下一代产品。
    4. API 全部提供 Promise。这是应该的,小改善。
    5. 其它一大批杂七杂八的功能变化。

    其中 2 暂时不好评价,3 只能被动接受,4、5没啥好说的,所以接下来谈谈 1,background script => service worker。

    Background Scripts/Pages

    Background scripts/pages(后面简称 background script)的地位很重要,是浏览器扩展的重要组成部分,可以作为联系其它组件的中心、主控,在扩展功能复杂时,作用很大。比如我厂的 Showman,既需要用 content script 注入功能到目标网页,也需要单独打开页面让用户交互,这里每个 JS 都要跑在独立环境,彼此隔离,所以就需要有一个中控,一方面连接各个独立的组件,另一方面常驻内存存储一些全局数据。

    相信大家不难想到,这种常驻内存的东西,虽然给开发带来了便利,但是也会给内存带来不小的压力。况且,在 Manifest V2 阶段,甚至还支持一个浏览器扩展注册多个 background scripts 和 background pages。每个页面和脚本都要独享一个环境,哪怕只有短短几行代码,于是就会吃掉大量的内存。

    Chrome 对内存使用方面的不检点,被大家诟病已久。其开发团队当然也知道,所以最近几个版本 Chrome 都在试图改进这些问题。既然新版本要降低系统要求提高性能表现,尽量节省内存、同时减少运行时间就是必须考虑的事情。所以,新版本扩展规范规定,background script 有且只有一个,而且只能是 service worker。

    Service worker 只能注册事件侦听器,不能持续运行。这样一来,就可以让 background script 的执行之间降到最低,并且随需而动,减少内存占用。

    Service worker 的特殊用法

    1. 事件侦听器都要放在顶层

    事件侦听器都要放在顶层,非顶层的事件侦听器会被直接忽略。

    2. 使用 storage API 存储持久化状态

    不要用全局变量,把一些需要用到的数据都放到 storage 里,如下:

    // 不要这样做
    const foo = 'bar'
    chrome.runtime.onMessage.addListener(({ type, name }) => {
      if (type === "set-name") {
        name = foo;
      }
    });
    
    // 这样做
    chrome.runtime.onMessage.addListener(({ type, name }) => {
      const foo = await chrome.storage.local.get(['name']);
      if (type === "set-name") {
        name = foo.name || 'bar';
      }
    });

    3. 使用 chrome.alarms 取代定时器

    我们以前一般习惯于用 setTimeoutsetInterval 定时执行,但它们在 service worker 里都会失效。此时,要用专门的 Alarms API 代替,使用方法倒也不难:

    chrome.alarms.create({ delayInMinutes: 3 });
    chrome.alarms.onAlarm.addListener(() => {
      // do something
    });

    需要注意的是,content script 里虽然仍支持 setTimeout,但是太长的定时器会被直接忽略掉,时间阈值是 13s,即短于这个时间的仍然会触发,但是超过的会被忽略。建议如果 content script 需要定时器,那么也交给 background script 来做。

    4. 其它一些常见场景的处理

    包括解析 HTML/XML、处理音视频播放、使用 <canvas> 绘图等,因为我暂时都没用到,所以就先不说了。