メインコンテンツまでスキップ

IPCブリッジ

Electron adapterはHTTPソケットの代わりにElectronのIPC機構を使います。標準の Request/Response オブジェクトはIPCで安全に扱えるペイロードにシリアライズされ、プロセス間で送受信され、反対側でデシリアライズされます — controllerからは、他のadapterと同じWeb Fetch APIとして見えます。

仕組み​

テキストコンテンツ(JSON、HTML、XML)は文字列としてシリアライズされ、バイナリコンテンツは ArrayBuffer としてシリアライズされます。

設定​

3層すべてが同じchannel文字列で一致している必要があります:

// main
const electronZelt = await onElectron(app);
await electronZelt.electronIpc.listen(electronZelt.http, { channel: 'http://zelt-app' });

// preload
exposeIpc({ channel: 'http://zelt-app' });

// renderer
ipcFetch(input, init, { channel: 'http://zelt-app' });

channelは http:// または https:// で始まる必要があります。省略した場合のデフォルトは 'http://zelt-ipc' です。

Main Process: onElectron()​

const electronZelt = await onElectron(app, {
  warmup: true, // デフォルト: 起動時に全controllerを解決する
});
await electronZelt.electronIpc.listen(electronZelt.http, { channel: 'http://zelt-app' });

オプション​

オプション型デフォルト説明
warmupbooleantrue起動時に全controllerを解決する

戻り値(OnElectronApp)​

設定された各featureはそのkeyでnamespace化されます。electronIpc()は electronIpc.listen(http, options) を、httpServer()は httpServer.listen(http, options) を公開します。HTTP feature自体は接続先を選びません:

プロパティ型説明
http.fetch(request: Request) => Promise<Response>リクエストを直接処理します
http.request(input: string | Request, init?: RequestInit) => Promise<Response>URL/pathと RequestInit から組み立てたリクエストを処理します
electronIpc.listen(http: HttpFetchTarget, options?: ElectronIpcListenOptions) => Promise<ElectronIpcHandle>選択したHTTPをIPCへ公開します。electronIpc()の宣言が必要です
httpServer.listen(http: HttpFetchTarget, options?: HttpListenOptions) => Promise<ServerHandle>選択したHTTP featureをTCPで配信します。httpServer()の宣言が必要です
shutdown() => Promise<void>gracefulなシャットダウン
get<T>(Class) => Promise<T>DIからserviceを解決します

破壊的変更: onElectron()の ipcChannel / ipcFeature オプションと暗黙のIPC接続は廃止しました。electronIpc()を宣言し、electronIpc.listen(対象HTTP, { channel })で明示的に接続してください。HTTPやIPCを宣言しないアプリも起動できます。

IPC接続と終了​

electronIpc()はElectronの準備完了を待ちますが、listen()を呼ぶまでhandlerを登録しません。electronIpc({ name: 'desktop' })のようにfeature名も指定できます。

const binding = await runtime.electronIpc.listen(runtime.admin, { channel: 'http://admin' });
await binding.shutdown();

戻り値は channel と shutdown() を持ちます。個別の終了とruntimeの shutdown() は同じ解除処理を共有し、handlerを一度だけ解除します。複数のHTTPを公開する場合は、異なるchannelで listen()を呼んでください。

Warmup​

デフォルトでは、onElectron() は起動時に全controllerを即時解決します(warmup: true)。これにより、最初のリクエストの前にserviceが初期化されていることが保証されます。最初のリクエスト時に遅延初期化したい場合は warmup: false を設定してください。

Preload Script: exposeIpc()​

exposeIpc({ channel: 'http://zelt-app' });

exposeIpc() はIPC sender関数を登録し、contextBridge.exposeInMainWorld()(context isolationが無効な場合は globalThis)経由でrendererに公開します。

公開されるkeyはchannel文字列そのものなので、renderer側の ipcFetch は globalThis[channel] から参照できます。

Renderer: ipcFetch()​

const response = await ipcFetch('http://zelt-app/hello/world', undefined, {
  channel: 'http://zelt-app',
});
const data = await response.json();

ipcFetch() は fetch() と同じシグネチャに加え、channelを指定する省略可能な第3引数を持ちます。リクエストをIPCペイロードに変換してpreloadブリッジ経由で送信し、標準の Response を返します。

ラッパーを作成する​

実際には、channelを一度だけ設定するように ipcFetch をラップします:

const CHANNEL = 'http://zelt-app';

export const apiFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
  ipcFetch(input, init, { channel: CHANNEL });

Hono Clientと組み合わせる​

型安全なAPI呼び出しには、@zeltjs/hono-clientと組み合わせます:

const zeltIpcFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
  ipcFetch(input, init, { channel: 'http://zelt-app' });

export const client = hc<AppType>('http://zelt-app', {
  fetch: zeltIpcFetch,
});

AppType はアプリ定義から生成されます — CLI plugin(zelt build)経由、または、電子ビルドがZelt CLIではなくelectron-viteによって駆動される場合はプログラム的にGeneratorService.generateFromApp()経由で生成されます。main processのビルド成果物をimportできないrendererコードでは、portable: true で生成してください。

IPCイベントへのアクセス​

controller内では、ipcEvent() を使って背後の IpcMainInvokeEvent にアクセスできます:

@Controller('/system')
export class SystemController {
  @Get('/sender')
  getSender() {
    const event = ipcEvent();
    return { processId: event?.processId };
  }
}

シャットダウン​

Zeltのシャットダウンを、Electronのquitライフサイクルに接続します:

const electronZelt = await onElectron(app);
await electronZelt.electronIpc.listen(electronZelt.http, { channel: 'http://zelt-app' });

const electronApp = await electronZelt.get(ElectronAdaptor);
electronApp.ready.app.on('will-quit', () => {
  void electronZelt.shutdown();
});

will-quit を使うことで、Electronが実際にquitするまでHTTPブリッジが生きたままになり、rendererはteardown中も最後のIPC呼び出しを送信できます。