2018年11月15日

Create React AppでWeb Workerを使うには

「Web Worker」を使うと、ブラウザ上のJavascriptでも複数のスレッドを使うことが出来るようになります。


Web Worker を使用する - Web API | MDN https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Using_web_workers#Web_Workers_API


これによって、CPU負荷の高い計算処理などを別スレッドで実行することで、画面の反応が鈍くなったりするのを防ぐことが出来ます。


今回は、Create React Appで作成したプロジェクトでWeb Workerを使う方法を調査しました。


1. publicフォルダにWorkerのJSファイルを配置して読み込む。


⇒ 最も基本的で単純な方法です。publicフォルダに配置したファイルはそのままデプロイされるので下の方法で素直に読み込んで使うことが出来ます。

const myWorker = new Worker("worker.js");
myWorker.postMessage("hoge");


短所としては、TypeScriptやES2017などを使っている場合は別途自分でビルドしてpublicフォルダに配置する必要があることが挙げられます。


2. ejectしてからWebPackの設定に worker-loader または worker-plugin を追加する。


⇒ worker-loaderを使うと、別スレッドで動かしたい処理をModuleとして作成しておいて、使いたい箇所でそれをimportしてnewするだけでWeb Workerとして動作するようになります。コード例は、下のようになります。

import HelloWorker from './hello.worker.js';
const helloWorker = new HelloWorker();
helloWorker.postMessage('hoge');

詳細はこちら。
Add support for WebWorker with worker-loader (#3660) by iansu · Pull Request #3934 · facebook/create-react-app 

worker-plugin を使う場合はこんな感じ。

const worker = new Worker('./worker', { type: 'module' });
worker.postMessage('hoge');

詳細はこちら。
Add support for WebWorker with worker-plugin by Krivega · Pull Request #5464 · facebook/create-react-app  




3. ejectせずに react-app-rewired を使ってWebPackの設定に worker-loader または worker-plugin を追加する。


⇒ 下のブログ記事にejectせずに上の2と同じ事をする方法が書かれていました。

How to use web workers with react-create-app and not ejecting in the attempt

ただ残念ながら、react-app-rewiredは最近のVersionには非対応なのでこの方法は現在は使えないようです。


4. WorkerのJSファイルをBlobとして読み込んでからWorkerスレッドを生成する。


⇒ 下のissueへのコメントで紹介されている方法です。

Is it possible to use load webworkers? · Issue #1277 · facebook/create-react-app

関数をtoString()で文字列化して、さらにそれをBlobにします。こうすると、WorkerのコンストラクタにObjectURLとして渡してWorkerインスタンスを生成出来るようです。

let code = worker.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));
const blob = new Blob([code], {type: "application/javascript"});
const worker = new Worker(URL.createObjectURL(blob));

少々トリッキーですが、試してみたところ少なくともMacのChrome/Safari/FireFoxでは問題なく動いています。

この方法なら、WebPackの設定を変更する必要がありません。また上記1のようにWorkerのJSファイルだけ自前でビルド・配置したりする必要もありません。

具体的なコード例は後述します。


5. 公式にサポートされるまで待つ。


⇒ 上の2で挙げた方法がPull Requestとして上がっており公式にも検討されているみたいですが、どうやらブラウザ互換性の問題(?)があるようで2018年11月時点ではまだマージされるには至っていません。将来的には公式にサポートされる可能性はあるので、気には止めておいた方が良いかも知れません。

