🔖

Electron + React の開発環境構築 (esbuild 編)

2022/08/12に公開約7,700字

サマリー

React でつくる Electron アプリのボイラープレートです。
メインプロセス、レンダラープロセスともにホットリロード可能な開発環境の構築を目指します。

完成したテンプレートはこちら:

https://github.com/sprout2000/electron-react-esbuild#readme

前提

Node.jsGit Bash(もしくは何らかの UNIX シェル)はインストール済みであることを想定しています。

https://nodejs.org/ja/

https://git-scm.com/

インストール

⚙️ Node.js のプロジェクトとして初期化

npm コマンドは Node.js に同梱されています。

bash
$ mkdir zenn
$ cd zenn
$ npm init --yes

📥 React のインストール

bash
npm install react react-dom

📥 Electron のインストール

  • 本体
bash
npm install --save-dev electron
  • ホットリロードに必要なパッケージ
npm install --save-dev electron-reload

📥 esbuild(バンドラー)のインストール

https://esbuild.github.io/
  • 本体

    • npm i -Dnpm install --save-dev の省略形です。
bash
npm i -D esbuild
  • バンドルされた JavaScirpt ファイルを HTML の <script> ~ </script> タグへ差し込むプラグイン
bash
npm i -D @craftamap/esbuild-plugin-html

https://www.npmjs.com/package/@craftamap/esbuild-plugin-html

⚙️ esbuild の設定

プロジェクト直下に設定ファイル esbuild.js を作成します。

esbuild.js
// 開発モードか否か?
const isDev = process.env.NODE_ENV === 'development';
// html のプラグインをインポート
const { htmlPlugin } = require('@craftamap/esbuild-plugin-html');

// メインプロセス向け設定
require('esbuild').build({
  // エントリーポイント
  entryPoints: ['src/main.js', 'src/preload.js'],
  // バンドルする
  bundle: true,
  // Node.js がターゲット
  platform: 'node',
  // 出力先フォルダ
  outdir: './dist',
  // この 2 つを指定しないと実行時エラーになる
  external: ['electron', 'electron-reload'],
  // 開発モードではファイルの変更をウォッチしてホットリロードする
  watch: isDev,
  // 軽量化
  minify: !isDev,
  // ソースマップ
  sourcemap: isDev,
});

// レンダラープロセス (React アプリ) 向け設定
require('esbuild').build({
  entryPoints: ['src/index.jsx'],
  bundle: true,
  // htmlPlugin のために必須
  metafile: true,
  // ターゲットは web に
  platform: 'browser',
  outdir: './dist',
  watch: isDev,
  minify: !isDev,
  sourcemap: isDev,
  plugins: [
    htmlPlugin({
      files: [
        {
          // 通常は↑のエントリーファイルと同じ
          entryPoints: ['src/index.jsx'],
          // 出力ファイル名
          filename: 'index.html',
          // テンプレート
          htmlTemplate: 'src/index.html',
        },
      ],
    }),
  ],
});

📥 各種ユーティリティのインストール

bash
npm i -D rimraf wait-on cross-env npm-run-all
  • rimraf
    どのプラットファームでも UNIX の 'rm -rf' と同等のコマンドを実現するユーティリティ。
  • wait-on
    指定したスクリプトが完了するのを待って別のスクリプトを実行してくれるユーティリティ。
  • cross-env
    どのプラットファームでも環境変数の設定が共通となるユーティリティ。
  • npm-run-all
    • run-s: スクリプトを順番に実行。
    • run-p: スクリプトを並列に実行。

⚙️ NPM スクリプトの設定

package.json
{
  "main": "dist/main.js",
  "scripts": {
    "predev": "rimraf dist",
    "dev": "run-p dev:*",
    "dev:esbuild": "cross-env NODE_ENV=\"development\" node ./esbuild.js",
    "dev:electron": "wait-on ./dist/index.html ./dist/main.js && cross-env NODE_ENV=\"development\" electron .",
    "build": "cross-env NODE_ENV=\"production\" node ./esbuild.js"
  }
}
  • main エントリには esbuild が出力するメインプロセスの JS ファイルを指定します。
  • scripts
    • dev:esbuild: esbuild で両プロセスをウォッチモードでバンドル&コンパイルします。
    • dev:electron: esbuild のバンドル結果が出力されるのを待って electron アプリを開発モードで起動します。
    • dev: npm run dev で上記 2 つのコマンドを並列に実行します。
    • predev: プロジェクトのスタート前に前回のビルド結果を削除します。
    • build: 本番向けビルドを production モードで実行します。

