chrome extension v3 version background service worker deep analysis
背景
由于插件开发人员对于 V3 的 service worker 是有畏惧感的,所以尽量绕开 background 的使用,但是 background 作为中转信息的关键点,不使用则会造成设计更加复杂,时间效率的浪费,所以要解决 background 的认知问题
background 配置
英文名: _generated_background_page.html 不生成一个 html,你 console 控制台打不开
V2 中区分两个概念
-
background page 就是一个完整的html页面,运行之后,作为一个独立的app运行,相当于一个独立运行的小型服务器,谷歌浏览器会开一个线程来运行这个 html 脚本,并且监听一些端口,同时将该 html 中写的监听函数绑定到监听端口的回调函数里
-
好处是,让开发者思维跨度不那么大,毕竟开发者都是前端技术人员,对于服务器啥的根本没有概念,类似提供接口这种事情完全不懂,运行在某端口上进行监听,更是迷惑,所以谷歌浏览器就把服务端概念给弱化了,反正你知道在这里写监听函数就行了
-
坏处就是谷歌浏览器要给每个插件都运行一个 window dom 环境,有可能该插件几乎用不到 window 里面的任何一种东西,但是谷歌浏览器仍然要加载在线程里,耗费了资源却没啥用处
-
-
event page 鉴于 background page 缺点,谷歌官方,在插件开发达到一定数量之后,认为前端开发者应该在 webpack 这种东西越来越普及的情况下,对 service worker 这种微型服务端概念逐渐成熟了,所以鼓励前端技术提高后端技术理解能力,同时也想降低谷歌浏览器的内存耗费,因此提出了 event page 的方式,也即 background 变成了一种短暂加载进内存的脚本,就类似于 PHP 脚本被 PHP-FPM 载入一样,这里 的 php-fpm 就是谷歌浏览器本身,php 脚本就是 background-script,因此脚本的变量值是不会常驻保存的,脚本可以多次被线程加载执行,执行完毕后就释放,释放时触发 onSuspend 事件
-
坏处是脚本因为不会被常驻内存了,因为谷歌统一管理了,这个概念在后端也有例子,如 springboot 或者 django 运行起来后本身就是一个独立的服务,代码请求时,只要是一个全局变量,就能统计数字,但是 PHP 因为每次执行都是一个进程单独加载,运行完释放,所以没有全局变量的概念,但是 PHP 可以实现代码不重启,即可更新,因为每次都重新执行一遍脚本,满足成百上千的项目,可以被一个 PHP-FPM 来运行,内存消耗极低,谁执行给谁分配内存
-
好处就是节省内存,更新之后不用重启,直接就可以以最新的代码运行
-
// 谷歌V2推荐模式,为V3做铺垫和准备{ "name": "My extension", ... "background": { "scripts": ["background.js"], "persistent": false // 如果为true的话则就是一个background page页面 }, ...}
V2 事件页面加载与卸载
从概念中我们得知,事件模式是运行脚本,运行完后释放,因此就有两个事件需要我们注意,那就是加载和卸载
https://developer.chrome.com/docs/extensions/mv2/background_pages/
触发加载的 4 个事件
-
扩展首次安装或更新到新版本。
-
后台页面正在侦听一个事件,并且该事件已被调度,debugger 也是事件,并且还会一直阻拦者卸载事件的发生
-
内容脚本或其他扩展会发送消息。
-
扩展中的另一个视图,例如弹出窗口,调用runtime.getBackgroundPage.
触发卸载事件
-
这里不能成为卸载,而应该成为暂挂,因为插件并没有卸载,只是脚本暂时从内存中释放,等待下次装载后使用所以用了onSuspend 事件,该事件很难测试出来,一般人会开 console 等着关闭,结果问题就是开启 console 时,会生成_generated_background_page.html 页面,因为只有 html 页面才有 console,而文档中有一句是:加载后,只要后台页面正在执行某个操作,例如调用 Chrome API 或发出网络请求,它就会一直运行。此外,在关闭所有可见视图和所有消息端口之前,不会卸载背景页面。请注意,打开视图不会导致事件页面加载,而只会阻止它在加载后关闭。该问题导致 onSuspend 迟迟不会触发,如果改为 console.log 打印日志的方式也输出不了,因为不知道输出到哪里了,最后的套路是让他打开一个新 tab,如果你想带点参数,可以放在 url?后面
chrome.runtime.onSuspend.addListener(function () { console.log("卸载事件被触发") chrome.tabs.create({ url: "https://www.baidu.com/" }); chrome.storage.local.set({"key_wangsen": "bengkuile"}, function () { console.log("设置成功") })})
V3 数据状态怎么存?
官方给的方式是用 chrome.storage.sync.set/get 来存取,在加载脚本的 4 个条件处 get,在暂存的事件中 set
V3 中 setTimeout 和 setInterval
是肯定不能用了,因为 setTimeout 是存在于线程中的事件循环的,V3 的 service worker 在不运行后几秒后,就暂存了,线程也被释放了,所以 seTimeout 根本就不执行,但谷歌提供了一个统一 alarms 管理器,这样就避免了大家乱建 setTimeout 和 setInterval,保证了非常多插件的情况各自开线程造成的耗费内存和阻塞问题
V3 background 迁移要点
https://developer.chrome.com/docs/extensions/mv3/migrating_to_service_workers/
链接中主要讲了几个点:
-
manifest 调整
-
不能嵌套监听,原理是一个监听类似一个 controller,如果监听中套一个 controller,就会导致第二个 controller 根本执行不了,因为异步的原因,当第一个 controller 执行完成后,该次线程就被关闭了,第二次还没来的及执行,就被从内存中销毁了。这种设计机制,注定 background 与其他页面主动通信,都必须放在 alar
-
那第一次接收的数据,在后续异步行为中要用到怎么办?这个数据怎么存?则使用插件的存储机制,chrome.storage.local.set/get 来解决,第一次 controller 中 set 进去,等到 alarm 拉起时暂停掉
-
所以异步延迟的事件,都不能在用 setTimeout 和 setInterval 了,必须用 alarm 来处理
-
要把 service worker 当成一个 worker 对待,worker 为一个临时性的线程,是 H5 中的一个新概念,为了解决流媒体中的视频流和音频流同时渲染的问题,原来的浏览器为了节省内存,采用事件循环的方式,也即所有异步回调或者延迟执行,都会先排在一个队列里,然后一个个被执行,类似于协程+线程处理方式,但视频需要音视频流同时渲染,而不是交替渲染,协程是交替渲染,所以就创造了 worker,而 worker 是没有 window 的 dom 结构等其他一些 window 的功能的,是纯 JS 语言环境,连 XMLHttpRequest 这种 http 请求对象也没有,为了解决这个问题,worker 自带了简洁版的 fetch
-
DOM 结构解析,请自行寻找解析库来用,如果非得用 Dom 结构或者 browser environment,则自己通过新打开一个 tab 或者 window,与 background 进行通信来解决
-
视频和音频捕捉怎么做?直接开 windows 和 tabs 来处理
-
canvas 画布还能用吗?可以用,换成了 workder 中的一个对象了,叫new OffscreenCanvas(width, height)
alarm 的使用
因为 background 跟 alarm 的关联性较大,这里单独来类比学习 alarm 这个定时器
alarm 因为是一个总控调度器,并不是某一个定时函数体,正如 chrome.tabs,是针对于所有的 tabs 的管理器,而不是某一个 tab 的处理器
管理器的工作就是收集和提醒的作用,具体你到时怎么做,你要根据管理器收集的名称,来具体设定函数体,所以 alarm 有两个函数,一个收集定时器,一个监听到期的定时器
create 函数,收集定时器信息
class AlarmCreateInfo { delayInMinutes?:number, // when之后延迟的分钟 periodInMinutes?:number, // 周期性执行的分钟 when?:number // 什么时候执行,单位毫秒}chrome.alarms.create( name?: string, alarmInfo: AlarmCreateInfo,)
onAlarm 事件,监听到期的定时器
class Alarm { name, periodInMinutes?:number, scheduledTime:number}chrome.alarms.onAlarm.addListener( callback: function(alarm: Alarm),)
clear 函数,提前清理不想再处理的定时器
chrome.alarms.clear( name?: string, callback?: function(wasCleared: boolean),)
get 函数
-
get 不到定时器时,返回的是 undefined,所以可以用 if (!chrome.alarms.get("xxxx"))