ElectronとReactでAPI叩いて何か表示するアプリを試してみたメモ

先日、会社の業務外開発イベントに参加してきた。 これは、自分が新卒の頃からやっているイベントの後継で、 テーマ自由でもくもく作業するハッカソン、所謂もくもくそん。

今回は、ずっと昔から気になっていたElectronとReact.jsを使ってみることにした。 大半はJavaScript力を向上したいという思惑がある。

作るもの・使うもの

Redmine上の自分のタスクを、取得してきて表示するだけのアプリ。

ViewのフレームワークReact.jsを利用して、 自社の Redmine Rest APIのRestAPIを叩いて表示。 それらをElectronでアプリ化する。

Vue.jsとどっち試そうか迷ったが、 なんかいろいろ応用が効きそうなReact.jsにしてみることに。

Step0: nvm, node Install

まずnvmをいれる

$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh | bash

そして、とりあえずnodeの安定版いれる

$ nvm install --lts
$ nvm use --lts

Step1: Scaffold

React.jsのテンプレートプロジェクトをcreate-react-appでつくる

こちらの手順を参考させていただいた。 https://qiita.com/chibicode/items/8533dd72f1ebaeb4b614

create-react-appインストール

$ npm install -g create-react-app

プロジェクト生成

$ create-react-app redmine-desktop

できたプロジェクトはすでにgitリポジトリになってる。楽。

Step2: Electron化

こちらの手順を参考にさせていただいた。というよりほぼそのままの手順。

https://medium.com/@impaachu/how-to-build-a-react-based-electron-app-d0f27413f17f

以下パッケージを追加

$ npm install cross-env electron-is-dev

以下パッケージをdev環境に追加

$ npm install -D concurrently electron electron-builder wait-on

以下electron.jsをPublic以下に配置

const electron = require("electron");
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const path = require("path");
const isDev = require("electron-is-dev");
let mainWindow;

function createWindow() {
  mainWindow = new BrowserWindow({ width: 320, height: 640 });
  mainWindow.loadURL(
    isDev
      ? "http://localhost:3000"
      : `file://${path.join(__dirname, "../build/index.html")}`
  );
  mainWindow.on("closed", () => (mainWindow = null));
}

app.on("ready", createWindow);
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (mainWindow === null) {
    createWindow();
  }
});

以下をpackage.jsonに追加

...
"description": "",
"author": "Mikito Yoshiya",
"build": {
    "appId": "net.mikinya.redmine-desktop"
},
"main": "public/electron.js",
"homepage": "./",
...

package.jsonのscripts項目を以下に変更

...
"scripts": {
    "react-start": "react-scripts start",
    "react-build": "react-scripts build",
    "react-test": "react-scripts test --env=jsdom",
    "react-eject": "react-scripts eject",
    "electron-build": "electron-builder",
    "release": "yarn react-build && electron-builder --publish=always",
    "build": "yarn react-build && yarn electron-build",
    "start": "concurrently \"cross-env BROWSER=none yarn react-start\" \"wait-on http://localhost:3000 && electron .\""
},
...

この時点で

$ npm run start

でアプリが開発モードで起動するように

$ npm run build

でapp化してdist以下にappと配布用のdmgを出力できるようになった。

Step3: Presentation, React Component構築

ビューのレイヤーを修正

公式のチュートリアル等を参考にごにょごにょいじった。

CSSも適当にbootswatchのcss.minを入れた。

cssはただ配置してApp.jsからimportしただけだけど、これでいいのだろうか。

できたReactのコンポーネントは以下みたいな感じ。

import React from 'react';

class TaskCard extends React.Component {
  render() {
    return (
      <div className="card text-white bg-primary mb-3" >
        <div className="card-header">{this.props.title}</div>
      </div >
    );
  }
}

export default TaskCard;
import React from 'react';
import ReactDOM from 'react-dom';
import './bootstrap.min.css'
import TaskCard from './TaskCard'
import IssueUseCase from './useCase/IssueUseCase'

class App extends React.Component {
  constructor(props) {
    super(props);
    this.useCase = new IssueUseCase();
  }

