Electron デスクトップアプリ開発完全ガイド:環境構築から本番リリースまで
なぜ Electron なのか
クロスプラットフォームのデスクトップアプリ開発は長年の課題でした。Windows は C# と WPF、macOS は Swift と AppKit、Linux は GTK か Qt——3 つのコードベース、3 倍のメンテナンスコスト。Electron の登場で状況は一変しました。1 つの HTML/CSS/JavaScript コードベースで、Windows、macOS、Linux 向けにパッケージングできます。
VS Code、Figma、Slack、Discord を使っている開発者は、実は毎日 Electron アプリを使っています。VSCode の起動速度、Figma のレンダリング性能は、適切なアーキテクチャで Electron がネイティブ並みの体験を実現できることを証明しています。
私は社内ツールを C# WinForms から Electron に移行しました。その後の 2 四半期で、新機能の開発速度は約 3 倍になりました。理由はシンプルです。フロントエンド開発者の採用は C# デスクトップ開発者よりはるかに容易で、コンポーネントのエコシステムも格段に豊富だからです。
この記事は私の移行後の完全なまとめです。API ドキュメントを列挙するのではなく、本番環境で実際に発生する問題とその解決策に焦点を当てます。
プロジェクト初期化:公式クイックスタートをコピーしない
公式の electron-quick-start は生の JavaScript を使用し、ビルドツールもモジュール化もホットリロードもありません。実際のプロジェクトでこの方法で始めると、2 ヶ月後にはスパゲッティコードの修正に追われることになります。
electron-vite を使用するか、Vite + Electron を手動でセットアップすることをお勧めします:
npm create @quick-start/electron@latest my-app -- --template vue-ts
または手動セットアップ:
mkdir my-electron-app && cd my-electron-app
npm init -y
npm i -D electron electron-builder vite @vitejs/plugin-vue typescript
npm i vue
推奨ディレクトリ構造:
my-electron-app/
├── electron/ # メインプロセス
│ ├── main.ts # エントリポイント
│ ├── preload.ts # セキュリティブリッジ
│ └── ipc/ # IPC チャネル管理
├── src/ # レンダラー(フロントエンド)
│ ├── App.vue
│ ├── main.ts
│ └── components/
├── resources/ # アプリアイコン等
├── electron-builder.yml
├── vite.config.ts
└── package.json
重要な設計判断:メインプロセスとレンダラープロセスを完全に分離する。メインプロセス (electron/) はシステムレベルの操作のみを担当し、レンダラー (src/) は標準的なフロントエンドプロジェクトです。
プロセスモデル:誰が何をするか
| プロセス | 実行環境 | できること | できないこと |
|---|---|---|---|
| メイン (Main) | Node.js | ウィンドウ作成、ファイル読み書き、システム API 呼び出し | DOM の直接操作 |
| レンダラー (Renderer) | Chromium | HTML/CSS のレンダリング、ユーザー操作の応答、DOM 操作 | デフォルトでは Node.js API アクセス不可 |
両者は IPC(プロセス間通信) を通じて連携します。メインプロセスは contextBridge を介して制限された API をレンダラーに公開します。
典型的な通信フロー:
ユーザーが「ファイルを保存」をクリック
→ レンダラー: ipcRenderer.invoke('save-file', fileData)
→ メイン: ipcMain.handle('save-file', async (event, data) => {
dialog.showSaveDialog() → fs.writeFileSync()
})
→ 結果をレンダラーに返す
Preload スクリプト——アプリのセキュリティゲート
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
saveFile: (content: string, defaultName?: string) =>
ipcRenderer.invoke('save-file', content, defaultName),
openFile: () =>
ipcRenderer.invoke('open-file'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
getPlatform: () => process.platform,
minimizeWindow: () => ipcRenderer.send('window-minimize'),
maximizeWindow: () => ipcRenderer.send('window-maximize'),
closeWindow: () => ipcRenderer.send('window-close'),
});
レンダラーからの呼び出し:
// src/components/SaveButton.vue
const handleSave = async () => {
const result = await window.electronAPI.saveFile(
editorContent.value,
'untitled.txt'
);
if (result.success) {
showToast(`保存完了: ${result.filePath}`);
}
};
セキュリティ:7 つの必須ルール
| # | ルール | 設定 | 理由 |
|---|---|---|---|
| 1 | contextIsolation: true |
webPreferences | レンダラーの JS コンテキストと preload を分離 |
| 2 | nodeIntegration: false |
webPreferences | レンダラーでの直接的な Node.js require を禁止 |
| 3 | sandbox: true |
webPreferences | OS レベルのサンドボックス |
| 4 | contextBridge を使用 |
preload.ts | ホワイトリスト方式の API 公開 |
| 5 | IPC メッセージの送信元検証 | ipcMain.handle | event.senderFrame の URL チェック |
| 6 | CSP なしでリモートコンテンツを読み込まない | webPreferences | コンテンツセキュリティポリシー必須 |
| 7 | 署名と公証 | electron-builder | macOS は公証必須、Windows は署名推奨 |
ウィンドウ管理:new BrowserWindow だけではない
フレームレスウィンドウとカスタムタイトルバー
const mainWindow = new BrowserWindow({
frame: false,
titleBarStyle: 'hidden',
...(process.platform === 'darwin'
? { titleBarStyle: 'hiddenInset' }
: { frame: false }),
});
レンダラーでのカスタムタイトルバー:
<!-- src/components/TitleBar.vue -->
<template>
<div class="title-bar" :class="{ 'is-mac': isMac }">
<div class="title-bar-drag" @dblclick="toggleMaximize">
<span class="app-title">{{ title }}</span>
</div>
<div class="window-controls" v-if="!isMac">
<button @click="minimize" class="control-btn minimize">─</button>
<button @click="toggleMaximize" class="control-btn maximize">□</button>
<button @click="close" class="control-btn close">✕</button>
</div>
</div>
</template>
自動更新:ユーザーに意識させないアップグレード
// electron/updater.ts
import { autoUpdater } from 'electron-updater';
export function setupAutoUpdater(mainWindow: BrowserWindow) {
autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://releases.your-app.com/updates/',
});
setInterval(() => {
autoUpdater.checkForUpdates();
}, 4 * 60 * 60 * 1000);
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'アップデート準備完了',
message: '新しいバージョンがダウンロードされました。今すぐ再起動してインストールしますか?',
}).then(() => {
autoUpdater.quitAndInstall();
});
});
setTimeout(() => autoUpdater.checkForUpdates(), 5000);
}
パッケージングと配布
# electron-builder.yml
appId: com.yourcompany.yourapp
productName: YourApp
directories:
output: release
files:
- dist/**/*
- electron/**/*.js
- package.json
win:
target:
- target: nsis
arch: [x64, arm64]
icon: resources/icon.ico
mac:
target:
- target: dmg
arch: [x64, arm64]
icon: resources/icon.icns
hardenedRuntime: true
notarize:
teamId: YOUR_TEAM_ID
linux:
target:
- target: AppImage
arch: [x64]
- target: deb
arch: [x64]
category: Development
パフォーマンス最適化
遅延読み込みによる非クリティカルウィンドウの管理
let settingsWindow: BrowserWindow | null = null;
ipcMain.handle('open-settings', async () => {
if (settingsWindow && !settingsWindow.isDestroyed()) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({ /* ... */ });
settingsWindow.loadURL('...');
settingsWindow.on('closed', () => { settingsWindow = null; });
});
メモリリークチェックリスト
| チェック項目 | ツール | 指標 |
|---|---|---|
| ウィンドウ閉鎖後のゾンビプロセス | タスクマネージャー / Activity Monitor | 残留 electron プロセス |
| 未破棄の webContents | webContents.getAllWebContents() |
増加し続ける数 |
| 未クリーンアップの IPC リスナー | ipcMain.eventNames() |
removeHandler の漏れ |
デバッグのヒント
メインプロセスのデバッグ
electron --inspect=5858 .
# Chrome で chrome://inspect を開いて接続
レンダラープロセスのデバッグ
if (process.argv.includes('--devtools')) {
mainWindow.webContents.openDevTools({ mode: 'detach' });
}
よくある落とし穴
落とし穴 1:macOS 公証の失敗
"notarization failed: The binary is not signed"
解決策:hardenedRuntime: true、正しい entitlements ファイル、Apple Developer ID 証明書の確認。
落とし穴 2:asar パッケージ後のネイティブモジュール読み込み失敗
# electron-builder.yml
asarUnpack:
- "node_modules/sharp/**"
- "node_modules/better-sqlite3/**"
- "native/**/*.node"
落とし穴 3:起動時の白画面が長すぎる
mainWindow.once('ready-to-show', () => { mainWindow.show(); });
関連ツール
- Base64 エンコード/デコード — インラインリソースとファイル転送の処理
- JSON フォーマッター — IPC メッセージと設定ファイルのデバッグ
- 画像圧縮 — アプリ内画像アセットの最適化
ブラウザローカルツールを無料で試す →