Feature image

seajs-worker | 无缝&模块化的Web Worker开发

介绍

Web Worker是HTML5标准增加的多线程方案,直接使用web worker的API开发是一件比较麻烦的事:

  • Worker脚本必须放在单独的文件里(虽然可以用blob inline worker,但是IE10/11不支持,并且官方也没有修改的意思
  • Worker和浏览器脚本需要通过message通信
  • 不利于模块化开发,不管是AMD还是CMD模块,都不能直接通过importScripts载入运行

RequireJS支持web worker,可以在worker脚本一开头importScript('path/to/require.js'),然后就愉快的一路AMD了(当然依然要message通信)。

而我最近项目里在使用的sea.js不支持,为了解决这个问题,前几天陆续向sea.js发了几个pull request,给sea.js增加了web worker支持。

在变更被merge过后,更进一步的折腾了下,把web worker的API封装了一遍,写了seajs-worker这个插件,实现了真正无缝的开发。开发者不需要关心worker的创建、生命周期管理,也不需要通过消息通信,而是直接通过清晰的异步方法调用。

链接

使用实例

示例源码在repo的的example目录里可以找到。

使用seajs-worker开发web worker只需要3步。

第一步,配置sea.js:

1
2
3
4
5
6
7
8
9
<!-- other sutff -->
<script>
seajs.config({
base: '../dist'
});

seajs.use('example/main');
</script>

<!-- other sutff -->

第二步,实现一个具体的worker类,继承自SeaWorker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# image-worker.coffee
define (require, exports, module) ->
SeaWorker = require '../worker'
# require other modules directly
require './image-util'
# @nodoc
class ImageWorker extends SeaWorker
@worker_service 'sepia', (src) ->
src.each (p) ->
r = 0.393 * p.r + 0.769 * p.g + 0.189 * p.b
g = 0.349 * p.r + 0.686 * p.g + 0.168 * p.b
b = 0.272 * p.r + 0.534 * p.g + 0.131 * p.b
src.setRGBA p.i, r, g, b, p.a
return src

SeaWorker.register ImageWorker

module.exports = ImageWorker

可以看到worker类就是一个普通CMD模块的写法,可以自由require其它模块,只要执行路径里没有调用到web worker不允许访问的API(比如DOM)就行。

第三步,创建并调用worker。

创建:

1
2
Worker = require './image-worker'
worker = new Worker()

seajs-worker提供了两种调用方案。

单worker方案:

1
2
worker.sepia img_src, (err, result) ->
ctx.putImageData result, 0, 0

可以看到,再也不需要自己去实现message通信,只需要一个简单的方法调用。返回值的方法除了以上callback的方式以外,如果seajs-worker检测到了Q的存在,就会返回一个promise对象,此时就可以使用Promise/A+ Pattern避免callback的嵌套:

1
2
worker.sepia img_src
.then (result) -> ctx.putImageData result, 0, 0

多worker方案

仅仅使用一个worker还不足以发挥多线程的威力,seajs-worker提供了map-reduce语法,一句话实现worker pool:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Divide image into segments
segs = [seg_1, seg_2, ..., seg_n]

# Worker count
n = 10

# Map-Reduce
Worker.map segs, 'sepia', n
.then (dsts) ->
Worker.reduce dsts, ((ctx, s, i) ->
ctx.putImageData s, 0, i * step
return ctx
), dst_ctx

以上代码把图像分成若干块,放到一个Array里,然后通过map把每块交给一个worker进行处理,指定同时最多有10个worker进行操作。完成后传回一个Array,分别对应每块图像的处理结果,由reduce绘制到canvas上。

这篇blog用web worker API细线了同样的功能,对比起来代码的清晰程度显而易见。

JavaScript中使用

seajs-worker为JavaScript开发者提供了几个helper方法,例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
define(function(require, exports, module) {
// Require base
SeaWorker = require('path/to/sea/worker');

// Create a derived class
var Derived = SeaWorker.extend({
field: 'I am a field',
method: function () {
// I'm a function exists in both worker and browser
},
constructor: function (n) {
this.n = n;
// Call parent constructor if needed
// this.__super(n);
}
});

// Worker side methods
Derived.inWorker("methodInWorkerOnly", function() {});

// Browser side methods
Derived.inBrowser("methodInBrowserOnly", function() {});

// Worker service, running in worker, called from browser
Derived.service("foo", function() {});

// Register
SeaWorker.register(Derived);

module.exports = Derived;
});

项目状态

目前seajs-worker的所有代码均有文档、注释,并通过了单元测试。

API语句针对CoffeeScript设计优化,用JavaScript调用暂时还不够优雅,接下来会改善这个问题。