web worker脚本在tree-shaking场景下的正确使用

121 阅读3分钟

场景

某次,在开发一个npm组件包时,打算引入web worker来实现大量计算的性能优化

这个组件包的开发环境用的rollup + babel + typescript,用ts进行开发,rollup会自动打包、编译源码至js文件,web worker脚本我也希望是用ts编写,rollup自动转译

希望其他人用这个组件时,无需额外的配置,可直接用上组件内的web worker优化

于是有了最初的实现方式

// 主线程创建worker(ts源码)
const worker = new Worker('./worker.js', import.meta.url);
worker.postMessage('帮我计算文件md5');

// worker.ts
import SparkMD5 from 'spark-md5'
self.addEventListener('message', e => {
  // 处理逻辑...
});


// 打包后文件结构
dist/
├── index,js  # 主源码
├── worker.js # 转译后的worker脚本

这样组件方看起来没有问题,worker脚本正常暴露出来了

但实际使用方运行时,会找不到worker.js文件

因为使用方的项目,一般会支持tree-shaking,没有显式依赖的文件,不会打包到最终代码;也就丢失了worker.js文件;我们组件中,只是new Worker('./worker.js', import.meta.url);,理想的加载同目录下的worker脚本,这种不确定性很大

问题

const worker = new Worker('./worker.js', import.meta.url);直接这种方式,会找不到worker脚本

可以直接在源码中内联worker脚本字符串

// 主线程创建worker(ts源码)
const workerStr = `
  importScript(./spark-md5.min.js);
  // 或者从CDN加载
  importScript(https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.2/spark-md5.min.js);
  self.addEventListener('message', e => {
    // 处理逻辑...
  });
`const worker = new Worker('./worker.js', import.meta.url);
worker.postMessage('帮我计算文件md5');

也会有以下问题:

会增大主包体积,一方面希望主包较小,一方面又希望利用worker脚本优化性能,纠结

源码中字符串形式,不方便开发,还是希望worker脚本用ts开发,rollup帮助完成后续转译操作

第三方包spark-md5加载方式不可靠

考虑另一种方式,worker.js文件还是正常暴露在dist目录下,通过文档的形式,告知使用方,正确处理worker.js文件到最终的正确位置

会增加使用成本,希望开发出来的组件包,是能开箱即用的,避免复杂的配置

使用方显式import + 内联创建worker

经过查阅资料,比较理想的方案:

组件方:ts编写脚本源码,通过rollup自定转译为iife的脚本字符串,暴露这个脚本字符串

使用方:需要时,导入这个脚本字符串,通过参数传入组件即可

组件方:

// 主线程创建worker(ts源码)
// workerCode字符串通过参数传递
const blob = new Blob([workerCode], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.postMessage('帮我计算文件md5');

// rollup配置,新增一个配置,将worker ts源代码,编译后作为字符串导出
const workerConfig = {
  input: 'src/utils/calcMD5Worker.ts',
  output: {
    file: 'dist/worker.js',
    format: 'iife',
    name: 'WorkerCalcMD5',
    sourcemap: true
  },
  plugins: [
    resolve({
      extensions: ['.ts', '.js'],
      browser: true
    }),
    commonjs({
      requireReturnsDefault: 'predefined',
      dynamicRequireTargets: ['node_modules/spark-md5/*']
    }),
    typescript(),
    babel({
      babelHelpers: 'bundled',
      configFile: path.resolve(__dirname, './.babelrc'),
    }),
    {
      name: 'string-export',
      renderChunk(code) {
        return `export default \`${code.replace(/`/g, '\\`')}\`;`;
      }
    }
  ]
};

使用方:

import MyPackage from 'my-package';
import workerCode from 'my-package/dist/worker.js';

const useObj = new MyPackage({
  // 只需要设置这个参数,即可使用web worker优化
  workerCode,
});

这种方式的优点:

  • 配置简单可靠
  • 使用方可选择,需要时导入即可,不需要可以不传参数,也就不会往最终代码中加入无用代码
  • worker脚本的源码支持ts编写,方便开发