vite+vue3+ts+electron桌面应用web端桌面端开发=>IPC进程通信!

Electron 进程通信 IPC(Inter-Process Communication)

前言

首先大家需要了解一些 Electron 的前置知识。众所周知 Electron 是将 chrome Chromium 内核Node.js 一起嵌入到一个二进制文件中来实现构建跨平台桌面应用的框架。其中 Chromium 提供 WebAPI(DOM、BOM) 的能力,Node.js 提供了调用 系统 API 的能力。

什么是进程?

进程(Process) 是操作系统中一个核心概念,用于描述程序的动态执行过程。它代表了程序在计算机内存中的一次运行实例,是操作系统进行资源分配和调度的基本单位。

上面的解释取自文心一言,我理解的进程便是 "微信" 与 "QQ" 的关系,两个应用便是两个进程。而操作系统便是他们的管理员,对它们进行资源分配和调度。

chromium 为什么是多进程架构?

Chromium 的多进程架构通过隔离故障、限制权限、并行计算和动态资源管理,实现了浏览器在稳定性、安全性、性能和资源利崩溃的平衡,尤其适应现代 Web 的复杂需求。

1. 稳定性保障

  • 崩溃隔离:每个标签页或插件运行在独立进程,单进程崩溃不影响其他页面或浏览器整体。
  • 内存泄漏可控:渲染进程内存泄漏仅影响自身,关闭进程即可释放资源,避免全局卡顿。

2. 安全性强化

  • 沙盒机制:渲染和插件进程在沙盒中运行,权限受限,恶意代码无法直接攻击系统。
  • 站点隔离:不同域名网页分配独立进程,防止跨站攻击(如 Spectre 等漏洞利用)。

3. 性能优化

  • 并行计算:多进程利用多核 CPU,GPU 进程独立加速渲染,提升复杂页面(如视频、游戏)性能。
  • 资源动态分配:根据进程优先级(如最近使用标签页)动态调整内存,减少磁盘交换,提升响应速度。

4. 资源灵活管理

  • 进程模型可配置:支持单进程(低资源设备)到多进程(高性能设备)的灵活切换。
  • 进程复用策略:相同域名页面共享进程,避免进程数量激增;进程超载时合并资源,平衡负载。

Electron 的进程模型

Electron 继承了 Chromium 的多进程架构,这使得该框架在架构上与现代 Web 浏览器非常相似。在 Electron 应用中主要分为 主进程(main)渲染进程(renderer),他们分别为:

  • 主进程 :作为 Electron 应用的入口进程,它运行在 Node 环境中,因此它可以作为一个服务器给渲染进程提供调用系统级 API 的支持。主进程的主要目的是使用 BrowserWindow 模块创建和管理应用窗口。
  • 渲染进程 :每个 Electron 应用都会为每个打开的 BrowserWindow(以及每个 Web 嵌入)生成一个单独的渲染进程。顾名思义,渲染器负责渲染 Web 内容。可以调用 chromium 提供的 WebAPI,它跟一个 chrome 浏览器没有任何区别,所以渲染进程中运行的代码的行为应符合 Web 标准。

可以理解为每个渲染进程都是一个独立的浏览器窗口,它们具备 chromium 浏览器所有的功能。

安全问题

是否开启 Node 集成(nodeIntegration)

BrowserWindow.webPreferences.nodeIntegration: true 时,为渲染进程集成 Node API。默认:false

这是极其不推荐且非常不安全的做法,虽然它为我们在渲染进程操作系统级 API 带来了便利,但是相对应的也为恶意代码留下了可乘之机。例如:

js 复制代码
// 假设存在 XSS 漏洞,攻击者注入以下代码:
const fs = require('fs');
fs.writeFileSync('/tmp/malicious.sh', '恶意脚本内容');
const { exec } = require('child_process');
exec('chmod +x /tmp/malicious.sh && /tmp/malicious.sh');

上下文隔离(contextIsolation)

上下文隔离是一项功能,可确保你的 preload 脚本和 Electron 的内部逻辑在与你在 webContents 中加载的网站不同的上下文中运行。这对于安全目的很重要,因为它有助于防止网站访问 Electron 内部结构或预加载脚本有权访问的强大 API。

