スポンサーリンク

HTML5の「Web Workers API」を,別ファイルを使わずページ単体で利用するサンプル (createObjectURLがあれば,1ファイルでマルチスレッドのJSコーディングが可能)



HTML5の「Web Workers API」(非同期処理の仕組み)を,外部スクリプトを使わずに,1ファイルだけで実行するサンプルコード。


ワンライナーで(たった1行で)Workerスレッドを起動できる。

簡単に書くとこんな感じ。

// スレッド内容を別ファイルに分離しなくてよい
new Worker( window.URL.createObjectURL( new Blob([ elem.innerHTML ],{type:"text/javascript"}) ) );

以下は解説。


Web Workers APIは,JavaScriptでマルチスレッド・プログラミングをするための仕組み。


正確に言えば,UIとは別に裏側で,重い処理を非同期で実行してくれるAPI。

Workerというオブジェクトに「外部スクリプト・ファイルのURL」を渡せば,その外部スクリプトが非同期で走る。


ここで,うまく使えば,外部スクリプトの内容をWebページに取り込んでしまえる。

静的な外部スクリプトが存在しなくても,動的にURLを生成してWorkerに認識させることが可能だ。

そうすればWebページとスクリプトを1ファイルに統合できるから,Workerの利用が簡単になる。



外部ファイル不要のWorkerのサンプルを示す。

PCからアクセスすること。(HTML5未サポートのIE8などは対象外。)

動作サンプル
http://name-of-this-site.org/coding/html5/web_worker_with_internal_js.html


これは,UIとは別のスレッドで素数を計算するJavaScriptコードである。

UIの描画処理の裏側で,Workerが素数を求めている。


このサンプルで,Workerの存在する意味を図で書いてみよう。

           →→→ setTimeoutのタイマー間隔のたびに
           ↑ ↓ UIに再描画を命令できる
  UIスレッド    ↑ ↓
 ――――――――――――――――――――――――
   ↑|
   || postMessage()で,互いに情報や命令を伝達し合う
   |↓
 ――――――――――――――――――――――――
  Workerスレッド  ↓ ↑
           ↓ ↑ 処理が完了するたびに
           →→→ UIに再描画を命令できる
 

Workerの実行コードはUIスレッドと分離されているので,画面がフリーズしなくて済む。


そして本サンプルの特徴は,Workerの実行コードが外部ではなく,Webページの内部に存在するという点だ。

1ファイル完結なので,マルチスレッドのコードが書きやすい。


ソースは下記の通り。★印の付いた「window.URL.createObjectURL」がポイント。

<!doctype html>
<meta charset="utf-8">
<title>HTML5のWeb Workerを,別ファイルに分割せずページ単体で利用するサンプルコード</title>


<div style="width:99%;height:200px;position:fixed;background-color:lightgray;">


	<h2>HTML5のWeb Workerを,別ファイルに分割せずページ単体で利用するサンプルコード</h2>


	いくつまでの素数をリストアップするか?
	<input type="text" value="200000" size="20" id="maxNum">

	<br>
	<br>

	バックグラウンド計算の開始ボタン:
	<input type="button" value="バックグラウンドで計算を開始"
	 onclick="this.disabled=true;f();">

	<br>
	<br>

	バックグラウンドの計算を停止:
	<input type="button" value="計算を停止" onclick="g()">

</div>
<div style="top:220px;width:99%;height:50%;position:fixed;overflow-y:scroll;" id="resultDiv">



	計算結果:
	<div id="fuga"></div>



</div>

<script>

var worker, tid;


function f()
{
	// ----- workerの生成 -----

	// タグ内のJSコードを文字列として取得
	var js_code = document.getElementById("hoge")
		.innerHTML
		.replace(/&gt;/g, ">")
		.replace(/&lt;/g, "<")
	;
	
	// JSコードをBLOBオブジェクトに変換
	var b = new Blob( [ js_code ], {type : "text/javascript"} );
	
	// BLOBオブジェクトをURLに変換
	var worker_url = window.URL.createObjectURL( b );	// ★
	
	// URLを使ってWeb Workerを生成
	worker = new Worker( worker_url );
		// 通常はこの引数を外部スクリプトのURLにする。
		// ここでは動的に内部で生成している
	

	// ----- workerの起動 -----
	
	// 処理の開始日時
	var d1 = new Date();
	
	// workerからメッセージを受け取った時のイベント
	worker.onmessage = function(event){
		
		// workerから受け取ったメッセージ
		var msg = event.data;

		// 全て終了?
		if( msg.all_completed )
		{
			// UI側のタイマーを停止
			clearInterval( tid );
		
			// 処理の終了日時
			var d2 = new Date();
			alert(
				"全てのバックグラウンド計算が終了しました。\n"
				+ "開始:" + d1 + "\n"
				+ "終了:" + d2
			);
		}
		else
		{
			// 部分的な計算結果を画面に表示
			document.getElementById("fuga").innerHTML += msg.str;
		}
	};
	
	// workerにメッセージを送って,計算を開始させる
	var maxNum = parseInt(document.getElementById("maxNum").value, 10);
	worker.postMessage({ num : maxNum });
	
	
	// ----- UI側のタイマー起動 -----
	
	
	// 1秒おきに画面上に時間を通知
	tid = setInterval(function(){

		// 経過時間ミリ秒
		var ms = (new Date()).getTime() - d1.getTime();
		
		// 結果表示に割り込んで表示
		document.getElementById("fuga").innerHTML += "<b>[" + ms/1000 + "s 経過]</b> ";
		
		// 結果を一番下にスクロール
		var rdiv = document.getElementById("resultDiv");
		rdiv.scrollTop = rdiv.scrollHeight;
		
	}, 1000);
}