  render() {
    this.useCase.getOwnIssues(issues => {
      var list = [];
      for (var i in issues) {
        list.push(<TaskCard key={issues[i].id} title={issues[i].subject} />);
      }
      ReactDOM.render(list, document.getElementById('issues'));
    });

    return (
      <div className="container">
        <div className="row">
          <div className="col-lg-12">
            <h2>Tasks</h2>
          </div>
        </div>
        <div className="row">
          <div className="col-lg-4">
            <div className="bs-component">
              <div id="issues"></div>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

この時点ではRestAPIからcurlで叩いたjsonをファイルで保存したデータをモックデータとして、Viewに表示してみる。記法やお作法の正しいやりかたは全然不明。

うん。これでとりあえずデモはできる。

f:id:miki05:20191113010021p:plain

Step4: DataStore, API実装

実際にAPIを叩いてサーバーのデータを取ってくるようにする。 通信と非同期の処理にaxiosをつかってみる。 XMLHttpRequstのラッパーでPromiseでイベントの制御ができるらしい。

$ npm install axios

通信部分はとりあえずトークンとかクエリとかベタがき。 あとでUI上から設定できるようにしよう...

import axiosbase from 'axios'

class RedmineApi {
  constructor() {
    this.axios = axiosbase.create({
      baseURL: 'https://url_to_redmine',
      headers: {
        'Content-Type': 'application/json',
        'X-Redmine-API-Key': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
      },
      responseType: 'json',
    })
  }

  getIssues() {
    return this.axios
      .get('/issues.json?limit=100&tracker_id=4&status_id=1&assigned_to_id=270')
  }
}

export default RedmineApi;

ここで二つの問題がでた。 一つめはCORS(Cross-Origin Resource Sharing)で、異なるドメインへの通信を制限するJavaScriptフロントのよくあるやつ。 現状の開発状態でもlocalhostで立ち上げてるぽいし、うん確かにそうなるわな。 でもAppにバンドルした場合はどうなるんだ? これは、以下のElectron側の設定で解決できた。

electron.jsのnew BrowserWindow();のパラメータに以下を追加

...
  mainWindow = new BrowserWindow({
    width: 320, height: 640,
    webPreferences: {
      webSecurity: false
    }
  });
...

参考:https://qiita.com/yasuflatland-lf/items/4f57b6dd311d4918e8f5

もう一つは会社で運用しているRedmineが自己証明書で、ERR_CERT_AUTHORITY_INVALIDエラーがでて、 XMLHttpRequst系ではそれを無視できる設定にできなかったこと。 curlではkオプションを追加するだけだが。

こちらも同様に、以下のElectron側の設定で解決。 electron.jsに以下を追加

...
app.on('certificate-error', function (event, webContents, url, error, certificate, callback) {
  event.preventDefault();
  callback(true);
});
...

参考:https://qiita.com/yuya-oc/items/2764bf7a33c751498858

完成

とりあえず動くものができた。 APIトークン埋め込み、ユーザーID埋め込みなので、完全個人しか使えない。 全然時間足りなかった。

所感

  • Electreon便利, React良さそう
  • JSの作法もっと知りたい
  • webpackとかなんちゃらとか、いろいろ覚えることある
  • TypeScript覚えたい
  • Rxを導入してみたい
  • JSでテストどう書くんだ
  • Electreon上のプロジェクトでClearn Archtectureを適用してみたい
  • Appまでビルドして配布形式までとりあえず1日で走りきれたので満足
  • たまには違ったコンテキストの作業は刺激になる
  • 割と簡単に改良できそうなので、この後少しいじってみることにする

Unity Excel Importerをv1.1にバージョンアップ

ちょっと時間があったので、 去年作ったUnity上でエクセルをインポートするプラグインをアップデートした。

github.com

Release Note

  • SerializeFieldアトリビュートをサポート(非パブリックなフィールドでも利用可能に)
  • 空のセルを基本Default Valueとして扱う
  • Excel Assetのクラス名と実際のエクセルファイル名のひも付きをオプションで変更可能に
  • インポート時にログを出力するオプションを利用可能に
  • NameSpace付きのExcelAssetにも対応
  • インポートエラー時にシート名も表示

このプラグインは、 以下のUnityプログラミングバイブルのマスターデータ構築部分を執筆する際に作成したプラグインで、 結構丁寧に考えながら設計している(つもり)。

Unityゲーム プログラミング・バイブル

Unityゲーム プログラミング・バイブル

導入の仕方は以下 qiita.com

が、しかし、Unity Excel Importerでしらべると、 大御所テラシュールさんのExcel Importer Makerも候補に表示されググラビリティが低い。 Excel Importer Makerの紹介記事は結構でたりして少し寂しい。。。

そもそも、このプラグインを作った背景には、 いくつか自分の思う思想でエクセルのインポートを扱いたいという思いと、 書籍で紹介する上では、それらの機能に責任を持ちたいと思ったからだ。

もうちょい、いろんな人に使ってほしいので紹介する。

Unity Excel Importerの思想とかメリット

以下の三つが主な特徴だとおもう。

1. シンプル&高いポータビリティ

まず、一番重要視したのは、シンプルさ。メインのソースはExcelImporter.csのみ。 一部Editor上から導入をサポートするコードもあるが、そのあたりはなくても動く。 Editor拡張を利用しない場合は、数行のコードを自前で書けばよい。

ほぼEditor拡張を使わないようにしたことで、癖がなくどんな案件でも導入しやすいはず。 Unity Editorのメニューをほとんど汚すこともないし、冗長なコードが増えることもない。

また、Excel内のテーブルのスキーマは利用者がソースコードで定義する(Enityソース)。 これにより、以下二つのメリットがうまれる。

  1. jsonの様なシリアライズ形式にそのまま移行できる
  2. エンジニアはソースをいじるだけでいい(GUIをぽちぽちしなくてよい)

1.に関しては、結構重要。 プロジェクトの開発初期では、とりあえずエクセルでマスターデータを構築しておいて、 開発が進んだあと、サーバーから取得する形に変更するなど、 データのシリアライズ形式をスイッチするとめっちゃスムーズ。 つまり、プラグインのフローや形式になるべく依存せず、 プロジェクト内の都合を極力優先できるということ。

2.に関して、意外とエンジニアはGUIの手続きを操作するのが嫌いだったりする。 手順を間違えたり、既存のソースのコピペができなかったり、独自の自動生成フローが使えないから。

2. 必要スクリプトは自分でマネージできる

テラシュールさんのExcel Importer Makerはその名の通り、 ImporterをMakeするプラグインで、 エクセルごとにツールを使って専用のImporterを、専用の配置場所に自動生成する形になる。

反面、このプラグインは完全に「Excel Importer」でインポートロジックコードの追加生成はしないし、 スキーマを定義するソースの配置等は利用者側に委ねている。 (その代わりリフレクションをバリバリつかっているけど、RunTimeではインポート済みだから問題ない)

そのため、ちょっとしたデータ構造の変更とか、 フィールド名の変更とかも気軽にできるし、削除する際も気兼ねなくできる。

3. 実プロジェクトで培われたユースケースに準拠

実はこのプラグインのもとになったのは 2011年くらいにUnityを会社で使い始めたときに、 レベルデザインを簡単にするために作られたエクセルインポートスクリプトだった。

このスクリプトがめっちゃ便利で、 いろんなプロジェクトで秘伝のタレのように使われたり改修されてきた経緯がある。 つまり、実際のプロジェクトで運用に耐えうる実績を持つフローと言えると思う。

特にコメントアウトとかEnumの利用は結構重要だとおもう。

そして、プラグイン化した今も、 新しいプロジェクトのプロトタイプとかでも利用しているし、 データ層の仮実装として相当な開発速度向上に繋がっている(上記のとおり後でスイッチも可能)。

また、都度新しいユースケースも見つかったりして、 しばらくはちゃんと対応していく予定(今回v1.1)。

まとめ

つまり、実際の利用と運用を考えて頑張って設計したプラグインなので、 是非ともつかってみてほしいということを言いたい。

あと仕様はだいぶ出揃っているから、本当はテスト書きたい。

ちなみに宣伝になっちゃうけど、 このExcel Importerを使ってライトゲーム向けのマスターデータを構築する例を 上記のUnityプログラミングバイブルで紹介しているのでよければ参考にしてみてほしい。

シンプルなNodeアプリをDocker化してElastic Beanstalkに移行してみた

DBも使わない超シンプルなnodeアプリをHerokuで管理していたが、 いつぞやからか無料枠では月終わりにスリープするように体系がかわってしまったので移行を考えていた。

とりあえず勉強もかねて、AWSでDocker化してみることに。

今回のゴール

  • HerokuのHobbyプラン7$/月よりも低コストでホスティングする
    • その代わり最低限のスペック・冗長化でよい
  • 基本的なDockerのサービス使ってみることで使いまわせる枠組みを得る
    • ミニマムなデモアプリでの利用
    • 開発環境構築とワークフローの汎用化
  • Herokuレベルな触りごごちで簡単にデプロイ・管理できるようにしておく
  • HTTPS化する

AWS上のサービスや構成の検討

とりあえず、どんな構成で行くかをちょっと使ってみたりして比較

今回はコストが最優先。 なので、インスタンスはt3.nano1台くらいでいきたい。 ロードバランサーはいらない。お遊びでやるには高い(立てとくだけで月2000円くらいか)。

これじゃDockerのメリットなくないって? 現状はあまり無いが、負荷が増えた時にスケーリングできる構成に柔軟に変えられる点がいいかも。

次はどんなサービスを使うか。以下検討

  • Fargate
  • ECS
    • 細かく設定をできそうだが、その反面自前でいろいろやる必要が出てきそう。今回は生で扱うのはオーバースペック感。特に構成管理やデプロイフロー周りをしっかり検討する必要がありそう。
  • EKS
    • 未調査。Kubernetesは今後のメインストリームに?今回はまずシンプルな構成の勉強も兼ねるので一旦考慮外
  • Elastic Beanstalk
    • マルチコンテナで利用する場合ECS上で動作するが、その管理あたりを丸っと任せられる。デプロイも容易。

ということで、今回つかうもの&ざっくり構成。

  • 複雑なことしない+管理を楽にしたいのでElastic Beanstalkを使う。
  • せっかく&独自イメージをプライベートなところに置きたいのでECR(Elastic Container Registry)もつかう。
  • シングルインスタンスロードバランサーは使わない。
  • LBなしでSSL化に伴う証明書の管理がAWS上でできないので、インスタンス内にNginxのコンテナを立てそこに証明書を配置する。
  • ドメインはお名前.comで取得しているが、こちらもせっかくなのでRoute53のDNSサービスを利用することに。

Local Docker Install

そもそもMacを新調してローカルにDockerが入っていなかったので、以下を参考にインストール。 登録とかめんどかったのでbrew経由でいれた。

qiita.com

$ brew install docker
$ brew cask install docker

Node App コンテナ化

まずは、アプリ自体をコンテナ化。 いろいろ検討した結果、すこし古い記事だが、基本が抑えられていたので以下を参考にDockerfileや開発フローを構築。 postd.cc

ベースとなるImageは設定の楽さとサイズの兼ね合いでnodeのslimを選択

これでも220MBくらいになるんで本当はもっと最適化した方が良さそうだが。

以下のようなDockerfileを構築

また、Imageのbuild時にアプリケーションのソースを丸っとコピーするが、 モジュールのインストールはbuild時に行うのでnode_moduleはコピーの対象外にしておく。 (ローカルでnpm installしてた場合とか、問題は無いだろうが冗長なので)

.dockerignoreを作成

node_modules
npm-debug.log

この状態でも、 docker buildしてdocker runすればコンテナが起動してアプケーションに接続できる。

SSL化の準備・ローカルでのチェック環境

とりあえずSSL化のことについてちょっと考える。 まず証明書は無料で発行したい。ただALBは使わないので、とても楽なCertificate Managerとの連携がいまいち。

そこで以下を参考にhttps-portalを利用してみることにした。 qiita.com

基本はnginxのプロキシWebサーバーだが、Let's encryptから自動で証明書を取得・更新をしてくれる。 デモレベルのアプリであれば十分そう(自前で管理できないのはやりにくい面もあるが)

まずは、ローカルでhttps接続を確かめられる環境としてdocker-composeファイルをつくる。 ちなみに本番デプロイ前の確認用ということでdocker-compose.stg.ymlという名前にしている。

Let's encryptは証明の発行上限が週ごとにあるので、開発段階やチェック等で本番接続していると動かなくなる。 そこでenvironmentのSTAGEをlocalにしておく。

こうしておくと、オレオレ証明書を自前で発行して代わりに使ってくれる。

またローカルでhttps接続のチェック様にhostsを修正しておく

sudo vi /etc/hosts
...
127.0.0.1       mikinya.net.localhost
...

これでとりあえず、以下コマンドでImageのビルドとコンテナの起動ができる。

$ docker-compose -f docker-compose.stg.yml build
$ docker-compose -f docker-compose.stg.yml up

でブラウザからこのローカル用のドメインにアクセスした時に、https-portalとcontainerの連携のチェックができる。

開発環境・開発フロー

現状のままだとソースコードを編集するたびにdocker-compose buildしないといけないのでめんどい。 開発用のフローを別途用意する。

そもそも、node起動後ファイル更新検知・リロードが自動でできていなかったので、nodemonを導入した。 package.jsonのdependenciesにnodemonを追加

...
  "dependencies": {
    "nodemon": "^1.18.9",
...

そして、普段づかいのローカル開発環境用docker-composeを別途つくる。

こっちのポイントは以下

  • ローカル開発環境ではnginxのプロキシを利用しないでシンプルに単体で立てる
  • 起動コマンドを上書き、nodemonが起動する様にする
  • gitリポジトリローカルにあるソースをvolumesとしてマウントし、ローカルのソースを直接起動・コンテナ内の起動アプリをすぐに更新する
  • mode_modulesのvolumeを別途作成し、コンテナ内の領域からインストールしたmode_modulesを参照する

これで、package.jsonを更新後、コンテナ内からpackage-lock.jsonの更新が可能で、ローカルのリポジトリにコミットできる。しかもnode_modulesは開発環境では動的に更新が可能で、imageにビルドするときはpackage.json、package-lock.jsonできちんとインストールされる。

詳細はこちらを参考に

DockerでのNodeアプリ構築で学んだこと | POSTD

なお、Macだとファイルシステムの都合上、動作がめちゃくちゃ遅くなるという問題があるみたいだが、アプリケーションが小さいせいか、実用上全く気にならなかったのでそのままにしている。

もし問題がおきたら、docker-sync等を使うのが良いのかも。

qiita.com

まぁ、ひとまず、開発開始の流れがめっちゃ簡単になった。

  1. Macを買う、gitとdockerを入れる
  2. リポジトリをcloneする
  3. docker-compose up
  4. コード編集 5 git add & commit

以上、これで、locahostに開発環境が立ってソースの編集もできる。 npmもnode入れる必要もないし、npm installもいらない。楽

AWS準備

awsコマンドもインストールしていなかったのでインストール

brew install awscli

aws configureで適切な権限を持つユーザのAccess Key とかSecretとか入れる

また、Elastic Beanstalkのマルチコンテナを使う場合は何かしらのコンテナのレジストリサービスが必要。

シングルコンテナの場合はソースコードをアップロード後にbuildするみたいだが、 今回はNginxのコンテナもフロントに立てたいのでマルチコンテナでECRをつかってみる。

ECR

あらかじめECRのコンソールから適当な名前をつけてリポジトリを作成。

また、ゴミがたまるのも嫌だったのでLifecycle Policyも設定。

f:id:miki05:20190118171720p:plain これで3つ以上のタグが無いImageは消えるかな?(たぶん)

でローカルのビルドとpushが面倒だったのでスクリプトを作成(scripts/push-image.shに配置)。

Elastic Beanstalk準備

Elastic Beanstalkのコンソールから新しいアプリを作成。

プラットフォームをMulti Container Dockerにしておく。 とりあえずサンプルのアプリをデプロイする設定で、ロードバランサーなし、他はだいたいデフォルト。

構築が終わり提示されたURLにアクセスするとNginxが立っている様が見れる。 EC2に指定したインスタンスが立ち、EIPが振られ、ECSに項目として現れる。

Dockerrunファイル作成

Dockerrunファイルをつくる。基本docker-compose.ymlを単純に置き換える感じ。

ただ、独自にbuildしたアプリのimageの取得先がECRのURLになり、https-portalの証明書の取得を本番環境設定してやる必要がある。

Elastic Beanstalkロール権限の設定

Elastic Beanstalkの管理下でインスタンスからECRにアクセスしImageを取得できるようにしないといけない。

Elastic Beanstalkを利用し始めると、IAMにaws-elasticbeanstalk-ec2-roleが追加されているので、 こいつにAmazonEC2ContainerRegistryReadOnlyポリシーをアタッチする。

Route53設定

https-portalを利用していることもあって、おそらくDNS設定をしてからじゃ無いと証明書取得がこける気がする(未確認)。まぁ大したアプリじゃ無いので、一旦停止を良しとしDNSを最初に切り替えておく。

Route53にて新たに取得したDomainのCreate Hosted Zoneを作成。さらにCreate Record SetからRecordを追加

  • Type: A
  • Alias: Yes
  • Alias Target: Elastic Beanstalkで作成したアプリケーションURL

あとはお名前.comのDNS設定にNSレコードの情報を追加

セキュリティーグループ設定

Elastic Beanstalkがインスタンスに設定したセキュリティグループにのインバウンドにHTTPS/443を追加

Elastic Beanstalkデプロイ

さて、あとはデプロイ。基本的にコマンド一つでデプロイしたい。基本はebコマンドでできる。

ebコマンドをインストール&初期設定

brew install awsebcli
eb init

ただデフォルトのeb deployは、gitのアーカイブをアップロードする。ImageはすでにECRにpush済みなので、リポジトリのソースファイルはデプロイに必要ない。

大した容量にはならないが、必要なのはDockerrun.aws.jsonだけなので冗長なものはアプロードしたくない(s3にたまる)。

調べてみるとebコマンドはデプロイするものを任意指定でき、独自にデプロイのフローを制御できるらしい。ということで、自前でDockerrun.aws.jsonをzip圧縮してそれだけあげることに。

まずは、.elasticbeanstalk/config.ymlに以下を追加する

deploy:
  artifact: .elasticbeanstalk/artifact.zip

これでeb deployコマンドで.elasticbeanstalk/artifact.zipがアップロードされるようになる。

さらに独自のdeployスクリプトを用意する(scripts/deploy.shに配置)。

#!/bin/bash

set -eu

zip .elasticbeanstalk/artifact.zip Dockerrun.aws.json 
eb deploy

zip圧縮後deployコマンドを叩くかんじ。

ただ現状のデプロイフローはまだ問題がある

  • ERCへのpushスクリプトとdeployスクリプトを二回叩く必要がある
  • .elasticbeanstalk/config.ymlがignoreされているのでartifactの設定を他の端末でやり直す必要がある
  • gitのリビジョンとデプロイが紐づかない(Dockerrun.aws.jsonはコミット前でもデプロイできちゃう)

もろもろ考えると、さっき作ったimageをビルドしてECRにあげるスクリプトもデプロイスクリプトに組みこむといいのかも。 また、独自zipをつくるより.ebignoreファイルを作って、Dockerrun.aws.json以外は無視するほうがよいのかも。

開発とデプロイのまとめ

  • ローカル開発
    • docker-compose up
    • ブラウザからlocalhost:3000にアクセス
    • ファイルはそのまま編集して、コミットすればよい
  • Webフロントサーバー込みのローカルチェック
  • 本番デプロイ
    • scripts/push-image.sh
    • scripts/deploy.sh

目標コストに関して

さて、そんなこんなで、AWS上にアプリを移行しましたが、コストを計算。 ちなみに東京リージョンです。

  • EC2 : 0.0068USD/Hour * 24 * 30 = 4.896USD
  • EBS : 0.12USD/GB/Month (gp2) * (12 + 8)GB= 2.4USD
  • Route 53 : 0.5USD/HZ = 0.5USD
  • ECR : 0.10USD/GB/Month * 0.1GB = 0.01USD
  • EIP : 0USD
  • ECS : 0USD

合計:7.806USD

おっと...細かい費用がかさんで目標のHeroku Hobbyプラン7$より高くなってしまった...w

まぁあとは、 リージョンをオレゴンにするともう少し安くて、6.726USDでいけそう。 あとはリザーブインスタンスを使うってとこかな。

もう少し研究してみます。

Zenject InterfaceのBindにおけるIdの付け方

BindするInterfaceにIdをつける場合WithIdの書き方が限定される。 BindInterfacesAndSelfTo等を利用してまとめての指定はできなそう。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        // できない
        Container.BindInterfacesTo<Sample>()
            .WithId(1)
            .AsCached();

        // できない
        Container.BindInterfacesAndSelfTo<Sample>()
            .WithId(1)
            .AsCached();

        // できる
        Container.Bind<ISample>()
            .WithId(1)
            .To<Sample>()
            .AsCached();
    }
}

Zenjectとかが絡むコンポーネントのUnit Test

Zenjectが絡んだときのUnit Testの例を紹介します。 あくまで例です。

例えば以下のようなHomeUseCaseというクラスのテストを行う場合を考えます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Zenject;
using System;

public interface IHomeUseCase : IUseCase
{
    IObservable<IHomeViewModel> LoadHomeModel();
}

public class HomeUseCase : IHomeUseCase
{
    [Inject] IUserRepository userRepository;
        
    public IObservable<IHomeViewModel> LoadHomeModel()
    {
        return userRepository.Get()
            .Select(entity => new HomeViewModel(entity));
    }
}

HomeUserCaseはLoadHomeModel()をコールすると、 いろいろやってIHomeViewModelというインターフェースを取得するためのObservableを返却します。

また、HomeUseCaseはIUserRepositoryに依存しており、Injectアトリビュートで依存注入されています。

ちなみにUserRepositoryは以下のようなIOを担当するようなコンポーネントで、 通常はサーバー通信を行うコンポーネントとのインターフェースになります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using Zenject;
using System;

public interface IUserRepository : IRepository
{
    IObservable<UserEntity> Get();
}

public class UserRepository : IUserRepository
{
    [Inject] IUserDataStore dataStore;

    public IObservable<UserEntity> Get()
    {
        return dataStore.Get(); 
    }
}

この時点でHomeUseCaseをテストするためには以下の要点をどうにかする必要があります。

  1. TestコードでInjectアトリビュートやContainerをどう扱うか
  2. UserRepositoryをそのまま使うとサーバーへ通信してしまうし、そもそもHomeUseCaseのテストにフォーカスしたい
  3. UniRx使っていて非同期ぽいのをどうテストするか

解決方針

こんな状態のコードをテストをするために以下の2つの方針をたてます。

ZenjectUnitTestFixtureを使う

「1.」についてはZenjectのOptionalExtras以下にあるZenjectUnitTestFixtureを継承したクラスでテストを行うことで解決できます。 ZenjectUnitTestFixture継承したクラスでは、デフォルトでContainerを扱うことができ、 そこにテストに必要なコンポーネントをバインドしていくことでInjectアトリビュート等を扱ったテストが可能になります。

Mockフレームワークを使う

「2. 3.」に関してはMockを利用します。 Mockは本物のオブジェクトの代わりにテスト用のオブジェクトに差し替える仕組みです。 これにより、依存するオブジェクトをすべて用意する必要がなくなり、 テストしたいものだけにフォーカスできます。 また、依存するオブジェクトの状態も自由に用意できます。

ZenejctにはOptionalExtras以下にMockフレームワークのMoqが付属しているのでzipファイルを解答することで利用可能になります。

OptionalExtras以下にあるAutoMockingをダブルクリック後に展開されるファイルの中に Moq-Net35、Moq-Net46があるので、Unity Editorの状態に合わせて再度ダブルクリックします。 するとMoqディレクトリが展開されます。

f:id:miki05:20180821001353p:plain

実際のテストコード

このあたりを含め以下のようなテストコードを実装してみました。

using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
using Zenject;
using UniRx;
using Moq;

public class HomeUseCaseTest : ZenjectUnitTestFixture // ①
{
    [Inject] HomeUseCase useCase; // ⑥
    [Inject] Mock<IUserRepository> mockRepository; // ⑥

    [SetUp]
    public void CommonInstall()
    {
        Container.Bind<HomeUseCase>().AsSingle(); // ②

        var mockRepository 
            = new Mock<IUserRepository>(); // ③
        var mockDataObservable 
            = Observable.Return<UserEntity>(new UserEntity());
        mockRepository.Setup(x => x.Get()).Returns(mockDataObservable);

        Container.BindInstance(mockRepository.Object); // ④
        Container.BindInstance(mockRepository).AsSingle();  // ⑤

        Container.Inject(this); // ⑥
    }

    [Test]
    public void CallRepositoryAndReturnsHomeViewModel()
    {
        IHomeViewModel viewModel = null; 
        useCase.LoadHomeModel()
            .Subscribe(vm => viewModel = vm); // ⑦
        Assert.IsNotNull(viewModel);
        mockRepository.Verify(x => x.Get(), Times.Once()); // ⑧
    }
}

説明箇所ありすぎなので番号を振っています

①ZenjectUnitTestFixtureの継承

テストコードはZenjectUnitTestFixtureクラスを継承します。 これでContainer等の扱いが可能になり(Baseクラスで定義)、Zenjectのテストがしやすくなります。

②CommonInstall内でのContainerへのBind

テストに必要なコンポーネントはContainerへBindしていきます。 まずはテスト対象のHomeUseCaseをバインドしています。 CommonInstallはSetupアトリビュートによりテストの前に実行されます。

③依存コンポーネントのMock生成

通常のZenjectのContainerと同様、依存するコンポーネントはContainerの中にBindしておく必要があります。 テスト対象のHomeUseCaseはIUserRepositoryに依存しているためにどうにか用意しBindします。

しかしUserRepository自体をBindすると以下の問題がでます。

  1. UserRepositoryが依存しているクラスもBindする必要がある
  2. HomeUseCaseのテストなのにUserRepositoryの状態に深く影響される

ということで、ここでMockを利用しています。 Mockを利用するためには対象のクラスにインターフェース定義が必要です。

ここではIUserRepositoryが定義されており、 HomeUseCaseでもすべてインターフェースで取り回ししているため、Mockでのテストが可能になります。

Moqの詳しい利用法は省略しますが、新しいMockオブジェクトを生成し、 Get()メソッドをスタブし、「即時UserEntityを返却するObservable」を返却するようにしています。

④MockUserRepositoryのBind

MockオブジェクトのObjectプロパティをBindすることで、Mockオブジェクトのインターフェースをbindできます

⑤Mockオブジェクト自体のBind

今回は振る舞いに関してもテストしたかったので(あとで解説)モックオブジェクト自体もBindしています

⑥テストクラス自体のBind、テスト対象のInject

最後にテストクラス自体もBindしてます。 これはちょっとしたテストコードで楽をするための工夫で、 こうすることにより、 冒頭に宣言したInjectアトリビュートのプロパティにContainer内のテスト対象のクラスがInejctされ、そのまま利用できるようになります。

⑦実際のテスト

LoadHomeModelメソッドを実際にコールし取得したObservableを稼働させることでテストを実行します。 ③で本来は非同期のObsevableを即時値を返すように設定しているので、 Subscribeをコールした直後、OnNextのストリームが流れSubscribe内のラムダに処理が移ります。 ここでIHomeViewModelがちゃんと返却されるかどうかをテストしています。

⑧UseCaseの振る舞いのテスト

一応、UseCase内でちゃんとUserRepositoryを利用しているかをチェックしています。 やろうと思えばUserRepositoryを経由せず、内部でクラスをインスタンス化しても⑦のテストは通すことはできるので。

⑧ではmockRepositoryのGetメソッドが1回呼ばれているかどうかをチェックしています。

まとめ

Zenejctとかを使う場合で依存が発生する場合のテストの例を挙げました

  • ZenjectUnitTestFixtureを使うとContainerを利用したUnit Testができる
  • MockフレームワークとInterfaceを利用することで対象のテストにフォーカスできる

正直UniRxの非同期の部分をこのようにテストすべきかどうかはちょっと不明です。 そのほかPlayModeTestを利用する方法もありますが、オーバーヘッドが大きいような?

それとMoqを利用しましたが、 MockライブラリとしてはNSubstitute(http://nsubstitute.github.io/)のほうが扱いやすい感じ。 ただMoqの方はZenjectにバンドルされており、単純なMockであればもっと簡単にかけたりします。

Zenject BindのAsSingle, AsCached, AsTransientの違い

Zenjectを使い始めて、最初の方に?ってなるのがAsSingle(), AsCached(), AsTransient()な気がします。 正直ZenjectのREADMEを何度読んでもイマイチよくわからない。 いろいろ挙動を調べたので解説します。

Scopeとは

AsSingle(), AsCached(), AsTransient()はZenjectでScopeと呼ばれるものです。 READMEによると「生成されたインスタンスが再利用される頻度」みたいなことが書いてあります。 日本語もしくは英語でOKな説明ですが、Scopeは対象のインスタンスがDIコンテナにて要求されたときに、 新しく生成するのか、同じものを使いまわしてInjectするのかを指定するメソッドになります。

ひとまず、Scopeの指定を何もせずに何かをBindしてみると...

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>();
    }
}

エラーになります。

ZenjectException: Assert hit! Scope must be set for the previous binding!  Please either specify AsTransient, AsCached, or AsSingle. Last binding: Contract: Sample, Identifier: NULL 

READMEではDefault: AsTransientみたいなことが書いてありますが、 何かしら指定しないといけないようです。

使い方は以下みたいなかんじです。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
    }
}

