はじめに
「デスクトップアプリを作りたい」と思ったときに、最近よく聞く Electron。
ちょっと使ってみたいと思っても、必要なファイルが多く、
理解するためにはなかなかハードルが高いです。
そこで、ここではなるべく簡単に、手を動かしながら最小限の構成を作り、
Electronを理解していきたいと思います。
Electron について
Electron とは
Electron を一言で言うと、
Node.js と Chromium(Chrome) を1つのパッケージにまとめた、
Web 技術でデスクトップアプリを作るためのフレームワーク です。
Webアプリを作っているフロントエンドエンジニアが、
既存の知識(HTML / CSS / JavaScript など)を そのまま使って、
Mac・Windows・Linuxで動くクロスプラットフォームアプリを構築できます。
Web アプリとは違う?
通常の Web ブラウザー(Chrome)と Electron の大きな違いの一つは、
Electron が OS の機能を利用できるアプリ側の仕組みを持っていることです。
通常の Web ブラウザーでは、Webページ上の JavaScript が
PC の OS やファイルシステムへ自由にアクセスできないよう、
セキュリティ上の制限が設けられています。
例えば、Webページからユーザーの PC 内にあるファイルを勝手に書き換えたり、
OS のネイティブ機能を自由に操作したりすることはできません。
一方 Electron は、
Chromium で画面を表示するためのプロセス(Renderer プロセス)と並行して、
Node.js を利用できるプロセス(Mainプロセス)が動作します。
これにより、Web 技術による UI を使いながら、
ファイル操作やネイティブダイアログなど、
デスクトップアプリならではの機能を利用できます。
Electron の構成
Electronのアプリは、
大きく「Mainプロセス」と「Rendererプロセス」に分かれて動作します。
そして、それら2つのプロセスをつなぐために、「Preload script」というものを利用します。
Main プロセス、Renderer プロセスについて
まず、アプリ全体を管理するのが 「Main プロセス」 で、
こちらでは Node.js や Electron 備え付けの API を利用できます。
一方、Chromiumを使って画面を描画するのが 「Renderer プロセス」 で、
こちらではHTML / CSS / JavaScriptを使ってUIを構築します。
この2つのプロセスは分離されており、
後述する 「IPC通信」 を使ってデータをやり取りします。
そして、Renderer から Main の機能を安全に利用できるようにする「橋渡し役」として、
後述する 「Preload script」 を利用します。
なぜ「Main プロセス」と「Renderer プロセス」に分かれているのか?
Electronでは、処理を
「Main プロセス」と「Renderer プロセス」という異なるプロセスに分けて実行します。
なぜ1つのJavaScriptファイルで全部書かないのか。
この理由は、Chromium のアーキテクチャーとセキュリティー設計に由来します。
理由1. 安定性のため
Chrome で1つのタブがクラッシュしても、
ブラウザー全体が終了しないのと同じ仕組みです。
画面を描画する処理(Renderer)と、アプリ全体を管理する処理(Main)を
別々のプロセスに切り離すことで、
Renderer 側でクラッシュが発生しても、
Main プロセスまで巻き込まれにくい構成になります。
理由2. セキュリティーのため
もしRenderer(Web画面)から直接Node.jsのフル権限を触れるようにしてしまうと、
Web画面側でクロスサイトスクリプティング(XSS)などの脆弱性が発生した瞬間、
攻撃者にPCのファイルを全消去されたり、
任意のコマンドを実行されたりする危険性(RCE)が発生します。
そのため、Electronでは、
画面を表示する「Renderer プロセス」と、OS を操作する「Main プロセス」を分離し、
Preloadを橋渡し役として利用して、
Rendererから必要な機能だけを安全に利用できるようにします。
「Preload」とは
前述の通り、セキュリティ上の理由から(nodeIntegration: false の構成とするため)
Renderer(画面)から Node.js を直接触ることはできません。
しかし、画面から「ファイルを開きたい」「OSの通知を出したい」など、
Node.js の機能を使いたい場面がいずれ出てきます。
そこで登場するのが Preload です。
Preload の中で contextBridge という機能を使うことで、
Rendererに対して、アプリが用意したAPIを限定的に公開できます。
そして、公開したAPIの内部で ipcRenderer を使うことで、Mainプロセスと通信できます。
Preload を利用することで、Main プロセスが提供する必要な機能を、
Renderer 側から利用できるようになるのです。
「IPC 通信」とは
IPC は Inter-Process Communication(プロセス間通信) の略です。
ここまで解説した通り、Electronでは
「Renderer プロセス(画面描画)」と「Main プロセス(OS操作)」が分離されています。
そのため、この2つの間でデータの受け渡しを行う仕組みが必要になります。
その仕組みこそが IPC 通信 です。
Preload は、Rendererに必要なAPIを公開する、いわば「窓口」であり、
その API の内部で IPC 通信を利用して、Main プロセスへとデータを受け渡すのです。
本編(実践編)-1 : アプリの立ち上げ
全体の構造と各役割(Main・Renderer・Preload・IPC)が分かったところで、
実際に最小限のコードを組み立てていきましょう!
まずは、Preload に API を実装せず、アプリを立ち上げるところまで作成してみます。
前提条件
-
Node.js がインストールされていること(
node -vで確認)
ディレクトリー構成
今回は、各プロセスの役割を整理するため、
以下のシンプルな構成で作っていきます。
my-electron-app/
├── src/
│ ├── main/ # 1. Main 側(アプリ管理、OS操作)
│ │ └── index.js
│ ├── preload/ # 3. Preload(橋渡し役)
│ │ └── index.js
│ └── renderer/ # 2. Renderer 側(画面描画処理)
│ ├── index.html
│ ├── script.js
│ └── style.css
└── package.json
プロジェクトの作成
まずは適当なフォルダを作成し、初期化しておきます。
mkdir my-electron-app
cd my-electron-app
npm init -y
npm install electron --save-dev
package.json の編集
プロジェクト直下の package.json を開き、
エントリーポイント(main)のパスを src/main/index.js に変更し、
起動コマンド(start)を追加します。
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "src/main/index.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^XX.X.X"
}
}
electron のバージョンは、その時の最新バージョンとしてください。
記事作成時は 44.4.5 でした。
1. Main プロセス (src/main/index.js)
まずは Main プロセス(src/main/index.js)から作ります。
最初は Preload script には API の実装を書きません。
内容としては、src/renderer/index.html を表示するだけです。
const { app, BrowserWindow } = require('electron');
const path = require("path");
// ウィンドウを生成する関数
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(
app.getAppPath(),
"src/preload/index.js"
),
// セキュリティ設定(ここでは明示的に設定しています)
contextIsolation: true, // コンテキスト分離を有効化
nodeIntegration: false // RendererでのNode.js直接使用を禁止
}
});
console.log("Main Process started.");
// 画面となるHTMLファイルを読み込む
mainWindow.loadFile('src/renderer/index.html');
}
// Electronの準備が終わったらウィンドウを生成
app.whenReady().then(createWindow);
// 全てのウィンドウが閉じられたらアプリを終了(macOS以外)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
webPreferencesのポイント
contextIsolation: true と nodeIntegration: false は、
どちらも安全なElectronアプリを作るうえで重要な設定です。
今回は「何が設定されているのか」を明確にするため、
デフォルトに頼らず明示しています。
2. Renderer プロセス (src/renderer/index.html ほか)
ElectronのRendererは、基本的にはWebページです。
普通のWebページを作るように、HTML / JS / CSS を作ります。
HTML (src/renderer/index.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Electron App</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>Electron App</h1>
<p>Hello, Electron!</p>
<script src="./script.js"></script>
</body>
</html>
JavaScript (src/renderer/script.js)
まずは、ログだけ追加しておきます。
console.log("Renderer process started.");
CSS (src/renderer/style.css)
最低限のCSSを追加しておきます。
body {
font-family: sans-serif;
}
3. Preload script (src/preload/index.js)
Main と Renderer の「橋渡し役」です。
まだロジックは実装せず、ログだけ追加しておきましょう。
console.log("Preload script started.");
4. 動作確認
すべてのファイルが保存されていることを確認したら、
ターミナルで以下のコマンドを実行します。
最初は時間がかかりますので、気長に待ってください。
npm start
動作確認のチェックポイント
- 800x600 のウィンドウが立ち上がる
- アプリ画面に「Hello, Electron!」が表示される
- コンソールに「Main Process started.」と表示される
- DevTools(Ctrl + Shift + I など)を開くと、
コンソールに「Preload script started.」「Renderer process started.」と表示されている
本編(実践編)-2 : Preload の実装
次に、「PC上のファイルを選択し、そのファイル名を画面に表示する」という機能を作りながら、
Preload について理解していきましょう。
ここで、IPC(プロセス間通信)が登場します。
1. Renderer プロセス (src/renderer/index.html ほか)
HTML (src/renderer/index.html)
HTML(src/renderer/index.html)を以下のように書き換え、
ファイル選択を行うボタンと、ファイル名を表示する部分を追加します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Electron App</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<h1>Electron App</h1>
<button id="select-file-button">ファイルを選択</button>
<p id="file-name"></p>
<script src="./script.js"></script>
</body>
</html>
JavaScript (src/renderer/script.js)
次に、ボタンを押した際のロジックを追加します。
console.log("Renderer process started.");
const selectFileButton = document.querySelector("#select-file-button");
const fileName = document.querySelector("#file-name");
selectFileButton.addEventListener("click", async () => {
const selectedFileName = await window.electronAPI.selectFile();
console.log(selectedFileName);
if (selectedFileName === null) {
return;
}
fileName.textContent = selectedFileName;
});
select-file-button を押すと、selectFile() が呼ばれる、という仕組みです。
selectFile() は、これから、アプリ用の自作の API として定義し、
Preload から Renderer に公開します。
2. Preload script (src/preload/index.js)
Preload から、selectFile() という API を、Renderer 側に公開します。
selectFile() が呼ばれたときに Main プロセスへ要求を渡す、「橋渡し」の処理です。
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
selectFile: () => {
return ipcRenderer.invoke("select-file");
}
});
console.log("Preload script started.");
上記は、「selectFile() が呼ばれたら、Main プロセスに select-file という要求を送る」という仕組みの実装です。
3. Main プロセス (src/main/index.js)
src/main/index.js を以下のように変更します。
select-file の実装です。
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const path = require("path");
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(
app.getAppPath(),
"src/preload/index.js"
),
contextIsolation: true,
nodeIntegration: false
}
});
console.log("Main Process started.");
mainWindow.loadFile("src/renderer/index.html");
}
ipcMain.handle("select-file", async () => {
console.log("Main: ファイル選択の要求を受け取りました");
const result = await dialog.showOpenDialog({
properties: ["openFile"]
});
if (result.canceled) {
return null;
}
const filePath = result.filePaths[0];
return path.basename(filePath);
});
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
変更点は以下です。
- Electron の、
ipcMainとdialogを読み込む - Main プロセス側で
select-fileを受け取る- 受け取ったら、Electron のファイル選択ダイアログを開く
- 選択されたファイルのパスからファイル名だけを取り出して返す
dialog.showOpenDialog() は、Electron が提供している(備え付けの)API です。
4. 動作確認
再び、動作確認を行いましょう。ターミナルで以下のコマンドを実行します。
npm start
動作確認のチェックポイント
- アプリ画面に「ファイルを選択」ボタンが表示される
- ファイルを選択すると、ファイル名が表示される
- その際、コンソールに「Main: ファイル選択の要求を受け取りました」と表示される
本編(実践編)-3 : アプリのビルド
ここまでで、Electronアプリを実際に動かせるようになりました。
最後に、作成したアプリの、Windows 向けのインストーラー(.exe)を作ってみましょう。
ビルドには、electron-builder を使います。
インストールしましょう。
npm install --save-dev electron-builder
package.json に設定を追加します。
{
"name": "my-electron-app",
"version": "1.0.0",
"main": "src/main/index.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
},
"build": {
"appId": "com.example.my-electron-app",
"productName": "My Electron App",
"win": {
"target": "nsis"
}
},
"devDependencies": {
"electron": "^XX.X.X",
"electron-builder": "^XX.X.X"
}
}
appId はアプリを識別するための ID、productName はユーザーに表示されるアプリ名です。
electron-builder のバージョンも、その時点の最新バージョンを使用してください。
以下のコマンドを実行し、アプリをビルドします。
npm run build
dist ディレクトリーに、Windows 向けのインストーラー(.exe)が生成されます!
※インストールしたアプリは、Windowsの設定から
通常のアプリと同じようにアンインストールできます。
まとめ
Electronでは、以下のように、それぞれの役割を分けてアプリを構築する。
- Main プロセス:アプリ全体の管理やOS操作を担当
- Renderer プロセス:HTML / CSS / JavaScriptでUIを構築
- Preload:RendererとMainの橋渡しを担当
- IPC通信:RendererとMainの間で通信する仕組み
おわりに
この記事が皆さんの役に立つと嬉しいです!
もし「参考になった」と思ったら、ストック・いいねしていただけますと幸いです。
※この記事は、一部 ChatGPT、Google Gemini を利用して作成しました。