Add support for WebWorker with worker-loader (#3660) by iansu · Pull Request #3934 · facebook/create-react-app 

Add support for WebWorker with worker-plugin by Krivega · Pull Request #5464 · facebook/create-react-app 



今回は上記4の方法を試してみたので、以下に紹介しておきます。

WorkerのJSファイルをBlobとして読み込んでからWorkerスレッドを生成するサンプル


index.jsと同じフォルダにcreateWorker.js, myWorker.jsの2つのファイルを作成します。

createWorker.js

---

myWorker.js

---


createWorkerとmyWorkerをimportして、下のようにすることでWorkerスレッドが使用可能になります。

import createWorker from './createWorker';
import myWorkerFunc from './myWorker';
const myWorker = createWorker(myWorkerFunc);


index.js

---









2018年11月4日

React/TypeScriptでリバーシゲームを作る (2) - ゲームロジックの実装

前回はボード上に石を配置するまでを実装しましたが、今回はさらにリバーシのルールを実装して実際に遊べるようにしたいと思います。


実際に動いているものはこちらで試すことが出来ます。

https://reversi-ocmiwdgcdj.now.sh



ゲームのルールを実装するには、大まかに分けて次の4つの処理が必要になります。


  1. 「次に石を配置可能な場所」の判定と表示
  2. 挟まれた石を裏返す
  3. 石を置ける場所が無い場合の「パス」処理
  4. ゲーム終了判定と勝敗の表示


1. 「次に石を配置可能な場所」の判定と表示


各手番が始まったときに、ボード上の全てのCellをループして次の処理を行います。


  • 現在のCellからタテ・ヨコ・ナナメ全ての方向に向かってひとつずつ次のCellの色を確認する。
    • 相手の石が置かれていたら、一時的な配列(arr)にそのCellの座標をプッシュして、さらにその方向の次のCellを確認。
    • 自分の石が置かれていたら、その方向への探索を終了。
    • 空白なら、その方向への探索を終了。
    • 座標がボードの範囲外に出た場合は、その方向への探索を終了。
  • 上の処理で最後に自分の石が見つかった場合で、かつ配列arrに要素が含まれている場合は、「挟める対象の相手の石が見つかった」ということなので、そのCellを「自分の石を配置可能なCell」としてマークしておく。(cell.placeableプロパティにtrueをセットする。)


Cellコンポーネントでは、対応するCellオブジェクトのplaceableプロパティが属性として渡されてくるので、その値がtrueの場合は、小さな黄色の円をSVGのcircleタグを使って表示するようになっています。


2. 挟まれた石を裏返す


プレーヤーがCellをクリックしたときには、CellコンポーネントのhandleClick()メソッドが実行され、さらにstoreのsetStone()メソッドにクリックされたCellの座標が渡されます。

store側では、Cellのplaceableプロパティがtrueでない場合は、そこには石を配置出来ないので何もせずにreturnしています。

そうでない場合は、クリックされた場所およびそこを起点として裏返し対象になるCellのcolorプロパティを全てプレーヤーの石と同じ色に変更します。

「裏返し対象のCell」というのは、あらかじめ上の「次に石を配置可能な場所」の判定処理の中で見つかったCellの座標一覧をcell.turnableCellsというプロパティに格納してあるので、それをループすることで取得できます。

裏返し対象のCellのcolorプロパティを全て変更する処理が終わった後、getNextTurn()メソッドを呼んで次のプレーヤーに手番を交替しています。

最後に'board_changed'イベントを発行して、各コンポーネントの描画を更新しています。



3. 石を置ける場所が無い場合の「パス」処理


現在の手番で石を置ける場所が見つからない場合は、ボードの placeableCount プロパティが 0 になります。

その場合は、ひとまず alert()でメッセージを表示した後、storeのskipTurn()メソッドを呼んで手番を「パス」します。

この処理はゲームの進行に関わる処理なので、Gameコンポーネント内で行っています。

skipTurn()メソッドの中では、ボードの状態を変更せずに、単に現在の手番を示す turn プロパティを更新して'board_changed'イベントを発行しています。



4. ゲーム終了判定と勝敗の表示


Gameコンポーネントでstoreから'board_changed'イベントを受け取った時に、finishedプロパティがtrueであればゲーム終了なのでその旨をalert()で表示します。

finishedプロパティがtrueになる条件は、以下の通りです。

  • 空白のCellが無くなった。
  • 黒または白のどちらかが全滅した。
  • 両者とも置く場所が無くなった。


ボードの状態を更新する際にこのいずれかに当てはまるかどうかを毎回チェックしてfinishedプロパティの値をセットしています。



以上でリバーシのルールを実装して実際に遊べる状態にまで持って行くことが出来ました。

次回は、コンピュータの思考ルーチンを作成してコンピュータとの対戦機能を追加したいと思います!


今回の時点でのソースコードはこちら:

https://github.com/mikehibm/reversi-react/tree/blog-2






 

2018年10月20日

React/TypeScriptでリバーシゲームを作る (1) - ボードの描画と石の配置

実は2011年に「Androidでオセロゲームを作る」というのをやった事があります。5回のブログ記事に分けて少しずつ作り込んで行ったのですが、このシリーズが今でも結構アクセスされているみたいです。

これを読み返しながら、ふと「今だったら、ReactでWeb版を作っておけばネイティブアプリ化も結構簡単に出来るし色々と応用が効くのでは?」と思い付きました。

そこで、作り始めて見ました。

現時点で動作しているものはこちらで試せます。
https://reversi-mngzdihdyn.now.sh/

タイトル画面


ゲーム画面

ソースコードはこちら

https://github.com/mikehibm/reversi-react/tree/blog-1


まだ画面をクリックして石を置くことが出来るようになっただけでゲームのルールが何も実装されていません。

しかし、リバーシゲーム作りは楽しいですね!


以下、ここまでに書いたコードの大まかな説明です。

コンポーネント階層


今のところ下のような階層になっています。

  • App (アプリケーションの大枠)
    • Menu (メニュー画面)
    • Game (ゲーム画面)
      • Board (ボードの背景や枠線などを描画するコンポーネント)
        • Cell (一つのセルを描画するコンポーネント)



状態管理


アプリケーションの状態は store.ts ファイルで一括管理しています。

ここでエクスポートしている Store クラスが EventEmitter を継承しており、状態に変更があったときにイベントを発生(emit)することでそれを各コンポーネントに通知して、各コンポーネントが画面を更新するという流れになっています。

例えば、メニュー画面でスタートボタンが押されたときには、setPage()メソッドが呼ばれて Store クラス内で保持している page 変数の値が変更されるので、その際に 'page_changed' というイベントを発生させています。

このイベントは App コンポーネントが監視していて、イベント発生時に最新の page の値を受け取って メニュー画面を表示するのかゲーム画面を表示するのかを切り替えています。





ボードとセルの描画


Boardコンポーネントでは、SVGを使ってボードの緑色の背景や縦と横の罫線などを描画しています。

また重要なのは、Cellコンポーネントの配列を作成してそれを子コンポーネントとして描画していることです。ボード上には 8 x 8 で64個のCellコンポーネントが配置されることになります。

Cellコンポーネントでは、propsとして渡された座標値にしたがってSVGのRectangleを描画します。このRectangleがマウスのクリックイベントを受け取る入り口になります。

また、propsのひとつとしてcolorが渡されますが、これはそのセルが空白なのか、黒の石が配置されているのか、または白の石が配置されているのか、を示します。

Cellコンポーネントではその値によって黒または白の円を描画する処理を行っています。

あるセル上でマウスがクリックされた時は、次の流れで最終的にセルに石が描画されるようになっています。


  1. handleClick()メソッドが呼ばれる。
  2. Storeクラスの setStone()メソッドが呼ばれる。
  3. クリックされた場所に石が無ければ、その場所の colorプロパティを現在の手番の色に変更する。
  4. Storeクラスが'board_changed'イベントを発生する。
  5. Boardコンポーネントがイベントを受け取り、ボード全体を再描画する。
  6. クリックされた場所に対応するCellコンポーネントが石を描画する。


こんな感じで、ひとまずボードの描画と石の配置までは出来るようになりました。

次回は、リバーシゲームとしてのルールを実装して実際に遊べるようになるまでを作ってみたいと思います!








2018年8月10日

ReactでCSVファイルを読み込んでクライアント側で帳票を出力する

やりたかった事


あるWebサービスからCSV形式でデータをエクスポート出来るのですが、そのデータからちょっとした帳票を印刷したいという要望がありました。

そこでなるべく手っ取り早く、ブラウザだけで動くものが出来ないかと考えて作ってみたのが今回のプログラムです。

大まかな流れは、
  1. CSVファイルを指定すると、ブラウザ上のJavaScriptでその内容を読み込む。
  2. 読み込んだ内容からHTMLで帳票を生成して表示する。
  3. ブラウザの印刷機能を使って手動で印刷またはPDFとして保存する。
という感じになります。


CSVファイルの例







作成したい帳票


実際に動いているもの

https://mikehibm.github.io/react-csv-example/




1. プロジェクトを作成する


プロジェクトは前回のエントリーと同様に Create React App + TypeScriptで作ることにします。
npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start

FileSelect.tsx と Report.tsx というファイルを新規作成して、App.tsx からこれらを呼び出して使うようにします。

最初は FileSelect コンポーネントを表示してCSVファイルの選択を行い、ボタンがクリックされたらファイルの内容を読み取って、Report コンポーネントに渡します。




Report コンポーネントが表示されている時は FileSelect コンポーネントは非表示になるようにします。




2. JavaScriptでCSVファイルの内容を読み込む


HTML5のFile APIを使います。FileReaderでの読み込みは非同期処理なので、処理をラップしてPromiseを返す関数を作りました。







3. ファイルの内容をパースしてオブジェクトの配列に変換する


上の readFileAsText() からファイル全体の内容が文字列として返ってくるので、それをパースしてオブジェクトの配列に変換します。

具体的には、まず改行文字で区切って行単位の配列に分け、さらにその各行について「,」で区切って列単位の配列に分けます。つまり最終的には string[][] (文字列の配列の配列)になります。




ここでは簡単に改行文字(\n)と「,」で区切っているだけですが、実際には改行文字が違う、値に「,」が含まれている、などさまざまなケースがあり得るのでCSVのパース処理はちゃんとやろうとすると実は結構大変です。

なので必要であれば下の記事で紹介されている csv-parser などを使った方が良いかもしれません。

ブラウザ上でCSVファイルをパースする
https://qiita.com/ledsun/items/e38ee0dff8f26bf8d930




その後、文字列の配列(の配列)から、今度は帳票を生成する時に扱いやすいように、WorkItemというクラスのインスタンスの配列に変換します。

この時に、文字列型から日付型への変換や、CSVファイルに無い項目の値(duration)の計算なども同時に行っています。





4. HTMLで帳票を生成する


WorkItemオブジェクトの配列を受け取って、tableタグで表形式のHTMLに変換します。

本当は帳票のタイトル、見出し、明細行などをそれぞれ別のコンポーネントに分けた方が良いのだと思いますが、今回はあまり時間がなかったので1コンポーネントで作ってしまいました。





あとは画面に表示された帳票をブラウザの印刷機能で印刷するだけです。デフォルトではおそらく余計なヘッダーやフッター(ページタイトルやURL)も印刷されてしまいますが、印刷時の設定画面でこれらをオフにしておけばきれいに出力されるはずです。


今回作ったアプリの全ソースコードは下記にあります。

https://github.com/mikehibm/react-csv-example







2018年8月8日

Create React App + TypeScript で最初につまずいたこと

ちょっとしたサンプルプログラム的なものを Create React App + TypeScript で作ろうと思ったのですが、思わぬところでいきなり詰まってしまいました。


Create React App(以下CRA)で TypeScript と言えば、

https://github.com/wmonk/create-react-app-typescript

が定番だと思います。


さて、下の通りにプロジェクトを作成して実行しました。

npm install -g create-react-app
create-react-app my-app --scripts-version=react-scripts-ts
cd my-app/
npm start


もちろんここまでは全く問題なし。早速VS Codeでプロジェクトのフォルダを開いて、アプリケーションを作り始めました。



ところが、あるタイミングでふと

console.log("hogehoge");

を入れて実行してみようと思ったところ、いきなりコンパイルエラーになってしまいました。


「Calls to 'console.log' are not allowed.」というエラーですが、これにはちょっと驚かされました。console.logを入れただけでアプリケーションが動かなくなるなんて。。。


これですが、デフォルトで設定されているTSLintのルールがめちゃくちゃ厳格なものになっているのが原因なようです。


厳格すぎてこれでは使いづらいという声も多く、GitHubのIssuesでも議論になっているようですね。


オリジナルのCRA作者のDan Abramov氏からも、「この厳しいルールはCRAを作った時の思想(初心者でもすぐに動くものを作り始められる。本当にクリティカルなもの以外はワーニングのみでエラーにはしない。)と合わないから、ゆるくした方が良いんじゃない?」 という趣旨のIssueをわざわざ上げられているぐらいです。


create-react-app-typescript リポジトリ作者のWill Monk氏によると、「最初は自分が職場で使っているルールをコピーして使っていたのだけれど、後で一般的に推奨されているプリセット("tslint:recommended", "tslint-react"など)を使うように変更した」との事。「それでもまだルールが厳しすぎるなら、誰かがオリジナルのCRAと同等のゆるいルールの設定を作ってPRしてくれたら喜んでそれをマージするよ。」とも言っているので、彼自身は特にどうしても厳しいルールを他の人に強制する気は無いようです。


その後このIssue上で議論は進んでいるようですが、このブログ執筆時点の最新版ではまだルールの変更には至っていないようです。


さて、なにはともあれとりあえず console.log を使えるようにする方法ですが、上のIssueでも言及されているのですが、Microsoftによる下記のページにわかりやすくはっきりと記載されていました。

https://github.com/Microsoft/TypeScript-React-Starter


プロジェクトのルートフォルダにある tslint.json ファイルを下のように書き換えればOKです。(赤字を削除、青字を追加。)

{
-  "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"],
+  "extends": [],
+  "defaultSeverity": "warning",

"linterOptions": {
     "exclude": [
       "config/**/*.js",
       "node_modules/**/*.ts"
     ]
   }
 }