这意味着你的预加载脚本有权访问的 window 对象实际上与网站有权访问的对象不同。例如,如果你在预加载脚本中设置了 window.hello = 'wave' 并启用了上下文隔离,则当网站尝试访问 window.hello 时,window.hello 将是未定义的。

自 Electron 12 起,上下文隔离已默认启用,并且它是所有应用的推荐安全设置。

上文我是直接粘贴官网的解释。简单来说就是 renderer.js 中的 windowpreload.js 中的 window 是否共享。

示例:

  1. 创建 src/typings/global.d.ts, 为 global 对象声明全局变量 username
  2. 创建 electron/preload.ts, 设置 window.username = 'adimn'
  3. main.ts 中配置 BrowserWindow.webPreferences.preload: path.join(__dirname, "preload.js")
  4. App.vue 中打印 console.log(window.username)
  5. vite.electron.config.ts 修改 electron 插件配置 为 electron([{ entry: "electron/main.ts" }, { entry: "electron/preload.ts" }])

打印结果为 admin,证明 preloadrenderer 使用的是同一个 window 对象。

  • 上述代码不变,仅修改 contextIsolation: true 时:

可以看到打印是 undefined 也就证明了此时 window 是各自独立的。也就是文中所说的 是否共享 window

预加载脚本(preload)

出于安全原因,渲染进程默认只运行网页不运行 Node.js。所以为了将 Electron 的不同进程桥接在一起,我们需要使用一个称为预加载的特殊脚本。总而言之 preloadElectron 提供的一个可以安全访问 系统 API 的沙盒环境,是渲染进程安全访问 系统API 的桥梁。

  • preload 脚本中可以访问 WebAPI 以及有限的 Node.jsElectron API

从 Electron 20 开始,预加载脚本默认被沙箱化,并且不再能够访问完整的 Node.js 环境。

可用的 API 细节
Electron 模块 渲染进程模块
Node.js API eventsurltimers
Polyfill 全局变量 BufferprocessclearImmediatesetImmediate
contextBridge

preload 脚本中,使用 contextBridge.exposeInMainWorld(apiKey, api)contextBridge.exposeInIsolatedWorld(worldId,apiKey, api) 方法将 api 暴露给渲染进程。

注意:即便是开启了上下文隔离 跟关闭Node 集成 也需要规范小心的使用 contextBridge Api 才能保证安全的将接口提供给渲染进程使用。例如:

preload.ts 复制代码
// ❌ 错误做法
contextBridge.exposeInMainWorld('myAPI', {  
  send: ipcRenderer.send  
})

// ✅ 正确做法  
contextBridge.exposeInMainWorld('myAPI', {  
 loadPreferences: () => ipcRenderer.invoke('load-prefs')  
})

第一种方式直接公开强大的 API,无需任何类型的参数过滤。这将允许任何网站发送任意 IPC 消息,而你不希望这种情况发生。公开基于 IPC 的 API 的正确方法是为每个 IPC 消息提供一种方法。

进程通信

进程间通信(IPC)是在 Electron 中构建功能丰富的桌面应用的关键部分。由于主进程和渲染进程在 Electron 的进程模型中具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改等。

  • ipcMainipcMain 模块是 Event Emitter。 当在主进程中使用时,它处理从渲染器进程(网页)发送的异步和同步消息。从渲染器发送的消息将被发送到该模块

  • ipcRendereripcRenderer 模块是 EventEmitter。它提供了一些方法,以便你可以从渲染进程(网页)向主进程发送同步和异步消息。你还可以接收来自主进程的响应

进程通信的四种模式

根据 ipcRenderer.sendipcMain.onipcRenderer.invokeipcMain.handle 以及 MessageChannel API 可以组合为 4 种基本的通信模式。

一、 渲染进程 ==> 主进程(单向)

这种模式通常是渲染进程需要使用主进程系统 api 的能力来完成一些需求,且无需等待主进程的执行结果时使用。例如下面一个动态修改窗口 title 的示例。

  1. 在主进程中添加 ipcMain.on(channel, callback) 方法监听渲染进程发送的消息动态修改标题。
  2. 使用 ipcRenderer.send(channel, ...args) 方法发送消息通知主进程修改标题。

