前回からのつづき

前回、JSZip ライブラリを使用したサンプルプログラムをみていただきました。

今回は、このサンプルプログラムを Web Worker 化することにより、コードがどの様に変化するか、また、「キャンセル」処理のタイムラグが解消されるのかなどを中心にみていきたいと思います。


Web Worker 化の変更概要

変更は、HTML と JavaScript のみとし、現在のメインスレッドでの Zip 化の処理の部分を Web Worker として、別ファイルに切り出す形となります。

┌─────────────────────────────────────┐                 ┌────────────────────────┐
│ メインスレッド                       │                 │ バックグラウンドスレッド │
│                                     │                 │                        │
│  ┌────────────────▶┐                │                 │  ┌──────────────────┐  │
│  │  イベントループ  │                │                 │  │  Web Worker      │  │
│  │      ┌──────────┿──────────┐     │                 │  │                  │  │
│  │      ▼          ▼          ▼     │                 │  │                  │  │
│  │   画面描画  ユーザー入力  Zip圧縮 ────── 処理依頼 ─────▶ │  JSZipによる圧縮  │  │
│  │      │          │          │     │                 │  │                  │  │
│  │      └──────────┿──────────┘     │                 │  └──────────────────┘  │
│  │                 ▼                │                 │                        │
│  └◀────────────────┘                │                 │                        │
└─────────────────────────────────────┘                 └────────────────────────┘

また、それに伴い、Web Worker では、メインスレッドからメッセージでファイル一覧を受け取り、Zip圧縮処理を行う形にします。

圧縮の進捗状況は、従来通りコールバックにて拾い、都度メッセージにより、メインスレッドに返す形にします。

処理結果の Zip データはメッセージによりメインスレッドに受け渡し、ダウンロード処理します。

又、圧縮処理のキャンセルは、タイムラグをなくす為、従来のコールバックでの判定ではなく、Web Worker の terminate() 関数で行うことにします。

┌─────────────────────────────────────┐                 ┌────────────────────────┐
│ メインスレッド                       │                 │ バックグラウンドスレッド │
│                                     │                 │                        │
│    ・生成と起動      ───────── new ──────────────▶│                        │
│                                     │                 │                        │
│    ・メッセージ送信      ────────── ファイル一覧 ─────────▶  ・メッセージ受取り    │
│                                     │                 │                        │
│    ・進捗メッセージ受取               │                 │  ・JSZipによる圧縮    │
│      進捗描画       ◀───────── 進捗状況 ──────────────    進捗メッセージ送信  |
│      Zipダウンロード     ◀───────── 処理結果 ──────────────    結果メッセージ送信 |
│                                     │                 │                        │
│    ・キャンセル      ───────── terminate() ──────▶│                        │
│                                     │                 │                        │
└─────────────────────────────────────┘                 └────────────────────────┘


サンプルプログラムの動作イメージの確認

まずは、Web Worker を使用したサンプルプログラムを使って実際の動きを確認していただこうと思います。

下記のボタンを押下することで、ポップアップ画面が表示され、サンプルプログラムが起動します。

機能や使い方については、前回の Web Worker を使用していないものと同じです。詳しくは、前回の記事、「モダンJavaScript入門/Web Worker の動作確認①」を参照してください。

※ ポップアップ画面外をクリックすると、画面が閉じ、サンプルプログラムは終了します。


コードの変更内容

HTML( index.js ) の変更

  • JSZipモジュールをWeb Worker で読み込む為、jszip.min.js の <script> タグを削除

CSS( style.css ) の変更

  • 変更なし

メインスレッド( script.js )の変更

  • JSZipによる圧縮処理を 圧縮用 Web Worker ( worker.js )の読み込み、起動に変更
  • ファイル一覧を圧縮用 Web Worker にメッセージで送信
  • 圧縮用 Web Worker からの進捗メッセージを受け取り、進捗を画面に表示
  • 圧縮した結果は、 Web Worker からメッセージで受け取り、ダウンロード
  • キャンセル時に、圧縮用 Web Worker 停止

Web Worker( worker.js ) スレッドの新規作成

  • メッセージを受信し、渡された FileList を JSZip に順番に受け渡し、Zipファイルの作成を行う。
  • コールバックにより、進捗状況をメインスレッドにメッセージで送信する。
  • 圧縮完了した Zip データをメインスレッドにメッセージで送信する。

変更後のサンプルプログラムのコード紹介

下記に、変更後のサンプルプログラムのコードを記します。

※ JSZip ライブラリについては、バージョン変更による互換性のリスク低減の為、ダウンロードした UMD 配布版(v3.10.1)を利用しています。

