Electron进程通信

在 Electron 中,利用 ipcMainipcRenderer 模块,进程之间可以通过开发者定义的"通道"传递消息来进行通信。

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.invokeipcMain.handle 来实现的。

再来了解一个方法: 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.jsfs 模块,应用可以绕过用户的"另存为"对话框,直接将配置文件或媒体资源写入本地磁盘。

其次是网络请求的代理与跨域规避。在 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>
相关推荐
splage2 小时前
spring-boot-starter和spring-boot-starter-web的关联
前端
张元清2 小时前
使用 Hooks 构建无障碍 React 组件
前端·javascript·面试
Mahut2 小时前
从零构建神经影像可视化库:neuroviz 的架构设计与实现
前端·javascript·github
慧一居士3 小时前
VueUse 功能介绍使用场景及完整使用示例
前端·vue.js
奇怪的猫3 小时前
浏览器窗口最小化的时候,setInterval 执行变慢,解决方案
前端·javascript
多租户观察室3 小时前
工作流新生态:2026年工作流与Coding的重新分工
前端·人工智能·后端·低代码
cmd3 小时前
别再混淆了!JS类型转换底层:valueOf vs toString vs Symbol.toPrimitive 详解
前端·javascript
用户15815963743703 小时前
AI Agent 说"完成了",但其实没有——任务验收机制的工程实践
javascript
Carsene3 小时前
开源项目文档架构设计:Git Submodule 实现文档与代码的优雅分离
前端·后端