在 Electron 中,利用 ipcMain 和 ipcRenderer 模块,进程之间可以通过开发者定义的"通道"传递消息来进行通信。
1. 渲染器进程到主进程(单向)
要将单向 IPC 消息从渲染进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收消息。
案例:动态设置窗口标题
先来了解一个方法: 在窗口实例身上有个setTitle方法。

1.使用 ipcMain.on 监听事件
在主进程中,通过 ipcmain.on API 在 set-title 通道上设置一个 IPC 监听器:

TypeScript
// 处理设置窗口标题的IPC事件
function handleSetTitle(event: Electron.IpcMainEvent, title: string): void {
const webContents = event.sender // 获取发送事件的WebContents对象
// 从WebContents对象获取对应的BrowserWindow实例
const win = BrowserWindow.fromWebContents(webContents)
if (win) {
win.setTitle(title) // 设置窗口标题
}
}
// 当Electron应用准备就绪时触发
app.whenReady().then(() => {
// 监听渲染进程发送的'set-title'消息,调用handleSetTitle函数处理事件
ipcMain.on('set-title', handleSetTitle)
createWindow() // 创建主窗口
})
handleSetTitle 回调函数有两个参数:一个 ipcMainEvent 结构体和一个 title 字符串。 每当消息通过 set-title 通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle API。
2.通过预加载脚本暴露 ipcRenderer.send
要将消息发送到上面创建的监听器,可以使用 ipcRenderer.send API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,咱需要使用 contextBridge API 来选择要从预加载脚本中暴露哪些 API。
我是通过@quick-start/electron官方脚手架生成的项目结构(上个文章中有),这个项目结构中已经配置好了,在这里:

人家直接挂载到了window上,我们在渲染进程中直接通过window去用就好了。
html
// 方法
const ipcHandle = (): void => window.electron.ipcRenderer.send('set-title', '鼻屎拌蛔虫')
// 按钮
<div class="action">
<a target="_blank" rel="noreferrer" @click="ipcHandle">Send IPC</a>
</div>
直接 window.electron.ipcRenderer.send 这样用就好了。
效果:
这样就成功向主进程发送消息了,因为渲染进程不能直接更改系统窗口标题,只能在主进程中修改,所以我们就通过ipcRender.send 向主进程发送消息,而在主进程中,我们早就监听了一个set-title方法,在这个方法中修改了窗口标题。看效果(左上角标题):

最后我在把完整代码贴过来,要不容易乱。
2. 渲染器进程到主进程(双向)
双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这是通过搭配使用 ipcRenderer.invoke 和 ipcMain.handle 来实现的。
案例:动态设置logo
再来了解一个方法: dialog.showOpenDialog


