Electron + React + TypeScript の開発環境構築
はじめに
React と TypeScript でつくる Electron アプリのボイラープレートです。
メインプロセス、レンダラープロセスともにホットリロード可能な開発環境の構築を目指します。
前提
Node.js と Git Bash(もしくは何らかの UNIX シェル)はインストール済みであることを想定しています。
プロジェクトディレクトリの作成
プロジェクト用のフォルダ(ディレクトリ)を作成し、その中で必要なパッケージのインストールや設定を行います。
⚙️ Node.js のプロジェクトとして初期化
npm
コマンドは Node.js に同梱されています。
- プロジェクトフォルダの作成と移動
$ mkdir myapp
$ cd myapp
- Node.js プロジェクトとして初期化
npm init --yes
📁 想定するディレクトリ構成
% tree -a -I 'node_modules'
.
├── dist
├── package-lock.json
├── package.json
├── src
│ ├── main.ts
│ ├── preload.ts
│ └── web
│ ├── App.css
│ ├── App.tsx
│ ├── index.html
│ └── index.tsx
├── tsconfig.json
├── tsconfig.node.json
└── webpack.config.ts
3 directories, 11 files
-
dist/
: webpack の出力先フォルダ -
src/main.ts
: メインプロセスのエントリファイル -
src/preload.ts
: プリロードスクリプト -
src/web/
: レンダラープロセス (= React アプリケーション) ソースコード置き場 -
tsconfig.json
: TypeScript 設定ファイル -
tsconfig.node.json
: 開発時にメインプロセスのみをコンパイルするための設定ファイル -
webpack.config.ts
: webpack 設定ファイル
TypeScript のインストールと設定
TypeScript や周辺ツールのインストールと設定を行います。
📥 TypeScript のインストール
npm install --save-dev typescript ts-node @types/node
--save-dev
オプションを使ってインストールすると、そのパッケージは package.json
の devDependencies
エントリへ追加されます。
-
typescript
: 本体 -
ts-node
: TypeScript のスクリプト (webpack.config.ts
など) をそのまま実行するためのツール -
@types/node
: Node.js のための型定義ファイル
tsconfig.json
の作成
⚙️ TypeScript 設定ファイル:
tsconfig.json
を作成する
1. プロジェクトフォルダ直下に {
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"esModuleInterop": true,
"moduleResolution": "node",
"strict": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
}
}
}
-
target
: 出力する JavaScript のバージョンを設定します。 -
module
: 出力される JavaScript がどの形式のモジュールを読み込むかを指定します。 下のesModuleInterop
をtrue
に設定しておき、ES*
を指定しておくのが無難でしょう。 -
esModuleInterop
: CommonJS と ES Modules 間で相互運用可能なコードを出力します。 -
moduleResolution
: モジュール解決の方法を指定します。現状ではnode
の指定でほぼ問題ないでしょう。 -
lib
: 必須ではないので設定しません。target
が指定されていれば、そのtarget
で使われているライブラリは自動的に追加されるからです。逆にlib
を指定すると、明示的にtarget
のどのlib
を使うかも明記しなければいけなくなります。 -
strict
: TypeScript による幅広い型チェックの挙動を有効化します。これだと厳しすぎる場合には、個別のstrict
モードファミリーを無効化する必要があります。 -
jsx
: JSX 構文がどのように JavaScript ファイルに出力されるかを設定します。.tsx
で終わるファイルの JS 出力にのみ影響します。-
preserve
: JSX を変更せずに.jsx
ファイルを出力します。 -
react
: JSX を等価なreact.createElement
に変換して.js
ファイルを出力します。 -
react-jsx
: JSX を_jsx
呼び出しに変更した.js
ファイルを出力します。多くのモジュールで React 本体をインポートする必要がなくなります。
-
-
include
: プログラムに含めるファイル名またはパターンのリストを指定します。 ファイル名はtsconfig.json
ファイルを含んでいるディレクトリからの相対パスとして解決されます。 -
ts-node
: ts-node で Node.js スクリプトを実行する(webpack.config.ts
などの TypeScript で書かれた設定ファイルを評価する)にはmodule: CommonJS
の指定が必要です。
tsconfig.node.json
を作成する
2. 同じくプロジェクトフォルダ直下に この TypeScript 設定ファイルは、開発時にメインプロセス(とプリロードスクリプト)のみをコンパイルするために利用します。
{
"extends": ".",
"compilerOptions": {
"module": "CommonJS",
"outDir": "dist"
},
"include": ["src/*.ts", "src/@types/*"]
}
-
extends
: 上記tsconfig.json
の設定を継承します。逆に言うとその他の明示的に指定したエントリの設定は上書きされます。
Electron アプリの作成
必要最低限の機能を持った Electron アプリを作成します。
📥 Electron のインストール
Electron は package.json
の devDependencies
へエントリされている必要があります。
- 本体
npm install --save-dev electron
- Electron をホットリロードしてくれるツール
npm install --save-dev electron-reload
⚒️ メインプロセスの作成
import path from 'node:path';
import { BrowserWindow, app } from 'electron';
// 開発時には 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",
),
forceHardReset: true,
hardResetMethod: "exit",
});
}
app.whenReady().then(() => {
// アプリの起動イベント発火で BrowserWindow インスタンスを作成
const mainWindow = new BrowserWindow({
webPreferences: {
// tsc or webpack が出力したプリロードスクリプトを読み込み
preload: path.join(__dirname, 'preload.js'),
},
});
// レンダラープロセスをロード
mainWindow.loadFile('dist/index.html');
});
// すべてのウィンドウが閉じられたらアプリを終了する
app.once('window-all-closed', () => app.quit());
⚒️ プリロードスクリプトの作成
ここではとりあえず空のファイルとします。
console.log('preloaded!');
React アプリ(レンダラープロセス)の作成
こちらも最低限の体裁を備えたアプリ(= WebView 部分)を作成します。
📥 React のインストール
- 本体
npm install react react-dom
--save-dev
オプションなし(もしくは --save
or -S
オプション付き)でインストールすると、そのパッケージは package.json
の dependencies
エントリへ追加されます。
- 型定義ファイル
npm install -D @types/react @types/react-dom
-D
オプションは --save-dev
の省略形です。
⚒️ レンダラープロセスの作成
画面に Hello.
と表示するだけのシンプルな React アプリを作成します。
📋 src/web/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>
📋 src/web/App.css
.container {
font-family: sans-serif;
text-align: center;
}
📋 src/web/App.tsx
import './App.css';
export const App = () => {
return (
<div className="container">
<h1>Hello.</h1>
</div>
);
};
📋 src/web/index.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
バンドラー (webpack) の設定
webpack を使って、開発時にはレンダラープロセスのみを、本番ビルドではメイン、レンダラープロセスともにビルドします。
📥 Webpack のインストール
本体
npm i -D webpack webpack-cli
バンドルするための各種ローダー
- TypeScript と CSS のローダー
npm i -D ts-loader css-loader
各種プラグイン
npm i -D html-webpack-plugin mini-css-extract-plugin
-
html-webpack-plugin: バンドルされた JavaScirpt ファイルを HTML の
<script> ~ </script>
タグへ差し込むプラグイン - mini-css-extract-plugin: CSS を JS へバンドルせず単独のファイルとして出力するプラグイン
⚙️ 設定ファイル webpack.config.ts の作成
開発時には watch モードを用いて、レンダラープロセスのファイルが変更されるたびにコンパイル&バンドルします。
その結果が dist
ディレクトリへ出力されると、electron-reload
がその変更を検知して、 Electron アプリをリスタートさせます。
/** エディタで補完を効かせるために型定義をインポート */
import { Configuration } from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
// 開発者モードか否かで処理を分岐する
const isDev = process.env.NODE_ENV === 'development';
// 共通設定
const common: Configuration = {
// モード切替
mode: isDev ? 'development' : 'production',
// モジュール解決に参照するファイル拡張子
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],
},
/**
* macOS でビルドに失敗する場合のワークアラウンド
* https://github.com/yan-foto/electron-reload/issues/71
*/
externals: ['fsevents'],
// 出力先:デフォルトは 'dist'
output: {
// 画像などのアセット類は 'dist/assets' フォルダへ配置する
assetModuleFilename: 'assets/[name][ext]',
},
module: {
// ファイル種別ごとのコンパイル & バンドルのルール
rules: [
{
/**
* 拡張子 '.ts' または '.tsx' (正規表現)のファイルを 'ts-loader' で処理
* ただし node_modules ディレクトリは除外する
*/
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
{
// 拡張子 '.css' (正規表現)のファイル
test: /\.css$/,
// use 配列に指定したローダーは *最後尾から* 順に適用される
// セキュリティ対策のため style-loader は使用しない
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
// 画像やフォントなどのアセット類
test: /\.(ico|png|svg|eot|woff?2?)$/,
/**
* アセット類も同様に asset/inline は使用しない
* なお、webpack@5.x では file-loader or url-loader は不要になった
*/
type: 'asset/resource',
},
],
},
// 開発時には watch モードでファイルの変化を監視する
watch: isDev,
/**
* development モードではソースマップを付ける
*
* なお、開発時のレンダラープロセスではソースマップがないと
* electron のデベロッパーコンソールに 'Uncaught EvalError' が
* 表示されてしまうことに注意
*/
devtool: isDev ? 'source-map' : undefined,
};
// メインプロセス向け設定
const main: Configuration = {
// 共通設定を読み込み
...common,
target: 'electron-main',
// エントリーファイル(チャンク名の 'main.js' として出力される)
entry: {
main: './src/main.ts',
},
};
// プリロードスクリプト向け設定
const preload: Configuration = {
...common,
target: 'electron-preload',
entry: {
preload: './src/preload.ts',
},
};
// レンダラープロセス向け設定
const renderer: Configuration = {
...common,
// セキュリティ対策として 'electron-renderer' ターゲットは使用しない
target: 'web',
entry: {
// React アプリのエントリーファイル
app: './src/web/index.tsx',
},
plugins: [
// CSS を JS へバンドルせず別ファイルとして出力するプラグイン
new MiniCssExtractPlugin(),
/**
* バンドルしたJSファイルを <script></scrip> タグとして差し込んだ
* HTMLファイルを出力するプラグイン
*/
new HtmlWebpackPlugin({
// テンプレート
template: './src/web/index.html',
}),
],
};
// 上記 3 つの設定を配列にしてデフォルト・エクスポート
// ただし開発時にはレンダラープロセスのみをバンドルし、メイン or プリロードは tsc に処理させる
export default isDev ? renderer : [main, preload, renderer];
NPM スクリプトの設定
開発時や本番ビルド時に走らせるスクリプトを package.json
へ追記します。
📥 各種ユーティリティのインストール
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 スクリプトの作成
{
"main": "dist/main.js",
"scripts": {
"dev": "rimraf dist && run-p dev:*",
"build": "rimraf dist && cross-env NODE_ENV=\"production\" webpack --progress",
"dev:tsc": "tsc -w -p tsconfig.node.json",
"dev:webpack": "cross-env NODE_ENV=\"development\" webpack --progress",
"dev:electron": "wait-on ./dist/index.html ./dist/main.js && cross-env NODE_ENV=\"development\" electron ."
}
}
- main エントリには webpack が出力するメインプロセスの JS ファイルを指定します。
-
scripts
- dev:tsc: メインプロセスとプリロードスクリプトをウォッチモードの tsc でコンパイルします。
- dev:webpack: レンダラープロセスを webpack でコンパイル&バンドルします。
- dev:electron: tsc と webpack のコンパイル結果が出力されるのを待って electronを起動します。
-
dev:
npm run dev
で、前回のビルド結果を削除したあと上記 3 つのコマンドを並列に実行します。 -
build 本番向けビルドを
production
モードで実行します。
実行テスト
React アプリ(レンダラープロセス)やメインプロセスのコードを編集し、ホットリロードが有効であることを確認します。
npm run dev
開発時のホットリロード(Electron の再起動)
こちらもよろしくお願いします
Discussion
記事を参考にelectronデビュー致しました。 詳細な解説をありがとうございます。
1つ躓いた点がございまして、お答えいただけるとありがたいです。
リリースビルドしたあと exe を実行すると エラーになります。
これはつまり、「devDependenciesのモジュールを参照しようとしたこと」が原因と判断しました。
該当モジュール使用箇所をコメントアウトしたところ エラーは解消されました。
ですが、実際その対策は意図したものではないと考えられます。
同じ場所で躓いている方をお見かけしないので、何か私にミスがあると思うのですがわかりませんでした。
もし何かアドバイスがございましたらよろしくお願い致します。
@kentaro さん
ご指摘ありがとうございます。
本番向けにビルドする場合には、環境変数
NODE_ENV
をproduction
にセットしてください(記事本文にも追記しました)。記事中で言及した拙作レポジトリでの例で言えば、リリースビルドは以下の手順となります。
補足 Electron アプリケーションにおける devDependencies について
production ビルド用に
node_modules
をインストールする場合、以下のようなコマンドは利用しないでください。なぜなら、これらは
devDependencies
に指定されたパッケージをインストールから除外してくれますが、electron
本体やelectron-builder
等のパッケージャーはdevDependencies
に登録されている必要があるからです。これら以外のパッケージで、本当に production ビルドに必要のないパッケージを除外したいのであれば、それらは
optionalDependencies
へ移動させ、かつifdef-loader
等を用いてソースコード内でそのモジュールを利用している部分を production ビルドでは評価しないようにする必要があります。これらは
node_modules
の減量 (≒ ビルドサイズの減量) とコードの保守・拡張の容易さとのバランスを勘案して利用する必要があるでしょう。記事を参考にとりあえず環境構築ができたものです(^^)
さて、私が移行したいと考えているプロジェクトでは、
import ... from "src/○/○" ;
のようにsrcフォルダを基点にした絶対パスでインポートする文が多く使われており、これを可能にするために全プロジェクトでは**tsconfig.jsonのcompolerOptions.baseUrlに"."**を設定しました。本記事ではtsconfig.jsonはレンダラープロセス用、tsconfig.main.jsonはメインプロセス用の設定ファイルという理解をしたため、絶対パスでのインポートを可能にするためtsconfig.jsonのcompolerOptions.baseUrlに"."を設定しましたが、以下のようにパスが解決できないという旨のエラーメッセージが表示されてしまいます。
tsconfig.jsonのcompolerOptions.baseUrlに"."を設定するだけではなにか足りないのでしょうか?
そのエラーがレンダラープロセス側で生じているのであれば tsconfig-paths-webpack-plugin などを利用して webpack にモジュール解決をさせれば良いでしょうが(下記参照)、メインプロセス側でのエラーだとするとちょっと根本的に仕組みを考え直すしかないのではないでしょうか。もしくはメインプロセス側のコードだけは相対パスによるインポートを使うとか・・・
ブラウザ(アプリのウィンドウ)にも同様のエラーメッセージがあったためどうやらレンダラープロセス側で起こっていたみたいです。
@Ke Touge様のおっしゃる通りにwebpack.config.tsに設定を加えると解決できました!
(webpackの勉強不足でした。。。)
ただメインプロセスの方は、上記の対策をしても当然ながら絶対パスが使えなかったので、諦めて相対パスで頑張ることにしました!
ご回答ありがとうございました。
はじめまして、この記事を参考にはじめて触った者です。ある程度動作するものできたので、しexeにしてみようと思い
npm run build
とやってみたところ最終的にwebpack compiled successfully
と表示されているにもかかわらずどこにもフォルダが生成されていません。ログは以下のようになっていました。お答えいただけると幸いです。試しにelectron-packagerを使ったところ生成されましたが、そちらでは
Cannot find module 'electron-search-devtools'
と出ています以下がログです。
コメントありがとうございます。
npm run build
コマンドは Electron が起動できるようにコンパイルするだけで、アプリをexe
などの形式へパッケージするには別途 electron-packager や electron-builder が必要です。electron-builder については私のレポジトリ(↓)の
scripts/build.ts
などを参考にしてみてください。NPM スクリプトの
NODE_ENV
(cross-env) の設定は適切ですか?また、サンプルレポジトリ(↓)の
src/main.ts
のisDev
変数なども試してみてください。とても素早い対応ありがとうございます。なんとかexeにすることができました。しかし今度はこれまでは使えていた機能(websocket関係です)が使えなくなっていました。ファイアウォールを切ってみるなどしましたが特に変化はなかったのですが、ビルドした後にコンソールなどを見るには、ログファイルを書き出したりウィンドウに表示するほかないでしょうか。
webSocket についてよく知らないので何とも言えないのですが…
Development モードで動いている機能がパッケージング後に使えなくなるのであれば、そのモジュールを asarUnpack (electron-builder の場合)に指定してみるのも良いかもしれません。
あとは CSP や webContents オプションの設定を見直すくらいでしょうか。
ビルド後も(正式リリースまでは)devTools を表示させることは可能ですし、インスペクタの WebSocket URL の公開方法もコマンドラインから設定できるようですよ。
Discord の Electron サーバー の Help チャネルで質問してみても(※英語)良いかもしれませんね。
ありがとうございます。
npm run dev
やnpm run build
で出力した後electron .
で起動するという方法で調べてみたところパッケージではなくnpm run build
の時点で正常に動作していないようなのですが、なにかそういったことに関してご存じないでしょうか。package.jsonとビルド時のログ、実行時のエラーはこのようになっていました。また、npm run dev:electron
の場合も同じエラーが出ています。何度もお時間いただいて申し訳ありません。
とのメッセージなので、メインプロセスのコードに誤りがあるのではないでしょうか。
メインプロセスのデバッグについては、この記事で作成したプロジェクトをベースにしてつばめさんが↓の記事を執筆しておられます。
React + Electron + TypeScriptの構成での開発環境の構築に手間取っていたのでものすごく助かりました!素敵な記事とライブラリをありがとうございます!
@Hiroto さん
コメントとバッジをありがとうございます。とても励みになります。