オウルです。
今回は、ハマっているドラクエウォークを題材にWeb Workerを使ってドラクエウォークのこころ情報を取得してみます。まぁ、この程度の処理であれば、わざわざWeb Workerを使用しなくともFetchだけで事足りますが、そこはWeb Workerを動かす!が目的ということで。
WSL v1 | Ubuntu18.04 LTS |
App | ASP.NET Core 3.1.302 |
DB | MongoDB |
MongoDBは次回使用する予定です。
Web Workerとは
Web Worker とは、ウェブアプリケーションにおけるスクリプトの処理をメインとは別のスレッドに移し、バックグラウンドでの実行を可能にする仕組みのことです。時間のかかる処理を別のスレッドに移すことが出来るため、 UI を担当するメインスレッドの処理を中断・遅延させずに実行できるという利点があります。
本記事はWeb Workerのサンプルを作って動作させることを主目的としているため、もっと詳しくWeb Workerを知りたいという方は、MDNのWeb Workerの概念と使い方を参照することをお勧めします。
Web Workerには、いくつかの種類がありますが、今回使用するのはDedicated Workerです。
Dedicated Workerは、Workerスレッド(メインスレッドとは別)で処理(スクリプト)を実行します。ドラクエウォークのこころ情報を取得するサンプルを作成する前に、少しDedicated Workerの制限について紹介します。
Dedicated Workerの制限
バックグラウンドのスレッドで処理を実行してくれる便利なDedicated Workerですが、何でもできるわけではありません。
特に知っておく必要があることは、WorkerからDOMを直接操作することは出来ないということです。
また、Workerは、windowとは別のグローバルなコンテキスト(DedicatedWorkerGlobalScope)で実行されるため、windowにデフォルトで用意されているメソッドやプロパティにアクセスするとエラー(使用できない)になるものもあります。その他詳細については、MDNのWeb Workers が使用できる関数とクラスを、参照ください。
ドラクエウォークのこころを取得
Web Workers が使用できる関数とクラスにあるように、使用できる関数とクラスには、制限があります。今回前提するブラウザはChromeです。そのため、こころ情報の取得にはFetchを使用することとします。
フロント
では、まず画面からです。シンプルに、キラーマシンかキングスラムを選択するとBar Chartsでこころ情報が表示される仕様にします。Bar Chartsの描画は、google chartsを使用します。
Index.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<h1>Web Worker</h2> <div class="row"> <div class="col col-sm-12 col-md-4"> <div class="form-group"> <label>モンスター</label> <select id="monsters" class="form-control"> <option value="0">---</option> <option value="1">キラーマシン</option> <option value="2">キングスライム</option> </select> </div> </div> </div> <div class="row"> <div class="col col-sm-12 col-md-4"> <!-- ここにBar Chartsを表示 --> <div id="barchart_values"></div> </div> </div> @section scripts { <script src="~/js/main.js"></script> } |
main.js
main.js内でDedicated Workerを生成、Workerへのメッセージ送信、Workerからのメッセージに応答の処理を定義しています。
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
// google.charts google.charts.load("current", { packages: ["corechart"] }); const loadHandler = { handleEvent: (event) => { const input = document.querySelector("#monsters"); // Dedicated Workerを生成します const worker = new Worker("js/worker.js"); /** * MonsterController.HeartStatusアクションへのURLを取得します * @param {string} id */ const getUrl = (id) => { const url = new URL(window.location); url.pathname = "/Home/Status"; url.searchParams.append("id", id); return url.href; } /** * Dedicated Workerにメッセージを送信します * @param {Worker} worker * @param {HTMLInputElement} el */ const postMessage = function (worker, el) { this.handleEvent = function (event) { const input = event.currentTarget; if (input.value === "0") return; // Dedicated WorkerにMonsterController.HeartStatusアクションへのURLを送信します worker.postMessage(getUrl(input.value)); } el.removeEventListener("change", this); el.addEventListener("change", this, false); }; /** * メインスレッドでDedicated Workerから返されたメッセージに応答します * @param {Worker} worker */ const onMessage = function (worker) { this.handleEvent = function (event) { // Workerから返されたメッセージは、event.dataで取得します if (event.data) { const chart = new DrawChart(document.getElementById("barchart_values")); // Google Chartsを描画します chart.drawChart(event.data); } else { console.log("empty message received from worker"); } } worker.removeEventListener("message", this, false); worker.addEventListener("message", this, false); } const p = new postMessage(worker, input); const m = new onMessage(worker); } } if (window.Worker) { window.addEventListener("DOMContentLoaded", loadHandler, false); } else { console.log('Your browser doesn\'t support web workers.'); } |
Workerとメインスレッド間は、postMessage()を使用してメッセージを送信して、onmessageイベントハンドラーによってメッセージに応答します。ここでの注意は、メインページとWorker間で送られるメッセージは共有ではなく、コピーとなります。補足情報としてTransferable Object(Transferable インターフェイスを実装するオブジェクト)は、コピーではなく転送となります。Rustでいう所有権の移動に近いでしょうか(ムーブセマンティクス)。
Google Chartsを描画するのはECMAScript 2015で導入されたJavaScriptクラス(プロトタイプベース継承の糖衣構文)で定義しています。
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 32 33 34 35 36 37 38 39 40 41 42 43 |
class DrawChart { constructor(element) { this.element = element; } drawChart(data) { var chartData = google.visualization.arrayToDataTable([ ["Element", "パラメーター", { role: "style" }], ["さいだいHP", Number(data.hp), "color: #1a73e8"], ["さいだいMP", Number(data.mp), "color: #c71585"], ["ちから", Number(data.power), "color: #ff0000"], ["みのまもり", Number(data.defense), "color: #ffff00"], ["こうげき魔力", Number(data.at_magical), "color: #800080"], ["かいふく魔力", Number(data.def_magical), "color: #008000"], ["すばやさ", Number(data.agility), "color: #4169e1"], ["きようさ", Number(data.dexterity), "color: #ff4500"] ]); const view = new google.visualization.DataView(chartData); view.setColumns([0, 1, { calc: "stringify", sourceColumn: 1, type: "string", role: "annotation" }, 2]); const options = { title: data.name + " status", width: 600, height: 400, bar: { groupWidth: "95%" }, legend: { position: "none" }, }; var chart = new google.visualization.BarChart(this.element); chart.draw(view, options); } } |
worker.js
Workerはメッセージを受け取り、サーバから取得したこころ情報をメインスレッドに返しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const onMessage = function () { this.handleEvent = async function (event) { const url = new URL(event.data); const response = await fetch(url.href, { method: "get", headers: { "accept": "application/json" }, credentials: "same-origin" }); if (response.ok) { const result = await response.json(); self.postMessage(result); } else { console.log("error: " + response.status); self.postMessage(""); } } self.removeEventListener("message", this, false); self.addEventListener("message", this, false); } const m = new onMessage(); |
サーバ
ここはハードコードとなっていますが、受け取ったIDから、こころ情報を取得してクライアントに返します。
MonsterController.cs
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
public class MonsterController : Controller { private readonly ILogger<MonsterController> _logger; public MonsterController(ILogger<MonsterController> logger) { _logger = logger; } public IActionResult Index() { return View(); } [HttpGet] public IActionResult HeartStatus(string id) { // 「キラーマシーン」と「キングスライム」のこころ情報を作成します var source = new [] { new { id = "1", name = "キラーマシン", hp = "114", mp = "48", power = "58", defense = "61", at_magical = "24", def_magical = "17", agility = "25", dexterity = "16" }, new { id = "2", name = "キングスライム", hp = "40", mp = "58", power = "81", defense = "23", at_magical = "28", def_magical = "31", agility = "120", dexterity = "74" }, }; var result = source.Where(m => m.id == id).FirstOrDefault(); if(result == null) { // 404 return NotFound(); } else { // 200 return Ok(result); } } } |
結果

次回は、こころ情報をMongoDBから取得するように変更して、式木(Expression Trees)とMongoDB C#/.NET Driverの動的条件における実装のしやすさを検証します。