📋

Create React App(typescript)をベースにelectron環境を構築する

2022/02/19に公開

Create React App(typescript)をベースにelectronの環境を構築する手順メモ。Windows環境でのみ検証。

Create React App

Create React Appを実行する。

npx create-react-app electronapp --template typescript

npm install

Create React Appで作ったプロジェクトにelectronに必要なパッケージをインストールしていく。

cd electronapp
npm i -D concurrently cross-env electron electron-builder wait-on

electron/electron.ts

electronディレクトリを作ってメインファイルを作成する。electronフォルダからbuildフォルダにビルドして利用する前提。

mkdir electron

electronのmainファイル。やっていることは、ほぼelectronのチュートリアルのまま。
app.isPackagedを使って、dev環境ではlocalhostを読み込みつつdevコンソールを開くように設定。

electron/electron.ts
import { app, BrowserWindow } from "electron";
import * as path from "path";
import * as url from "url";

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });

  const appURL = app.isPackaged
    ? url.format({
        pathname: path.join(__dirname, "../index.html"),
        protocol: "file:",
        slashes: true,
      })
    : "http://localhost:3000";

  win.loadURL(appURL);

  if (!app.isPackaged) {
    win.webContents.openDevTools();
  }
};

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

app.whenReady().then(() => {
  createWindow();
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

electron/preload.ts

preload.tsを作成し、メインプロセスとレンダラーをつなぐためにcontextBridgeを作っておく。詳細は後ほど説明。

electron/preload.ts
import { contextBridge } from "electron";

contextBridge.exposeInMainWorld("myAPI", {
  counter: (count: number) => {
    return count + 1;
  },
});

electron/tsconfig.json

electronのメインプロセス用のtsconfig.json。reactに使うプロジェクトルートにあるものと別に作成する。rootDir'../'にすることで build/electron/*として出力される。
electronのパッケージを作るときは、buildフォルダはCreate React Appと共通で使うので要注意。

electron/tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "../build",
    "rootDir": "../"
  },
  "include": ["*"]
}

package.jsonを編集

package.jsonmainhomepageを追加。mainはビルド後のjsファイルを指定。

package.json
+"main": "build/electron/electron.js",
+"homepage": "./",

ついでにbrowserslistをproduction・development両方とも"last 1 electron version"のみに変更

package.json
  "browserslist": {
    "production": [
+     "last 1 electron version",
-     ">0.2%",
-     "not dead",
-     "not op_mini all"
    ],
    "development": [
+     "last 1 electron version",
-     "last 1 chrome version",
-     "last 1 firefox version",
-     "last 1 safari version"
    ]
  },

public/index.htmlを編集

public/index.htmlのheadにCSPのmetaタグを追加。

public/index.html
  <head>
+   <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />

package.jsonにscriptsを追加

package.jsonにscriptsにdev用のscriptsを追加。

"scripts": {
+   "electron:dev": "concurrently \"cross-env BROWSER=none npm start\" \"wait-on http://localhost:3000 && tsc -p electron -w\" \"wait-on http://localhost:3000 && tsc -p electron && electron .\""

dev環境の立ち上げテスト

npm run electron:dev

ここでdev環境を立ち上げると、CRAのスタートページがelectron上で表示される。

レンダラーとメインプロセスをつなぐ

次は、メインプロセスとはreact(レンダラー)をつなぐ。
preload.ts側でcontextBridge.exposeInMainWorldに登録すると、react側でwindow.myAPI.counterのように使えるようになる。

テストとして、メインプロセス側でカウンターを作ってをれをreactから呼び出してみる。

preload側でカウンターを作成する。

electron/preload.ts
import { contextBridge } from "electron";

contextBridge.exposeInMainWorld("myAPI", {
  counter: (count: number) => {
    return count + 1;
  },
});

App.tsx側はwindow.myAPI.counter(count)で呼び出す。

src/App.tsx
import React, { useState } from "react";
import logo from "./logo.svg";
import "./App.css";

function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>{count}</p>
        <button
          onClick={() => {
            console.log();
            setCount(window.myAPI.counter(count));
          }}
        >
          count
        </button>
      </header>
    </div>
  );
}

export default App;

このままだと、windowにmyAPIが無いと怒られるので、src/@types/global.d.tsを作り、型を拡張しておく

src/@types/global.d.td
export declare global {
  interface Window {
    myAPI: {
      counter: (count: number) => number;
    };
  }
}

dev環境でメインプロセスとレンダラーの通信を確認

レンダラー側でcountボタンを押すと、メインプロセスで計算して、またレンダラー側で受け取って描画しているのが確認できる。

ビルドしてアプリを作る(windows)

最後にビルドしてelectronのアプリを作る。windowsしか確認できていません……

package.jsonにbuildを追加する。設定内容は、「Electronで1からデスクトップアプリを作り、electron-builderを使ってビルド・リリースするまで」が詳しいのでこちらを参考ください。

package.json
+ "build": {
+   "extends": null,
+   "files": [
+     "build/**/*"
+   ],
+   "directories": {
+     "buildResources": "assets"
+   }
+ }

scriptsにインストールせずに使えるポータブルアプリ用の"electron:build:portable"とインストールアプリの"electron:build:nis"をpackage.jsonのscriptsに追加する。

package.json
 "scripts": {
+   "electron:build:portable": "npm run build && tsc -p electron && electron-builder --win --x64 --dir",
+   "electron:build:nis": "npm run build && tsc -p electron && electron-builder --win --x64"

アプリはdistフォルダにビルドされるので.gitignoreにdistを追加。

.gitignore
+dist

これでelectronアプリができているのが確認出来ると思います。

参考リンク

https://create-react-app.dev/

https://www.electronjs.org/ja/docs/latest/tutorial/quick-start

https://www.electronjs.org/ja/docs/latest/tutorial/process-model

https://developer.mozilla.org/ja/docs/Web/HTTP/CSP

https://qiita.com/yhirose/items/22b0621f0d36d983d8b0

https://mmazzarolo.com/blog/2021-08-12-building-an-electron-application-using-create-react-app/

https://qiita.com/yhirose/items/22b0621f0d36d983d8b0

Discussion