Section 1: フォルダ構成を最初に見よう【最重要】
Electronアプリを理解する最速の方法は、フォルダ構成から全体像をつかむこと。コードを読む前にまずここを見よう。
book_app/ ← アプリのルートフォルダ
│
├── main.js ← 🧠 メインプロセス(Node.js側)
│ ファイルの読み書き・シェル起動・
│ ウィンドウ作成を担当(963行)
│
├── package.json ← 📦 アプリの「設計書」
│ 使うライブラリと起動コマンドを定義
│
├── package-lock.json ← 🔒 ライブラリのバージョン固定
│ (自動生成・触らなくてOK)
│
├── icon.ico ← 🎨 Windowsタスクバーアイコン
│
├── src/
│ └── index.html ← 🖥️ レンダラープロセス(ブラウザ側)
│ 画面デザイン+ターミナルUIを担当(3163行)
│
└── node_modules/ ← 📚 インストール済みライブラリ
├── electron/ ← Electron本体
├── node-pty/ ← ターミナル制御
├── @xterm/xterm/ ← ターミナルUI描画
├── @xterm/addon-fit/ ← ターミナル自動リサイズ
└── @xterm/addon-web-links/ ← URL自動リンク化- • Node.js環境で動く
- • ファイル読み書き(fs)
- • シェル(PowerShell/Bash)起動
- • ウィンドウ作成(BrowserWindow)
- • IPCハンドラー40種以上
- • Chromiumブラウザで表示
- • HTML/CSSでレイアウト
- • xterm.jsでターミナルUI
- • JavaScriptでUI操作
- • アプリ名・バージョン定義
- • "main": "main.js" で起動ファイル指定
- • 依存ライブラリ一覧
- • 起動コマンド(scripts)
node_modules/ フォルダは絶対にGitにコミットしないこと! .gitignore に node_modules/ と書いておく。他のメンバーは npm install で再インストールできる。
Section 2: Electronとは3行で
- 1 ブラウザ(Chromium)= 画面を作る
- 2 サーバー(Node.js)= ファイルやOSを操作する
- 3 この2つをデスクトップアプリとして合体させたのがElectron
比較表
| 比較項目 | ブラウザWebアプリ | Node.jsサーバー | Electronアプリ |
|---|---|---|---|
| 動作環境 | ブラウザの中 | サーバー | ユーザーのPC |
| ファイル操作 | ❌ | ✅ | ✅ |
| 画面(HTML/CSS) | ✅ | ❌ | ✅ |
| OS操作 | ❌ | ✅ | ✅ |
| オフライン動作 | △ | ✅ | ✅ |
| 配布方法 | URLでアクセス | サーバー起動 | .exeインストール |
ElectronはChromiumとNode.jsを丸ごと内包しているため、アプリサイズが大きくなりやすい(最低でも100MB程度)。でもその分、一度インストールすれば外部依存なしで動く。
Section 3: package.jsonを読む
プロジェクトの「設計書」となるファイル。コメント付きで読んでみよう。
{
"name": "book-dashboard", // アプリ名(英小文字・ハイフンのみ)
"version": "1.0.0", // バージョン番号
"description": "書籍管理ダッシュボード",
"main": "main.js", // ← ここが起動ファイル!必須
"scripts": {
"dev": "electron .", // npm run dev で起動
"start": "electron ." // npm start でも起動
},
"dependencies": { // 実行時に必要なライブラリ
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"node-pty": "^1.0.0"
},
"devDependencies": { // 開発時だけ必要なライブラリ
"electron": "^33.2.1"
}
}パッケージ解説
| パッケージ | 役割 | なぜ必要? |
|---|---|---|
| electron | デスクトップアプリ基盤 | Chromium+Node.js本体 |
| node-pty | 疑似端末(PTY)作成 | 本物のシェルを起動するため |
| @xterm/xterm | ターミナルUI描画 | ブラウザ上でターミナルを表示 |
| @xterm/addon-fit | 自動リサイズ | ウィンドウサイズに追従 |
| @xterm/addon-web-links | URLリンク化 | ターミナル内URLをクリッカブルに |
"main": "main.js" がないと、Electronは何を起動すればいいかわからずエラーになる。絶対に忘れずに。
npm install # 依存関係をインストール(初回のみ) npm run dev # アプリを起動
Section 4: 2つのプロセスを理解する
Electronの最重要概念。メインプロセスとレンダラープロセスという2つの世界が存在する。
┌──────────────────────────────────────────────────────┐ │ Electronアプリ │ │ │ │ ┌───────────────────┐ ┌───────────────────────┐ │ │ │ メインプロセス │ │ レンダラープロセス │ │ │ │ (main.js) │◄──►│ (src/index.html) │ │ │ │ │IPC │ │ │ │ │ • Node.js で動く │ │ • Chromiumで動く │ │ │ │ • ファイル読み書き│ │ • HTML/CSS/JS │ │ │ │ • シェル起動 │ │ • ターミナルUI │ │ │ │ • ウィンドウ管理 │ │ • ユーザー操作 │ │ │ └───────────────────┘ └───────────────────────┘ │ │ │ │ 1つのアプリに「Node.js」と「ブラウザ」が共存! │ └──────────────────────────────────────────────────────┘
実際のmain.js冒頭(コメント付き)
// Electronから必要な機能を取り込む
const { app, BrowserWindow, ipcMain, shell } = require('electron');
const path = require('path');
const fs = require('fs'); // ファイル読み書き
const pty = require('node-pty'); // シェル起動
const os = require('os'); // OS情報取得
let mainWindow; // ウィンドウを変数に保持(後でIPC通信に使う)
let ptyProcess; // ターミナル(PTY)を変数に保持
// ウィンドウを作る関数
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400, // 横幅(ピクセル)
height: 900, // 縦幅(ピクセル)
title: 'Book Dashboard',
backgroundColor: '#1a1a2e', // ダーク背景(ちらつき防止)
webPreferences: {
nodeIntegration: true, // レンダラーでNode.jsを使う設定
contextIsolation: false, // セキュリティ分離を無効にする設定
}
});
mainWindow.loadFile('src/index.html'); // HTMLを読み込んで表示
}
// Electronが起動完了したらウィンドウを作る
app.whenReady().then(() => {
createWindow();
createPty(); // ターミナルも同時に起動
});
// 全ウィンドウが閉じられたらアプリ終了(Mac以外)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});nodeIntegration: true はレンダラー(HTML画面)側からファイルシステム等にアクセスできる便利な設定だが、セキュリティリスクがある。外部URLを表示しないローカルアプリ限定の設定。本格アプリでは preload.js + contextBridge 方式を使うこと。
app.whenReady() は Electron が OSへの登録を完了したタイミングで発火する。このタイミング前にウィンドウを作ろうとするとエラーになる。
Section 5: ターミナルUIの仕組み
Book Dashboardの核心機能。node-ptyとxterm.jsを組み合わせて「本物のシェルが動くターミナルUI」を実現している。
┌────────────────────────────────────────────────────────┐ │ ターミナル実装の仕組み │ │ │ │ 【node-pty】 IPC通信 【xterm.js】 │ │ メインプロセス側 レンダラー側 │ │ │ │ ┌──────────────────┐ terminal-data ┌─────────────┐ │ │ │ PowerShellを │ ──────────────► │ 文字を │ │ │ │ 起動・制御する │ │ 画面に表示 │ │ │ │ │ ◄────────────── │ │ │ │ │ コマンドを実行 │ terminal-input │ キー入力を │ │ │ │ して結果を返す │ │ 受け取る │ │ │ └──────────────────┘ └─────────────┘ │ │ ↑ │ │ 本物のシェルが動く │ └────────────────────────────────────────────────────────┘
Step 1: node-ptyでシェルを起動(main.js)
const pty = require('node-pty');
const os = require('os');
function createPty() {
// OSに合わせてシェルを選択
const shell = os.platform() === 'win32'
? 'powershell.exe' // Windows
: 'bash'; // Mac/Linux
// 疑似端末(PTY)を作成してシェルを起動
ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color', // カラー対応ターミナル
cols: 80, // 横幅(文字数)
rows: 30, // 縦幅(行数)
cwd: 'C:\\Users\\...\\book', // 起動ディレクトリ
env: process.env, // 環境変数をそのまま引き継ぐ
useConpty: false, // Windows: 古い互換モード(安定性向上)
});
// シェルの出力をレンダラーに転送
ptyProcess.onData((data) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('terminal-data', data);
}
});
}node-ptyはネイティブモジュール(C++のコンパイルが必要なライブラリ)。npm install 後にエラーが出たら Electron 用に再コンパイルが必要。
npm install --save-dev @electron/rebuild ./node_modules/.bin/electron-rebuild
Step 2: xterm.jsでターミナルUIを表示(src/index.html)
const { ipcRenderer, shell, clipboard } = require('electron');
const { Terminal } = require('@xterm/xterm');
const { FitAddon } = require('@xterm/addon-fit');
const { WebLinksAddon } = require('@xterm/addon-web-links');
// ターミナルインスタンスを作成
const terminal = new Terminal({
theme: {
background: '#1e1e1e', // VS Codeライクなダーク背景
foreground: '#d4d4d4', // 文字色(明るいグレー)
cursor: '#d4d4d4', // カーソル色
},
fontSize: 14,
fontFamily: 'Consolas, monospace', // 等幅フォント必須
cursorBlink: true, // カーソルを点滅させる
scrollback: 50000, // スクロールで遡れる行数
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon); // リサイズ対応
// URLをクリックでブラウザを開く
terminal.loadAddon(new WebLinksAddon((e, uri) => {
shell.openExternal(uri);
}));
// HTMLのdivにマウント
terminal.open(document.getElementById('terminal'));
fitAddon.fit(); // 親要素のサイズに自動フィットStep 3: IPC通信でつなぐ
// キー入力 → メインプロセス → シェルへ
terminal.onData((data) => {
ipcRenderer.send('terminal-input', data);
});
// シェルの出力 → ターミナルUIへ
ipcRenderer.on('terminal-data', (event, data) => {
terminal.write(data);
});Step 4: リサイズ対応(デバウンス付き)
// 連続リサイズ中に何度も処理しないよう50ms待ってから実行
let fitTimeout = null;
function debouncedFit() {
if (fitTimeout) clearTimeout(fitTimeout);
fitTimeout = setTimeout(() => {
fitAddon.fit(); // UIを親要素に合わせる
// シェル側にも新しいサイズを通知(これを忘れると折り返しがずれる)
ipcRenderer.send('terminal-resize', {
cols: terminal.cols,
rows: terminal.rows
});
}, 50);
}
window.addEventListener('resize', debouncedFit);
new ResizeObserver(debouncedFit).observe(document.getElementById('terminal'));fitAddon.fit() はxterm.jsの表示サイズをDOMに合わせるだけ。シェル側にも terminal-resize でサイズを伝えないと、コマンドの折り返し位置がずれる。2つセットで実行すること。
Section 6: IPC通信の2つのパターン
メインプロセスとレンダラープロセスは直接関数を呼び合えない。IPC(Inter-Process Communication)というメッセージのやり取りで通信する。
パターン1: send / on(返値なし・速い)
// ===== レンダラー側(index.html) =====
ipcRenderer.send('terminal-input', 'ls -la'); // 送るだけ・返値なし
// ===== メイン側(main.js) =====
ipcMain.on('terminal-input', (event, data) => {
ptyProcess.write(data); // シェルにコマンドを書き込む
});パターン2: invoke / handle(返値あり・Promise)
// ===== レンダラー側(index.html) =====
const projects = await ipcRenderer.invoke('load-projects'); // awaitで待つ
// ===== メイン側(main.js) =====
ipcMain.handle('load-projects', () => {
const data = fs.readFileSync('projects.json', 'utf-8');
return JSON.parse(data); // returnした値がレンダラーのawaitに届く
});使い分け
| パターン | 使う場面 | 特徴 |
|---|---|---|
| send / on | ターミナル入力、リサイズ通知 | 返値なし・速い |
| invoke / handle | ファイル読み込み・データ保存 | 返値あり・Promise |
このアプリの主要IPCハンドラー一覧
| チャンネル名 | 種類 | 説明 |
|---|---|---|
| terminal-input | send/on | キー入力をシェルへ転送 |
| terminal-resize | send/on | ターミナルサイズ変更をシェルへ通知 |
| terminal-data | メイン→レンダラー | シェル出力をUIへ転送 |
| load-projects | invoke | 書籍プロジェクト一覧を読み込む |
| create-new-book | invoke | 新規書籍フォルダ+ファイルを作成 |
| run-ai-generate | invoke | ClaudeへのAI生成指示を送信 |
| count-words | invoke | Markdown文字数カウント |
| open-folder | invoke | エクスプローラーでフォルダを開く |
| convert-for-notebooklm | invoke | MD→NotebookLM形式に変換 |
合計40種類以上
チャンネル名('terminal-input'等)はただの文字列。スペルミスしてもエラーが出ないので注意。送信側と受信側のチャンネル名が一致していることを確認しよう。
Section 7: Bracket Paste Mode — 長文を安全に送る
AIに長いプロンプトを送るとき、普通にテキストを流し込むと途中で誤実行されることがある。これを防ぐ技法。
// ❌ 普通の方法:長いプロンプトが途中で誤認識される場合がある
ptyProcess.write(longPrompt + '\r');
// ✅ Bracket Paste Modeで包む(推奨)
// \x1b[200~ = 「ここから貼り付け開始」のエスケープシーケンス
// \x1b[201~ = 「貼り付け終了」のエスケープシーケンス
// \r = Enterキー
ptyProcess.write('\x1b[200~' + longPrompt + '\x1b[201~\r');\x1b はエスケープ文字(ESC)。\x1b[200~ のような文字列を「エスケープシーケンス」と呼ぶ。Bracket Paste Modeは現代的なシェルがサポートしている標準機能で、多行テキストを安全に貼り付けられる。
Section 8: UIレイアウト設計
デスクトップアプリらしい「スクロールなし・全画面」UIの作り方。
┌──────────────────────────────────────────────────────┐ │ HEADER(固定) │ │ 統計 | [Claude起動] [Sonnet起動] [Opus起動] [新規作成]│ ├──────────────┬───────────────────────────────────────┤ │ │ [ターミナル] [詳細] [コメント](タブ) │ │ SIDEBAR ├───────────────────────────────────────┤ │ (300px固定)│ ターミナルタブ: │ │ │ ┌─────────────────────────────────┐ │ │ ・フィルタ │ │ xterm.js ターミナル │ │ │ ・書籍リスト│ │ │ │ │ (縦スクロール)│ │ PS C:\book> claude │ │ │ │ │ ▌ │ │ │ │ └─────────────────────────────────┘ │ └──────────────┴───────────────────────────────────────┘
主要CSS
body {
display: flex;
flex-direction: column;
height: 100vh; /* 画面全体の高さを使う */
overflow: hidden; /* スクロールバーを消す */
}
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 300px; overflow-y: auto; } /* 縦スクロール可能 */
.content { flex: 1; display: flex; flex-direction: column; }
#terminal-panel { flex: 1; overflow: hidden; } /* xtermが内部でスクロール管理 */Section 9: Ctrl+C/V のカスタマイズ
ターミナルのCtrl+CはSIGINT(プロセス中断)として機能するが、テキストをコピーしたい場合もある。条件分岐でうまく切り分ける。
terminal.attachCustomKeyEventHandler((e) => {
if (e.ctrlKey && e.key === 'c') {
const selected = terminal.getSelection();
if (selected) {
clipboard.writeText(selected); // 選択テキストをコピー
terminal.clearSelection();
return false; // xterm.jsのデフォルト処理をキャンセル
}
return true; // 選択なし → SIGINT(プロセス中断)としてシェルに通す
}
if (e.ctrlKey && e.key === 'v') {
const text = clipboard.readText();
if (text) ipcRenderer.send('terminal-input', text);
return false;
}
return true;
});return false で xterm.js のデフォルト処理をキャンセル、return true で通す。この切り分けで「コピーしたいとき」と「SIGINTを送りたいとき」を分けられる。
Section 10: まとめ
Electronアプリ開発の流れと重要概念を整理する。
開発の流れ
- 1
package.jsonを作り"main": "main.js"を指定する - 2
main.jsでBrowserWindowを作り HTMLを読み込む - 3 必要なら
node-ptyでシェルを起動する - 4
src/index.htmlでxterm.jsを使いターミナルUIを作る - 5
ipcMain/ipcRendererで2つのプロセスをつなぐ - 6
npm run devで起動
重要概念振り返り
| 概念 | 一言説明 |
|---|---|
| メインプロセス | Node.jsが動くOS制御側(main.js) |
| レンダラープロセス | Chromiumが動く画面表示側(index.html) |
| IPC通信 | 2プロセス間のメッセージやり取り |
| node-pty | 本物のシェルを起動・制御するライブラリ |
| xterm.js | ターミナルの画面を描画するライブラリ |
| Bracket Paste Mode | 長文を安全にシェルに送る技法 |
参考リンク
- Electron公式ドキュメント — electronjs.org/docs/latest/
- Zenn「Electron入門」— sprout2000著
- Qiita「たぶん入門者が本当に知りたいことだけをまとめたElectron入門」