シンプルな場合のAsSingle, AsCached, AsTransientの違い

まず、前置きとして、 同じコンテナに同じタイプのインスタンスは、そのままでは複数個バインドできません。 これはScopeをつけても同じです。

つまり、以下はいずれもエラーになります。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
        Container.Bind<Sample>().AsSingle(); // エラー
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsTransient();
        Container.Bind<Sample>().AsTransient();  // エラー
    }
}

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Bind<Sample>().AsCached();  // エラー
    }
}

それを踏まえて三者の違いを比較します。

AsSingle

AsSingleはコンテナ内でインスタンスが1つであることを保証するように動作します。 複数のオブジェクトからInjectの要求があったときに、必ず同じインスタンスが注入されます。

f:id:miki05:20180814222232p:plain

AsCached

AsCachedはもし利用できるインスタンスがコンテナ内に存在すれば、 そのインスタンスをInject時に利用します。 ぶっちゃけ、このシンプルな例ではAsSingleとほぼ等価です。

f:id:miki05:20180814222223p:plain

AsTransient

AsTransientはインスタンスの再利用をまったく行いません。 Injectの要求があるたびに、コンストラクションのフローが走り、 それぞれのクラスに、別々のインスタンスが注入されます。

f:id:miki05:20180814222211p:plain

