Electron IPC 設計チートシート:チャネル命名・型安全・エラーハンドリング定型
Electron アプリケーション開発において、メインプロセスとレンダラープロセス間の通信(IPC)は中核的な機能です。しかし、チャネル名の命名が統一されていなかったり、型安全性が確保されていなかったりすると、バグの温床になってしまいます。本記事では、実務で即活用できる IPC 設計の定型パターンをチートシート形式でご紹介します。チャネル命名規則から型安全な実装、堅牢なエラーハンドリングまで、具体的なコード例とともに解説していきますので、ぜひ最後までご覧ください。
早見表
チャネル命名規則早見表
| # | 分類 | 命名パターン | 例 | 用途 |
|---|---|---|---|---|
| 1 | 要求・応答 | 操作:対象:動詞 | window:main:minimize | メインプロセスへの操作要求 |
| 2 | データ取得 | get:対象:詳細 | get:user:profile | データ取得リクエスト |
| 3 | データ送信 | send:対象:詳細 | send:form:submit | データ送信 |
| 4 | 通知 | notify:対象:イベント | notify:download:complete | イベント通知 |
| 5 | 購読 | subscribe:対象:更新 | subscribe:settings:change | 状態変更の購読 |
型安全パターン早見表
| # | 手法 | メリット | 適用場面 |
|---|---|---|---|
| 1 | TypeScript 型定義 | コンパイル時チェック | 全ての IPC チャネル |
| 2 | Zod バリデーション | 実行時型検証 | ユーザー入力、外部データ |
| 3 | 型アサーション | 型推論サポート | レスポンス受信時 |
| 4 | ジェネリクス活用 | 再利用性向上 | 共通ヘルパー関数 |
エラーハンドリング定型パターン早見表
| # | パターン | 実装場所 | エラー種別 |
|---|---|---|---|
| 1 | try-catch + Result 型 | ipcMain.handle | 同期エラー |
| 2 | Promise reject | 非同期処理 | 非同期エラー |
| 3 | イベントエラー通知 | ipcMain.on | リスナー内エラー |
| 4 | タイムアウト制御 | レンダラープロセス | 応答遅延エラー |
| 5 | リトライ機構 | ヘルパー関数 | 一時的なエラー |
背景
Electron IPC の基本構造
Electron では、セキュリティの観点から メインプロセス(Node.js 環境)と レンダラープロセス(ブラウザ環境)が分離されています。これらのプロセス間でデータをやり取りするには、IPC(Inter-Process Communication)という仕組みを使います。
主な IPC の方式は以下の通りです。
- ipcRenderer.invoke / ipcMain.handle:要求・応答型の双方向通信
- ipcRenderer.send / ipcMain.on:一方向のイベント送信
- webContents.send:メインからレンダラーへのプッシュ通知
これらの IPC を活用することで、レンダラープロセスから OS 機能へのアクセスや、データベース操作、ファイルシステムの操作などを安全に実行できます。
プロセス間通信の全体像
以下の図は、Electron の IPC がどのように機能するかを示したものです。
mermaidflowchart TB
renderer["レンダラープロセス<br/>(React/Vue等)"]
preload["preload.js<br/>(contextBridge)"]
main["メインプロセス<br/>(ipcMain)"]
os["OS機能<br/>(ファイル/DB等)"]
renderer -->|"invoke('channel')"| preload
preload -->|"ipcRenderer.invoke"| main
main -->|"handle('channel')"| os
os -->|"戻り値"| main
main -->|"Promise resolve"| preload
preload -->|"型付き戻り値"| renderer
上記の図からわかるように、レンダラープロセスは直接メインプロセスにアクセスせず、preload.js を経由することでセキュリティを確保しています。
IPC 設計における課題の発生要因
Electron の IPC は柔軟である一方、以下のような問題が起きやすくなっています。
- 命名の自由度が高い:チャネル名を任意の文字列で定義できるため、プロジェクトが大きくなると命名が乱れる
- 型情報の欠如:JavaScript では実行時までデータ型がわからず、予期しないデータ構造でエラーが発生
- エラーハンドリングの複雑さ:非同期処理が絡むため、エラーが見逃されやすい
これらの課題を放置すると、保守性が低下し、バグの原因特定に多大な時間を要することになります。
課題
チャネル名の命名が統一されない問題
IPC のチャネル名は文字列で指定しますが、命名規則がないと以下のような問題が発生します。
- 可読性の低下:
getUserData、get-user-profile、user:fetchなど、命名スタイルがバラバラ - 重複リスク:同じ機能に対して異なるチャネル名が複数存在
- 検索性の悪化:コードベース全体でチャネル名を追跡しづらい
例えば、あるエンジニアが window-minimize というチャネル名を使い、別のエンジニアが minimizeWindow を使うと、同じ機能が重複してしまいます。
型安全性が確保されない問題
JavaScript のデフォルトでは、IPC で送受信するデータの型が保証されません。これにより以下の問題が起きます。
typescript// レンダラー側
const result = await window.api.getUser(123);
// result の型が不明、string か object か number か?
// 実行時エラーが発生しやすい
console.log(result.name.toUpperCase()); // result が undefined だとエラー
上記のように、受け取ったデータの構造がわからないため、実行時にエラーが発生するリスクが高まります。
エラーハンドリングの欠如
IPC は非同期処理が中心のため、エラーハンドリングを適切に実装しないと以下の問題が発生します。
- エラーの握りつぶし:try-catch を書き忘れるとエラーがユーザーに伝わらない
- 不明瞭なエラーメッセージ:「Error: Unknown」だけでは原因がわからない
- エラー発生箇所の特定困難:メインプロセスとレンダラープロセスのどちらでエラーが起きたか不明
これらの課題を解決するには、体系的な設計パターンが必要です。
課題の関係性
以下の図は、IPC 設計における課題がどのように関連しているかを示しています。
mermaidflowchart TD
naming["命名規則の欠如"]
type["型安全性の欠如"]
error["エラーハンドリングの欠如"]
bug["バグの増加"]
maintenance["保守コスト増大"]
naming --> bug
type --> bug
error --> bug
bug --> maintenance
naming -.->|"コード検索困難"| maintenance
type -.->|"リファクタ困難"| maintenance
これらの課題は独立しているのではなく、相互に影響し合って保守コストを増大させます。
解決策
チャネル命名規則の統一
チャネル名には 一貫性のある命名パターン を適用することが重要です。以下のパターンを推奨します。
パターン 1:操作型チャネル(操作:対象:動詞)
ウィンドウ操作やアプリケーション制御など、何かを実行する場合に使用します。
typescript// 命名例
'window:main:minimize'; // メインウィンドウを最小化
'window:main:maximize'; // メインウィンドウを最大化
'app:system:restart'; // アプリケーション再起動
'dialog:file:open'; // ファイル選択ダイアログを開く
このパターンでは、操作対象(window、app など)と動作(minimize、restart など)が明確になります。
パターン 2:データ取得型チャネル(get:対象:詳細)
データベースや設定ファイルからデータを取得する場合に使用します。
typescript// 命名例
'get:user:profile'; // ユーザープロファイル取得
'get:settings:theme'; // テーマ設定取得
'get:database:records'; // データベースレコード取得
'get:file:content'; // ファイル内容取得
パターン 3:データ送信型チャネル(send:対象:詳細)
データを保存したり、サーバーに送信したりする場合に使用します。
typescript// 命名例
'send:form:submit'; // フォーム送信
'send:settings:update'; // 設定更新
'send:log:error'; // エラーログ送信
パターン 4:通知型チャネル(notify:対象:イベント)
メインプロセスからレンダラープロセスへの通知に使用します。
typescript// 命名例
'notify:download:complete'; // ダウンロード完了通知
'notify:update:available'; // アップデート利用可能通知
'notify:network:offline'; // オフライン通知
型安全性の確保
TypeScript を活用し、IPC のチャネルとデータ構造を型定義します。
型定義ファイルの作成
まず、共通の型定義ファイルを作成します。
typescript// types/ipc.ts
// ユーザープロファイルの型定義
export interface UserProfile {
id: number;
name: string;
email: string;
createdAt: Date;
}
// 設定情報の型定義
export interface AppSettings {
theme: 'light' | 'dark';
language: 'ja' | 'en';
autoSave: boolean;
}
上記のように、送受信するデータの構造を明確に定義します。
チャネル定義オブジェクトの作成
チャネル名と型を紐付けるオブジェクトを作成します。
typescript// types/channels.ts
import { UserProfile, AppSettings } from './ipc';
// IPC チャネルの型定義
export const IPC_CHANNELS = {
'get:user:profile': {
request: { userId: 0 as number },
response: {} as UserProfile,
},
'get:settings:theme': {
request: null,
response: {} as AppSettings,
},
'send:settings:update': {
request: {} as Partial<AppSettings>,
response: { success: true as boolean },
},
} as const;
// チャネル名の型
export type ChannelName = keyof typeof IPC_CHANNELS;
このオブジェクトにより、チャネル名とデータ型が一元管理されます。
型安全な invoke ヘルパーの作成
ジェネリクスを活用して、型安全な IPC ヘルパー関数を作成します。
typescript// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
import {
IPC_CHANNELS,
ChannelName,
} from './types/channels';
// 型安全な invoke 関数
async function typedInvoke<T extends ChannelName>(
channel: T,
data: (typeof IPC_CHANNELS)[T]['request']
): Promise<(typeof IPC_CHANNELS)[T]['response']> {
return await ipcRenderer.invoke(channel, data);
}
上記の関数により、チャネル名を指定すると、自動的にリクエストとレスポンスの型が推論されます。
contextBridge での公開
作成したヘルパー関数を contextBridge で公開します。
typescript// preload.ts(続き)
contextBridge.exposeInMainWorld('api', {
invoke: typedInvoke,
});
これにより、レンダラープロセスから型安全に IPC を呼び出せます。
レンダラープロセスでの利用
レンダラープロセスでは、型推論が効いた状態で IPC を利用できます。
typescript// renderer.ts
// 型推論が効く!
const profile = await window.api.invoke(
'get:user:profile',
{ userId: 123 }
);
// profile は UserProfile 型として推論される
console.log(profile.name); // エラーが出ない
// 型チェックが効く!
await window.api.invoke('send:settings:update', {
theme: 'dark', // OK
language: 'ja', // OK
// typo: 'error' // コンパイルエラー!
});
Zod によるランタイムバリデーション
型定義だけでは実行時の型安全性は保証されません。Zod を使ってランタイムバリデーションを追加します。
Zod スキーマの定義
typescript// schemas/user.ts
import { z } from 'zod';
// ユーザープロファイルのスキーマ
export const UserProfileSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1).max(100),
email: z.string().email(),
createdAt: z.date(),
});
// TypeScript 型を生成
export type UserProfile = z.infer<typeof UserProfileSchema>;
メインプロセスでのバリデーション
受け取ったデータを Zod でバリデーションします。
typescript// main.ts
import { ipcMain } from 'electron';
import { UserProfileSchema } from './schemas/user';
ipcMain.handle('get:user:profile', async (event, data) => {
// リクエストデータのバリデーション
const validated = z
.object({ userId: z.number() })
.parse(data);
// データベースから取得(仮)
const user = await fetchUserFromDB(validated.userId);
// レスポンスデータのバリデーション
const validatedUser = UserProfileSchema.parse(user);
return validatedUser;
});
上記により、不正なデータが混入した場合は即座にエラーが発生し、バグを早期発見できます。
Result 型によるエラーハンドリング
エラーを明示的に扱うために、Result 型パターンを導入します。
Result 型の定義
typescript// types/result.ts
// 成功型
export type Success<T> = {
success: true;
data: T;
};
// 失敗型
export type Failure = {
success: false;
error: {
code: string;
message: string;
details?: unknown;
};
};
// Result 型
export type Result<T> = Success<T> | Failure;
Result 型を使うことで、成功と失敗を型レベルで区別できます。
メインプロセスでの Result 型適用
typescript// main.ts
import { ipcMain } from 'electron';
import { Result } from './types/result';
import { UserProfile } from './schemas/user';
ipcMain.handle(
'get:user:profile',
async (event, data): Promise<Result<UserProfile>> => {
try {
const validated = z
.object({ userId: z.number() })
.parse(data);
const user = await fetchUserFromDB(validated.userId);
const validatedUser = UserProfileSchema.parse(user);
// 成功時
return {
success: true,
data: validatedUser,
};
} catch (err) {
// 失敗時
return {
success: false,
error: {
code: 'USER_FETCH_ERROR',
message: 'ユーザー情報の取得に失敗しました',
details: err,
},
};
}
}
);
上記のように、すべてのエラーを catch し、統一された形式で返します。
レンダラープロセスでのエラーハンドリング
typescript// renderer.ts
const result = await window.api.invoke('get:user:profile', {
userId: 123,
});
if (result.success) {
// 成功時の処理
console.log('ユーザー名:', result.data.name);
displayUserProfile(result.data);
} else {
// 失敗時の処理
console.error(`エラーコード: ${result.error.code}`);
console.error(
`エラーメッセージ: ${result.error.message}`
);
showErrorNotification(result.error.message);
}
Result 型により、エラーハンドリングが型安全かつ明示的になります。
タイムアウト制御の実装
長時間応答がない場合に備えて、タイムアウト処理を実装します。
typescript// utils/timeout.ts
// タイムアウト付き invoke ヘルパー
export async function invokeWithTimeout<T>(
channel: string,
data: unknown,
timeoutMs: number = 5000
): Promise<T> {
return Promise.race([
window.api.invoke(channel, data),
new Promise<never>((_, reject) =>
setTimeout(
() =>
reject(
new Error(
`Timeout: ${channel} が ${timeoutMs}ms 以内に応答しませんでした`
)
),
timeoutMs
)
),
]);
}
このヘルパーを使うことで、応答遅延によるアプリケーションのフリーズを防げます。
タイムアウトヘルパーの利用例
typescript// renderer.ts
try {
const result = await invokeWithTimeout(
'get:user:profile',
{ userId: 123 },
3000
);
console.log('取得成功:', result);
} catch (err) {
if (err.message.startsWith('Timeout:')) {
console.error('タイムアウトエラー:', err.message);
showTimeoutError();
} else {
console.error('その他のエラー:', err);
}
}
IPC 設計パターンの全体像
以下の図は、これまで説明した設計パターンがどのように組み合わさるかを示しています。
mermaidflowchart TD
naming["チャネル命名規則<br/>(操作:対象:動詞)"]
types["TypeScript型定義<br/>(IPC_CHANNELS)"]
validation["Zodバリデーション<br/>(実行時検証)"]
result["Result型<br/>(エラーハンドリング)"]
timeout["タイムアウト制御<br/>(応答遅延対策)"]
ipc["堅牢なIPC設計"]
naming --> ipc
types --> ipc
validation --> ipc
result --> ipc
timeout --> ipc
ipc --> quality["高品質・高保守性"]
これらのパターンを組み合わせることで、保守性の高い IPC 設計が実現します。
具体例
ファイル選択ダイアログの実装例
ここでは、ファイル選択ダイアログを開いて、選択されたファイルのパスを取得する実装例を示します。
型定義とスキーマ
typescript// types/file.ts
import { z } from 'zod';
// ファイル選択リクエストのスキーマ
export const FileDialogRequestSchema = z.object({
title: z.string().optional(),
filters: z
.array(
z.object({
name: z.string(),
extensions: z.array(z.string()),
})
)
.optional(),
});
// ファイル選択レスポンスのスキーマ
export const FileDialogResponseSchema = z.object({
filePath: z.string().nullable(),
canceled: z.boolean(),
});
// TypeScript 型の生成
export type FileDialogRequest = z.infer<
typeof FileDialogRequestSchema
>;
export type FileDialogResponse = z.infer<
typeof FileDialogResponseSchema
>;
メインプロセスの実装
typescript// main.ts
import { ipcMain, dialog } from 'electron';
import { Result } from './types/result';
import {
FileDialogRequestSchema,
FileDialogResponseSchema,
FileDialogResponse,
} from './types/file';
ipcMain.handle(
'dialog:file:open',
async (
event,
data
): Promise<Result<FileDialogResponse>> => {
try {
// リクエストバリデーション
const validated = FileDialogRequestSchema.parse(data);
// ファイル選択ダイアログを表示
const result = await dialog.showOpenDialog({
title: validated.title,
filters: validated.filters,
properties: ['openFile'],
});
// レスポンス作成
const response: FileDialogResponse = {
filePath: result.filePaths[0] || null,
canceled: result.canceled,
};
// レスポンスバリデーション
const validatedResponse =
FileDialogResponseSchema.parse(response);
return {
success: true,
data: validatedResponse,
};
} catch (err) {
return {
success: false,
error: {
code: 'FILE_DIALOG_ERROR',
message:
'ファイル選択ダイアログでエラーが発生しました',
details: err,
},
};
}
}
);
preload.js での公開
typescript// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('api', {
openFileDialog: async (options) => {
return await ipcRenderer.invoke(
'dialog:file:open',
options
);
},
});
レンダラープロセスでの利用
typescript// renderer.ts
async function selectFile() {
const result = await window.api.openFileDialog({
title: '画像ファイルを選択してください',
filters: [
{ name: '画像', extensions: ['jpg', 'png', 'gif'] },
{ name: 'すべてのファイル', extensions: ['*'] },
],
});
if (result.success) {
if (result.data.canceled) {
console.log('ファイル選択がキャンセルされました');
return;
}
console.log(
'選択されたファイル:',
result.data.filePath
);
loadImage(result.data.filePath);
} else {
console.error('エラーコード:', result.error.code);
console.error(
'エラーメッセージ:',
result.error.message
);
alert('ファイル選択中にエラーが発生しました');
}
}
この実装により、型安全性とエラーハンドリングが両立された堅牢なファイル選択機能が実現できます。
データベース操作の実装例
次に、データベースからユーザー一覧を取得する例を示します。
型定義とスキーマ
typescript// types/database.ts
import { z } from 'zod';
// ユーザー一覧取得リクエスト
export const GetUsersRequestSchema = z.object({
limit: z.number().int().positive().max(100).default(10),
offset: z.number().int().nonnegative().default(0),
sortBy: z.enum(['name', 'email', 'createdAt']).optional(),
});
// ユーザー情報
export const UserSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
email: z.string().email(),
createdAt: z.string(), // ISO8601形式
});
// ユーザー一覧レスポンス
export const GetUsersResponseSchema = z.object({
users: z.array(UserSchema),
total: z.number().int().nonnegative(),
});
export type GetUsersRequest = z.infer<
typeof GetUsersRequestSchema
>;
export type User = z.infer<typeof UserSchema>;
export type GetUsersResponse = z.infer<
typeof GetUsersResponseSchema
>;
メインプロセスのデータベース処理
typescript// main.ts
import { ipcMain } from 'electron';
import { Result } from './types/result';
import {
GetUsersRequestSchema,
GetUsersResponseSchema,
GetUsersResponse,
} from './types/database';
import { db } from './database'; // 仮のDBモジュール
ipcMain.handle(
'get:database:users',
async (
event,
data
): Promise<Result<GetUsersResponse>> => {
try {
// リクエストバリデーション
const validated = GetUsersRequestSchema.parse(data);
// データベースクエリ実行
const users = await db.query(
'SELECT * FROM users ORDER BY ? LIMIT ? OFFSET ?',
[
validated.sortBy || 'createdAt',
validated.limit,
validated.offset,
]
);
const total = await db.query(
'SELECT COUNT(*) as count FROM users'
);
// レスポンス作成
const response: GetUsersResponse = {
users: users.map((u) => ({
id: u.id,
name: u.name,
email: u.email,
createdAt: u.created_at.toISOString(),
})),
total: total[0].count,
};
// レスポンスバリデーション
const validatedResponse =
GetUsersResponseSchema.parse(response);
return {
success: true,
data: validatedResponse,
};
} catch (err) {
return {
success: false,
error: {
code: 'DATABASE_QUERY_ERROR',
message: 'ユーザー一覧の取得に失敗しました',
details: err,
},
};
}
}
);
レンダラープロセスでのページネーション
typescript// renderer.ts
async function loadUsers(
page: number = 1,
pageSize: number = 10
) {
const offset = (page - 1) * pageSize;
const result = await window.api.invoke(
'get:database:users',
{
limit: pageSize,
offset: offset,
sortBy: 'name',
}
);
if (result.success) {
const { users, total } = result.data;
console.log(`ユーザー数: ${users.length} / ${total}`);
// UIに表示
displayUserList(users);
updatePagination(page, Math.ceil(total / pageSize));
} else {
console.error('エラーコード:', result.error.code);
showErrorMessage('ユーザー一覧の取得に失敗しました');
}
}
リトライ機構の実装
一時的なエラー(ネットワーク切断など)に対応するため、リトライ機構を実装します。
typescript// utils/retry.ts
// リトライ設定の型
interface RetryConfig {
maxRetries: number; // 最大リトライ回数
delayMs: number; // リトライ間隔(ミリ秒)
backoffMultiplier: number; // 遅延倍率(指数バックオフ用)
}
// デフォルト設定
const defaultConfig: RetryConfig = {
maxRetries: 3,
delayMs: 1000,
backoffMultiplier: 2,
};
リトライヘルパー関数
typescript// utils/retry.ts(続き)
export async function invokeWithRetry<T>(
channel: string,
data: unknown,
config: Partial<RetryConfig> = {}
): Promise<T> {
const { maxRetries, delayMs, backoffMultiplier } = {
...defaultConfig,
...config,
};
let lastError: Error;
let currentDelay = delayMs;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await window.api.invoke(channel, data);
return result; // 成功
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
console.warn(
`リトライ ${
attempt + 1
}/${maxRetries}: ${channel}`
);
await sleep(currentDelay);
currentDelay *= backoffMultiplier; // 指数バックオフ
}
}
}
throw new Error(
`${maxRetries}回のリトライ後も失敗: ${lastError.message}`
);
}
// スリープ関数
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
リトライヘルパーの利用例
typescript// renderer.ts
async function fetchUserWithRetry(userId: number) {
try {
const result = await invokeWithRetry(
'get:user:profile',
{ userId },
{
maxRetries: 5,
delayMs: 500,
backoffMultiplier: 2,
}
);
console.log('取得成功:', result);
return result;
} catch (err) {
console.error('リトライ後も失敗:', err.message);
showPersistentErrorMessage(
'ユーザー情報を取得できませんでした'
);
throw err;
}
}
この実装により、一時的なネットワークエラーなどに対して自動的にリトライし、成功率を向上させることができます。
イベントベース通知の実装例
メインプロセスからレンダラープロセスへの通知機能を実装します。
通知データの型定義
typescript// types/notification.ts
import { z } from 'zod';
// ダウンロード進捗通知
export const DownloadProgressSchema = z.object({
fileName: z.string(),
progress: z.number().min(0).max(100),
downloadedBytes: z.number().nonnegative(),
totalBytes: z.number().positive(),
});
export type DownloadProgress = z.infer<
typeof DownloadProgressSchema
>;
メインプロセスでの通知送信
typescript// main.ts
import { BrowserWindow } from 'electron';
import { DownloadProgressSchema } from './types/notification';
// ダウンロード処理(仮)
async function downloadFile(
url: string,
mainWindow: BrowserWindow
) {
// ダウンロードロジック
let downloaded = 0;
const total = 1024 * 1024 * 10; // 10MB
while (downloaded < total) {
downloaded += 1024 * 100; // 100KB ずつ
const progress = {
fileName: 'sample.zip',
progress: Math.floor((downloaded / total) * 100),
downloadedBytes: downloaded,
totalBytes: total,
};
// バリデーション
const validated =
DownloadProgressSchema.parse(progress);
// レンダラープロセスに通知
mainWindow.webContents.send(
'notify:download:progress',
validated
);
await new Promise((resolve) =>
setTimeout(resolve, 100)
);
}
// 完了通知
mainWindow.webContents.send('notify:download:complete', {
fileName: 'sample.zip',
});
}
preload.js でのイベントリスナー登録
typescript// preload.ts
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('api', {
onDownloadProgress: (callback: (data: any) => void) => {
ipcRenderer.on(
'notify:download:progress',
(event, data) => {
callback(data);
}
);
},
onDownloadComplete: (callback: (data: any) => void) => {
ipcRenderer.on(
'notify:download:complete',
(event, data) => {
callback(data);
}
);
},
});
レンダラープロセスでのイベント購読
typescript// renderer.ts
// ダウンロード進捗の監視
window.api.onDownloadProgress((data) => {
console.log(`ダウンロード進捗: ${data.progress}%`);
updateProgressBar(data.progress);
const downloaded = (
data.downloadedBytes /
1024 /
1024
).toFixed(2);
const total = (data.totalBytes / 1024 / 1024).toFixed(2);
console.log(`${downloaded} MB / ${total} MB`);
});
// ダウンロード完了の監視
window.api.onDownloadComplete((data) => {
console.log(`ダウンロード完了: ${data.fileName}`);
showSuccessNotification(
`${data.fileName} のダウンロードが完了しました`
);
hideProgressBar();
});
まとめ
本記事では、Electron IPC 設計における以下の定型パターンをご紹介しました。
チャネル命名規則
チャネル名は 操作:対象:動詞 のパターンを基本とし、データ取得は get:、データ送信は send:、通知は notify: のプレフィックスを使うことで、コードベース全体で一貫性を保つことができます。命名規則を統一すると、コードの検索性が向上し、チーム開発でもスムーズに連携できるでしょう。
型安全性の確保
TypeScript の型定義と Zod のランタイムバリデーションを組み合わせることで、コンパイル時と実行時の両方で型安全性を確保できます。特に、チャネル定義オブジェクトを使った型推論により、IPC 呼び出し時に自動的に型チェックが働くため、開発効率が大幅に向上します。
エラーハンドリング
Result 型パターンにより、成功と失敗を明示的に扱うことができます。エラーコードとメッセージを統一形式で返すことで、エラー発生時のデバッグが容易になります。また、タイムアウト制御やリトライ機構を組み込むことで、ネットワークエラーなど一時的な障害にも強いアプリケーションを構築できるでしょう。
実務への適用
これらのパターンは、新規プロジェクトだけでなく既存プロジェクトのリファクタリングにも適用可能です。まずはチャネル命名規則から統一し、次に型定義を追加、最後にエラーハンドリングを強化するという段階的なアプローチが効果的です。
Electron IPC は柔軟な反面、設計の自由度が高く、プロジェクトの規模が大きくなるほど保守性が課題になります。本記事で紹介したチートシートを活用することで、チーム全体で統一された IPC 設計を実現し、高品質なアプリケーション開発を進めていただければ幸いです。
関連リンク
articleElectron IPC 設計チートシート:チャネル命名・型安全・エラーハンドリング定型
articleElectron Mac 向けビルド:Universal(Intel/Apple Silicon)を作る手順
articleElectron IPC 方式比較:ipcRenderer vs contextBridge + postMessage の安全性
articleElectron ビルド失敗解決:native module 再ビルドと node-gyp 地獄からの脱出
articleElectron アーキテクチャ超図解:Main/Renderer/Preload の役割とデータフロー
articleElectron 運用:コード署名・公証・アップデート鍵管理のベストプラクティス
articleElectron IPC 設計チートシート:チャネル命名・型安全・エラーハンドリング定型
articleDocker セキュアイメージ設計:非 root・最小ベース・Capabilities 削減の実装指針
articleJotai × tRPC 初期配線:型安全 RPC とローカル状態の統合
articleDevin による段階的リファクタリング設計:ストラングラーパターン適用ガイド
articleJest のカバレッジが 0% になる原因と対処:sourceMap/babel 設定の落とし穴
articleGitHub Copilot で機密文字列を誤提案しないための緊急対策:ポリシーと検知ルール
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来