2019年2月16日

React StaticでGoogle Analyticsを使うには

React StaticでSPAアプリケーションを作ったときに、Google Analyticsでアクセス解析をするにはどうすればよいか、というメモです。

Reactだと react-ga というライブラリがよく使われているようです。これを使うとGoogle Analyticsへのトラッキング情報の送信が楽になるようですが、ブラウザ上でのページ遷移のタイミングは自分で検知して送信するようにコードを書かなければなりません。

React Routerを使う場合のサンプルがGitHubのdemoフォルダ内にありました。

https://github.com/react-ga/react-ga/blob/master/demo/app/withTracker.jsx

これを見ると、withTrackerというHigher Orderコンポーネントを作ってlocationの変更を検知してGAに送信するようになっています。


今回はReact Staticを使って作ったサイトだったので、ルーティングの仕組みが少し違ってこのサンプルコードをそのまま使うことが出来ず、少し手を加えたものを作って一応動くようにはなったのですが、なんとなくしっくり来ませんでした。


そこであらためてGoogle Analyticsのドキュメントを読み返してみると、下のような記述を見つけました。

https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications
--
デフォルトの JavaScript トラッキング スニペットは、ユーザーが新しいページを読み込むたびに実行されるため、従来のウェブサイトでは正常に動作しますが、SPA の場合、サイトで新しいページを読み込むときに、ページ全体を読み込むのではなくコンテンツを動的に読み込むため、analytics.js スニペット コードが実行されるのは一度だけとなります。つまり、以降のページビュー(仮想ページビュー)は、新しいコンテンツが読み込まれるときに手動でトラッキングする必要があります。

--

青枠の注釈部分が特に重要です。

SPA用に、既にGoogleからurlChangeTrackerというプラグインが提供されているのです。これを使うことで、自分でページ遷移のタイミングを拾ってトラッキング情報を送信するという処理を書く必要が無くなります。

つまり、やるべきことは以下の通りになります。

1. 初回表示時にanalytics.jsを読み込んで初期化 & 初回トラッキング情報を送信。
2. urlChangeTrackerを読み込む。(以降のページ遷移は自動的に送信される。)

結局、「react-gaを使わずに最初から自分で書いた方がシンプルになるのでは?」と思ったので一から書き直しました

--

--

render()メソッド内で autotrack.custom.js というスクリプトファイルを読み込んでいますが、これは urlChangeTrackerのドキュメントにしたがって必要なプラグインだけをカスタムビルドして作成したものを public フォルダ内に配置したものです。

このGoogleAnalyticsコンポーネントをApp.js内で読み込んで下のように使うことで、React Staticで作ったサイトでGoogle Analyticsがちゃんと機能するようになりました。






 

2019年1月13日

React/TypeScriptでリバーシゲームを作る (3) - 思考ルーチンの実装その1

前回はリバーシのルールを実装して、人対人の対戦が出来るようになりました。

今回は、一人でも遊べるようにコンピュータの思考ルーチンを実装します。


動くものはこちらです。
https://reversi-992nmioqt.now.sh/



タイトル画面でレベル1〜3が選べるようになっています。
(現段階ではどれを選んでも実際には難易度は同じです。)

ゲーム画面は前回と同じですが、コンピュータの手番になると自動的に思考ルーチンが呼び出され、
結果が出るとその座標に石を配置します。




現時点でのソースコードはこちらにあります。
https://github.com/mikehibm/reversi-react/tree/blog-3


さて、コンピュータとの対戦を実現するにあたって工夫する必要があるのは、「思考ルーチンを別スレッドで動かして、UIをブロックしないようにする」ことです。


通常ブラウザ上で動くJavaScriptアプリケーションでは一つのスレッドしか使えないので、時間がかかってCPUパワーを消費する処理を行うとその間UIが固まったりカクカクした感じになったりして操作性が悪くなります。

これを防ぐために、「Web Workers API」を使います。


ReactでWeb Workersを使う方法は、下の記事に書いたとおりです。

Create React AppでWeb Workerを使うには

この記事で調べた、「WorkerのJSファイルをBlobとして読み込んでからWorkerスレッドを生成する」という方法を使うことにしました。


ひとまず基本的な仕組みを実装することに主眼を置いたので、思考ルーチンの内容は単に「配置可能な座標のなかからランダムに選ぶ」だけの動作になっています。この処理だと実際には一瞬で終わってしまうのでWorkerスレッドを使う意味は全くありません。

Workerスレッドを使う意味があるような、もっとヘビーにCPUを使う思考ルーチンへの改良については、次回以降の記事で書くことにします。


playersフォルダ内のcomputerPlayer1.tsを見ると、thinkProc() という関数があり、そこに思考ルーチンの実態が入っているのが分かります。
---



この thinkProc関数を createWorker というユーティリティ関数に渡すことで thinkWorkerという名前のWorkerスレッドを作成しています。
const thinkWorker = createWorker(thinkProc);


対戦中にコンピュータの番が来たときには、

const result = await thinkWorker.execute({ board: { cells: board.cells } });

のように execute()メソッドを呼んで思考ルーチンを起動しています。このexecute()メソッドというのは、JavaScript標準のWorkerクラスを継承して作成した独自クラスで定義したメソッドで、 Workerクラスの postMessage()を呼んだ後のイベントハンドリングを抽象化したものになっています。


思考ルーチンの処理が終わると、戻り値のresult変数にはコンピュータが選択した座標が入っているので、後は人間がクリックした時と同じように store.setStone(result) を呼んでその座標を渡します。そうすると store内部で盤面の状態が適宜更新されてイベントが発行されるので、それを受けて画面の表示が自動的に更新されることになります。



ここまででコンピュータとの対戦機能を実現するための基本的な仕組みを実装することが出来ました。次回はそれなりに強い思考ルーチンを作ることにチャレンジしたいと思います!




 

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

---