AsSingleとAsCachedのちがい

正直、AsSingleとAsTransientはわかりやすいです。 AsSingleはSingletonのように1つしか存在しない、AsTransientは毎回別物が生成されるといった覚え方で大丈夫そう。

ではAsCachedとAsSingleのちがいは...?

実はいくつかのケースで違いがあります。 その一つがIdを指定するケースです。 Zenjectでは以下のようにWithIdメソッドでBindするインスタンスにIdをつけ、Inject時にそのIdを指定できます。

using UnityEngine;
using Zenject;

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Bind<Sample>().WithId(1).AsCached();
        Container.Bind<Sample>().WithId(2).AsCached();
    }
}

class ClassA 
{
    [Inject(Id = 1)] Sample sample;
}

つまり、Idをつけ識別可能な状態であれば、 AsCachedは複数のBindの指定が可能で、別々のインスタンスを生成します。

f:id:miki05:20180814222155p:plain

逆にAsSingleはIdをつけようが何をしようがエラーになります。

f:id:miki05:20180814222119p:plain

ちなみにAsTransientでもIdをつけられますが、 インスタンスの再利用はされないため、あんまり意味がないです。 (WithArgumentsを使う場合は意味あるかも)

Sub Container(Context間で親子関係)がある場合

Scopeはコンテナに対しての指定です。 コンテナが異なれば、例え親のコンテナに同じオブジェクトがすでにバインドされていても再度バインドできます。