// 処理を途中で停止
function g()
{
	if( worker )
	{
		worker.terminate();
		clearInterval( tid );
	}
}

</script>



<!-- Workerを動かすためのコードを文字列として格納 -->
<div style="display:none;" id="hoge">

// workerがメッセージを受け取った時のイベント
onmessage = function( event )
{
	// workerが受け取ったメッセージ
	var msg = event.data;
	var num = msg.num;
	
	// 素数を計算する
	var continue_flag = true;
	var cnt = 2;
	while( continue_flag )
	{
		// 素数判定
		calcPrime( cnt );
		
		// 継続の判定
		if( cnt >= num )
		{
			continue_flag = false;
		}
		else
		{
			cnt ++;
		}
	}
	
	// 全て完了をUIに通知
	postMessage({ all_completed : true });
};


// 素数のインデックス
var ind = 1;

// 出力用のバッファ文字列
var buff = "";


// 引数の素数判定
function calcPrime( num )
{
	// 割り切れるか判定
	for( var i = 2; i <= num/2; i++ )
	{
		if( num % i == 0 )
		{
			return false;
		}
	}
	
	// バッファに蓄積
	buff += "(" + ind + ")" + num + " ";
	
	// 100個ずつ出力
	if( ind % 100 == 0 )
	{
		// 計算結果を「部分的に」UIにメッセージ送信
		postMessage( { str : buff } );
	
		// バッファをクリア
		buff = "";
	}
	
	ind ++;
	return true;
}

</div>


</body>

こうすれば,Workerの利用は単一ファイルで済み,複数ファイルへの分割は不要だ。

要点を抽出すると:

	// ----- workerの生成 -----

	// タグ内のJSコードを文字列として取得
	var js_code = document.getElementById("hoge").innerHTML;
	
	// JSコードをBLOBオブジェクトに変換
	var b = new Blob( [ js_code ], {type : "text/javascript"} );
	
	// BLOBオブジェクトをURLに変換
	var worker_url = window.URL.createObjectURL( b );
	
	// URLを使ってWeb Workerを生成
	worker = new Worker( worker_url );

もし,これを普通に書くと・・・

	// 外部スクリプトを使ってWeb Workerを生成
	worker = new Worker( "./hoge.js" );

となり,本来ならば外部URLの指定が必要である。


createObjectURLには,バイナリデータとしてBlobオブジェクトを渡す。

そのバイナリの中身に,JavaScriptのコードをString形式で詰め込んでおく。


Stringの調達の方式は,JSコード内に直接書いてもいいし,HTML内に「隠して」おいてもいい。

HTML内にWorker用のJSコードを文字列として埋め込んだ場合,コード内の>と<を復元する処理をかければよい。


なお,AndroidやiOSでは,この「createObjectURLで遷移先URLを生成」という技が通用しない。

スマホ・タブレットではWorkerを単一ファイルで利用することはできない,ということだ。残念。

そういう場合はスクリプトを外部ファイルに切りだす。


ここで取り上げたコーディング方法は,PC向けのWebページを作る際,

1ページだけでマルチスレッド(非同期処理)にしたい,という時に活用できる。

追記:


下記のようなコメントを頂いた。

つまり,こういう事だ。

本サンプルではWorkerの起動形式が

new Worker( window.URL.createObjectURL( new Blob([ elem.innerHTML ],{type:"text/javascript"}) ) );

だったのを

new Worker( window.URL.createObjectURL( new Blob([ (function(){〜〜〜}).toSource() ],{type:"text/javascript"}) ) );

のように変更すれば,JSコード内にJSコードを埋め込む事ができる。

ワンライナーで,たった1行でWorkerスレッドが起動できる。


ただしその場合は,Worker用のコードが別の空間で動作するのが分かりづらくなってしまう。

