すっかり最近はフロントエンドエンジニアのキャリアを歩み出しています。気が付けば、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を実行する
いつもの!
create-react-app sample --typescript
2. electronとelectron-builderを依存関係に追加する
今回はelectron-builder
を使います。最終成果物が1つのファイルに纏まってすっきりするので。
npm i -D electron electron-builder@21.0.2
ポイントはelectron-builder
のバージョン。最新ではなく21.0.2を使います。ビルドファイルで書き込む設定情報が正しく解釈されないバグがあるからです。
3. package.jsonを修正その1
electron実行時のエントリーポイントとなるファイルを指定します。
"version": "0.1.0",
"main": "./electron/electron.js",
"homepage": "./",
- main
- homepage
の二箇所を設定しましょう。
4. electron.jsを作成
エントリーポイントとなるJavaScriptファイルを作成します。
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
を用意します。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"electron-start": "electron ."
},
この状態で
npm run build
npm run electron-start
と実行すればウィンドウが立ち上がってアプリが実行されます。
6. 配布用ビルドの準備
前述の方法はあくまで開発用途です。というわけで,技術に明るくない人でも簡単に使えるようにパッケージングしたいところです。electron-builder
を使えばそんなことが出来るようになります。(昔に比べると最終成果物のファイルも小さくなりましたね……)
ビルド設定情報を記述した設定ファイルを作成します。
'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
をインストールしておきましょう。
"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
を使えるようにしておきます。
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
の取得処理です。
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プロセス側。
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: データ受信時の処理
});
こんな感じに実装すればプロセス間通信を実現出来ます。
お試しあれ!