これで問題なく開発が続けられるようになりました!






 

2018年5月6日

Dockerコンテナ内でASP.NET Core 2.1 Webアプリをビルド・実行する

やりたかった事:


Macに.Net SDKを入れずにASP.NET Core 2.1のWebアプリを開発したい。


試した事:


Docker(Version 18.03.1-ce-mac64)でMS公式の.NET 2.1 SDKイメージを動かしてみました。

まずは、SDKの公式イメージからコンテナを起動します。
docker run --rm -it \
  -v `pwd`:/app/ \
  -w /app/aspnetapp \
  microsoft/dotnet:2.1-sdk
これを実行すると、Mac側のカレントディレクトリがコンテナの/app/ディレクトリにマウントされて、かつコンテナ内のカレントディレクトリが/app/aspnetapp になった状態でシェルが起動します。

ここから、
dotnet new webapi 
を実行すればWeb APIのひな形のコードが生成されます。


さらに、
dotnet run
を実行すればデフォルトの5000番ポートでWeb APIが実行されますが、このままだとポートマッピングの設定をしていないのでホスト側からはアクセス出来ません。



とりあえずこれはCtrl+Cで終了してコンテナを終了し、次にdocker runコマンドに -p オプションを付けた上で、シェルを開くのではなく「dotnet watch run」コマンドを実行してみます。

