本記事ではいかにチョッパヤでアプリ化するかというところに焦点を当てるので、開発時の便利さ(live reloadなど)は省きます。この辺をいい感じに設定する記事も後日執筆予定です。
Electronに関する前提知識
electronはアプリケーションを「メインプロセス」「preload」「レンダラープロセス」の3つのモジュールで構成されるという概念でできています。 メインプロセスはアプリケーションの基礎部分で、例えばウィンドウを作成したり、ファイルシステムにアクセスするなどの低いレベルをサポートします。
対して、レンダラープロセスはhtmlとそれをコントロールするjavascriptといった普段のweb環境です。 セキュリティの観点からレンダラープロセスができることは非常に限られており、例えば先に例を挙げたウィンドウの作成やファイルシステムへのアクセスはレンダラーから直接行うことができません。
なので、例えば画面上のボタンを押したときに新しいウィンドウを開きたいといった場合は、レンダラープロセスからipcというプロセス間通信の仕組みを使うことで、レンダラープロセスからメインプロセスを呼び出すことができます(and vice versa)。 ですが、ipcを含めそういった仕組みを無条件にアクセスできるのであれば、セキュリティの効果が薄くなってしまいますし、極端に制限されてても自由度が下がってしまいます。
そこで、preloadという、レンダラーが起動する直前に走る特権モードで、必要なものだけをレンダラープロセスに橋渡しする(コンテキストブリッジ)といった仕草が存在します。
electron自体は、エントリーポイントのjsを食わせてあげるだけでとりあえずアプリにすることができて、内容はエントリーポイントのjsからhtmlを指定してウィンドウを作成することで表示するといった感じになります。
実際にReactプロジェクトをelectronで立ち上げる
まずいつものcreate-react-appで普段どおりReactプロジェクトを作成します。 そして、ついでにelectronもインストールしておきます
npm i electron electron-builder
次にメインプロセスとpreload用のファイルを作成します。後々のためにこれらはelectronフォルダを作ってその配下に入れることにします。
/electron/main.js
const { BrowserWindow, app} = require("electron");
const path = require("path");
let mainWindow;
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.resolve(__dirname, "preload.js"),
},
});
mainWindow.loadFile("build/index.html");
};
app.whenReady().then(createWindow);
app.once('window-all-closed', () => app.quit());
/electron/preload.jsはファイルとして作っておくだけで、中身は空で構いません。 main.jsのこれもマジで最低限の内容だけなので、特に説明無くともぱっと見で理解できると思います。
でもって、package.jsonも編集します(いい感じに下記を追加してください)
package.json
{
"main": "electron/main.js",
"homepage": "./",
"build": {
"appId": "com.example.myapp",
"productName": "myapp",
"extends": null,
"files": [
"electron/**/*",
"build/**/*"
]
},
}
mainはelectron向けの設定で、エントリーポイントの指定です。
homepageはreact向けの設定で、パスの基準をどこに置くかの指定です。
buildの中身はelectronでパッケージ化するときに必要な設定で、アプリの名前とかアイコンとかもここで設定できます。
できたら、reactプロジェクトのビルド→Electronの起動でもうアプリとして立ち上がるはずです。
npm run build
npm exec electron .
パッケージ化は
npm run build
npm exec electron-builder --mac --x64 --dir # macの場合
でできます。

やった~~~ これでreact-projectをアプリ化できました。
プロセス間通信をする
この記事の最初の方で、システム側によった処理をするためにはプロセス間通信が欠かせないと書きました。
例えば、アプリ内のhtmlのaタグなんかは、アプリ内でそのページを開いてしまうのですが、それこそ「ブラウザで開く」ためにはメインプロセスでしか読み込めない「shell」といったモジュールの機能が必要です。
この章ではメインプロセスでshellを使って「ブラウザで開く」関数を作成し、preloadでipcをレンダラープロセスに渡し、レンダラープロセスでpreloadから渡されたipcを叩いてメインプロセスに作成した関数を叩くことで、「ブラウザで開く」ボタンを実装します。 プログラムは以下の通りです。
/electron/main.js
const { BrowserWindow, app, ipcMain, shell } = require("electron"); // ipcMainとshellを追加
const path = require("path");
let mainWindow;
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.resolve(__dirname, "preload.js"),
},
});
mainWindow.loadFile("build/index.html");
};
app.whenReady().then(createWindow);
app.once('window-all-closed', () => app.quit());
// ここが追加された
ipcMain.on("openExternal", (event, data) => {
shell.openExternal(data);
});
/electron/preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'preload', {
'ipcRenderer': ipcRenderer
}
);
src/App.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
const ipcRenderer = (window as any).preload.ipcRenderer;
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
onClick={() => ipcRenderer.send("openExternal", "https://reactjs.org")}
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
これで"Lern React"のリンクをクリックすると、ブラウザで開くはずです。
でも変更するたびにbuildするのめんどくさくない?
そうなんですよね~。ただ、現状の仕組みでelectronでライブリロードするのはちょっとむずかしいです。
というのも、electronは/buildフォルダの内容を使ってコンテンツを表示していますが、create-react-appの開発モード(create-react-app start)は 素早く変更をブラウザに反映するために、ビルド結果を/buildに書き出さずに内部的にしか持ってくれません。
書き出すオプションがほしいというissue(Provide a watching build mode that writes to the disk in development #1070)が出てはいるものの、2016年のissueで流石に望み薄です。
なので、便利なcreate-react-appから離れて、地でwebpackを使わなきゃなーといった感じです。
といわけで、webpackを使ってゴリゴリにカスタマイズする記事は こちら… 現在執筆中です…