この場合、インスタンスが要求された際、 同じコンテナ内のインスタンスが優先して解決されます。

f:id:miki05:20180814222109p:plain

また、Idを指定することで親のコンテナの中のインスタンスを指定することもできます。

f:id:miki05:20180814222100p:plain

つまりAsSingleを指定した場合であったとしても、 コンテナごとに複数の同じクラスのインスタンスが生成されるということになります。 ここがSingletonとは違うところですね。

ちなみに親と子でIdが重複したときの挙動ですが、 Bindは可能で自身のコンテナ内のインスタンスを優先している感じです。

InterfaceをBindする場合

InterfaceをBindする場合、実は以下のような記法もエラーになりません。

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Bind<ISample>().To<Sample>().AsCached();
    }
}

その場合、SampleをInjectした方と、ISampleをInjectした方で、違うインスタンスが解決されます。 (おそらくBindInterfacesAndSelfToを使ったほうが予期した動作になりそう)

ちなみにAsSingleを利用したときの以下はエラーになります。(前のバージョンではできた?)

public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
        Container.Bind<ISample>().To<Sample>().AsSingle();
    }
}

Unbindした場合

Zenject的にはUnbindはあんまりよくない使い方らしいですが、以下のような挙動になります。

  • 同じクラスのAsSingleによるUnbind、再Bindはエラーになる。
