一文搞懂 Web Worker(原理到实践)
共 12835字,需浏览 26分钟
· 2021-01-28
作者:poetry
原文地址:https://mp.weixin.qq.com/s/XF7qOhbBtYlwADCiyxbT-w
Web Worker 作为浏览器多线程技术,在页面内容不断丰富,功能日趋复杂的当下,成为缓解页面卡顿,提升应用性能的可选方案。
发展历史
简介
曾经的浏览器对于 JS 的处理模式是单线程模式,页面更新要先 串行 做 2 件事情。
随着 Web Worker 的发布,2 件事情可以 并行 完成。
![](https://filescdn.proginn.com/92a014038a4653c4edee0e6daf25a336/14becd5e46625c8b307c8f4d093d1afd.webp)
可以直观地联想:并行可能会 提升执行效率;运行任务拆分能 减少页面卡顿。
技术规范
Web Worker 属于 HTML 规范,规范文档见 Web Workers Working Draft ,2009 年就提出了草案。
目前,除了 ie10 以下,主流浏览器都已经得到了兼容。
DediactedWorker 和 SharedWorker
Web Worker 规范中包括:DedicatedWorker 和 SharedWorker 。
![](https://filescdn.proginn.com/bebb15390cb9c3840bee95acd6835329/deb1d509edad24b146699a058e30ad1d.webp)
如上图所示,DedicatedWorker 简称 Worker,其线程只能与一个页面渲染进程(Render Process)进行绑定和通信,不能多 Tab 共享。DedicatedWorker 是 最早实现并广泛支持的 Web Worker 能力。
而 SharedWorker 可以多个浏览器 Tab 中访问到同一个 Worker 实例,实现多 Tab 共享数据,共享 websocket 等,但是 safari 放弃了 SharedWorker 支持,因为 webkit 引擎的技术原因。如下图所示,只在 safari 5~6 中短暂支持过。
因此,社区中主要针对兼容性更好的 DedicatedWorker 进行拓展,文章后续也主要以 DedicatedWorker 进行讲解。
主线程和多线程
用户使用浏览器一般会打开多个页面(Tab),现代浏览器使用单独的进程渲染每个页面,以提升页面性能和稳定性,并进行操作系统级别的内存隔离。
![](https://filescdn.proginn.com/316923942b50d24beb458ab7a9e16d6c/2d22d3248d6847cb1fd46acafcb8d92b.webp)
主线程(Main Thread)
页面中,内容渲染和用户交互主要由 Render Process 中的主线程进行管理,主线程渲染页面每一帧(Frame)。
如下图所示,会包含 5 个步骤:JavaScript -> Style -> Layout -> Paint -> Composite
,如果 JS 的执行修改了 DOM,还会暂停 JS,插入并执行 Style 和 Layout。
![](https://filescdn.proginn.com/b0e90c8fc56741e935755e4af318e77f/9bb277fa304d3bd6db6ee4c1f35e4e9b.webp)
我们熟知的 JS 单线程和 Event Loop,是主线程的一部分。JS 单线程的机制避免了多线程开发中的复杂场景(如竞态和死锁),但单线程的主要困扰是:主线程同步 JS 执行耗时过久时(浏览器理想帧间隔约 16ms),会阻塞用户交互和页面渲染。
![](https://filescdn.proginn.com/4b5f2f427bfddccbbb2f3c8e031472bf/49c10cde8b45100cf40b96dce88ab87f.webp)
如上图所示中,长耗时任务执行时,页面无法更新,业务发响应用户的交互事件,如果卡死太久,浏览器会抛出卡顿提示。
多线程
Web Worker 会创建 操作系统级别的线程。
The Worker interface spawns real OS-level threads. -- MDN
JS 多线程,是有独立于主线程的 JS 运行环境,如下图所示:Worker 线程有独立的内存空间,Message Queue,Event Loop,Call Stack 等,线程间通过 postMessage 通信。
![](https://filescdn.proginn.com/217830b5adf14213e8a2d4e27fba6b42/897bf6ecbea35a066da174e2f5df5f9c.webp)
多个线程可以 并行 运行 JS。
这里的 并行 区别于单线程中的 并发,单线程中的 并发 准确的说叫 Concurrent,如下图所示,运行时 只有一个函数调用栈,通过 Event Loop 实现不同 Task 的上下文切换(Context Switch),这些 Task 通过 BOM API 调起其他线程为主线程工作,但回调函数代码逻辑 仍然由 JS 串行运行。
Web Worker 是 JS 多线程运行技术,准确来说是 Parallel,其与 Concurrent,如下图所示,运行时 有多个函数调用栈,每个调用栈可以独立运行 Task,互不干扰。
![](https://filescdn.proginn.com/732e15158acee97f690e9c2cfe98577b/58797bec470bd2a72df9c4ca9796cc02.webp)
应用场景
讨论完主线程和多线程,我们能更好地理解 Worker 多线程的应用场景:
可以减少主线程卡顿 可能会带来性能提升
减少卡顿
目前主流显示器的刷新率为 60Hz,即一帧为 16ms,因此播放动画时建议小于 16ms,用户操作响应建议小于 100ms,页面打开到开始呈现内容建议小于 1000ms。-- 根据 Chrome 团队提出的用户感知性能模型 RAIL。
逻辑异步化
减少主线程卡顿的主要方法是逻辑异步化,比如播放动画,将同步任务拆分为多个小于 16ms 的子任务,然后在页面的每一帧前通过 requestAnimationFrame
按计划执行一个子任务,直到全部子任务执行完毕。
![](https://filescdn.proginn.com/b5900726d21b0475ebe73c2da8106b5d/1f4ae38feb6002440314dc807718c3a7.webp)
拆分同步逻辑的异步方案对大部分场景有效果,但并非一劳永逸,有以下几个问题:
并非所有 JS 逻辑都可拆分:比如数组排序,树的递归查找,图像处理算法,执行中需要维护当前状态,且调用上非线性,无法轻易拆分成子任务;
可以拆分的逻辑难以把控颗粒度:如下图所示,拆分的子任务在高性能机器上可以控制在 16ms 以内,但在性能落后的机器上就超过了 deadline。16ms 的用户感知时间并不会因为用户手上机器的差别而变化,Google 的建议是再拆小到 3~4ms;
img 拆分的子任务并不稳定:对同步 JS 逻辑的拆分,需要根据业务场景寻找原子逻辑,而原子逻辑会随着业务发生变化,每次改动业务都需要去 review 原子逻辑。
Worker 一步到位
Worker 的多线程能力,使得同步 JS 任务拆分一步到位:从宏观上将整个同步 JS 任务异步化。不需要再去苦苦寻找原子逻辑,逻辑异步化的设计上也更加简单和可维护。
这给我们带来更多的想象空间,如下图所示,在浏览器主线程渲染周期内,将可能阻塞页面渲染的 JS 任务迁移到 Worker 线程中,进而减少主线程的负担,缩短渲染间隔,减少页面卡顿。
![](https://filescdn.proginn.com/9eb7bd13d7d09e7cd3fbefc15d28dc1a/a40219b3460262224dc78d3e39bc2095.webp)
性能提升
Worker 多线程并不会直接带来计算性能的提升,能否提升与设备 CPU 核数和线程策略有关。
多线程和 CPU 核数
CPU 的单核和多核离前端似乎有点遥远,但在页面运用多线程技术时,核数会影响线程创建策略。
进程是操作系统 资源分配 的基本单位,线程是操作系统 调度 CPU 的基本单位,操作系统对线程能占用的 CPU 计算资源有复杂的分配策略,如下图所示:
单核多线程通过时间切片交替执行; 多核多线程可在不同核中真正并行。
![](https://filescdn.proginn.com/9d0d01c7d9df09043183b34c003c0dfa/e39392ab69072e7eacb7d7420d9ff304.webp)
Worker 线程策略
一台设备上相同任务在个线程中运行的时间是一样的,如下图所示:我们将主线程 JS 任务交给新建的 Worker 线程,任务在 Worker 线程上运行并不会比原本主线程更快,而线程新建消耗和通信开销使得渲染间隔可能变得更久。
![](https://filescdn.proginn.com/57b45c2645fce7ab12ee228dac06a12e/1dd973815ea7a934557ce165ec74c78b.webp)
在单核机器上,计算资源是内卷的,新建的 Worker 线程并不能为页面争取到更多的计算资源。在多核机器上,新建的 Worker 线程和主线程都能做运算,页面总计算资源增多,但对单词任务来说,在哪个线程上运行耗时是一样的。
真正带来性能提升的是 多核多线程并行。
如多个没有依赖关系的同步任务,在单线程上只能串行执行,在多核多线程中可以并行执行。
值得注意的是,目前移动设备的核心数有限,受限于功耗,移动设备 CPU 中的多核通常是大小核,所以在创建多条 Worker 线程时建议区分场景和设备。
把主线程还给 UI
Worker 的应用场景,本质上是把主线程的逻辑剥离,让主线程专注于 UI 渲染,这种架构设计并非 Web 独创。
安卓和 IOS 原生开发中,主线程负责 UI 工作;前端领域热门的小程序,实现原理上就是渲染和逻辑完全分离。
本该如此。
Worker API
通信 API
![](https://filescdn.proginn.com/8e79926c40fc95dea03ce402b26167dd/f2142e2c3831012e3a547ada18470d68.webp)
如上图所示,Worker 的通信十分简单,具体可以参考 Web Worker 使用教程。
双向通信代码十分简单,只需要 7 行:
// main.js
const worker = new Worker('./worker.js')
worker.postMessage('hello')
worker.onmessage = event => {
console.log(event.data) // 'world'
}
// worker.js
self.onmessage = event => {
console.log(event.data) // 'hello'
postMessage('world')
}
postMessage 会在接收线程创建一个 MessageEvent,传递的数据添加到 event.data
,再触发该事件;MessageEvent 的回调函数进入 Message Queue,成为 待执行的宏任务。因此 postMessage 顺序发送 的消息,在接收线程中会 顺序执行回调函数。而且我们无需担心实例化 Worker 过程中 postMessage 的信息丢失问题,对此 Worker 内部机制已经处理。
Worker 事件驱动的通信 API 虽然简洁,但大多数场景下通信需要等待响应,并且多次同类型通信要匹配到各自的响应,所以业务使用一般会封装成 Promise。
运行环境
在 Worker 线程中运行 JS,会创建 独立于主线程的 JS 运行环境,称之为 DedicatedWorkerGlobalScope,开发者需要关注 Worker 环境和主线程环境的异同,以及 Worker 在不同浏览器中的差异。
Worker 环境和主线程环境的异同
Worker 是无 UI 线程,无法调用 UI 相关的 DOM/BOM API,具体可参考 MDN 的 functions and classes available to workers。
![](https://filescdn.proginn.com/89c788535f6f4754f4fc230d557c5e45/e14e4b0d2ea902fd4098cd5487679995.webp)
上图展示了 Worker 线程和主线程的异同,它们的共同点包括:
包含完整的 JS 运行时,支持 ES 规范定义的语言语法和内置对象; 支持 XMLHTTPRequest,能独立发送网络请求和后端进行交互; 包含只读的 Location,指向 Worker 线程执行的 script url,可通过 url 传递参数给 Worker 环境; 包含只读的 Navigator,用于获取浏览器信息; 支持 setTimeout / setInterval 计时器,可用于实现异步逻辑; 支持 WebSocket 进行网络 I / O,支持 IndexDB 进行文件 I / O。
从共同点上说,Worker 线程非常强大,除了利用线程独立执行重度逻辑以外,其网络 I / O 和文件 I / O 能力给业务和技术方案带来很大的想象空间。
![](https://filescdn.proginn.com/97dc3a64190d9df81485aeb85532d9e8/9479c4a0668e38d985dc6898334359ba.webp)
另一方面,Worker 线程运行环境和主线程的差异点有:
Worker 线程没有 DOM API,无法新建和操作 DOM,也无法访问到主线程的 DOM Element; Worker 线程和主线程内存独立,Worker 线程无法访问页面上的全局变量(window,document等)和 JS 函数; Worker 线程不能调用 alert()
和confirm()
等 UI 相关的 BOM API;Worker 线程被主线程控制,主线程可以新建和销毁 Worker; Worker 线程可以通过 self.close
自行销毁。
从差一点上看,Worker 线程无法操作 UI,并受主线程控制。
Worker 在不同浏览器中的差异
各家浏览器实现 Worker 规范有差异,对比主线程,部分 API 功能不完备,如:
IE10 发送的 ajax 请求没有 referer,请求可能会被后端拒绝响应; Edge18 上字符编码 / Buffer 的实现有问题。
好家伙,都是你浓眉大眼的微软系浏览器,解决这些问题得通过 polyfil。
另一方面,一些新增的 HTML 规范 API 只在较新的浏览器中得到实现,Worker 运行环境甚至主线程上没有,使用 Worker 时需要进行判断和兼容。
多线程同构代码
Worker 线程不支持 DOM,这一点和 node.js 非常像,我们在使用 node.js 做前后端 ssr 时,经常会遇到调用 BOM / DOM API 的错误。
在开发 Worker 前端项目,或迁移已有业务代码到 Worker 时,可以通过构建变量区分代码逻辑,或运行时动态判断所在线程,实现同构代码在不同线程环境下运行。
通信速度
Worker 多线程虽然实现了 JS 的并行运行,但是也带来了额外的 通信开销。如下图所示,从线程 A 调用 postMessage 发送数据到线程 B,onmessage 接收到数据有时间差,这段时间差成为 通信消耗。
![](https://filescdn.proginn.com/465259d42e5895f410d3e82b72cdd7cb/2122135e047e4a911558faf8a679e532.webp)
在线程计算能力的前提下,要通过多线程提升更多的性能,需要尽量 减少通信消耗。
而且主线程 postMessage 会占用主线程同步执行,占用时间与数据传输方式和数据规模相关。要避免多线程通信导致的主线程卡顿,需选择合适的传输方式,并控制每个渲染周期内的数据传输规模。
数据传输方式
我们先来聊聊主线程和 Worker 线程的数据传输方式。根据计算机进程模型,主线程和 Worker 进程同属一个进程,可以访问和操作进程的内存空间,但为了降低多线程并发的逻辑复杂度,部分传输方式直接隔离了线程间的内存,相当于默认加了锁。
通信方式有三种:
Structured Clone Transfer Memory Shared Array Buffer
Structured Clone
Structured Clone 是 postMessage 默认的通信方式,如下图所示,复制一份线程 A 的 js object 内存给到线程 B,线程 B 能获取和操作新复制的内存。
![](https://filescdn.proginn.com/6f3c33fd3f02ffa0fea87aeb069add5a/d7da59045b4ff5556e4edf811b1c61f8.webp)
Structured Clone 通过复制内存的方式简单有效的隔离了不同线程的内存,避免冲突;且传输的 object 数据结构很灵活,但复制过程中,线程 A 要 同步执行 Object Serialization,线程 B 要 同步执行 Object Deserialization,如果 object 规模过大,会占用大量的线程时间。
Transfer Memory
Transfer Memory 意味着转移内存,它不需要序列化和反序列化,能大大减少传输过程占用的线程时间。如下图所示,线程 A 将制定内存的所有权和操作权转交给线程 B,但转然后线程 A 无法在访问这块内存。
![](https://filescdn.proginn.com/9bec586e370861edec7163894f9b1de6/95f0c01918845c772c1ce23fdb3402ae.webp)
Transfer Memory 以失去控制权来换取高效传输,通过内存独占给多线程并发加锁,但只能转让 ArrayBuffer 等大小规整的二进制数据,对矩阵数据(比如 RGB图片)比较适用,实践上要考虑从 js object 生成二进制数据的运算成本。
Shared Array Buffers
Shared Array Buffers 是共享内存,线程 A 和线程 B 可以 同时访问和操作 同一块内存空间,数据都共享了,也就没什么传输的事了。
但多个并行的线程共享内存,会产生竞争问题,不像前两种传输方式默认加锁,Shared Array Buffers 把难题抛给了开发者,开发者可以用 Atomics 来维护这块共享的内存。作为较新的传输方式,浏览器兼容性可想而知,目前只有 Chrome 68+ 支持。
传输方式小结
全浏览器兼容的 Structured Clone 是较好的选择,但要考虑数据传输的规模,下文我们会详细展开; Transfer Memory 兼容性也不错(IE11+),但数据独占和数据类型的限制,使得它是特定场景的最优解,而不是常规解; Shared Array Buffers 当下糟糕的兼容性和线程锁的开发成本,建议先暗中观察。
数据传输规模
Structured Clone 的序列化和反序列化执行耗时 主要受数据对象复杂度影响,这很好理解,因为序列化和反序列化至少要以某种方式遍历对象。数据对象的复杂度本身难易度量,可以用序列化后的数据规模作为参考。
2015 年的 How fast are web workers 在中等性能手机上进行了测试: postMessage 发送数组的通信速率为 80KB/ms,相当于理想渲染周期(16ms)内发送 1300KB。
2019 年 Surma 对 postMessage 的数据传输能力进行了更深入研究,具体见 Is postMessage slow。高性能机器(macbook) 上的测试结果如下图所示:
![](https://filescdn.proginn.com/4096531b8857c72dbeb016b038781e3b/ff99d4e44724b9e2aeea0aa49b3a6d43.webp)
其中:
测试数据为嵌套层数 1 到 6 层(payload depth,图中纵坐标),每层节点的子节点 1 到 6 个(payload breadth,图中横坐标)的对象,数据规模从 10B 到 10MB 在 MacBook 上,10MB 的数据传递耗时 47ms,16ms 内可以传递 1MB 级别的数据
**低性能机器(nokia2)**上的测试结果如下:
![](https://filescdn.proginn.com/28a8f66c362c79f89a6722fa3308b6e5/622df9c2ea2ca3b07d9051a4b537baa8.webp)
其中:
在 nokia2 上传输 10MB 数据的耗时是 638ms,16ms 内可以传递 10KB 级别的数据 高性能机器和低性能机器有超过 10 倍的传输效率差距
综上,不管用户的机器性能如何,用户对流畅的感受是一致的:前端同学的老朋友 60ms 和 100ms。
Surma 兼顾低性能机型上 postMessage 容易造成主线程卡顿,提出的数据传输规模的建议是:
如果 JS 代码里面不包括动画渲染(100ms),数据传输规模应该保持在 100KB 一下; 如果 JS 代码里面包括动画渲染(16ms),数据传输规模应该保持在 10KB 一下。
兼容性
兼容性是前端最关键的一点,毕竟我们无法控制用户使用哪一款浏览器的那一个版本。对 Web Worker 更是如此,因为 Worker 的多线程能力要么业务场景根本用不上,要么就是重度依赖。
从 caniuse 上面可以看到,Web Worker 的兼容性做的还是挺不错的。
![](https://filescdn.proginn.com/62e78fd847f86d884e10a50fb3342beb/ff9409a518ab8a5591648174c19e9b69.webp)
使用 Web Worker 并非一锤子买卖,我们不止关注浏览器 Worker 能力有无,还需要关注它是否完备可用,因此可以用以下几个指标来进行评测:
是否有 Worker 能力:通过浏览器是否有 window.Worker
来判断能否实例化 Worker:通过监控 new Worker()
是否报错来判断;能否跨线程通信:通过测试双向通信来验证,并设置超时; 首次通信耗时:页面开始加载 Worker 脚本到首次通讯完成的耗时,该指标与 JS 资源加载时长,同步逻辑执行耗时相关。
Service Worker
模仿 Web Worker,利用现代浏览器支持多线程运行的机制,实现了一个独立于主线程的子线程。
作用
离线缓存 消息推送 后台数据同步 响应来自其他源的资源请求 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据 在客户端运行 CoffeeScript,Less,CJS / AMD 等模块编译和依赖管理(用于开发目的) 后台服务钩子 自定义模版用于特定 URL 模式 性能增强,比如预取用户可能需要的资源,比如相册中后面数张照片
局限性
https:Service Worker 必须运行在 HTTPS 协议上,但在本地环境中 http://localhost 或者 http://127.0.0.1 也可以
浏览器的兼容性
img 我们可以看到 IE 完全不兼容,早期的 IOS 也不兼容。
调试
以 Google Chrome 为例:
chrome://serviceworker-internals
img 开发者模式的 Application
img img
生命周期
Service Worker 生命周期的反应:
installing installed activating activated
其中,installed 用来缓存文件,activated 用来更新缓存
![](https://filescdn.proginn.com/92c83d56b811db36708e9767497bc82d/5e2cf00c9b3871e2d9aef32d22a4c7cc.webp)
用法
html 中
if ('serviceWorker' in navigator) {
// 开始注册service workers
navigator.serviceWorker.register('./sw-demo-cache.js', {
scope: './'
}).then(function (registration) {
console.log('注册成功');
var serviceWorker;
if (registration.installing) {
serviceWorker = registration.installing;
console.log('安装installing');
} else if (registration.waiting) {
serviceWorker = registration.waiting;
console.log('等待waiting');
} else if (registration.active) {
serviceWorker = registration.active;
console.log('激活active');
}
console.log('=>serviceWorker:', serviceWorker);
if (serviceWorker) {
console.log(serviceWorker.state);
serviceWorker.addEventListener('statechange', function (e) {
console.log(' 状态变化为', e.target.state);
});
// 创建信道
var channel = new MessageChannel();
// port1留给自己
channel.port1.onmessage = e => {
console.log('main thread receive message...');
console.log(e);
}
console.log('给对方', window.RES_MAP);
// port2给对方
serviceWorker.postMessage(window.RES_MAP, [channel.port1]);
serviceWorker.addEventListener('statechange', function (e) {
// logState(e.target.state);
});
}
}).catch(function (error) {
console.log('注册没有成功');
});
} else {
console.log('不支持');
}引进 sw-demo-cache.js
// sw
self.addEventListener('message', ev => {
console.log('sw receive message..');
console.log(ev);
fileMap = ev.data.RES_MAP;
var arr1 = [].slice.call(fileMap); // ['a', 'b', 'c']
// 取main thread传来的port2
ev.ports[0].postMessage('Hi, hello too');
});
// var fs = require('fs');
// console.log(fs);
// 缓存
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(VERSION).then(function(cache) {
return cache.addAll([
'./index.html',
]);
})
);
});
// 缓存更新
self.addEventListener('activate', function(event) {
console.log('two now ready to handle fetches!');
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
console.log('cacheName:', cacheName);
// 如果当前版本和缓存版本不一致
if (cacheName !== VERSION) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 捕获请求并返回缓存数据
self.addEventListener('fetch', function (event) {
try{
event.respondWith(
caches.match(event.request).then(function(res){
if(res){
return res;
}
requestBackend(event);
})
)
} catch {
console.log(event);
}
});
function requestBackend(event){
var url = event.request.clone();
return fetch(url).then(function(res){
//if not a valid response send the error
if(!res || res.status !== 200 || res.type !== 'basic'){
return res;
}
var response = res.clone();
console.log('VERSION:', VERSION);
caches.open(VERSION).then(function(cache){
cache.put(event.request, response);
});
return res;
})
}webpack 中获取文件目录,引入第三个模块 glob,递归获取打包后的文件目录
exports.resMap = function () {
var entryFiles = glob.sync(PAGE_PATH + '/*/*.js')
var map = {}
entryFiles.forEach((filePath) => {
var filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
map[filename] = filePath;
})
var entryFiles2 = glob.sync(PAGE_PATH2 + '/*')
var map2 = {}
findPath(entryFiles2, map2);
console.log('map2', map2);
return map2;
};
function findPath(entryFiles2, map2) {
entryFiles2.forEach(filePath => {
var filename = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.'));
if (filePath.indexOf('.') <= 0) {
let pathRes = path.resolve(__dirname, filePath);
let files = glob.sync(pathRes + '/*');
findPath(files, map2);
map2[filename] = filePath;
}
map2[filename] = filePath;
});
}导出目录:通过 webpack 的 DefinePlugin 插件,导出上一步获取的目录
Web 和 Service Worker 的通信:通过 postMessage 实现 Web 和 Service Worker 间的通信
// 创建信道
var channel = new MessageChannel();
// port1留给自己
channel.port1.onmessage = e => {
console.log('main thread receive message...');
console.log(e);
}
console.log('给对方', window.RES_MAP);
// port2给对方
serviceWorker.postMessage(window.RES_MAP, [channel.port1]);
serviceWorker.addEventListener('statechange', function (e) {
// logState(e.target.state);
});
// sw
self.addEventListener('message', ev => {
console.log('sw receive message..');
console.log(ev);
fileMap = ev.data.RES_MAP;
var arr1 = [].slice.call(fileMap); // ['a', 'b', 'c']
// 取main thread传来的port2
ev.ports[0].postMessage('Hi, hello too');
});