electron化したReactアプリのプロセス間通信

　すっかり最近はフロントエンドエンジニアのキャリアを歩み出しています。気が付けば、**JavaScriptでWebアプリ、スマホアプリ、デスクトップアプリ何でも作れる**ようになって凄い時代になったなぁと思っています。昔はJavaが`write it once run anyware`と言ってましたが、最近はわりとJavaScriptが近いポジションに居るのではないでしょうか。最低限ブラウザさえあれば始められる敷居の低さも好感触です。ぜひ、プログラミングに興味を持った方にはJavaScriptを触って欲しいですね。

　さて、閑話休題。普段遣いするツールをちょっとelectron + React with TypeScriptで作ってみた時にハマった話を整理してみます。

# やりたいこと
- create react appで作ったReactアプリをelectron化する
- MainプロセスとBrowserプロセス間で通信する
- パッケージングを行う

# 前提
- create react appを利用する
- TypeScriptで開発する
- `eject`はしない
- シンプルを目指す

# 段取り

## 1. create react appを実行する
　いつもの！

```bash
create-react-app sample --typescript
```

## 2. electronとelectron-builderを依存関係に追加する
　今回は`electron-builder`を使います。最終成果物が1つのファイルに纏まってすっきりするので。

```bash
npm i -D electron electron-builder@21.0.2
```

　ポイントは`electron-builder`のバージョン。最新ではなく21.0.2を使います。ビルドファイルで書き込む設定情報が正しく解釈されないバグがあるからです。

- [Upgrade electron-builder to latest version](https://github.com/floriankraft/vocabulary-test/issues/3)

## 3. package.jsonを修正その1
 electron実行時のエントリーポイントとなるファイルを指定します。

```json
  "version": "0.1.0",
  "main": "./electron/electron.js",
  "homepage": "./",
```

- main
- homepage

の二箇所を設定しましょう。

## 4. electron.jsを作成
　エントリーポイントとなるJavaScriptファイルを作成します。

```js:electron.js
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

const path = require('path');
const url = require('url');
const fs = require('fs');

let mainWindow;

function createWindow() {
    mainWindow = new BrowserWindow({width: 1000, height: 800, webPreferences: {
        nodeIntegration: true
    }});
    mainWindow.setMenu(null);
    const startUrl = process.env.ELECTRON_START_URL || url.format({
        pathname: path.join(__dirname, '/../build/index.html'),
        protocol: 'file:',
        slashes: true
    });    
    mainWindow.loadURL(startUrl);
    mainWindow.on('closed', function () {
        mainWindow = null
    })
    mainWindow.webContents.openDevTools();
}

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

app.on('activate', function () {
    if (mainWindow === null) {
        createWindow()
    }
});
```

　これで最低限electronでReactを実行出来るようになりました。

## 5. package.jsonを修正その2
　electron実行用に`script`を用意します。

```json
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron-start": "electron ."
  },
```

　この状態で

```bash
npm run build
npm run electron-start
```

　と実行すればウィンドウが立ち上がってアプリが実行されます。

## 6. 配布用ビルドの準備
　前述の方法はあくまで開発用途です。というわけで，技術に明るくない人でも簡単に使えるようにパッケージングしたいところです。`electron-builder`を使えばそんなことが出来るようになります。(昔に比べると最終成果物のファイルも小さくなりましたね……)

　ビルド設定情報を記述した設定ファイルを作成します。

```js:build-win.js
'use strict';

const builder = require('electron-builder');
const fs = require('fs');
const packagejson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));

builder.build({
  platform: 'win',
  config: {
    'appId': `com.example.${packagejson.name}`,
    'win': {
      'target': 'portable',
      "icon": "icon.ico",
    },
  },
});
```

## 7. package.jsonを修正その3
　パッケージング用のスクリプトをpackage.jsonに追加します。ファイルコピーを容易にするために`cpx`をインストールしておきましょう。

```json:package.json
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron-start": "electron .",
    "electron-build": "npm run build && cpx electron/electron.js build/ && cpx electron/build/icon.ico build && node ./electron/build/build-win.js"
  },
```

　あとは`npm run electron-build`を実行すればOKです。大体50MByteのexeファイルが出力されるはず。

## 8. electronとReact間でのプロセス通信
　electron本体と，Reactが動くのは，それぞれ

- electron: Mainプロセス
- React: Renderプロセス

　と異なるため，そのままでは直接データ連携することは不可能です。
よって，以下のような要件があった場合はどうしても一手間かかってしまいます。

- Reactで保持しているデータをファイル保存する
- アプリのメニューを押下したタイミングでReactのイベントを発火する

　それを実現するのがプロセス間通信です。今回はReactからMainプロセスにデータを送信する例で実験してみます。`electron`クラスの取り出し方に少しコツが要ります。まず，Windowsクラスを拡張して`require`を使えるようにしておきます。

```ts:index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './views/App';

ReactDOM.render(<App />, document.getElementById('root'));

/**
 * Electron用拡張
 */
declare global {
    interface Window {
      require: any;
    }
}
```

　続いてMainプロセスとの通信で使う`ipcRenderer`の取得処理です。

```ts:App.ts

const electron = window.require('electron');

class App extends React.Component<{}, AppState> {

  /**
   * Electron用
   */
  private ipcRenderer = electron.ipcRenderer;

  /**
   * 画面描画メソッド
   */
  public render() {
    return <div><button onClick={this.onClickHandler}>PUSH</button></div>
  }

  private onClickHandler = () => {
    // Mainプロセスに通知する
    ipcRenderer.send('notifyText', "hogehoge");
  }
}
```

　今度はそれを受け取るMainプロセス側。

```js:electron.js
const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const ipcMain = electron.ipcMain;

const path = require('path');
const url = require('url');
const fs = require('fs');

let mainWindow;

function createWindow() {
    mainWindow = new BrowserWindow({width: 1000, height: 800, webPreferences: {
        nodeIntegration: true
    }});
    mainWindow.setMenu(null);
    const startUrl = process.env.ELECTRON_START_URL || url.format({
        pathname: path.join(__dirname, '/../build/index.html'),
        protocol: 'file:',
        slashes: true
    });    
    mainWindow.loadURL(startUrl);
    mainWindow.on('closed', function () {
        mainWindow = null
    })
    mainWindow.webContents.openDevTools();
}

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

app.on('activate', function () {
    if (mainWindow === null) {
        createWindow()
    }
});

/**
 * Renderプロセスからの通知を受信
 */
ipcMain.on('notifyText', (event, args) => {
  //TODO: データ受信時の処理
});
```

　こんな感じに実装すればプロセス間通信を実現出来ます。

　お試しあれ！