Electron アーキテクチャ超図解:Main/Renderer/Preload の役割とデータフロー
デスクトップアプリケーション開発において、Electron は非常に人気のあるフレームワークです。しかし、Main プロセス、Renderer プロセス、Preload スクリプトという 3 つの要素がどのように連携するのか、初めて学ぶ方には少し複雑に感じられるかもしれません。
この記事では、Electron のアーキテクチャを図解を交えながら、それぞれの役割とデータの流れを詳しく解説していきます。これを読めば、Electron アプリケーションの設計思想が手に取るようにわかるでしょう。
背景
Electron が生まれた理由
Electron は、Web 技術(HTML、CSS、JavaScript)を使ってデスクトップアプリケーションを開発できるフレームワークです。Visual Studio Code、Slack、Discord など、多くの有名アプリケーションが Electron で構築されています。
従来、デスクトップアプリケーションを開発するには、プラットフォームごとに異なる言語や技術が必要でした。Windows 向けには C#や C++、Mac 向けには Swift、Linux 向けには C++といった具合です。しかし Electron を使えば、Web 技術だけでクロスプラットフォームなアプリケーションを作成できるようになりました。
Electron の基本構造
Electron は、Chromium と Node.js を組み合わせた構造になっています。Chromium はブラウザエンジンとして画面の描画を担当し、Node.js はファイルシステムへのアクセスやネットワーク通信などのバックエンド処理を担当しますね。
以下の図は、Electron アプリケーションの全体像を示しています。
mermaidflowchart TB
subgraph electron["Electron アプリケーション"]
main["Main プロセス<br/>(Node.js 環境)"]
subgraph window1["ウィンドウ 1"]
preload1["Preload スクリプト"]
renderer1["Renderer プロセス<br/>(Chromium 環境)"]
end
subgraph window2["ウィンドウ 2"]
preload2["Preload スクリプト"]
renderer2["Renderer プロセス<br/>(Chromium 環境)"]
end
end
main -->|"BrowserWindow 生成"| window1
main -->|"BrowserWindow 生成"| window2
preload1 -.->|"contextBridge"| renderer1
preload2 -.->|"contextBridge"| renderer2
renderer1 <-->|"IPC 通信"| main
renderer2 <-->|"IPC 通信"| main
図の要点:Main プロセスが中心となり、複数のウィンドウ(Renderer プロセス)を管理し、各ウィンドウに Preload スクリプトが橋渡し役として機能しています。
プロセス分離の必要性
なぜ Electron はプロセスを分離する設計になっているのでしょうか。
これはセキュリティと安定性を確保するためです。Web ページ(Renderer プロセス)から直接ファイルシステムや OS の機能にアクセスできてしまうと、悪意のあるコードによってユーザーのシステムが危険にさらされる可能性があります。
そのため、Electron では権限を持つ Main プロセスと、制限された Renderer プロセスを分離し、両者の間で安全な通信を行う仕組みを採用しているのです。
課題
セキュリティリスクの管理
Electron アプリケーション開発における最大の課題は、セキュリティの確保です。Web 技術を使う以上、クロスサイトスクリプティング(XSS)やコードインジェクションなどの脅威にさらされます。
特に以下のような問題が発生しやすいです。
| # | 課題 | リスク内容 |
|---|---|---|
| 1 | nodeIntegration の有効化 | Renderer から直接 Node.js API にアクセス可能になり、任意のコード実行のリスク |
| 2 | contextIsolation の無効化 | Preload スクリプトと Renderer が同じコンテキストを共有し、情報漏洩のリスク |
| 3 | IPC 通信の不適切な実装 | 入力検証の不足により、予期しない動作や権限昇格のリスク |
以下の図は、セキュリティ設定が不適切な場合の危険なデータフローを示しています。
mermaidflowchart LR
web["外部Webコンテンツ"] -->|"XSS攻撃"| renderer["Renderer プロセス"]
renderer -->|"nodeIntegration: true<br/>制限なしアクセス"| nodejs["Node.js API"]
nodejs -->|"ファイル削除<br/>情報窃取"| os["OS リソース"]
style web fill:#ff6b6b
style nodejs fill:#ff6b6b
style os fill:#ff6b6b
図の要点:nodeIntegration を有効にすると、Renderer プロセスが直接 Node.js API にアクセスでき、セキュリティホールとなります。
プロセス間通信の複雑さ
Main プロセスと Renderer プロセスが分離されているため、両者間でデータをやり取りする際に IPC(Inter-Process Communication)という仕組みを使う必要があります。
この仕組みは強力ですが、以下のような複雑さを伴います。
初めて実装する際には、以下のような疑問が湧いてくるでしょう。
- どのプロセスでどのコードを書けばいいのか
- データはどのように送受信すればいいのか
- 非同期処理をどう扱えばいいのか
コードの役割分担の不明確さ
Electron アプリケーションでは、Main、Renderer、Preload の 3 つの実行環境があるため、どこに何を書くべきか迷うことがあります。
特に以下のような機能を実装する際に悩みやすいです。
- ファイルの読み書き
- データベースへのアクセス
- 外部 API との通信
- ウィンドウの制御
これらの役割分担を正しく理解していないと、メンテナンスしにくいコードになってしまいます。
解決策
Main プロセスの役割と実装
Main プロセスは、Electron アプリケーションの中核となる部分です。Node.js 環境で動作し、アプリケーションのライフサイクルを管理します。
Main プロセスの主な責務
| # | 責務 | 具体例 |
|---|---|---|
| 1 | アプリケーションライフサイクル管理 | 起動、終了、ウィンドウの生成・破棄 |
| 2 | ウィンドウ管理 | BrowserWindow の生成、サイズ変更、最小化・最大化 |
| 3 | システムリソースへのアクセス | ファイルシステム、ネイティブダイアログ、メニュー |
| 4 | IPC 通信のハンドリング | Renderer からのリクエスト処理、イベント発火 |
まず、基本的な Main プロセスのエントリーポイントを見ていきましょう。
typescript// main.ts - アプリケーションのエントリーポイント
import { app, BrowserWindow } from 'electron';
import * as path from 'path';
// アプリケーションの準備完了時に実行
app.whenReady().then(() => {
createWindow();
});
このコードでは、Electron アプリケーションが起動準備を完了したタイミングでウィンドウを生成しています。app.whenReady()は Promise を返すため、非同期処理として扱えますね。
次に、ウィンドウを生成する関数を実装します。
typescript// main.ts - ウィンドウ生成関数
function createWindow(): void {
// メインウィンドウの作成
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// セキュリティのための重要な設定
nodeIntegration: false, // Rendererから直接Node.jsを使わせない
contextIsolation: true, // PreloadとRendererのコンテキストを分離
preload: path.join(__dirname, 'preload.js'), // Preloadスクリプトのパス
},
});
// HTMLファイルを読み込み
mainWindow.loadFile('index.html');
}
webPreferencesの設定が非常に重要です。nodeIntegration: falseとcontextIsolation: trueにより、Renderer プロセスからの不正なアクセスを防ぎます。
続いて、アプリケーションのライフサイクルを管理するコードを追加しましょう。
typescript// main.ts - ライフサイクル管理
// すべてのウィンドウが閉じられたときの処理
app.on('window-all-closed', () => {
// macOS以外ではアプリケーションを終了
if (process.platform !== 'darwin') {
app.quit();
}
});
// アプリがアクティブになったときの処理(macOS向け)
app.on('activate', () => {
// ウィンドウがない場合は新規作成
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
macOS では、すべてのウィンドウが閉じられてもアプリケーションは終了しないという慣習があるため、プラットフォームごとに挙動を変えています。
Renderer プロセスの役割と実装
Renderer プロセスは、ユーザーインターフェースを描画する部分です。Chromium 環境で動作し、通常の Web ページと同じように HTML、CSS、JavaScript を使えます。
Renderer プロセスの主な責務
| # | 責務 | 具体例 |
|---|---|---|
| 1 | UI の描画とレンダリング | HTML/CSS による画面表示 |
| 2 | ユーザー操作の処理 | クリック、入力、ドラッグ&ドロップ |
| 3 | Main プロセスへのリクエスト | IPC 経由でのデータ取得・保存要求 |
| 4 | 受信データの表示 | Main から受け取ったデータを UI に反映 |
まず、基本的な HTML ファイルを作成します。
html<!-- index.html - Rendererプロセスで表示されるHTML -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Electron App</title>
</head>
<body>
<h1>Electronアプリケーション</h1>
<button id="readFileBtn">ファイルを読み込む</button>
<div id="fileContent"></div>
<!-- Rendererプロセスのスクリプト -->
<script src="renderer.js"></script>
</body>
</html>
Content Security Policy を設定することで、外部からのスクリプト実行を防ぎ、セキュリティを強化しています。
次に、Renderer プロセスで動作する JavaScript を実装します。
typescript// renderer.ts - Rendererプロセスのスクリプト
// PreloadスクリプトでwindowオブジェクトにセットされたAPIを使用
const { electronAPI } = window as any;
// ボタンクリック時の処理
document
.getElementById('readFileBtn')
?.addEventListener('click', async () => {
try {
// Mainプロセスにファイル読み込みを依頼
const content = await electronAPI.readFile(
'sample.txt'
);
// 取得したコンテンツを表示
const contentDiv =
document.getElementById('fileContent');
if (contentDiv) {
contentDiv.textContent = content;
}
} catch (error) {
console.error('ファイル読み込みエラー:', error);
}
});
Renderer プロセスでは、直接 Node.js の API を呼び出すことはできません。代わりに、Preload スクリプトで公開された API を通じて Main プロセスと通信します。
UI の状態を管理する処理も追加してみましょう。
typescript// renderer.ts - UI状態管理
// Mainプロセスからの通知を受け取る
electronAPI.onFileUpdate((fileName: string) => {
console.log(`ファイルが更新されました: ${fileName}`);
// UIに通知を表示
const notification = document.createElement('div');
notification.className = 'notification';
notification.textContent = `${fileName} が更新されました`;
document.body.appendChild(notification);
// 3秒後に通知を削除
setTimeout(() => {
notification.remove();
}, 3000);
});
この仕組みにより、Main プロセスからの双方向通信が可能になります。
Preload スクリプトの役割と実装
Preload スクリプトは、Main プロセスと Renderer プロセスの橋渡し役として機能します。Node.js 環境で動作しながら、Renderer プロセスのコンテキストにアクセスできる特殊な位置づけです。
Preload スクリプトの主な責務
| # | 責務 | 具体例 |
|---|---|---|
| 1 | 安全な API の公開 | contextBridge で Renderer に必要最小限の API を提供 |
| 2 | IPC 通信の抽象化 | ipcRenderer を直接公開せず、ラップした関数を提供 |
| 3 | データの検証とサニタイズ | Renderer から受け取ったデータの妥当性確認 |
以下の図は、Preload スクリプトがどのように機能するかを示しています。
mermaidflowchart LR
renderer["Renderer プロセス"] -->|"window.electronAPI"| bridge["contextBridge"]
bridge -->|"expose API"| preload["Preload スクリプト"]
preload -->|"ipcRenderer.invoke"| main["Main プロセス"]
main -->|"ipcMain.handle"| handler["ハンドラ関数"]
handler -->|"return value"| main
main -->|"response"| preload
preload -->|"return"| renderer
style bridge fill:#4ecdc4
style preload fill:#4ecdc4
図の要点:contextBridge が Renderer と Preload の間で安全に API を公開し、ipcRenderer を通じて Main プロセスと通信します。
それでは、Preload スクリプトの実装を見ていきましょう。
typescript// preload.ts - 基本構造とインポート
import { contextBridge, ipcRenderer } from 'electron';
// Rendererプロセスに公開するAPIの型定義
interface ElectronAPI {
readFile: (fileName: string) => Promise<string>;
writeFile: (
fileName: string,
content: string
) => Promise<void>;
onFileUpdate: (
callback: (fileName: string) => void
) => void;
}
型定義により、TypeScript の恩恵を受けながら安全に API を設計できます。
次に、contextBridge を使って API を公開します。
typescript// preload.ts - contextBridgeでAPIを公開
contextBridge.exposeInMainWorld('electronAPI', {
// ファイル読み込みAPI
readFile: (fileName: string): Promise<string> => {
// Mainプロセスの'read-file'ハンドラを呼び出し
return ipcRenderer.invoke('read-file', fileName);
},
// ファイル書き込みAPI
writeFile: (
fileName: string,
content: string
): Promise<void> => {
// Mainプロセスの'write-file'ハンドラを呼び出し
return ipcRenderer.invoke(
'write-file',
fileName,
content
);
},
// Mainプロセスからの通知を受け取るAPI
onFileUpdate: (
callback: (fileName: string) => void
): void => {
// 'file-updated'イベントをリスン
ipcRenderer.on('file-updated', (_event, fileName) => {
callback(fileName);
});
},
} as ElectronAPI);
contextBridge.exposeInMainWorldにより、Renderer プロセスのwindowオブジェクトに安全に API が追加されます。ipcRendererを直接公開しないことで、セキュリティが確保されますね。
IPC 通信の実装
IPC は、Main プロセスと Renderer プロセス間でデータをやり取りするための仕組みです。主に以下の 2 つの方法があります。
| # | 方法 | 用途 | 特徴 |
|---|---|---|---|
| 1 | invoke / handle | 非同期リクエスト-レスポンス | 戻り値が必要な処理(ファイル読み込みなど) |
| 2 | send / on | イベント送信 | 一方向の通知(進捗更新など) |
まず、Main プロセス側で IPC ハンドラを実装します。
typescript// main.ts - IPC ハンドラの実装(インポート部分)
import { app, BrowserWindow, ipcMain } from 'electron';
import * as fs from 'fs/promises';
import * as path from 'path';
必要なモジュールをインポートしています。fs/promisesを使うことで、非同期処理が簡潔に書けます。
次に、ファイル読み込みのハンドラを実装します。
typescript// main.ts - ファイル読み込みハンドラ
// アプリ起動時にIPCハンドラを登録
app.whenReady().then(() => {
// 'read-file'リクエストのハンドラ
ipcMain.handle(
'read-file',
async (_event, fileName: string) => {
try {
// ファイルパスの検証(セキュリティ対策)
const safePath = path.join(
app.getPath('userData'),
fileName
);
// ファイル内容を読み込んで返す
const content = await fs.readFile(
safePath,
'utf-8'
);
return content;
} catch (error) {
console.error('ファイル読み込みエラー:', error);
throw new Error(
`ファイル読み込み失敗: ${fileName}`
);
}
}
);
});
app.getPath('userData')を使うことで、アプリケーション専用のディレクトリ内に限定し、任意のパスへのアクセスを防いでいます。
ファイル書き込みのハンドラも実装しましょう。
typescript// main.ts - ファイル書き込みハンドラ
ipcMain.handle(
'write-file',
async (_event, fileName: string, content: string) => {
try {
// ファイルパスの検証
const safePath = path.join(
app.getPath('userData'),
fileName
);
// 入力値の検証
if (typeof content !== 'string') {
throw new Error(
'コンテンツは文字列である必要があります'
);
}
// ファイルに書き込み
await fs.writeFile(safePath, content, 'utf-8');
// すべてのウィンドウに更新通知を送信
BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send('file-updated', fileName);
});
} catch (error) {
console.error('ファイル書き込みエラー:', error);
throw new Error(`ファイル書き込み失敗: ${fileName}`);
}
}
);
ファイル書き込み後、すべてのウィンドウに通知を送ることで、複数ウィンドウ間でのデータ同期が実現できます。
以下は、双方向の IPC 通信フローを示した図です。
mermaidsequenceDiagram
participant R as Renderer
participant P as Preload
participant M as Main
participant FS as FileSystem
R->>P: electronAPI.readFile('sample.txt')
P->>M: ipcRenderer.invoke('read-file', 'sample.txt')
M->>M: パス検証
M->>FS: fs.readFile()
FS-->>M: ファイル内容
M-->>P: return content
P-->>R: return content
R->>R: UIに表示
Note over R,FS: 書き込み時の通知フロー
R->>P: electronAPI.writeFile('sample.txt', 'new content')
P->>M: ipcRenderer.invoke('write-file', ...)
M->>FS: fs.writeFile()
FS-->>M: 完了
M->>R: send('file-updated', 'sample.txt')
R->>R: 通知表示
図の要点:リクエスト-レスポンスの流れと、書き込み後の通知がどのように伝播するかが明確になっています。
具体例
ファイル管理アプリケーションの実装
ここでは、実際にファイルの一覧表示、読み込み、書き込み機能を持つシンプルなアプリケーションを実装してみましょう。
まず、プロジェクトの構造を確認します。
perlmy-electron-app/
├── src/
│ ├── main.ts # Mainプロセス
│ ├── preload.ts # Preloadスクリプト
│ ├── renderer.ts # Rendererプロセス
│ └── index.html # UIのHTML
├── package.json
└── tsconfig.json
それでは、Main プロセスに複数の機能を追加していきます。
typescript// main.ts - ファイル一覧取得機能
import {
app,
BrowserWindow,
ipcMain,
dialog,
} from 'electron';
import * as fs from 'fs/promises';
import * as path from 'path';
// ファイル一覧を取得するハンドラ
ipcMain.handle('list-files', async () => {
try {
const userDataPath = app.getPath('userData');
const files = await fs.readdir(userDataPath);
// ファイル情報を詳細に取得
const fileInfos = await Promise.all(
files.map(async (fileName) => {
const filePath = path.join(userDataPath, fileName);
const stats = await fs.stat(filePath);
return {
name: fileName,
size: stats.size,
modified: stats.mtime,
isDirectory: stats.isDirectory(),
};
})
);
return fileInfos;
} catch (error) {
console.error('ファイル一覧取得エラー:', error);
throw new Error('ファイル一覧の取得に失敗しました');
}
});
Promise.allを使うことで、複数のファイル情報を並列で取得し、パフォーマンスを向上させています。
次に、ファイル選択ダイアログを表示する機能を追加します。
typescript// main.ts - ファイル選択ダイアログ
ipcMain.handle('select-file', async (event) => {
// ダイアログを開く
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{
name: 'テキストファイル',
extensions: ['txt', 'md'],
},
{ name: 'すべてのファイル', extensions: ['*'] },
],
});
// キャンセルされた場合
if (result.canceled) {
return null;
}
// 選択されたファイルのパス
const filePath = result.filePaths[0];
// ファイル内容を読み込んで返す
const content = await fs.readFile(filePath, 'utf-8');
return {
path: filePath,
name: path.basename(filePath),
content: content,
};
});
ネイティブのファイル選択ダイアログを使うことで、OS に統合された自然な UI を提供できます。
Preload スクリプトに新しい API を追加しましょう。
typescript// preload.ts - 拡張されたAPI定義
interface FileInfo {
name: string;
size: number;
modified: Date;
isDirectory: boolean;
}
interface SelectedFile {
path: string;
name: string;
content: string;
}
interface ElectronAPI {
// 既存のAPI
readFile: (fileName: string) => Promise<string>;
writeFile: (
fileName: string,
content: string
) => Promise<void>;
onFileUpdate: (
callback: (fileName: string) => void
) => void;
// 新しいAPI
listFiles: () => Promise<FileInfo[]>;
selectFile: () => Promise<SelectedFile | null>;
}
型定義を明確にすることで、Renderer プロセスでの実装時に補完が効き、ミスを減らせますね。
contextBridge で新しい API を公開します。
typescript// preload.ts - 新しいAPIの公開
contextBridge.exposeInMainWorld('electronAPI', {
// 既存のAPI(省略)
// ファイル一覧取得
listFiles: (): Promise<FileInfo[]> => {
return ipcRenderer.invoke('list-files');
},
// ファイル選択ダイアログ
selectFile: (): Promise<SelectedFile | null> => {
return ipcRenderer.invoke('select-file');
},
} as ElectronAPI);
次に、Renderer プロセスでこれらの機能を使って UI を実装します。
html<!-- index.html - UIの拡張 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>ファイル管理アプリ</title>
<style>
body {
font-family: sans-serif;
padding: 20px;
}
.file-list {
margin: 20px 0;
}
.file-item {
padding: 10px;
border-bottom: 1px solid #ddd;
}
.file-item:hover {
background-color: #f0f0f0;
}
button {
margin: 5px;
padding: 10px 20px;
}
#editor {
width: 100%;
height: 300px;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>ファイル管理アプリケーション</h1>
<div>
<button id="listFilesBtn">ファイル一覧を更新</button>
<button id="selectFileBtn">ファイルを開く</button>
<button id="saveFileBtn">保存</button>
</div>
<div id="fileList" class="file-list"></div>
<textarea
id="editor"
placeholder="ここにテキストを入力..."
></textarea>
<script src="renderer.js"></script>
</body>
</html>
シンプルながら必要な機能がすべて揃った UI です。
Renderer プロセスでファイル一覧表示機能を実装します。
typescript// renderer.ts - ファイル一覧表示
const { electronAPI } = window as any;
// ファイル一覧を表示する関数
async function displayFileList(): Promise<void> {
try {
const files = await electronAPI.listFiles();
const fileListDiv = document.getElementById('fileList');
if (!fileListDiv) return;
// ファイル一覧をHTMLに変換
fileListDiv.innerHTML = files
.filter((file: any) => !file.isDirectory)
.map((file: any) => {
const sizeKB = (file.size / 1024).toFixed(2);
const modified = new Date(
file.modified
).toLocaleString('ja-JP');
return `
<div class="file-item">
<strong>${file.name}</strong><br>
サイズ: ${sizeKB} KB | 更新: ${modified}
</div>
`;
})
.join('');
} catch (error) {
console.error('ファイル一覧表示エラー:', error);
}
}
// ボタンクリック時にファイル一覧を更新
document
.getElementById('listFilesBtn')
?.addEventListener('click', displayFileList);
// 初期表示
displayFileList();
ファイル情報を見やすくフォーマットして表示しています。
ファイル選択と編集機能も実装しましょう。
typescript// renderer.ts - ファイル選択と編集
let currentFilePath: string | null = null;
// ファイル選択ボタンのクリックハンドラ
document
.getElementById('selectFileBtn')
?.addEventListener('click', async () => {
try {
const file = await electronAPI.selectFile();
if (file) {
// エディタにファイル内容を表示
const editor = document.getElementById(
'editor'
) as HTMLTextAreaElement;
if (editor) {
editor.value = file.content;
currentFilePath = file.path;
}
console.log(`ファイルを開きました: ${file.name}`);
}
} catch (error) {
console.error('ファイル選択エラー:', error);
}
});
選択したファイルの内容をエディタに読み込み、編集可能にしています。
最後に、保存機能を実装します。
typescript// renderer.ts - ファイル保存
document
.getElementById('saveFileBtn')
?.addEventListener('click', async () => {
try {
const editor = document.getElementById(
'editor'
) as HTMLTextAreaElement;
if (!editor || !currentFilePath) {
alert('ファイルが選択されていません');
return;
}
// エディタの内容を取得
const content = editor.value;
const fileName =
currentFilePath.split('/').pop() || 'file.txt';
// ファイルに書き込み
await electronAPI.writeFile(fileName, content);
alert('保存しました');
// ファイル一覧を更新
await displayFileList();
} catch (error) {
console.error('ファイル保存エラー:', error);
alert('保存に失敗しました');
}
});
これで基本的なファイル管理アプリケーションが完成しました。
アーキテクチャ全体のデータフロー
ここまで実装してきた機能全体のデータフローを図で確認しましょう。
mermaidflowchart TB
subgraph ui["UI層 (Renderer)"]
button["ボタンクリック"]
editor["テキストエディタ"]
list["ファイル一覧"]
end
subgraph api["API層 (Preload)"]
bridge["contextBridge API"]
validate["データ検証"]
end
subgraph logic["ロジック層 (Main)"]
ipc["IPC ハンドラ"]
business["ビジネスロジック"]
end
subgraph resource["リソース層"]
fs["ファイルシステム"]
dialog["ネイティブダイアログ"]
os["OS API"]
end
button --> bridge
bridge --> validate
validate --> ipc
ipc --> business
business --> fs
business --> dialog
business --> os
fs -.->|"データ返却"| business
dialog -.->|"ユーザー選択"| business
business -.->|"レスポンス"| ipc
ipc -.->|"結果"| bridge
bridge -.->|"データ"| editor
bridge -.->|"データ"| list
図の要点:各層が明確に分離され、データが一方向に流れることで、保守性の高いアーキテクチャが実現されています。
セキュリティのベストプラクティス
実装したアプリケーションにセキュリティ対策を追加しましょう。
typescript// main.ts - セキュリティ強化されたウィンドウ設定
function createSecureWindow(): BrowserWindow {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// 必須のセキュリティ設定
nodeIntegration: false, // Node.js統合を無効化
contextIsolation: true, // コンテキスト分離を有効化
sandbox: true, // サンドボックスを有効化
// 追加のセキュリティ設定
webSecurity: true, // 同一オリジンポリシーを有効化
allowRunningInsecureContent: false, // 混在コンテンツを禁止
preload: path.join(__dirname, 'preload.js'),
},
});
// Content Security Policy を設定
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
],
},
});
}
);
return mainWindow;
}
これらの設定により、XSS 攻撃やコードインジェクションのリスクを大幅に軽減できます。
Preload スクリプトでの入力検証も重要です。
typescript// preload.ts - 入力検証付きAPI
contextBridge.exposeInMainWorld('electronAPI', {
writeFile: (
fileName: string,
content: string
): Promise<void> => {
// ファイル名の検証
if (!fileName || typeof fileName !== 'string') {
return Promise.reject(
new Error('無効なファイル名です')
);
}
// パストラバーサル攻撃を防ぐ
if (
fileName.includes('..') ||
fileName.includes('/') ||
fileName.includes('\\')
) {
return Promise.reject(
new Error('ファイル名に不正な文字が含まれています')
);
}
// コンテンツの検証
if (typeof content !== 'string') {
return Promise.reject(
new Error('コンテンツは文字列である必要があります')
);
}
// サイズ制限(例: 10MB)
if (content.length > 10 * 1024 * 1024) {
return Promise.reject(
new Error('ファイルサイズが大きすぎます')
);
}
return ipcRenderer.invoke(
'write-file',
fileName,
content
);
},
});
クライアント側での検証は必須です。悪意のあるデータが Main プロセスに到達する前にブロックできますね。
以下は、セキュリティが確保されたアーキテクチャの図です。
mermaidflowchart TB
subgraph secure["セキュアな境界"]
renderer["Renderer<br/>(制限された環境)"]
subgraph isolation["コンテキスト分離"]
preload["Preload<br/>(検証層)"]
end
main["Main<br/>(特権環境)"]
end
subgraph checks["セキュリティチェック"]
csp["CSP ヘッダー"]
validate["入力検証"]
sanitize["パス検証"]
end
renderer -->|"contextBridge のみ"| preload
csp -.->|"保護"| renderer
preload -->|"検証済みデータ"| validate
validate --> sanitize
sanitize --> main
style isolation fill:#90EE90
style checks fill:#FFD700
図の要点:各層でセキュリティチェックを行い、多層防御によって安全性を確保しています。
まとめ
Electron アーキテクチャの核心は、Main、Renderer、Preload という 3 つのプロセス間の役割分担とデータフローにあります。
Main プロセスは、Node.js 環境で動作し、アプリケーションのライフサイクル管理、ウィンドウ制御、システムリソースへのアクセスを担当します。特権を持つプロセスとして、セキュリティ上重要な処理を一手に引き受けますね。
Renderer プロセスは、Chromium 環境で動作し、ユーザーインターフェースの描画とユーザー操作の処理を担います。セキュリティのために制限された環境で動作し、システムリソースへの直接アクセスはできません。
Preload スクリプトは、両者の橋渡し役として機能し、contextBridge を通じて安全な API を Renderer に公開します。この層で入力検証を行うことで、セキュリティが大幅に向上します。
IPC 通信により、プロセス間で安全にデータをやり取りでき、invoke/handleパターンで非同期リクエスト-レスポンスを、send/onパターンでイベント通知を実現できました。
セキュリティのベストプラクティスとして、nodeIntegration: false、contextIsolation: true、sandbox: trueの設定は必須です。さらに、Content Security Policy の適用、入力検証、パストラバーサル対策も欠かせません。
このアーキテクチャを理解すれば、保守性が高く、セキュアな Electron アプリケーションを設計できるようになるでしょう。
関連リンク
articleElectron アーキテクチャ超図解:Main/Renderer/Preload の役割とデータフロー
articleElectron 運用:コード署名・公証・アップデート鍵管理のベストプラクティス
articleElectron オフライン帳票・PDF 生成を Headless Chromium で実装
articleElectron スクリーンレコーダー/キャプチャツールを desktopCapturer で作る
articleElectron クリーンアーキテクチャ設計:ドメインと UI を IPC で疎結合に
articleElectron セキュリティ設定チートシート:webPreferences/CSP/許可リスト早見表
articleJotai でフォームを分割統治:フィールド粒度の atom 設計と検証戦略
articleElectron アーキテクチャ超図解:Main/Renderer/Preload の役割とデータフロー
articleJest の ESM/NodeNext 設定完全ガイド:transformIgnorePatterns と resolver 設計
articleDocker イメージ署名と検証:cosign でサプライチェーンを防衛する運用手順
articleGitHub Copilot Enterprise 初期構築:SSO/SCIM・ポリシー・配布ロールアウト設計
articleDevin が強い開発フェーズはどこ?要件定義~運用までの適合マップ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来