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(); });

関連ツール

ブラウザローカルツールを無料で試す →

#Electron#桌面应用#跨平台#Node.js#教程