public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsSingle();
        Container.Unbind<Sample>();
        Container.Bind<Sample>().AsSingle();
    }
}
  • 同じクラスのAsCachedによるUnbind、再Bindはできる。
public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Unbind<Sample>();
        Container.Bind<Sample>().AsCached();
    }
}
  • AsCachedをAsSingleで再Bindするとエラーになる
public class SceneInstaller : MonoInstaller<SceneInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind<Sample>().AsCached();
        Container.Unbind<Sample>();
        Container.Bind<Sample>().AsSingle();
    }
}

まとめ

ZenejctのScopeについて解説しました。

  • AsSingleはコンテナ内でなんだかんだ1つのインスタンスになる
  • AsTransientは要求されるたびに毎回インスタンシングフローが走る
  • AsCachedはできる限りインスタンスを使いましてWithIdとかを利用すると識別とかができる
  • AsSingleでもContainerの親子関係を作って複数インスタンスを使いわけることもできる

ただ、Idを使いまくったり、Unbindを使いまくったたり、Interfaceを変な風に使いまくると 多分わけわかんなくなるのでやめたほうが良さげなきがします。

CryptoKittiesでにゃんこ飼ってみた

仮想通貨とかブロックチェーンとか、 大分前から盛り上がってますよね。

どんなことができるのか、 技術的な特徴みたいなところに興味あってちょっとずつ勉強してる次第です。

