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

1. 渲染器进程到主进程(单向)

要将单向 IPC 消息从渲染进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收消息。

案例:动态设置窗口标题

先来了解一个方法: 在窗口实例身上有个setTitle方法。

1.使用 ipcMain.on 监听事件

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

// 处理设置窗口标题的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去用就好了。

// 方法
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 调用。

// 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') 的值,拿到值我们就可以为所欲为了。

// 依旧精简

// 处理打开系统文件对话框的事件
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:
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 :
<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>

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