Web Worker 入门

2019/6/23 webworkerwork-loaderHtml5

  1. webworker
  2. FileReader

# 一. 什么是 Web Worker

先说说webworker的来历:网上的文档都是说这货的出现,是因为作为这世界上最好的编程语言没有之一的JavaScript是一门单线程语言。不能像Java一样用代码控制和创建一个后台进程。因此,在项目优化性能的时候就遇到了很难解决的问题。比如这次我在工作中遇到的文件秒传功能。

# 二. Web Worker 能为我们带来哪些好处

JavaScript 引擎是单线程运行的,JavaScript 中耗时的 I/O 操作都被处理为异步操作,它们包括键盘、鼠标 I/O 输入输出事件、窗口大小的 resize 事件、定时器(setTimeout、setInterval)事件、Ajax 请求网络 I/O 回调等。当这些异步任务发生的时候,它们将会被放入浏览器的事件任务队列中去,等到 JavaScript 运行时执行线程空闲时候才会按照队列先进先出的原则被一一执行,但终究还是单线程。 平时看似够用的异步编程(promise、async/await),在遇到很复杂的运算,比如说图像的识别优化或转换、H5游戏引擎的实现,加解密算法操作等等,它们的不足就将逐渐体现出来。长时间运行的 js 进程会导致浏览器冻结用户界面,降低用户体验。那有没有什么办法可以将复杂的计算从业务逻辑代码抽离出来,让计算运行的同时不阻塞用户操作界面获得反馈呢? HTML5 标准通过了 Web Worker 的规范,该规范定义了一套 api,它允许一段 js 程序运行在主线程之外的另一个线程中。工作线程允许开发人员编写能够长时间运行而不被用户所中断的后台程序, 去执行事务或者逻辑,并同时保证页面对用户的及时响应,可以将一些大量计算的代码交给web worker运行而不冻结用户界面。

# 三. Web Worker 的类型

我这也是从文档上看到的,没有自己去用过共享类型,我在项目中用到的类型只是专用类型

  1. 专用 Worker, Dedicated Web Worker
  2. 共享 Worker, Shared Web Worker
  3. 「专用 Worker」只能被创建它的页面访问,而「共享 Worker」可以在浏览器的多个标签中打开的同一个页面间共享。
  4. 在 js 代码中,Woker 类代表 Dedicated Worker;Shared Worker 类代表 Shared Web Worker。

# 四. 如何创建 worker

主线程采用new命令,调用Worker()构造函数,新建一个 Worker 线程。

const worker = new Worker(./worker.js)
1

Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。

由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如404错误),Worker 就会默默地失败。

# 五. 主线程 和 worker线程 之间通信 并且 如何终止worker线程

  • 通过监听onmessage事件接收 调用postMessage方法发送
  • 如果在某个时机不想要 Worker 继续运行了,那么我们需要终止掉这个线程,可以调用在主线程 Worker 的 terminate 方法 或者在相应的线程中调用 close 方法:
  1. 应用文件 app.js
// 创建 worker 实例
const worker = new Worker('./worker.js'); // 传入 worker 脚本文件的路径即可
// 监听消息
worker.onmessage = function(evt){
  // 主线程发送的数据在evt.data
  // 主线程收到工作线程的消息后做完相应的处理后可以使用 terminate()终止线程
  ...
  terminate()
};
// 主线程向工作线程发送消息
worker.postMessage({
  value: '主线程向工作线程发送消息'
});
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 应用文件 worker.js
// 监听消息 (self.addEventListener('message',function(e){})或 self.onmessage = function(e){})
self.onmessage = function(evt){
  // 工作线程收到主线程的消息(evt.data)
  //在worker线程中,可以通过close()终止线程
  close()
};
// 向主线程发送消息
self.postMessage({
  value: '工作线程向主线程发送消息'
});
1
2
3
4
5
6
7
8
9
10

  1. 使用 Web Worker 最重要的一点是要知道,它所执行的 js 代码完全在另一作用域中,与当前主线程的代码不共享作用域。在 Web Worker 中,同样有一个全局对象和其他对象以及方法,但其代码无法访问 DOM,也不能影响页面的外观。
  2. Web Worker 中的全局对象是 worker 对象本身,也即 this 和 self 引用的都是 worker 对象,说白了,就像上一段在 worker.js 的代码,this 完全可以换成 self,甚至可以省略。
  3. 为便于处理数据,Web Worker 本身也是一个最小化的运行环境,其可以访问或使用如下数据:
    • 最小化的 navigator 对象 包括 onLine, appName, appVersion, userAgent 和 platform 属性
    • 只读的 location 对象
    • setTimeout(), setInterval(), clearTimeout(), clearInterval() 方法
    • XMLHttpRequest 构造函数

# 六. 数据通信(该段内容我是从阮一峰老师的文章上直接copy的)

前面说过,主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。 事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。 主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。下面是一个例子。

// 主线程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker 线程
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
1
2
3
4
5
6
7
8
9
10
11
12
13

但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects (opens new window)。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。

如果要直接转移数据的控制权,就要使用下面的写法。

