系列篇1: Electron 通信方式總結
背景和前言
Electron底層是基于瀏覽器內核實現的,所以它和瀏覽器一樣有多進程的概念,就是有一個主進程和N多個渲染進程,那么我們在開發過程中如何實現這些渲染進程之間的通信,是我們后續開發過程中必須要解決的問題
接下來我會輸出一系列的文章和解決方案,歡迎各位開發和讀者訂閱和指點
Electron通信方式
先看下Demo默認配置
.main.ts
mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
contextIsolation: false,
},
})
preload.ts
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer;
模式 1:渲染器進程到主進程(單向)ipcRenderer / ipcMain
render.ts
// 發送事件
const sendMessageToMain = () => {
window.ipcRenderer.send('messageToMain', 'hello main');
}
main.ts
// 監聽事件
ipcMain.on('messageToMain', (event, arg) => {
console.log('messageToMain 主進程收到的消息:', arg, event.sender.id)
});
結論: 這種方式主要用于總渲染進程給主進程發消息,并且不支持消息回調
模式 2:主進程到渲染器進程 ipcRenderer / ipcMain
main.ts
// 發送事件
mainWindow?.webContents.send('mainSendMessageToRanderer', '主進程廣播的消息', arg, event.sender.id)
mainWindow?.webContents 這個是接受消息的主體即渲染在任意window上的渲染進程主題
render.ts
// 監聽事件
window.ipcRenderer.on('mainSendMessageToRanderer', (event, arg) => {
console.log('從主進程收到的消息:', arg, event);
})
結論: 模式1和模式2只是單項發送數據和監聽事件不支持,監聽回調
模式 3:渲染器進程到主進程(雙向) ipcRenderer / ipcMain
render.ts
// 發送事件
const sendMessageToMainCallback = async () => {
window.ipcRenderer.invoke('messageToMainCallback', '從渲染進程發送的消息').then(response => {
console.log('從主進程收到的響應:', response);
}).catch(err => {
console.error('從主進程收到的錯誤:', err);
})
}
main.ts
// 監聽事件并且返回
ipcMain.handle('messageToMainCallback', async (event, arg) => {
console.log('messageToMainCallback 主進程收到的消息:', arg, event.sender.id)
const sum = await new Promise((resolve) => {
let total = 0;
for (let i = 1; i <= 99; i++) {
total += i;
}
resolve(total);
});
return sum;
});
結論: 前三種模式我們看到,消息通信是主進程和渲染進程之間的通信,那如果我們想實現渲染進程和渲染進程通信我們怎么辦啊?
很遺憾,Electron官方并沒有直接的Api支持這種模式,它不允許渲染進程之間相互通信
模式 4:渲染器進程到主進程(雙向)MessageChannel 基于 port
注意: 要使用這種模式下面的屬性必須打開,否則將導致收不到從主進程發送的消息,導致 port1 是空的
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
render.ts
const sendMessageToMainFromPort = () => {
const { port1, port2 } = new MessageChannel();
window.ipcRenderer.postMessage('port', null, [port1]);
if (port2) {
port2.onmessage = (event: { data: any }) => {
console.log('從主進程收到的消息:', event.data);
};
port2.postMessage({ message: 'hello main' });
}
}
main.ts
// 監聽port消息
ipcMain.on('port', (event) => {
const port = event.ports[0]
console.log('主進程收到的port:', port)
port.on('message', (event) => {
const data = event.data
console.log('主進程發送的消息:', data)
port.postMessage(data)
})
port.start()
})
不打開也可以但是需要做中轉
preload.ts
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
contextBridge.exposeInMainWorld('ipcRendererCustom', {
createMessageChannelPort: () => {
const { port1, port2 } = new MessageChannel()
ipcRenderer.postMessage('port', '', [port1]);
return {
postMessage: (message: object) => port2.postMessage(message),
onmessage: (listener: ((this: MessagePort, ev: MessageEvent<any>) => any) | null) => port2.onmessage = listener,
};
},
});
reder.ts
// 發送消息
const sendMessageToMainFromPort = () => {
const port = window.ipcRendererCustom.createMessageChannelPort();
port.onmessage((event) => {
console.log('從主進程收到的消息:', event.data);
});
port.postMessage({ message: 'hello main' });
}
main.ts
// 監聽port消息
ipcMain.on('port', (event) => {
const port = event.ports[0]
port.on('message', (event) => {
const data = event.data
console.log('主進程發送的消息:', data)
port.postMessage(data)
})
port.start()
})
模式 5:渲染器進程到主進程(雙向)MessageChannel 基于 port
render.ts 讓渲染進程觸發事件
// 發送消息, 讓渲染進程發消息
const sendMessageToMainFromMainPort = () => {
window.ipcRenderer.send('request-main-channel')
window.ipcRenderer.once('provide-main-channel', (event) => {
const [port] = event.ports
console.log('port:', port)
port.onmessage = (event) => {
console.log('received result:', event.data)
}
port.postMessage({ message: 'hello main' })
})
}
main.ts
// 在主進程中, 監聽消息并且把端口發送給渲染進程
ipcMain.on('request-main-channel', (event) => {
const { port1, port2 } = new MessageChannelMain();
port2.on('message', (event) => {
console.log('主進程收到的消息:', event.data);
port2.postMessage('這是測試數據');
});
port2.start();
event.sender.postMessage('provide-main-channel', null, [port1]);
});
結論: 基于端口的消息專遞我們看到是實現點對點的數據傳遞,他還是需要借助主進程去完成消息轉發
模式 6:渲染器進程到主進程(雙向)worker渲染進程基于 port
其實如果我們不想讓主進程承擔太多的邏輯, 那可以新啟動一個后臺渲染進程,把事件都掛在在這個后臺渲染進程中
main.ts
在創建主window的地方,同時創建后臺渲染進程
async function createWindow() {
workerWindow = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, 'workerProload.js'),
nodeIntegration: true,
contextIsolation: false,
}
})
const workerPath = path.join(__dirname, `../src/renderer/worker.html`)
await workerWindow.loadFile(workerPath);
// 創建主窗口
}
workerProload.ts
import { ipcRenderer } from 'electron'
ipcRenderer.on('new-client', (event) => {
const [port] = event.ports
port.onmessage = (event) => {
console.log('worker: 接受數據: ', event.data)
port.postMessage('從worker返回的數據給其他渲染進程')
}
port.start()
})
render.ts
const sendMessageToWorkerFromMainPort = () => {
window.ipcRenderer.send('request-worker-channel')
window.ipcRenderer.once('provide-worker-channel', (event) => {
const [port] = event.ports
console.log('port:', port)
port.onmessage = (event) => {
console.log('received result:', event.data)
}
port.postMessage('hello worker')
})
}
main.ts
// 給我另一個渲染進程發
ipcMain.on('request-worker-channel', (event) => {
console.log('workerWindow: event.sender: ', event)
const { port1, port2 } = new MessageChannelMain()
workerWindow?.webContents.postMessage('new-client', null, [port2])
event.sender.postMessage('provide-worker-channel', null, [port1])
})
小結
基于上面的通信模式如果僅僅是在項目比較簡單的項目中實現通信已經足夠了, 但是如果你的項目很復雜,你覺得你會把越來越多的業務通信放到主進程嗎,顯然不合理,我們需要自己開發一個優秀的框架,能夠實現渲染進程和渲染進程之間的通信而不再自己單獨維護一套邏輯
關于自己實現通信框架,后續文章再說, 我們還有一個問題需要處理: Electron開發中不可能沒有Web界面嵌入, 那我們如何實現Web界面和Electron通信將是我們需要解決的第二個問題,下一篇文章 我將詳細介紹