その中でもブロックチェーンをゲームっぽいものに応用した CryptoKittiesというサービスを前々から耳にしており、 気になっていたのでやってみました。

ただ、仮想通貨も一切保持したりしてなかったので全部最初からです。

というかモチベーションは「猫飼いたい」の一言につきます。 なので、ビットコインとか仮想通貨で儲けたいとか一切ないです。 最短で猫の所持に向かいます。

CryptoKitties

CryptoKittiesは分散アプリケーションのプラットフォームであるEthereum上にて、仮想の猫を売買したり、育てたりするブロックチェーン技術を応用したゲームです。

CryptoKitties | Collect and breed digital cats!

各猫には遺伝子のようなものが設定されており、 それによって見た目が一匹一匹異なります。

猫の絵柄自体は画面のキャプチャとかとればコピーできますが、 その猫の所有権はブロックチェーンの分散台帳技術により明確に決まっているのです。

つまり、猫を単純に愛でる以外にも、 超レアなこの猫は俺の猫!っていう所有欲的な何かを満たして楽しむこともできます。

そして猫の売買もできるので、 過去には1300万円みないな超絶金額で取引された事例もあるそうです。

おおまかな流れ

  1. ウォレット(MetaMask)の登録
  2. CryptoKittiesに登録
  3. 仮想通貨取引の口座開設
  4. 仮想通貨、Ethereum (ETH)の購入
  5. ウォレットに送金
  6. 猫の選定・購入