(クロージャの性質として「いつでも外部定義変数を呼び出せる」というのがあり,Workerはその性質に反する挙動になってしまうから,合理性に欠けるコードになってしまうのだ。)

短くコーディングする分には役立つだろう。

参考資料:

Web Workersの存在意義について。重い処理をバックグラウンドで実行できる:

Firefox 3.1のWeb Workersでマルチスレッド・プログラミング - builder by ZDNet Japan
http://builder.japan.zdnet.com/html-css/sp_firefox-3-for-developer-2008/20386518/

  • スクリプトの実行中はページの表示処理が止まってしまうため、あまり長い時間のかかる処理を行うのは好ましくない。その点Web Workersを利用すればバックグラウンドでスクリプトを実行しながら、同時に他の処理を継続するできる


コイケアキヨシ blog: HTML5のWeb Workersを使ってみた
http://akiyoshi220.blogspot.jp/2010/06/html5web-workers.html

  • バックグラウンドで動くことができるので、重たい処理をJavaScriptで処理させてもブラウザが固まらない!っていうメリットがあります
  • ただし、WebWorkersからはDOMを操作ができないので注意!alertも使えない


WebTecNote - [HTML5API] Web Workersで手抜きツール作ってみた
http://tenderfeel.xsrv.jp/mootools/876/

  • 使うメリットは、重い処理を全部裏側でやっつけられるので表の動作が軽くなるという事


return new Bug();: HTML5 WebWorkersでjQueryはロードできない
http://returnnewbug.blogspot.jp/2012/08/html5-webworkersjquery.html

  • サーバサイドでやってるようなこともクライアント側で出来たらレスポンシビリティの高いインタフェースが作れる。でもユーザの操作を阻害するような重い処理は避けたい。
  • そこで今までになかったバックプロセス、別スレッドでの処理実行があると便利ってことでWebWorkersが策定されている
  • バリバリ働く若い男にはUI系ライブラリを使った「見た目」の処理をバンバンやらせて、裏ではナイスミドルがジットリと職人仕事をする
  • サポートされるスレッド数はブラウザ、バージョンによって異なる。CPUのスレッド数を上回る同時実行なんてのはできるわけがないので、並行処理として有効なのは主プロセスも含めてせいぜい3プロセス程度
  • 処理速度を上げるというよりは、別プロセスで実行されることでユーザ操作をロックしないのが目的
  • WebWorkersのサンプルを探すとほとんどが大規模な数値計算、例えば3Dモデルの座標計算であったりとか、比較的単純な計算の膨大な繰り返し実行

Web Workersの実用的な応用例:

404 Blog Not Found:javascript - Web workers を万能にする workaround
http://blog.livedoor.jp/dankogai/archives/51504379.html

  • 素数を100万まで探索させています。Safariではあっという魔に終わりますが、Firefoxでは結構かかります。途中で飽きたらterminateしてください。setTimeout()なしでもアニメーションできるのは本当に楽ですね。


Webteko 第11回で Web Workers について発表した - mollifier delta blog
http://mollifier.hatenablog.com/entry/20100124/p1

  • 1%分完了するたびに進捗状況を表示


第4回 クロスドキュメントメッセージングやWebSocketを使ってみる | Think IT
http://thinkit.co.jp/story/2012/05/15/3542/page/0/1

  • 整数を入力して、計算ボタンを押すと、下のフィールドにその整数の素因数分解がJavaScriptの数式の形式で出力されるというものです。Worker不使用のサンプルとWorker使用のサンプルを作成して比較しました。
  • Worker不使用のサンプルでは応答が止まってしまう。ブラウザの応答が止まってしまい、ウィンドウにアクセスしようとすると強制終了するかを問うダイアログが表示されます。


Web Workers | GCgate エンジニアブログ
http://www.gcgate.jp/engineerblog/html5/web-workers/

  • WAV変換はRecorder.jsを利用させていただいています。 Recorder.jsはWeb Workersを利用して、別スレッドでWAVにエンコードすることができます。

Workerの発展でShared Workerというのがあるが,その実装はまだ十分でなく,仕様の確定もあやふやなので,特に覚えようとする必要はない:

マルチスレッドによるユーザビリティの向上 Web Workers
http://kaihooo.com/web-workers/

  • 共有ワーカは1つのワーカを複数のページで共有して参照できるものである。Same Origin Policyの制約はあるが、複数の異なるウィンドウから1つの共有ワーカを参照できる。
  • 共有ワーカの応用例として、共有ワーカを経由したウィンドウ間メッセージングや、共有ワーカをハブとしたサーバコネクションの1本化などが考えられている。