Electron アプリの作成

⚒️ レンダラープロセス (React アプリケーション) の作成

📋 src/index.html

src/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!-- CSP の設定 https://developer.mozilla.org/ja/docs/Web/HTTP/CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self';" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Electron App</title>
  </head>
  <body>
    <!-- React アプリのマウントポイント -->
    <div id="root"></div>
  </body>
</html>

https://www.electronjs.org/docs/latest/tutorial/security#6-define-a-content-security-policy

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

📋 src/index.jsx

src/index.jsx
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

import './index.css';

const App = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="container">
      <h1>{count}</h1>
      <button onClick={() => setCount((count) => count + 1)}>Count</button>
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

📋 src/index.css

src/index.css
body {
  margin: 0;
  font-family: sans-serif;
}

.container {
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

⚒️ メインプロセスの作成

📋 メイン

src/main.js
import path from 'path';
import { BrowserWindow, app } from 'electron';

// 開発モードの場合はホットリロードする
if (process.env.NODE_ENV === 'development') {
  require('electron-reload')(__dirname, {
    electron: path.resolve(
      __dirname,
      process.platform === 'win32'
        ? '../node_modules/electron/dist/electron.exe'
        : '../node_modules/.bin/electron'
    ),
  });
}

// アプリの起動イベント発火で BrowserWindow インスタンスを作成
app.whenReady().then(() => {
  // レンダラープロセスをロード
  new BrowserWindow().loadFile('dist/index.html')
});

// すべてのウィンドウが閉じられたらアプリを終了する
app.once('window-all-closed', () => app.quit());

📋 プリロードスクリプト(とりあえず空ファイル)

src/preload.js
console.log('preloaded!');

実行テスト

bash
npm run dev

TypeScript でもやってみる

📥 追加インストール

  • TS 本体、インタプリタおよび Node.js 向け型定義ファイル
bash
npm i -D typescript ts-node @types/node
  • React 向け型定義ファイル
bash
npm i -D @types/react @types/react-dom

⚙️ tsconfig.json の作成

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "esModuleInterop": true,
    "isolatedModules": true,
    "moduleResolution": "Node",
    "lib": ["DOM", "ESNext"],
    "strict": true,
    "jsx": "react"
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS"
    }
  }
}
例) 型を再エクスポートする場合など
// NG: コンパイルエラー
export { Config } from './types';

// OK: 型のエクスポートであることを明示する
export type { Config } from './types';
  • ts-node: esbuild のコンフィグファイルを TypeScript で書く場合は "module": "CommonJS" の指定が必要です。

https://esbuild.github.io/content-types/#isolated-modules

https://zenn.dev/yuichkun/scraps/25ef6dfbf6b30d

⚙️ NPM スクリプトのアップデート

package.json
  {
    "scripts": {
      "predev": "rimraf dist",
      "dev": "run-p dev:*",
+     "dev:esbuild": "cross-env NODE_ENV=\"development\" ts-node ./esbuild.ts",
      "dev:electron": "wait-on ./dist/index.html ./dist/main.js && cross-env NODE_ENV=\"development\" electron .",
+     "build": "cross-env NODE_ENV=\"production\" ts-node ./esbuild.ts"
    }
  }

⚙️ js から ts

esbuild.ts
+ import { build } from 'esbuild';
  const isDev = process.env.NODE_ENV === 'development';
- const { htmlPlugin } = require('@craftamap/esbuild-plugin-html');
+ import { htmlPlugin } from '@craftamap/esbuild-plugin-html';

こちらもよろしくお願いします

https://zenn.dev/sprout2000/books/6f6a0bf2fd301c

https://zenn.dev/sprout2000/books/3691a679478de2

Discussion

ログインするとコメントできます