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>
相关推荐
nujnewnehc6 小时前
ps, ai, ae插件都可以用html和js开发了
前端·javascript
Jagger_9 小时前
整洁架构三连问:是什么,怎么做,为什么要用
前端
一个处女座的程序猿O(∩_∩)O9 小时前
React 完全入门指南:从基础概念到组件协作
前端·react.js·前端框架
前端摸鱼匠10 小时前
Vue 3 的defineEmits编译器宏:详解<script setup>中defineEmits的使用
前端·javascript·vue.js·前端框架·ecmascript
里欧跑得慢10 小时前
Flutter 测试全攻略:从单元测试到集成测试的完整实践
前端·css·flutter·web
Jagger_10 小时前
前端整洁架构详解
前端
徐小夕10 小时前
我花一天时间Vibe Coding的开源AI工具,一键检测你的电脑能跑哪些AI大模型
前端·javascript·github
英俊潇洒美少年10 小时前
Vue3 企业级封装:useEventListener + 终极版 BaseEcharts 组件
前端·javascript·vue.js
嵌入式×边缘AI:打怪升级日志11 小时前
使用JsonRPC实现前后台
前端·后端
小码哥_常12 小时前
深度剖析:为什么Android选择了Binder
前端