Shared Workers復活?、CSSOM View更新ほか、2013年12月のWeb標準化動向 | HTML5Experts.jp
http://html5experts.jp/myakura/4888/

  • Shared Workersを削除する必要があるのかといった議論が、WebApps WGのメーリングリストで交わされています。

Workerに動的にファイルを指定することもできる:

動的にWebWorkerを作成して動かす - jsdo.it - Share JavaScript, HTML5 and CSS
http://jsdo.it/kjunichi/naSo

  • Web Workersはワーカーのjsソースを指定するが、 File APIで動的にソース(に指定出来るオブジェクト?)を作れるので、 これを使えば、動的にWeb Workersを作成できて便利


BlobBuilder で 外部ファイルの要らない WebWorkers はつくれる - つまみ食う
http://d.hatena.ne.jp/mohayonao/20111123/1322028946

  • BlobBuilderっていうのを使ってコードを組み立てる
  • すごいと評判の createObjectURL でURLを取得 new Worker(url) する

動的にURLを生成するための,createObjectURLメソッドの仕様上の注意点:

iOS 6.0のSafariでBlob URIを開く事が出来ないのか - 人生が二度あれば
http://memo.overknee.info/post/33503147722

  • iOS 6.0のSafariはURLインターフェース (ベンダープレフィックスが付いてwebkitURL) が存在し、createObjectURLメソッドを使用してBlob URIの生成が出来るようになりました。 生成が出来るようにはなっているのですが、ただしその生成したURIを開く事が出来ません。blob:プロトコルのそれらしいURIが確かに生成されるのですが、そのURIを開こうとしても「"ページが開けません。アドレスが無効です。"」といった無情なエラーメッセージが表示されてしまい、開けません。


JavaScript - createObjectURLを使ったらrevokeObjectURLを呼ぶ - Qiita
http://qiita.com/ykst/items/a367359cd4ee96cde802

  • バイナリオブジェクトを実体化する際に重宝するcreateObjectURLですが、 ちゃんと使用オブジェクトを解放しても一定回数同じページで使用し続けると404 errorが発生して、 以後URLが作れなくなってしまうという現象がありました。(Chrome Version 32.0.1700.107) 本来はガベージコレクションで解決して欲しいところですが、 とりあえず一度作ったURLはrevokeObjectURLで解放

BlobおよびBlobBuilderについて:

html5のW3C FileAPIについて、調べてまとめた。 - それマグで!
http://takuya-1st.hatenablog.jp/entry/20120106/1325835641

  • blobは、読み込んだデータそのものを表す。 Fileは Blobの拡張で定義されてる、なぜか?ファイルを読み込んだら、バイナリが出てくるからに他かならない。 出てきたバイナリはそのままCanvasに入れるなり、Imageにいれるなり、Videoで再生するなどになる。


BLOB とは何か - slowjet
http://5509.hatenablog.com/entry/2013/04/26/012658

  • File API で定義されているのは Blob インターフェースのみ、 つまり File API を通して利用できるのは BLOB だけ。


File API で作成したファイルをダウンロード - dimrosの日記
http://d.hatena.ne.jp/dimros/20130720/1374278398

  • ファイルをBlobで作成し、 ダウンロードという形


File API で作成した blob をダウンロードする | Hebikuzure's Tech Memo
http://hebikuzure.wordpress.com/2012/12/16/file-api-%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%97%E3%81%9F-blob-%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B/

  • HTML5 では BlobBuilder オブジェクトを作成して、ファイルをメモリ上に読み込んだり、あるいはユーザーの入力やサーバーから取得したデータを元にしてメモリ上にファイルの内容を作成することが可能です。
  • 作成した Blob は XML HTTP Request (XHR) の FormData としてサーバーにアップロードすることも可能です。

その他,Webページのレイアウトについて:

HPの背景で画面の上半分を赤、下半分を青に設定するには、どういうしたらいいで... - Yahoo!知恵袋
http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1325141828

  • position を fixed にすれば、スクロールしてもずれない


overflow-y:scrollな要素を一番下までスクロールする - ぼくはまちちゃん!(Hatena)
http://d.hatena.ne.jp/Hamachiya2/20090624/scroll

  • peroZone1.parentNode.scrollTop = peroZone1.parentNode.scrollHeight;

関連する記事:

JavaScriptの動かないコード (中級編) setTimeoutのタイマーが指定時刻に動かないエラー (JavaScriptがマルチスレッドだという誤解) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20090614/p1


今から3分で,HTML5のドラッグ&ドロップAPIと File APIを習得しよう(JSの実装サンプル付き) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20140330/HTMLfiveDragDropAndFileAPISample


Javaの非同期処理を,シングルスレッドのようにシンプルにコーディングするための設計パターン (並列処理を逐次処理にする) - 主に言語とシステム開発に関して
http://d.hatena.ne.jp/language_and_engineering/20120205/p1