注意:所有在 renderer 中调用的 API 都是在 contextIsolation: true 以及 未开启 nodeIntegration: false 的沙箱环境下在 preload 中使用 contextBridge API 提供的。

类型声明别漏了🫡

ts 复制代码
# global.d.ts

export interface ElectronAPI {
  setTitle(title: string): void;
}

declare global {
  var electronAPI: ElectronAPI;
}

export {};
ts 复制代码
# main.ts

import { ipcMain } from 'electron'

ipcMain.on("set-title", (event, title) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win?.setTitle(title);
});
ts 复制代码
# preload.ts

import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title: string) => {
        ipcRenderer.send('set-title', title)
    }
})
ts 复制代码
# App.vue

<script setup lang="ts">
import { ref } from "vue";
const title = ref("App Title");

function setTitle() {
  window.electronAPI.setTitle(title.value);
}
</script>

<template>
  <input type="text" v-model="title" />
  <button @click="setTitle">确认</button>
</template>

二、 主进程 <==> 渲染进程

双向 IPC 的常见应用场景通常是渲染进程需要使用主进程系统 API 的某些能力并等待返回结果时。通过使用 ipcRenderer.invokeipcMain上述andle 配对来完成。添加初始化时获取当前窗口 title 的示例:

  1. 在主进程使用 ipcMain.handle('get-title') 添加一个处理器等待 ipcRenderer.invoke('get-title') 触发。
  2. preload 脚本中 调用 ipcRenderer.invoke('get-title')getTitle API 提供给 renderer
  3. renderer 中调用 window.electronAPI.getTitle() 获取当前窗口的默认标题。
ts 复制代码
export interface ElectronAPI {
  getTitle(): Promise<string>;
}

declare global {
  var electronAPI: ElectronAPI;
}

export {};
ts 复制代码
# main.ts

import { ipcMain } from "electron";

ipcMain.handle("get-title", (event) => {
  const webContents = event.sender;
  const win = BrowserWindow.fromWebContents(webContents);
  return win?.getTitle();
});
ts 复制代码
# preload.ts

import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  getTitle: () => {
    return ipcRenderer.invoke("get-title");
  }
});
ts 复制代码
# App.vue

<script setup lang="ts">
import { ref } from "vue";
const title = ref("App Title");

window.electronAPI.getTitle().then((value) => {
  title.value = value;
});
</script>

<template>
  <h1>{{ title }}</h1>
</template>

可以看到 h1 标签中显示的是 Vite + Vue + TS,而不是默认的 App Title,就证明我们已经通过主进程的 api 获取了窗口默认的标题。

ipcRenderer.invokeelectron 7 中添加的新 api,也可以使用 ipcRenderer.sendipcRenderer.sendSync 来实现双向通信,但并不推荐。其原因便是开发代码的冗余跟同步代码带来的性能阻塞问题。参考

三、 主进程 ==> 渲染进程

当从主进程向渲染器进程发送消息时,需要指定哪个渲染器正在接收该消息。消息需要通过渲染进程的 WebContents 实例发送到渲染进程。此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。添加一个点击系统菜单提醒用户的功能示例:

  1. 在主进程使用 Menu.buildFromTemplate 添加一个 "提醒用户" 的菜单按钮,并添加 send 事件。 2.preload 中使用 ipcRenderer.on('message') 添加 onMessage 函数暴露给渲染进程。
  2. renderer 中调用 window.electron.onMessage() 监听主进程发送的消息
  3. 还可以在 preload 中额外添加一个 sendMessage 函数来处理渲染进程需要回复消息的情况。当然,这不是必须的。
ts 复制代码
# global.d.ts

export interface ElectronAPI {
  onMessage: (callback: (...args: any[]) => void) => void;
  replyMessage: (...args: any[]) => void;
}

declare global {
  var electronAPI: ElectronAPI;
}

export {};
ts 复制代码
# main.ts

import { app, BrowserWindow, ipcMain, Menu } from "electron";
import path from "node:path";