使ったコマンドはこちら。
PORT=8080
docker run --rm -it --name watcher \
  -p $PORT:$PORT \
  -v `pwd`:/app/ \
  -w /app/aspnetapp \
  microsoft/dotnet:2.1-sdk \
  dotnet watch run $PORT
任意のポート番号でアプリを動かせるように、事前にProgram.csファイルにちょっと細工をしておきました。

#########
#########

起動後、指定したポート番号でアプリが動いているのが分かります。




分かった事:


docker runでコンテナを起動する際に -v オプションでカレントディレクトリをコンテナ内のディレクトリにマウントしておけば、ソースコードはホスト(Mac本体)側で編集し、BuildやPublishはコンテナ内で行うという事が出来ました。

コンテナ内で動かすコマンドを「dotnet watch run」としておけばコードを変更したら自動でアプリを再起動してくれます。

これでMac本体に.Net SDKを入れずにASP.NET Core 2.1のWebアプリを開発するという目的が達成出来ました!




参考URL:
dotnet-docker/aspnet-docker-dev-in-container.md at master
https://github.com/dotnet/dotnet-docker/blob/master/samples/aspnetapp/aspnet-docker-dev-in-container.md
MS公式Dockerイメージを使って.NET Core開発を行う(Mac) - ryuichi111stdの技術日記
http://ryuichi111std.hatenablog.com/entry/2016/11/07/020326