// 主线程
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
1
2
3
4
5
6
7

# 七. Worker 的错误处理机制

  • 具体来说,Worker 内部的 js 在执行过程中只要遇到错误,就会触发 error 事件。
  • 发生 error 事件时,事件对象中包含三个属性:filename, lineno 和 message,分别表示发生错误的文件名、代码行号和完整的错误消息。
worker.addEventListener('error', (evt) => {
  console.log('-MAIN-: ', '---ERROR---', evt);
  console.log('-filename-:' + evt.filename + '-message-:' + evt.message + '-lineno-:' + evt.lineno);
});
1
2
3
4

# 八. worker线程中引入第三方脚本

Worker 线程能够访问一个全局函数 importScripts() 来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源;以下例子都是合法的:

importScripts();                        /* 什么都不引入 */
importScripts('foo.js');                /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js');      /* 引入两个脚本 */
1
2
3
  • 浏览器加载并运行每一个列出的脚本。每个脚本中的全局对象都能够被 worker 使用。如果脚本无法加载,将抛出 NETWORK_ERROR 异常,接下来的代码也无法执行。而之前执行的代码(包括使用 window.setTimeout() 异步执行的代码)依然能够运行。importScripts() 之后的函数声明依然会被保留,因为它们始终会在其他代码之前运行。
  • 在woker

# 九. 在使用 webpack 进行构建的前端工程中使用 Web Worker

首先需要了解一下,一个webpack的loader。worker-loader

  1. 安装依赖 worker-loader
$ npm install -D worker-loader
# 或
$ yarn add worker-loader --dev
1
2
3
  1. 代码中直接使用worker-loader
// 在app.js中 引入worker.js
  const MyWorker = require("worker-loader?name=worker.[hash:10].js!./worker.js"); //可以配置10位hash值来重新命名脚本文件
//const MyWorker = require("worker-loader?inline=true&fallback=false!./worker.js");

const worker = new MyWorker();
worker.postMessage({a: 1});
worker.onmessage = (event) =>{ /* 操作 */ };
worker.addEventListener("message", (event)=> { /* 操作 */ });

1
2
3
4
5
6
7
8
9

优点:写 worker 逻辑的脚本文件可以任意命名,只要传进 worker-loader 中处理即可; 缺点:每引入一次 worker 逻辑的脚本文件,就需要写一次如上所示的代码,需要多写 N(N>=1) 次的 "worker-loader!"

  1. 在 webpack 的配置文件中引入 worker-loader
{
  module: {
    rules: [
      {
        // 匹配 *.worker.js
        test: /\.worker\.js$/,
        use: {
          loader: 'worker-loader',
          options: {
            name: '[name]:[hash:8].js',
            // inline: true,
            // fallback: false
            // publicPath: '/scripts/workers/'
          }
        }
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

其中配置,可以设置 inline 属性为 true 将 worker 作为 blob 进行内联; 要注意,内联模式将额外为浏览器创建 chunk,即使对于不支持内联 worker 的浏览器也是如此; 若这种浏览器想要禁用这种行为,只需要将 fallback 参数设置为 false 即可;

# 十 devServer 模式下报错 "window is not defined"

若使用了 webpack-dev-server 启动了本地调试服务器,则有可能会在控制台报错: "Uncaught ReferenceError: window is not defined" 当时我好不容易通过学习我们老大的代码,准备大干一番时,愉快的敲完自己最拿手的命令npm run start时,居然一片红,看到是window is not defined ,先想到到的是可能是worker线程中的self是不是有问题,再挣扎了一会后,我向Google搜索寻求场外求援,果然一个关于此报错的issue: Webpack 4.0.1 | WebWorker window is not defined (opens new window) 最后问题解决了只需要在 webpack 的配置文件下的 output 下,加一个属性对:globalObject: 'this'

output: {
  ...
  globalObject: 'this',
},

1
2
3
4
5

# 十一 同源策略

Web Worker 严格遵守同源策略,如果 webpack 的静态资源与应用代码不是同源的, 那么很有可能就被浏览器给墙掉了,而且这种场景也经常发生。对于 Web Worker 遇到这种情况,有两种解决方案。

  1. 通过设置 worker-loader 的选项参数 inline 把 worker 内联成 blob 数据格式,而不再是通过下载脚本文件的方式来使用 worker:
// worker-loader在文件直接使用的方法
const workerFile = require("worker-loader?inline=true&fallback=false!./worker.js");
/*
或者去webpack.config.js中去配置loader
{
  loader: 'worker-loader'
  options: { inline: true }
}
然后在要使用的文件中:
import workerFile from './worker.js';
*/

const worker = new Worker(workerFile);
/*
如果没有用loader可以使用:
import workerFile from './worker.js'
const blob = new Blob([workerFile]);
const url = window.URL.createObjectURL(blob);
const worker = new Worker(url);
*/
worker.addEventListener('message',(evt)=>{
  console.log('监听worker线程发来的消息',evt.data);
})
worker.postMessage({value:'我来自主线程'})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 参考资料

Web Worker 使用教程(阮一峰) (opens new window)

Last Updated: 2021/6/16 上午10:06:57