const createWindow = () => {
  const win = new BrowserWindow({
    width: 960,
    height: 600,
    webPreferences: {
      nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
      contextIsolation: true, // 设置是否启用上下文隔离模式。
      preload: path.join(__dirname, "preload.js"),
    },
  });

  ipcMain.on("reply-message", (_, message) => {
    console.log(message); // 打印:"收到消息了😉"
  });

  const menu = Menu.buildFromTemplate([
    {
      label: "提醒用户",
      click: () => {
        win.webContents.send("message", "测试消息");
      },
    },
  ]);

  win.setMenu(menu);

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
  } else {
    win.loadFile(path.join(__dirname, "../dist/index.html"));
    // win.loadFile(path.join(__dirname, '../index.html'));
    // win.loadFile(path.join(__dirname, '../web-dist/index.html'));
  }
};

app.whenReady().then(createWindow);
ts 复制代码
# preload.ts

import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  onMessage: (callback: (...args: any[]) => void) => {
    ipcRenderer.on("message", (_, ...args) => callback(...args));
  },
  replyMessage: (message: string) => {
    ipcRenderer.send("reply-message", message);
  },
});
ts 复制代码
# App.vue

<script setup lang="ts">
import { ref } from "vue";
const title = ref("App Title");

window.electronAPI.onMessage((value) => {
  window.alert(value);
  window.electronAPI.replyMessage("收到消息了😉");
});
</script>

<template>
  <h1>{{ title }}</h1>
</template>

四、 渲染进程 1 <==> 渲染进程 2

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染进程之间发送消息。想要实现这种模式,有两种选择:

  • 使用主进程作为渲染进程之间的消息代理,由渲染进程向主进程发送消息,在由主进程将消息转发到另一个渲染进程。
  • 使用 MessagePort 从主进程传递到两个渲染器。这将允许在初始化后渲染器之间进行直接通信。

开始之前先做一点前置准备

  • 新建 public/login.html 文件作为第二个渲染进程。
  • electron/main.ts 新增 createLoginWindow 函数,用于创建 login 渲染进程。
  • 新建 electron/loginPreload.ts 文件作为 login 渲染进程的 preload 脚本。
  • 修改 App.vue 的代码,添加一个 登录按钮,用于打开 LoginWindow
  • electron/main.tselectron/preload.tsApp.vue 分别添加跟调用 open-login-view 这个事件的函数。
  • vite.electron.config.ts 中 添加 electron([...,{ entry: "electron/loginPreload.ts" }])
ts 复制代码
# global.d.ts

export interface ElectronAPI {
  openLoginView: () => void;
}

declare global {
  var electronAPI: ElectronAPI;
}

export {};
html 复制代码
# login.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Login</h1>
    <input type="button" value="Login" onclick="login()" />
    <script>
      function login() {
        // 登录成功
        console.log("登录成功");
      }
    </script>
  </body>
</html>
ts 复制代码
# main.ts

import { app, BrowserWindow, ipcMain } from "electron";
import path from "node:path";

const createWindow = () => {
  const win = new BrowserWindow({
    width: 960,
    height: 600,
    webPreferences: {
      nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
      contextIsolation: true, // 设置是否启用上下文隔离模式。
      preload: path.join(__dirname, "preload.js"),
    },
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
  } else {
    win.loadFile(path.join(__dirname, "../dist/index.html"));
    // win.loadFile(path.join(__dirname, '../index.html'));
    // win.loadFile(path.join(__dirname, '../web-dist/index.html'));
  }
};

// 🚀
const createLoginWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
      contextIsolation: true, // 设置是否启用上下文隔离模式。
      preload: path.join(__dirname, "loginPreload.js"),
    },
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    // path.join(process.env.VITE_DEV_SERVER_URL, "login.html") => http://localhost:3000/login.html
    win.loadURL(path.join(process.env.VITE_DEV_SERVER_URL, "login.html"));
  } else {
    win.loadFile(path.join(__dirname, "../dist/index.html"));
  }
};

// 🚀
ipcMain.handle("open-login-view", () => {
  createLoginWindow();
});

app.whenReady().then(() => {
  createWindow();
});
ts 复制代码
# preload.ts

import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("electronAPI", {
  openLoginView: () => ipcRenderer.invoke("open-login-view"),
});
html 复制代码
# App.vue

<script setup lang="ts">
function openLoginView() {
  window.electronAPI.openLoginView();
}
</script>

<template>
  <h1>首页</h1>
  <button @click="openLoginView">登录</button>
