渲染进程与渲染进程之间的通信有两种:
- 通过主进程进行消息转发(通过组合主进程与渲染进程之间的单向、双向通信可以实现,可以自己动手尝试,该篇不讲解)
- 通过消息端口进行直接通信
该篇主要用示例讲解下单项目内多个窗口的渲染进程之间相互通信的流程(也就是通过消息端口进行通知)初始版本项目结构可参考项目:https://github.com/ylpxzx/electron-forge-project/tree/init_project

渲染进程与渲染进程之间的消息端口通信
实现整项目示例:https://github.com/ylpxzx/electron-forge-project/tree/message_port
MessageChannelMain方法
示例:
jsx
const { port1, port2 } = new MessageChannelMain()
MessageChannelMain
方法用于创建一个消息通道,该通道包含两个端口 port1 和 port2。这两个端口可以用于在不同的上下文(如主进程和渲染进程)之间传递消息
通信逻辑
通过模拟主窗口(Main)与配置窗口(Settings)之间的通信,来了解下如何通过消息端口实现渲染进程之间的通信
新增Settings窗口预加载文件
-
创建
src/settingPreload.js
文件该预加载文件用于暴露可用的Electron API给Settings窗口页面调用
该文件创建完成后,需要在
forge.config.js
文件的plugins
字段进行配置javascript{ name: '@electron-forge/plugin-vite', config: { build: [ { entry: 'src/main.js', config: 'vite.main.config.mjs', target: 'main', }, { entry: 'src/preload.js', config: 'vite.preload.config.mjs', target: 'preload', }, { // 加入该配置 entry: 'src/settingPreload.js', config: 'vite.preload.config.mjs', target: 'preload', }, ], renderer: [ { name: 'main_window', config: 'vite.renderer.config.mjs', }, ], }, },
主进程下发消息端口给窗口
-
src/main.js文件
端口下发逻辑:
-
在主进程main.js文件中,调用消息体
MessageChannelMain
方法,得到两个可互相通信的端口port1、port2
-
将消息体端口
port1、port2
分别下发给准备就绪后的main窗口和settings窗口(即下发给两个渲染进程)javascript// 将port1端口下发给main窗口 mainWindow.once('ready-to-show', () => { mainWindow.webContents.postMessage('main-port', null, [port1]) }) // 将port2端口下发给settings窗口 settingsWindow.once('ready-to-show', () => { settingsWindow.webContents.postMessage('settings-port', null, [port2]) })
完整代码如下:
javascriptimport { app, BrowserWindow, MessageChannelMain } from 'electron'; import path from 'node:path'; import started from 'electron-squirrel-startup'; // Avoid Warning:Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true"; if (started) { app.quit(); } // crerate message channel const { port1, port2 } = new MessageChannelMain() // 创建Main窗口 const createWindow = () => { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), }, }); if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); } else { mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); } // webContents准备就绪后,使用postMessage向webContents发送端口main-port mainWindow.once('ready-to-show', () => { mainWindow.webContents.postMessage('main-port', null, [port1]) }) mainWindow.webContents.openDevTools(); }; // 创建Settings窗口 const createSettingsWindow = () => { const settingsWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeintegration: true, preload: path.join(__dirname, 'settingPreload.js'), }, }); if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { const settingUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/settings`; settingsWindow.loadURL(settingUrl); } else { mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); } // webContents准备就绪后,使用postMessage向webContents发送端口settings-port settingsWindow.once('ready-to-show', () => { settingsWindow.webContents.postMessage('settings-port', null, [port2]) }) settingsWindow.webContents.openDevTools(); }; app.whenReady().then(() => { // app准备好后,创建两个窗口 createWindow(); createSettingsWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } });
-
在预加载文件暴露可调用的Electron API给两个窗口页面
-
src/preload.js
javascriptconst { contextBridge, ipcRenderer } = require('electron/renderer') let electronMessage = null ipcRenderer.on('main-port', e => { electronMessage = e.ports[0] }) contextBridge.exposeInMainWorld('electronAPI', { // 向端口发送消息 pushMessageEvent: (message) => electronMessage.postMessage(message), // 监听端口消息 onMessagePort: (callback) => { const listener = (e) => { e.ports[0].onmessage = messageEvent => { callback(messageEvent.data) } } ipcRenderer.on('main-port', listener) return () => ipcRenderer.removeListener('main-port', listener) }, })
暴露给页面调用的API有两个:发送消息API、监听消息API; 都是通过
ipcRenderer.on
实现。-
发送消息API:需在上下文隔离外
(contextBridge)
先实例化消息示例(electronMessage)
, 然后通过postMessage
方法推送消息给端口。如果在
contextBridge
内直接调用e.ports[0].postMessage(message)
, 会发送不成功。如下错误示例(没有任何报错):javascriptpushMessageEvent: (message) => { ipcRenderer.on('main-port', e => { e.ports[0].postMessage(message) }) },
具体为啥会失败,目前没找到原因。如果有人能答下疑惑,可以在评论区解答下
-
监听消息API:通过
e.ports[0].onmessage
方法监听端口,接收到消息时触发对应的回调函数callback
-
-
src/settingPreload.js 同上
javascriptconst { contextBridge, ipcRenderer } = require('electron/renderer') let electronMessageTest = null ipcRenderer.on('settings-port', e => { electronMessageTest = e.ports[0] }) contextBridge.exposeInMainWorld('electronSettingAPI', { pushMessageEvent: (message) => electronMessageTest.postMessage(message), onMessagePort: (callback) => { const listener = (e) => { e.ports[0].onmessage = messageEvent => { callback(messageEvent.data) } } ipcRenderer.on('settings-port', listener) return () => ipcRenderer.removeListener('settings-port', listener) }, })
页面示例
通信逻辑实现后,接下来就用两个窗口页面来验证结果
-
src/vue-project/pages/home/index.vue
javascript<template> <h1>😁 Main Render Process</h1> <div style="padding-bottom: 10px;">Receive messages from the Settings window process:<span style="color: #71C25C;">{{ messageVal }}</span> </div> <div style="display: flex; gap: 10px;"> <input id="inputID" ref="inputRef" v-model="inputVal" /> <button @click="onClick">Send to Settings window</button> </div> </template> <script setup> import { ref, onUnmounted, onMounted } from 'vue' const inputRef = ref(null) const inputVal = ref('') const messageVal = ref(null) const onClick = () => { // 点击发送消息 electronAPI.pushMessageEvent(inputVal.value) } let removeLister = null onMounted(() => { // 监听接收settins窗口回传的消息 removeLister = electronAPI.onMessagePort(async (value) => { messageVal.value = value }) }) onUnmounted(() => { // 页面移除时,移除监听 removeLister() }) </script>
-
src/vue-project/pages/settings/index.vue
javascript<template> <h1>😊 Settings Render Process</h1> <div style="padding-bottom: 10px;">Received message from the main window process:<span style="color: #71C25C;">{{ message }}</span> </div> <div style="display: flex; gap: 10px;"> <input v-model="inputVal" /> <button @click="onClick">Send to Main window</button> </div> </template> <script setup> import { ref, onUnmounted, onMounted } from 'vue' const message = ref('') const inputVal = ref('') const onClick = () => { // 点击发送消息 electronSettingAPI.pushMessageEvent(inputVal.value) } let removeLister = null onMounted(() => { // 监听端口消息,当接收到消息时,再回传一段话给main窗口 removeLister = electronSettingAPI.onMessagePort((value) => { message.value = value electronSettingAPI.pushMessageEvent(`👋 Hello main window, I have received your message, message is ${message.value}`) }) }) onUnmounted(() => { // 页面移除时,移除监听 removeLister() }) </script>
-
src/vue-project/router/index.js
javascriptimport { createWebHashHistory, createRouter } from 'vue-router' import HomeView from '@/vue-project/pages/home/index.vue' import SettingsView from '@/vue-project/pages/settings/index.vue' const routes = [ { path: '/', component: HomeView }, { path: '/settings', component: SettingsView }, ] const router = createRouter({ history: createWebHashHistory(), routes, }) export default router;
-
src/vue-project/App.vue
javascript<template> <h1>🖥️ Hello World!</h1> <p>Welcome to your Electron application.</p> <p>🎉 Welcome to your Electron <span style="font-weight: 600;">{{ isMain ? 'Main' : 'Settings' }} Window </span>. 🎊 </p> <div style="margin-top: 20px; border: 1px solid grey; padding: 20px; border-radius: 6px; background-color: #23272E; color: #fff"> <router-view></router-view> </div> </template> <script setup> import { ref } from 'vue' const isMain = ref(true) if (window.electronAPI) { isMain.value = true } else { isMain.value = false } </script>
示例演示:

解决潜在问题:
问题1:从其他页面切换回来时,消息端口监听失效
-
src/vue-project/pages/empty/index.vue
新增一个空页面,用于复现从其他页面切换回来后,消息端口监听失效问题
javascript<template> <h1>😁Test whether the message communication is still valid after the page is switched back.</h1> </template> <script setup> </script>
-
src/vue-project/router/index.js
注册empty页面路由
javascriptimport { createWebHashHistory, createRouter } from 'vue-router' import HomeView from '@/vue-project/pages/home/index.vue' import SettingsView from '@/vue-project/pages/settings/index.vue' import EmptyView from '@/vue-project/pages/empty/index.vue' const routes = [ { path: '/', component: HomeView }, { path: '/settings', component: SettingsView }, { path: '/empty', component: EmptyView }, ] const router = createRouter({ history: createWebHashHistory(), routes, }) export default router;
-
src/vue-project/App.vue
javascript<template> <h1>🖥️ Hello World!</h1> <p>Welcome to your Electron application.</p> <p>🎉 Welcome to your Electron <span style="font-weight: 600;">{{ isMain ? 'Main' : 'Settings' }} Window </span>. 🎊 </p> <nav> <div v-if="isMain"> <RouterLink to="/">Go to Home</RouterLink> </div> <div v-if="isMain"> <div> <RouterLink to="/empty">Go to empty page<span style="font-size: 12px; color: #999; padding-left: 10px;"></span> </RouterLink> </div> </div> </nav> <div style="margin-top: 20px; border: 1px solid grey; padding: 20px; border-radius: 6px; background-color: #23272E; color: #fff"> <router-view></router-view> </div> </template> <script setup> import { ref } from 'vue' const isMain = ref(true) if (window.electronAPI) { isMain.value = true } else { isMain.value = false } </script>
-
问题复现
-
解决方法
vue页面没缓存的情况下,页面会直接被移除,导致之前的监听也丢失。需要给页面路由加上
<keep-alive>
src/vue-project/App.vue
javascript<!-- 该处得加入keep-alive,否则切换路由时会重新渲染组件,导致消息端口监听器失效 --> <router-view v-slot="{ Component }"> <keep-alive> <component :is="Component" /> </keep-alive> </router-view>
问题2:项目打包构建npm run make
后运行,settings窗口显示报错

-
解决方法:
报错的原因是在生产环境下,找不到settings窗口的路由导致的,更改下生产环境的页面路由路径就可以了
src/main.js
javascript// 其他代码... const createSettingsWindow = () => { const settingsWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeintegration: true, preload: path.join(__dirname, 'settingPreload.js'), }, }); if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { const settingUrl = `${MAIN_WINDOW_VITE_DEV_SERVER_URL}/#/settings`; settingsWindow.loadURL(settingUrl); } else { // 增加此行代码:hash: 'settings', 保证打包好的生产环境下正常跳转 settingsWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`), { hash: 'settings' }); } settingsWindow.once('ready-to-show', () => { settingsWindow.webContents.postMessage('settings-port', null, [port2]) }) settingsWindow.webContents.openDevTools(); }; // 其他代码...
项目结构
