Kitsune Gadget

気になったことをつらつらと

Nuxt + Typescript で worker-loader を使う

pixelart-scaler 制作の過程で並列処理を利用することになり、そのときにかなりハマってたので書きます。

ドット絵スケーラー (PixelArt-Scaler) は、主なピクセルアート用フィルターによる小さい画像のスケーリング拡大がWeb上で出来ます。遊んでみてください。

kitsunegadget.github.io

本題

developer.mozilla.org

javascript 上で並列処理をする場合 WebWorkers API が利用できます。 メインで使うスクリプトとは別ファイルの js を読み込んで、その内容を別スレッドで実行することができます。const worker = new Worker('foo.js'); で読み込むことができます。

フレームワークを利用しない場合は、上記ドキュメントの方法で利用できます。 しかしフレームワークを利用している場合、直接ディレクトリの js を利用しようとすると static ファイルしか拾えなくなります。そこで import を利用するのですが、 lint や TypeScript では型定義あたりでエラーを吐き出します。

目次

worker-loader を利用する

フレームワーク上で利用する場合は worker-loader モジュールを利用すると良いでしょう。

github.com

// nuxt.config
{
  build: {
    extend(config) {
      config.module.rules.push({
        test: /\.worker\.ts$/,
        use: {
          loader: 'worker-loader',
        },
      })
  }
}
// custom.d.ts
declare module "worker-loader!*" {
  class WebpackWorker extends Worker {
    constructor();
  }

  export default WebpackWorker;
}
// App.vue
<script lang="ts">
  import Vue form 'vue'
  import Worker from "worker-loader!./Worker";
  const worker = new Worker();
 
  export default Vue.extend ({     
    // ...
    methods: {
      work() {
        worker.postMessage({ a: 1 });
        worker.onmessage = (event) => {};
    }
  }
)}
</script>
// Worker.ts
const ctx: Worker = self as any;

// Post data to parent thread
ctx.postMessage({ foo: "foo" });

// Respond to message from parent thread
ctx.addEventListener("message", (event) => console.log(event));

このように、readme に沿って設定をすれば使えるようになりました。 使えるようになったんですが…並列処理側に書いた js の処理が行われません。

コンソールで見てみると、スレッドは立ち上がっているものの、その内容が実行されていないようでした。

グローバルコンテキストのエラー

Worker 内部では window でなく、DedicatedWorkerGlobalScope というグローバルコンテキストが動きます。それによって postMessageonMessage をそのまま利用できます。 Worker 内部では自身を表すのは this ではなく、DedicatedWorkerGlobalScope の self を利用します。

しかしながら、フレームワーク利用時は self も定義されていないというエラーが起こるかと思います。 これについては ビルドオプションに config.output.globalObject = 'this' を追加することで解決します。 注意したいのは、設定は this、利用は self です。

// nuxt.config
{
  build: {
    extend(config) {
      config.module.rules.push({
        test: /\.worker\.ts$/,
        use: {
          loader: 'worker-loader',
        },
      })
      config.output.globalObject = 'this' // 追加
  }
}

内容が実行されない問題

グローバルコンテキストのエラーは解決したものの、まだ内容が実行されない問題が残っています。 worker-loader の readme 通りでは動かないため、import で読み込むファイル文字列を Typescript 用に書いた declare 定義のものにせず、直接ファイル名を利用すると内容が実行されるようになりました。

しかし新たな問題。declare の定義を外したため、default export の定義が無いエラーが発生します。開発環境では動くものの、ビルド時はエラーが置きているのでビルド出来ません…。

default export 問題の解決

readme の declare で定義していたクラス宣言を default export に直接追加します。このときのコンストラクタは継承元の super() にします。これで import 時のエラーは発生しなくなりました。よって declare を定義した .ts ファイルも必要がなくなるので削除して問題ありません。

// Worker.ts
const ctx: Worker = self as any;

// Post data to parent thread
ctx.postMessage({ foo: "foo" });

// Respond to message from parent thread
ctx.addEventListener("message", (event) => console.log(event));

export default class WebpackWorker extends Worker {
  constructor() {
    super('')
  }
}

扱いの注意

new Worker() するたびにスレッドが増えてしまうので、Worker を利用する場合は、グローバルに変数を宣言し、mountedインスタンス化して destroyed で削除するのが良いでしょう。

// Test.vue
<script lang="ts">
  import Vue form 'vue'
  import Worker from "./Worker"
  const worker: Worker
 
  export default Vue.extend ({     
    // ...
    mounted() {
      worker = new Worker()
    },
    destroyed() {
      worker.terminate()
    },
    methods: {
      work() {
        worker.postMessage({ a: 1 })
        worker.onmessage = (event) => {}
    }
  }
)}
</script>

おわりに

今回は nuxtjs + Typescript の環境の問題であったため、他の環境では worker-loader の readme どおりで動く場合もあるでしょう。他のフレームワークでも同じような問題があった場合は今回の解決法も役立つかもしれません。