</template>
ts 复制代码
# vite.electron.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import electron from "vite-plugin-electron";
import { rmSync } from "node:fs";

// 先将dist、dist-electron、release文件夹强制删除在进行后续的打包流程
rmSync("dist", { recursive: true, force: true });
rmSync("dist-electron", { recursive: true, force: true });
rmSync("release", { recursive: true, force: true });

export default defineConfig({
  plugins: [
    vue(),
    electron([
      { entry: "electron/main.ts" },
      { entry: "electron/preload.ts" },
      // 🚀
      { entry: "electron/loginPreload.ts" },
    ]),
  ],
});

OK!准备就绪!下面实现一个模拟登录成功后通知主渲染进程来调用系统通知的 API 通知用户登录成功的操作。

1. 代理中转模式

这个模式我就不演示啦,流程就是 渲染进程1 <=> 主进程 <=> 渲染进程2

2.MessagePort 模式

这个模式是在主进程 使用 MessageChannel API 创建一个信息通道,分别给渲染进程去使用,实现两个渲染进程之间直接可以进行通信。就像一条管道一样,能够互相发送消息以及回复消息。

  1. 使用 MessageChannel API 创建一个消息通道,分别在 ready-to-show 事件中发送给 mainWindowloginWindow 两个渲染进程使用。
  2. 在主进程中新增名为 close-windowshow-notification 的处理器等待调用。
  3. loginPreload.ts 、跟 preload.ts 使用 ipcRenderer.on('port') 获取 MessagePort
  4. loginPreload.ts 添加 postMessage 函数暴露给 loginWindow 渲染进程。
  5. login.html 调用 window.electronAPI.postMessage 通知 mainWindow 已经登陆成功。
  6. preload.ts 文件中根据 data.type 处理对应事件逻辑。
ts 复制代码
# main.ts

import {
  app,
  BrowserWindow,
  ipcMain,
  MessageChannelMain,
  Notification,
  NotificationConstructorOptions,
} from "electron";
import path from "node:path";

// 🚀
const { port1, port2 } = new MessageChannelMain();

const createWindow = () => {
  const win = new BrowserWindow({
    width: 960,
    height: 600,
    webPreferences: {
      nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
      contextIsolation: true, // 设置是否启用上下文隔离模式。
      preload: path.join(__dirname, "preload.js"),
    },
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    win.loadURL(process.env.VITE_DEV_SERVER_URL);
  } else {
    win.loadFile(path.join(__dirname, "../dist/index.html"));
    // win.loadFile(path.join(__dirname, '../index.html'));
    // win.loadFile(path.join(__dirname, '../web-dist/index.html'));
  }

// 🚀
  win.once("ready-to-show", () => {
    win.webContents.postMessage("port", null, [port1]);
  });
};

const createLoginWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      nodeIntegration: false, // 设置是否在页面中启用 Node.js 集成模式
      contextIsolation: true, // 设置是否启用上下文隔离模式。
      preload: path.join(__dirname, "loginPreload.js"),
    },
  });

  if (process.env.VITE_DEV_SERVER_URL) {
    // path.join(process.env.VITE_DEV_SERVER_URL, "login.html") => http://localhost:3000/login.html
    win.loadURL(path.join(process.env.VITE_DEV_SERVER_URL, "login.html"));
  } else {
    win.loadFile(path.join(__dirname, "../dist/index.html"));
  }

// 🚀
  win.once("ready-to-show", () => {
    win.webContents.postMessage("port", null, [port2]);
  });
};

ipcMain.handle("open-login-view", () => {
  createLoginWindow();
});

// 🚀
ipcMain.handle("close-window", (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win?.close();
});

// 🚀
ipcMain.handle(
  "show-notification",
  (_, options: NotificationConstructorOptions) => {
    if (Notification.isSupported()) {
      new Notification(options).show();
    }
  }
);

app.whenReady().then(createWindow);
ts 复制代码
# loginPreload.ts

import { contextBridge, ipcRenderer } from "electron";

let port: MessagePort | null = null;

ipcRenderer.on("port", (event) => {
  port = event.ports[0];

  port.start();
});