1.使用 ipcMain.handle 监听事件
在主进程中,我们将创建一个 handleFileOpen() 函数,它调用 dialog.showOpenDialog 并返回用户选择的文件路径值。 每当渲染进程通过 dialog:openFile 通道发送 ipcRender.invoke 消息时,该函数就会作为回调函数来处理这个消息。 然后,返回值将作为一个 Promise 返回到最初的 invoke 调用。
TypeScript
// main主进程中: (精简代码)
// 返回用户选择的文件路径值
async function handleFileOpen(): Promise<string> {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
} else {
return ''
}
}
// 当Electron应用准备就绪时触发
app.whenReady().then(() => {
// 监听渲染进程发送的'dialog:openFile'消息,调用handleFileOpen函数处理事件
ipcMain.handle('dialog:openFile', handleFileOpen)
// 注册自定义协议以支持读取本地文件
protocol.handle('media', (request) => {
// 处理 media:///C:/path 这种情况,避免 host 解析问题
const url = request.url.replace(/^media:\/*/, '')
const decodedUrl = decodeURIComponent(url)
// 确保构建出 file:///C:/path 格式
return net.fetch(`file:///${decodedUrl}`)
})
createWindow() // 创建主窗口
})
2.通过预加载脚本暴露 ipcRenderer.invoke
在预加载脚本中,我们暴露了一个单行的 openFile 函数,它调用并返回 ipcRenderer.invoke('dialog:openFile') 的值,拿到值我们就可以为所欲为了。
html
// 依旧精简
// 处理打开系统文件对话框的事件
const url = ref(icon)
const handleOpenFile = async (): Promise<void> => {
const filePath = await window.electron.ipcRenderer.invoke('dialog:openFile')
if (filePath) {
const normalizePath = filePath.replace(/\\/g, '/')
url.value = `media:///${normalizePath}`
console.log(url.value, 'filePath')
console.log(normalizePath, 'normalizePath')
ElMessage.success(`选择的文件路径: ${filePath}`)
} else {
ElMessage.info('用户取消了文件选择')
}
}
// 主要结构
<img alt="logo" class="logo" :src="url" />
<div class="action">
<a target="_blank" rel="noreferrer" @click="handleOpenFile">打开系统文件</a>
</div>
效果:
涉及的前端语法,就不讲解了。看效果:

主进程:
在 Electron 项目的目录结构中,main.js 通常扮演着整个应用的入口角色,也就是所谓的**"主进程"** 。与负责 UI 渲染的**"渲染进程"** 不同,主进程运行在完整的 Node.js 环境中,拥有操作系统的最高权限。
主进程主要承担了以下几项纯 Web 端无法实现的关键任务。首先是文件系统的直接访问。利用 Node.js 的 fs 模块,应用可以绕过用户的"另存为"对话框,直接将配置文件或媒体资源写入本地磁盘。
其次是网络请求的代理与跨域规避。在 Web 开发中,CORS (跨域资源共享)策略常常限制了前端对第三方 API 的直接调用。在 Electron 的主进程中,我们可以利用 ipcMain 建立 API 代理层,由 Node.js 发起网络请求。由于 Node.js 不受浏览器同源策略的限制,这种方式完美解决了跨域问题,常用于对接各类复杂的 AI 模型接口。
主进程还负责自定义协议的注册。通过 protocol 模块,应用可以注册自定义协议,使得前端页面能够像访问网络资源一样,安全、便捷地加载本地硬盘中的图片或视频资源。
完整代码:
主进程main:
TypeScript
import { app, shell, BrowserWindow, ipcMain, screen, dialog, protocol, net } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
// 注册特权协议,必须在 app ready 之前调用
protocol.registerSchemesAsPrivileged([
{
scheme: 'media', // 注册 media 协议
privileges: {
secure: true, // 启用安全协议,要求所有资源都通过 HTTPS 加载
supportFetchAPI: true, // 启用 Fetch API 支持
bypassCSP: true, // 注意:虽然这里设置了 bypassCSP,但通常还是推荐在 meta 标签中配置 CSP
corsEnabled: true // 启用 CORS 跨域资源共享
}
}
])
// 创建主窗口
function createWindow(): void {
// 获取主屏幕的工作区域尺寸
const primaryDisplay = screen.getPrimaryDisplay()
const { width, height } = primaryDisplay.workAreaSize
const mainWindow = new BrowserWindow({
width,
height,
show: false, // 窗口创建时不显示
autoHideMenuBar: true, // 自动隐藏菜单栏
resizable: false, // 窗口不可调整大小
// frame: false, // 窗口无边框
// fullscreen: true, // 窗口全屏显示
...(process.platform === 'linux' ? { icon } : {}), // 在Linux平台上设置图标
webPreferences: {
preload: join(__dirname, '../preload/index.js'), // 预加载脚本路径
sandbox: false, // 禁用沙箱模式
defaultFontFamily: {
standard: 'MIcrosoft·YaHei' // 设置默认字体为微软雅黑
}
}
})
// 窗口准备好显示时触发
mainWindow.on('ready-to-show', () => {
mainWindow.show() // 窗口准备好显示时,显示窗口
})
// 处理窗口打开外部链接事件
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url) // 打开外部链接
return { action: 'deny' } // 拒绝打开新窗口
})
// 加载渲染进程HTML文件
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// 处理设置窗口标题的IPC事件
function handleSetTitle(event: Electron.IpcMainEvent, title: string): void {
const webContents = event.sender // 获取发送事件的WebContents对象
const win = BrowserWindow.fromWebContents(webContents) // 从WebContents对象获取对应的BrowserWindow实例
if (win) {
win.setTitle(title) // 设置窗口标题
}
}
// 返回用户选择的文件路径值
async function handleFileOpen(): Promise<string> {
const { canceled, filePaths } = await dialog.showOpenDialog({})
if (!canceled) {
return filePaths[0]
} else {
return ''
}
}
// 当Electron应用准备就绪时触发
app.whenReady().then(() => {
// 设置应用用户模型ID,用于Windows平台的任务栏分组
electronApp.setAppUserModelId('com.electron')
// 监听浏览器窗口创建事件,为窗口启用优化器的快捷键
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// 监听渲染进程发送的'set-title'消息,调用handleSetTitle函数处理事件
ipcMain.on('set-title', handleSetTitle)
// 监听渲染进程发送的'dialog:openFile'消息,调用handleFileOpen函数处理事件
ipcMain.handle('dialog:openFile', handleFileOpen)
// 注册自定义协议以支持读取本地文件
protocol.handle('media', (request) => {
// 处理 media:///C:/path 这种情况,避免 host 解析问题
const url = request.url.replace(/^media:\/*/, '')
const decodedUrl = decodeURIComponent(url)
// 确保构建出 file:///C:/path 格式
return net.fetch(`file:///${decodedUrl}`)
})
createWindow() // 创建主窗口
app.on('activate', function () {
// 当dock图标被点击,且没有其他窗口打开时,创建新窗口
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// 当所有窗口都关闭时触发
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit() // 非macOS平台上,所有窗口关闭时,退出应用
}
})
渲染进程renderer -> app :
html
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import Versions from './components/Versions.vue'
import icon from '@renderer/assets/electron.svg'
const ipcHandle = (): void => window.electron.ipcRenderer.send('set-title', '鼻屎拌蛔虫')
const handleClick = (): void => {
ElMessage.success('点击了按钮-' + import.meta.env.VITE_NAME)
}
// 处理打开系统文件对话框的事件
const url = ref(icon)
const handleOpenFile = async (): Promise<void> => {
const filePath = await window.electron.ipcRenderer.invoke('dialog:openFile')
if (filePath) {
const normalizePath = filePath.replace(/\\/g, '/')
url.value = `media:///${normalizePath}`
console.log(url.value, 'filePath')
console.log(normalizePath, 'normalizePath')
ElMessage.success(`选择的文件路径: ${filePath}`)
} else {
ElMessage.info('用户取消了文件选择')
}
}
</script>
<template>
<img alt="logo" class="logo" :src="url" />
<div class="creator">Powered by electron-vite</div>
<div class="text">
Build an Electron app with
<span class="vue">Vue</span>
and
<span class="ts">TypeScript</span>
</div>
<p class="tip">Please try pressing <code>F12</code> to open the devTool</p>
<div class="actions">
<div class="action">
<a href="https://electron-vite.org/" target="_blank" rel="noreferrer">Documentation</a>
</div>
<div class="action">
<a target="_blank" rel="noreferrer" @click="ipcHandle">Send IPC</a>
</div>
<div class="action">
<a target="_blank" rel="noreferrer" @click="handleOpenFile">打开系统文件</a>
</div>
</div>
<el-button type="primary" @click="handleClick">这是一个ElementPlus按钮</el-button>
<Versions />
</template>
