測驗:IPC 通訊:主程序與渲染程序的橋樑
共 5 題,點選答案後會立即顯示結果
1. 在 Electron 架構中,為什麼渲染程序預設無法直接存取 Node.js API?
2. 在 Electron 中,哪個模組用於在主程序中接收渲染程序的請求?
3. contextBridge.exposeInMainWorld() 的主要功能是什麼?
4. 現代 Electron 應用推薦使用哪種 IPC 通訊模式?
5. 在建立 BrowserWindow 時,為什麼建議將 nodeIntegration 設為 false?
前言
在上一篇文章中,我們建立了 Electron + React 的開發環境,也初步認識了主程序(Main Process)與渲染程序(Renderer Process)的分工。但你可能會好奇:如果 React 介面需要存取檔案系統、開啟對話框,這些只有主程序才能做的事情,該怎麼辦?
答案就是 IPC(Inter-Process Communication,程序間通訊)。這是 Electron 應用中最核心的溝通機制。
為什麼需要 IPC?
程序隔離的設計
Electron 採用 Chromium 的多程序架構:
┌─────────────────────────────────────────────────┐
│ 主程序 (Main) │
│ - 可存取 Node.js API(fs、path、child_process)│
│ - 管理視窗生命週期 │
│ - 處理系統層級操作 │
└─────────────────────────────────────────────────┘
│ ▲
│ IPC │ IPC
▼ │
┌─────────────────────────────────────────────────┐
│ 渲染程序 (Renderer) │
│ - 執行網頁內容(HTML、CSS、JavaScript) │
│ - React 應用在此執行 │
│ - 預設無法存取 Node.js API │
└─────────────────────────────────────────────────┘
Code language: CSS (css)這種隔離是刻意的設計,主要基於安全考量。如果讓渲染程序直接存取 Node.js API,惡意網頁腳本就可能讀寫你的檔案系統。
常見需求情境
在實際開發中,你會頻繁遇到這些需求:
- 從 React 介面讀取本機檔案
- 顯示系統原生對話框(開啟檔案、儲存檔案)
- 操作剪貼簿內容
- 發送系統通知
這些都需要透過 IPC 來完成。
IPC 通訊的核心模組
Electron 提供兩個主要模組處理 IPC:
| 模組 | 執行位置 | 用途 |
|---|---|---|
ipcMain |
主程序 | 接收渲染程序的請求 |
ipcRenderer |
渲染程序 | 發送請求給主程序 |
通訊模式:handle / invoke
現代 Electron 應用推薦使用 handle / invoke 模式,這是一種基於 Promise 的雙向通訊方式。
主程序端:ipcMain.handle()
// main.js(主程序)
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
// 註冊 IPC 處理器
ipcMain.handle('open-file-dialog', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
});
return result.filePaths[0]; // 回傳選擇的檔案路徑
});
Code language: JavaScript (javascript)這段程式碼做了什麼?
ipcMain.handle('open-file-dialog', ...)— 註冊一個名為'open-file-dialog'的處理器- 當收到請求時,呼叫系統原生的檔案對話框
- 回傳使用者選擇的檔案路徑
渲染程序端:ipcRenderer.invoke()
// 在渲染程序中呼叫
const filePath = await ipcRenderer.invoke('open-file-dialog');
console.log('使用者選擇的檔案:', filePath);
Code language: JavaScript (javascript)invoke() 回傳 Promise,可以搭配 async/await 使用,寫起來就像呼叫一般的非同步函式。
Preload Script 的角色
等等,上面的程式碼有個問題:渲染程序預設無法使用 ipcRenderer。
這就是 preload script 登場的時機。它是一個特殊的腳本,在渲染程序載入網頁之前執行,可以安全地橋接主程序與渲染程序。
建立 preload.js
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// 透過 contextBridge 安全地暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
readFile: (path) => ipcRenderer.invoke('read-file', path),
saveFile: (path, content) => ipcRenderer.invoke('save-file', path, content)
});
Code language: JavaScript (javascript)關鍵概念:contextBridge.exposeInMainWorld()
這個 API 做了幾件重要的事:
- 建立安全的橋樑 — 只暴露你明確指定的函式,而非整個
ipcRenderer - 注入到 window 物件 — 讓渲染程序可以透過
window.electronAPI存取 - 隔離執行環境 — 網頁腳本無法直接修改或存取 preload 的內部狀態
在主程序中指定 preload
// main.js
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 必須開啟(預設已開啟)
nodeIntegration: false // 建議關閉(預設已關閉)
}
});
Code language: JavaScript (javascript)實作範例:從 React 呼叫主程序功能
完整的程式碼結構
my-electron-app/
├── main.js # 主程序
├── preload.js # Preload 腳本
└── src/
└── App.jsx # React 元件
Code language: PHP (php)主程序(main.js)
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const fs = require('fs').promises;
const path = require('path');
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.loadURL('http://localhost:5173'); // Vite 開發伺服器
}
// IPC 處理器:開啟檔案對話框
ipcMain.handle('open-file-dialog', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [{ name: 'Text Files', extensions: ['txt', 'md'] }]
});
if (result.canceled) return null;
return result.filePaths[0];
});
// IPC 處理器:讀取檔案內容
ipcMain.handle('read-file', async (event, filePath) => {
const content = await fs.readFile(filePath, 'utf-8');
return content;
});
app.whenReady().then(createWindow);
Code language: JavaScript (javascript)Preload 腳本(preload.js)
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
readFile: (filePath) => ipcRenderer.invoke('read-file', filePath)
});
Code language: JavaScript (javascript)React 元件(App.jsx)
import { useState } from 'react';
function App() {
const [content, setContent] = useState('');
const [filePath, setFilePath] = useState('');
const handleOpenFile = async () => {
// 呼叫主程序的檔案對話框
const path = await window.electronAPI.openFileDialog();
if (path) {
setFilePath(path);
// 呼叫主程序讀取檔案
const fileContent = await window.electronAPI.readFile(path);
setContent(fileContent);
}
};
return (
<div>
<button onClick={handleOpenFile}>開啟檔案</button>
{filePath && <p>檔案路徑: {filePath}</p>}
<pre>{content}</pre>
</div>
);
}
export default App;
Code language: JavaScript (javascript)注意 window.electronAPI — 這就是 preload 腳本透過 contextBridge 暴露的物件。
TypeScript 型別提示
如果你使用 TypeScript,可以為 electronAPI 加上型別定義:
// src/types/electron.d.ts
export interface ElectronAPI {
openFileDialog: () => Promise<string | null>;
readFile: (filePath: string) => Promise<string>;
}
declare global {
interface Window {
electronAPI: ElectronAPI;
}
}
Code language: PHP (php)這樣在 React 元件中就能得到完整的型別提示與錯誤檢查。
常見問題與解決方案
Q1: window.electronAPI 是 undefined?
檢查以下幾點:
preload.js路徑是否正確?contextIsolation是否設為true?- 開發者工具的 Console 是否有錯誤訊息?
Q2: ipcMain.handle 沒有收到請求?
確認 channel 名稱完全一致:
// 主程序
ipcMain.handle('open-file-dialog', ...);
// preload
ipcRenderer.invoke('open-file-dialog'); // 名稱必須相同
Code language: JavaScript (javascript)Q3: 為什麼不直接用 nodeIntegration: true?
開啟 nodeIntegration 會讓渲染程序直接存取 Node.js API,這是嚴重的安全風險。如果你的應用載入任何外部內容(甚至是 CDN 的 library),惡意腳本就可能存取你的檔案系統。
本篇重點整理
| 概念 | 說明 |
|---|---|
| IPC | 程序間通訊,是主程序與渲染程序溝通的管道 |
| ipcMain.handle() | 主程序註冊處理器,回應渲染程序的請求 |
| ipcRenderer.invoke() | 渲染程序發送請求,回傳 Promise |
| preload.js | 在渲染程序載入網頁前執行的腳本 |
| contextBridge | 安全地將 API 暴露給渲染程序 |
下一篇預告
在下一篇文章中,我們將探討視窗管理與 BrowserWindow 設定,學習如何建立多視窗應用、設定視窗屬性,以及處理視窗事件。
進階測驗:IPC 通訊:主程序與渲染程序的橋樑
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個 Electron 應用,需要讓 React 介面能夠開啟系統的檔案選擇對話框。以下哪種做法最符合 Electron 的安全最佳實踐? 情境題
2. 小明在開發 Electron 應用時,發現 React 元件中 window.electronAPI 是 undefined。以下是他的 BrowserWindow 設定:錯誤診斷
最可能的原因是什麼?
3. 你需要在 Electron 應用中實作「讀取檔案」功能。主程序已經註冊了 ipcMain.handle('read-file', ...),但從 React 呼叫時一直沒有收到回應。以下是 preload.js 的內容:錯誤診斷
問題最可能出在哪裡?