contextBridge.exposeInMainWorld("electronAPI", {
  postMessage: (message: any, options?: StructuredSerializeOptions) => {
    port?.postMessage(message, options);
  },
});
html 复制代码
# login.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Login</h1>
    <input type="button" value="Login" onclick="login()" />
    <script>
      function login() {
        // 登录成功...

        // 通知主渲染进程
        window.electronAPI.postMessage({
          type: "login-success",
          response: {
            code: 200,
            data: { username: "admin", password: "admin" },
            message: "登录成功",
          },
        });
      }
    </script>
  </body>
</html>
ts 复制代码
# preload.ts
import {
  contextBridge,
  ipcRenderer,
  type NotificationConstructorOptions,
} from "electron";

ipcRenderer.on("port", (event) => {
  const port = event.ports[0];
  port.onmessage = (messageEvent) => {
    const { data } = messageEvent;
    switch (data.type) {
      case "login-success":
        const options: NotificationConstructorOptions = {
          title: "登录成功",
          body: `您好,${data.response.data.username}。恭喜登录成功,很高心见到你!`,
        };
        ipcRenderer.invoke("show-notification", options);
        break;
      default:
        console.warn("没有对应的事件处理");
        break;
    }
  };

  port.start();
});

contextBridge.exposeInMainWorld("electronAPI", {
  openLoginView: () => ipcRenderer.invoke("open-login-view"),
});

这就完成啦🫡!这种模式的通信流程便是 渲染进程1 <===> 渲染进程2,就不用像第一种模式那种需要主进程作为中间代理来实现了。

总结

Electron 中有 "主进程" 跟 "渲染进程" 两类进程模型。而进程的通信实现起来代码其实并不多,主要还是对进程与进程通信的概念方式的理解为主。方法就那么几个,可以灵活使用,当然最好是以官方推荐的为主。

  • 主进程: 主进程负责给渲染进程提供调用系统级 API 的支持。跟管理渲染进程的创建和管理。

  • 渲染进程: 渲染 Web 内容。提供了 WebApi。

  • preload : 出于安全原因,建议开启 "上下文隔离",关闭渲染模式的 "node 集成模式",并在 preload 中使用 contextBridge API 安全的暴露渲染进程需要使用的 api。

  • 四种通信模式:

    • 渲染进程 ==> 主进程: ipcRenderer.send/ipcRenderer.sendSync/ipcMain.onipcRenderer.invoke/ipcMain.handle
    • 主进程 <==> 渲染进程: ipcRenderer.send/ipcRenderer.sendSync/win.webContents.send/ipcMain.on/ipcRenderer.onipcRenderer.invoke/ipcMain.handle(推荐)。
    • 主进程 ==> 渲染进程: win.webContents.send/ipcRenderer.on
    • 渲染进程 <==> 渲染进程: MessageChannel API、主进程代理中转

这里额外补充一个最近出现的问题,就是最近刚写这篇文章的时候,重新 pnpm i 为我的项目安装依赖后,执行 pnpm electron:dev 命令会报错。这时候全局安装一个依赖 electron-fix 然后执行 electron-fix start 来修复错误,完成以后就可以了。

任务列表

  • 搭建electron项目
    • 运行 electron 桌面端开发环境
    • 运行 web 浏览器端开发环境
  • 打包
  • electron 通信
相关推荐
teeeeeeemo1 分钟前
CSS3 动画基础与技巧
前端·css·笔记·css3
年纪轻轻就扛不住4 分钟前
CSS3 渐变效果
前端·css·css3
Aisanyi8 分钟前
【鸿蒙开发】使用HMRouter路由的使用
前端·harmonyos
杉木笙13 分钟前
Flutter 代码雨实现(矩阵雨)DLC 多图层
前端·flutter
SouthernWind14 分钟前
Vista AI 演示—— 提示词优化功能
前端·vue.js
林太白15 分钟前
也许看了Electron你会理解Tauri,扩宽你的技术栈
前端·后端·electron
前端的日常18 分钟前
JavaScript 必看!算法 O 系列全攻略
前端
anganing22 分钟前
Web 浏览器预览 Excel 及打印
前端·后端
Chad23 分钟前
Vue3 + vite 首屏优化加载速度
前端
Ace_317508877633 分钟前
义乌购平台店铺商品接口开发指南
前端