HTML ( index.html )

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>ディレクトリを Zip 化 (Web Worker なし)</title>
  <link rel="stylesheet" href="./style.css">
  <script defer src="./script.js"></script>
</head>
<body>
  <p>
    <input id='input' type="file" webkitdirectory multiple>
    <div id='output'></div>
  </p>
  <button id='zipBtn' disabled>Zip 化してダウンロード</button>
  <button id='cancelBtn' disabled>キャンセル</button>
  <p>
    <div id='status'></div>
    <div id="progressWrap" style="display:none; margin-top: 8px;">
      <progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
      <span id="progressText">0%</span>
    </div>
  </p>
</body>
</html>

CSS( style.css )

body {
  padding: 1rem;
  background-color: rgb(253, 191, 76);
  text-align: center;
  overflow: hidden;
}
button {
  width: 200px;
}
#output {
  border: 1px solid;
  background-color: skyblue;
  text-align: left;
  width: 100%;
  height: 300px;
  overflow-y: auto;
}
#status {
    font-size: 1.25rem;
    font-weight: bold;
    color: red;
    margin-bottom: 1rem;
}

メインスレッド( script.js )

const input = document.getElementById("input");
const status = document.getElementById("status");
const output = document.getElementById("output");
const zipBtn = document.getElementById("zipBtn");
const cancelBtn = document.getElementById("cancelBtn");
const progressWrap = document.getElementById("progressWrap");
const progressBar = document.getElementById("progressBar");
const progressText = document.getElementById("progressText");
let worker = null;

input.addEventListener("change", (e) => {
  output.innerHTML = "";
  for (const file of input.files) {
    output.innerHTML += file.webkitRelativePath +"<br>";
  }
  if (input.files.length) status.textContent = "ファイル選択完了";
  zipBtn.disabled = false;
});

// 「Zip 化してダウンロード」ボタン押下時
zipBtn.addEventListener("click",  (e) => {
  zipBtn.disabled = true;
  cancelBtn.disabled = false;
  status.textContent = "圧縮開始";
  progressWrap.style.display = "block";
  progressBar.value = 0;
  progressText.textContent = "0%";

  worker = new Worker("./worker.js"); // Web Worker 起動

  worker.postMessage(input.files); // ファイル一覧メッセージ

  worker.onmessage = (e) => {
    const msg = e.data;
    if (msg.type == "progress") { // 進捗メッセージ
      progressBar.value = msg.percent;
      progressText.textContent = `${msg.percent}%`;
    }
    if (msg.type == "done") { // 完了メッセージ
      // Zip ファイルのダウンロード
      const url = URL.createObjectURL(msg.blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "archive.zip";
      a.click();
      URL.revokeObjectURL(url);

      status.textContent = "圧縮終了";
      zipBtn.disabled = false;
      cancelBtn.disabled = true;
    }
  }
});

// 「キャンセル」ボタン押下時
cancelBtn.addEventListener("click", (e) => {
  cancelBtn.disabled = true;
  status.textContent = "圧縮キャンセル";
  zipBtn.disabled = false;
  worker.terminate(); // Web Worker 強制停止
});

Web Worker( worker.js )

importScripts("./lib/jszip.min.js");

self.onmessage = async (e) => {
  const fileList = e.data;
  const zip = new JSZip();

  for (const file of fileList) {
    // webkitRelativePath には "選択したフォルダ名/サブフォルダ/ファイル名" が入っている
    zip.file(file.webkitRelativePath, file);
  }

  // Zip化
  const content = await zip.generateAsync(
    { type: "blob" },
    (metadata) => {
      const percent = Math.floor(metadata.percent);
      // 進捗メッセージ
      self.postMessage({ type: "progress", percent: percent });
    }
  );

  // 完了メッセージ
  self.postMessage({ type: "done", blob: content });
  // Web Worker 終了
  self.close();
};


Web Worker 化による効果

  • メインスレッドと Web Worker の間で、役割分担が明確になる為、コードの見通しがよくなる。
  • 圧縮ライブラリの変更が、メインスレッドに影響を与えにくい。
  • キャンセル処理のコールバック待ちでの判定がなくなり、コードがシンプルになる。

まとめ

今回のサンプルプログラムを起動してみて、前回と同様に大きなファイルやファイル数が多いディレクトリを Zip 圧縮して「キャンセル」ボタンでキャンセルしようとした場合、タイムラグやひっかかりのようなものがなく、即座に反応することがわかると思います。

実際問題、パソコンの性能があがれば、この差は小さくなりますが、プログラム設計において、ユーザーの感覚に影響を受ける画面の応答性を高める方法として、Web Worker による並列処理は十分に考慮に値する技術だと思われます。