.

2018年5月1日

JavaScriptで "B" + ["a", "a"].join("aaa" - 1) がBaNaNaになる理由

Ethereum開発者のVitalik氏がこんなツイートをしていたのを見ました。


試しにChromeのDeveloper Toolsを開いて実行して見ると、確かに「BaNaNa」と表示されました。
"B" + ["a", "a"].join("aaa" - 1)
"BaNaNa"

不思議に思ったので少しずつ変えながら何パターンか試してみます。

"B" + ["a", "a"].join("aaa") → "Baaaaa"
"B" + ["a", "a"].join()  → "Ba,a"
"B" + ["a", "a"].join("")  → "Baa"
"B" + ["a", "a"].join("" - 1)  → "Ba-1a"
"B" + ["a", "a"].join(1)  → "Ba1a"
"B" + ["a", "a"].join("x" - 1)  → "BaNaNa"

なるほど、分かってきました。

"aaa" - 1 の部分が「NaN」として出力されて、それがArray.join()の引数になるので
B + a + NaN + a
で 「BaNaNa」というわけですね。

なるほど!

 

2018年3月1日

TypeScriptを使うならstrictNullChecksオプションが便利

TypeScript の strictNullChecks オプションというものを知ったのでメモしておきます。

tsconfig.json の compilerOptions に

"strictNullChecks": true

を追加しておくと便利。

これをすることで、string や number などに null や undefined を代入することが出来なくなる。

let name: string;
name = null;                ← ここでコンパイルエラー

let count: number;
count = undefined;      ← ここでコンパイルエラー


null や undefined を代入したい場合は、型指定時に明示しておけばOK。

let name: string | null;
name = null;                ← これはOK

let count: number | null | undefined;
count = undefined;      ← これはOK


「ユニオン型」というらしいですね。
http://d.hatena.ne.jp/m-hiyama/20180117/1516157225

色々なシチュエーションで、「この変数の値は null/undefined になる可能性がある(もしくは無い)」という事を明示出来るのでnullチェック忘れや過剰なnullチェックを減らす事が出来ます。