Electron + Express.js + WebSocket + msgpack アプリ構築事例とサンプルソース

先日の Gotanda.js で、

> Electron 界隈は変化が速いので、古い情報だと役に立たないよねー

という旨の指摘がありました。

今年、2016年に、Electron + Express.js + WebSocket + msgpack を使って、
macOS アプリ、Windows アプリ、また、Node.js (Express.js) サーバを使った
ウェブブラウザアプリとして共用できるサービスを開発したときの事例を紹介してみます。

## （開発・運用方針まとめ）

１．開発時は、ローカルの Node.js で実行し、ウェブブラウザからアクセスして利用する

２．サーバが使える運用環境では、サーバ上の Node.js で実行し、ウェブブラウザからアクセスして利用する

３．サーバが使えない運用環境では、Electron アプリとして配布し、Mac/Windows 上で実行してもらう

## （サンプルアプリのソースコード）

実際に運用したアプリそのままではないですが、
Electron + Express.js + WebSocket + msgpack のポイントだけを
https://github.com/kawanet/electron-express に公開しました。

このサンプルでは、Electron アプリあるいは、ウェブブラウザから
http://127.0.0.1:3000/home/
にアクセスして使う、チャットアプリとしています。

![cap.png](https://qiita-image-store.s3.amazonaws.com/0/12419/16116299-d32f-4ce1-7e6f-9fd54b1f7ce9.png)

Electron アプリ単体で、このクリスマス前に一人二役してチャットしても寂しくなりますが、
WebSocket を使ってリアルタイムにデータを更新する事例として、とりあえず。

実運用では、処理に30分くらい時間がかかる大きめのバッチ処理に使っていました。
大人の事情でクラウドが使えないこともありますよね。

## （ポイント１） dependencies と devDependencies の分離

dependencies と devDependencies の package.json を分離しました。
今回は、以下のようなファイル構成としています。

- `package.json` - アプリ本体の dependencies を記述
- `build/package.json` - ビルド用の devDependencies を記述
- `lib/app.js` - バックエンド本体
- `main.js` - Electron 利用時の起動スクリプト
- `server.js` - Node.js 利用時の起動スクリプト
- `public/home/index.html` - フロントエンドの HTML

Node.js と Electron で共用する Express サーバ本体や、
本番利用する dependencies モジュール群を親ディレクトリ（ルート）に置くのに対し、
開発時や Electron のビルド時に使う devDependencies モジュール群を
子ディレクトリ build 配下に置いて、分離しています。

親子ディレクトリを分離しない場合、ビルド時にしか使わず、本番で利用しないモジュール群も
配布パッケージに含まれてしまい、アプリ容量が肥大化してしまうため、分離が必要です。
何度もビルドしたい用途では、prune するのも面倒なので、シンプルに分かりやすく分離した。

具体的には、今回のサンプルアプリでは以下の (dev)dependencies となっています。

```json
  "dependencies": {
    "body-parser": "^1.15.2",
    "express": "^4.14.0",
    "morgan": "^1.7.0",
    "msgpack-lite": "^0.1.26",
    "process.argv": "^0.1.0",
    "ws": "^1.1.1"
  }
```

```json
  "devDependencies": {
    "electron": "^1.4.10",
    "electron-packager": "^8.3.0",
    "jshint": "^2.9.4"
  }
```

dependencies 系と devDependencies 系を親子ディレクトリのどちらに配置するかの判断は、
趣味の問題かと思いますが、今回の用途では、ローカル上の Electron アプリだけではなくて、
Node.js アプリとしてもサーバ上などで利用したいので、dependencies 系を親にしています。

親ディレクトリ側の package.json の main 要素には、
Electron 用の main.js を指定します。
Node.js 用の server.js を指定すると、Electron アプリが起動しなくなるので注意。

## （ポイント２） バックエンド〜フロントエンド間は WebSocket で通信

Electron のバックエンドとフロントエンド間の通信には、WebSocket を使いました。

今回の用途では、ローカル上の Electron アプリとしての運用だけでなくて、
Node.js アプリとサーバ上で起動させて普通のウェブブラウザからも利用します。
Electron 自体にも、バックエンド・フロントエンド間の通信の仕組みがありますが、
WebSocket を使うことで汎用化しています。

あるいは、WebSocket ではなくて、通常の HTTP 通信でもいいのですが、
サーバ側で長いジョブを実行している途中の進捗ログを Electron フロントエンド側にも
ほぼリアルタイムに表示したかったため、WebSocket を利用しました。

なお、WebSocket では、文字列や JSON でなくて、msgpack を利用しています。
これにより、JSON では表現できないオブジェクトでも受け渡しできます。
JSON でなくて、msgpack を使っているのは、ほとんど趣味です。

## （ポイント３）　メモリを大量消費する処理は fork してオフロード

バックエンド側では、WebWorker みたいなものは使えません。
Node.js は、どんなに大量のメモリを積んだマシンでも 2GB のデータは載りません。
`--js-flags="--expose-gc"` しても、GC をコントロール仕切れないところがあります。
メモリを大量消費する処理は、別プロセスに分離した方が安全です。
今回のサンプルコードには含まれませんが、以下のような感じで fork できます。

```js
function run() {
  return new Promise(function(resolve, reject) {
    var args = ["bin/huge.cli.js", "--verbose"];
    var options = {};
    options.env = {ELECTRON_RUN_AS_NODE: "1"};
    options.cwd = process.cwd();
    var proc = child_process.execFile(process.execPath, args, options);
    proc.stdout.on("data", onLog);
    proc.stderr.on("data", onLog);
    proc.on("close", function(code) {
      return (code|0) ? reject(code) : resolve(code);
    }
  });
}
```

## （お試し手順）

以下の手順でお試しいただけます。

#### １．Node.js をインストールする。

（略）

#### ２．electron-express レポジトリを拾ってくる。

親子構成の都合で、２回の `npm install` が必要です。
また、`npm run` すると、あとで使うコマンドを確認できます。

```sh
git clone --depth=1 https://github.com/kawanet/electron-express.git
cd electron-express
npm install && (cd build && npm install)
npm run
```

#### ４．とりあえずローカルで Node.js で実行してみる

```sh
./server.js --port=3000 &
```

#### ５．ウェブブラウザからアクセスしてみる

```sh
open http://127.0.0.1:3000/home/
```

#### ６．とりあえずローカルで Electron で実行してみる

こちらも3000番ポートでWebサーバが起動するので、引き続きブラウザからもアクセス可能です。
手順４の Node.js が起動しているとポート番号が被るので、先に終了させる必要があります。
本番の用途によっては、ポート番号は固定でなくてランダムにするとか、
セキュリティのためにアクセス元 IP アドレスを制限するとかも、必要でしょう。

```sh
./build/node_modules/.bin/electron ./main.js &
```

#### ７．macOS アプリをビルドして、.dmg を作成する

配布するアプリに含めたくないディレクトリは、`--ignore=` で列挙していきます。

```sh
./build/node_modules/.bin/electron -v
./build/node_modules/.bin/electron-packager . --overwrite --platform=darwin --arch=x64 \
 --out=build --ignore=tmp/ --ignore=build/ --ignore=test/ --ignore=.idea/ --version=1.4.0
hdiutil create -srcfolder build/electron-express-darwin-x64/ build/electron-express-darwin-x64.dmg
```

#### ８．Windows アプリをビルドするための準備をする

macOS 上で Electron の Windows アプリをビルドするには、Wine が必要とのこと。
Wine は、homebrew でインストールできます。
20個以上の依存ライブラリがあるので、インストール完了まで、しばらく時間がかかります。
「brew install wine --without-x11」とすると xquartz なしで動くかもしれないが、
その場合はソースコードから wine をコンパイルするので、もっと時間がかかる。下記の方が速い。

```sh
brew cask install xquartz
brew install wine
```

#### ９．Windows アプリをビルドして、.zip を作成する

```sh
./build/node_modules/.bin/electron-packager . --overwrite --platform=win32 --arch=x64 \
 --out=build --ignore=tmp/ --ignore=build/ --ignore=test/ --ignore=.idea/ --version=1.4.0
cd build && zip -yDr electron-express-win32-x64.zip electron-express-win32-x64
```