1. MetaMask

まずウォレットとしてMetaMaskを導入します。 ウォレットは仮想通貨を保管するためのアプリケーションです。

いろいろなタイプやアプリケーションがあるようですが、 CryptoKittiesはMetaMaskというアプリが必要です。

MetaMask

f:id:miki05:20180807015240p:plain

なんかキツネ?でかわいい。

導入方法は検索するとたくさん出てきますが、 以下を参考にさせていただきました。

イーサリアムの便利ウォレットMetamask(メタマスク)の使い方について | ARUTOKO(あるとこ)

2. CryptoKittiesに登録

とりあえず、公式サイトから登録をします。

https://www.cryptokitties.co/

このとき、ページ下部に出るCookieの許可と、 Chrome ExtensionのMetaMaskにログインしておかないと、うまく進めません。

f:id:miki05:20180807003206p:plain

最近、日本語に対応した模様。

準備ができたら「始めるにゃ!」をクリックします。

登録フローは一般的なWebサイトと同様な感じです。

3. GMOコイン口座開設

CryptoKittiesはEthereumプラットフォーム上のアプリケーションであるため、通貨として仮想通貨Ethereumが必要です。

自分はこの時点で、 仮想通貨を持っていなかったので取引所の口座を開設し取得を行っています。

仮想通貨取引所は多数ありますが、自分はWebで適当に検索し、適当にGMOコインを選択しました。

仮想通貨(ビットコイン)のFX・売買なら | GMOコイン

以下、大まかなフローです。

  1. アカウント登録
  2. 免許証・通知カードアップロード
  3. 審査待ち
  4. 郵送された口座開設コード入力

アカウント作成後、審査に4日ほど、 そして、郵送送られてくる開設コードを入力する必要があります。

f:id:miki05:20180726231211p:plain

4. 仮想通貨、Ethereum (ETH)の購入

口座の開設が完了したら、Ethereumの取得を行います。

まずは取引所の口座に、日本円を入金します。 いくつかのネット銀行であればWebのフローから即時入金が可能です。

つぎに実際にETHを購入します。

ひとまず、0.03ETHほど買ってみます。

f:id:miki05:20180807005704p:plain

購入時のレートでは大体1600円くらいでした。

5. ウォレットに送金

取引所の口座にて、 ETHの取得ができた後はMetaMaskに送金します。

まずMetaMaskのAdressをコピーします。

f:id:miki05:20180807010601p:plain

そのアドレスを取引所の口座の送金先に指定し送付を実行。

f:id:miki05:20180807010239p:plain

しばらくするとMetaMaskのウォレットに反映されます。

さらに取引履歴からトランザクションがちゃんと処理されていることも確認でき、ブロックチェーンしてる感が味わえます。

6. 猫購入

さて、これで事前準備完了です。

再度CryptoKittiesのサイトに行き、猫を品定めします。 カタログメニューではなく検索メニューからセール用にチェックを入れて探すと、 安めのにゃんこが手に入りそうです。

f:id:miki05:20180807011712p:plain

購入するにゃんこを選んだら購入ボタンから購入を進めます。 その際、MetaMaskの確認画面が開き、取引ETH額と手数料が表示されます。この手数料がマイニング等で得る通貨になるのですね。中央サーバーの代わりに処理をしくれている端末に思いを馳せて、Submitを押します。

f:id:miki05:20180807012632p:plain

そして初めて、購入したにゃんこがこちら。

f:id:miki05:20180807012746p:plain

名前をカフェオレとつけました。 な、なんだ、かわいいじゃねーか、、、

まとめ

今回は、単純に猫が飼いたかったため、CryptoKittiesでにゃんこを購入してみました。

購入しただけで、他のフィーチャーは試せていませんが、 にゃんこ同士を交配させて、見た目を引き継ぐ子孫をつくったりもできます。

他にもCryptoKittiesのにゃんこを使ったサードパーティ制のアプリや、それを支援するプラットフォームKittyVerseなるものが発表されているらしいです。

にゃんこへの投機的な側面は薄れつつあるのかもしれませんが、どんな展開になるのか楽しみです。

また、仮想通貨を初めて触ったこともあり、いろいろ体験できてよかったです。理論や内部実装の勉強もしていますが、実際にサービスに登録し、リアルタイムに通貨送金のトランザクションを発行することで、システムが動作することの感動を感じられました。

参考

ブロックチェーンで猫を飼う!?CryptoKittiesを紹介