目录

利用本地 Express Web 服务解决复杂的 Electron 通信链路的问题

背景

Web 服务对前端同学来说并不陌生,你们开发其他前端界面请求的后端接口就是 Web 服务,你们 npm run dev启动的也是一个本地的 Web 服务,前端的 js,html,css 都有从这个服务上拉取到的资源。

我们在开发 Electron 时发现了 Electron 进程间通信(IPC)的弊端,弊端的主要来源是 webview 到 Main 的通信链路过长,需要从 page 发到 proload.js 文件,再从 preload 发到 render,再从 render 发送到 Main,这个过程中,需要反复定义类同命名的事件进行中转,大大降低了开发效率,还无意中增加了 render 进程的内存耗费。

ipcRenderer 通信知识补充

官网链接:ipcRenderer | Electron

ipcRenderer.send(channel, ...args)

  • 描述:从渲染进程向主进程发送异步消息。

  • 特点:

    • 单向通信,主进程通过 ipcMain.on 监听并处理消息。

    • 不会自动返回响应,主进程需要额外的机制(如再次发送消息)来回复渲染进程。

  • 使用场景:

    • 当渲染进程需要通知主进程执行某项操作,且不需要等待结果时。
  • 示例:

    • 通知主进程保存数据、打开新窗口或执行某些后台任务。
  • 代码示例:

javascript 复制代码
// 渲染进程
ipcRenderer.send('save-data', { key: 'value' });

// 主进程
ipcMain.on('save-data', (event, data) => {
  console.log('收到数据:', data);
});

2. ipcRenderer.invoke(channel, ...args)

  • 描述:从渲染进程向主进程发送消息,并等待主进程的响应。

  • 特点:

    • 异步双向通信,返回一个 Promise,可以通过 await 获取主进程的处理结果。

    • 主进程使用 ipcMain.handle 来处理请求并返回数据。

  • 使用场景:

    • 当渲染进程需要从主进程获取数据或等待某个操作完成时。
  • 示例:

    • 获取文件内容、查询数据库结果或执行耗时操作。
  • 代码示例:

javascript 复制代码
// 渲染进程
async function getFileContent() {
  const content = await ipcRenderer.invoke('get-file', 'file.txt');
  console.log('文件内容:', content);
}

// 主进程
ipcMain.handle('get-file', async (event, filename) => {
  return '文件内容示例';
});

3. ipcRenderer.sendSync(channel, ...args)

  • 描述:从渲染进程向主进程发送同步消息。

  • 特点:

    • 会阻塞渲染进程,直到主进程通过 event.returnValue 返回结果。

    • 同步操作会影响渲染进程的性能,可能导致 UI 卡顿。

  • 使用场景:

    • 当需要立即获取主进程的响应,且操作足够快时。

    • 不推荐广泛使用,因为阻塞 UI 线程会降低用户体验。

  • 示例:

    • 获取简单的配置值或状态。
  • 代码示例:

javascript 复制代码
// 渲染进程
const result = ipcRenderer.sendSync('get-config', 'theme');
console.log('配置:', result);

// 主进程
ipcMain.on('get-config', (event, key) => {
  event.returnValue = 'dark';
});

4. ipcRenderer.sendTo(webContentsId, channel, ...args)

  • 描述:从一个渲染进程向另一个渲染进程发送消息。

  • 特点:

    • 需要知道目标渲染进程的 webContentsId。

    • 消息直接发送到指定的渲染进程,不经过主进程。

  • 使用场景:

    • 当应用有多个窗口或 webview 时,需要在不同渲染进程之间直接通信。
  • 示例:

    • 一个窗口控制另一个窗口的行为或状态。
  • 代码示例:

javascript 复制代码
// 渲染进程 A
ipcRenderer.sendTo(2, 'update-status', 'ready'); // 2 是目标窗口的 webContentsId

// 渲染进程 B
ipcRenderer.on('update-status', (event, status) => {
  console.log('状态更新:', status);
});

5. ipcRenderer.sendToHost(channel, ...args)

  • 描述:从 webview 的渲染进程向其宿主窗口的渲染进程发送消息。

  • 特点:

    • 特定于 webview 场景,消息发送到宿主窗口的渲染进程,而不是主进程。

    • 宿主窗口通过 webview 标签的 on-ipc-message 事件接收消息。

  • 使用场景:

    • 当 webview 内部的代码需要与宿主窗口通信时。
  • 示例:

    • webview 中的页面通知宿主窗口某个事件发生。
  • 代码示例:

javascript 复制代码
// webview 中的渲染进程
ipcRenderer.sendToHost('page-event', 'loaded');

// 宿主窗口的渲染进程
document.querySelector('webview').addEventListener('ipc-message', (event) => {
  console.log('收到 webview 消息:', event.args);
});

总结对比

|------------|------------------|------|---------|--------------------|
| 函数名 | 通信方向 | 是否异步 | 返回值 | 推荐场景 |
| send | 渲染进程 → 主进程 | 是 | 无 | 单向通知,无需返回结果 |
| invoke | 渲染进程 → 主进程 | 是 | Promise | 双向通信,需等待主进程返回数据 |
| sendSync | 渲染进程 → 主进程 | 否 | 同步返回值 | 快速同步操作(不推荐,易阻塞 UI) |
| sendTo | 渲染进程 → 另一渲染进程 | 是 | 无 | 多窗口或多渲染进程通信 |
| sendToHost | webview → 宿主渲染进程 | 是 | 无 | webview 与宿主窗口通信 |

ipcRenderer.sendToHost 详解

这个是最复杂的,但是官网却是讲得最简单的,也没有举例子,外国人做事真得没的说

  1. 必须结合 preload.js 来实现中转通信,webview 不能直接与主渲染进程通信,至少从 electron 提供的官方文档里面是没有;

  2. 需要借助 webview 注入脚本的 window.postMessage 向 preload.js 中的 window.addEventListener('message', (event) 进行中转

  3. 需要 webview dom 对象本身才能收到来自 webview 里面发过来的消息

下面是 webview 与主进程通信的整个链路,使用 await 进行阻塞式等待的解决方案

webview 实现对 Main 进程的 await 请求

通信流程概述

  • webview 页面通过 postMessage 发送请求。
  • 预加载脚本捕获请求并通过 ipcRenderer.sendToHost 转发给宿主渲染进程。
  • 宿主渲染进程使用 ipcRenderer.invoke 向主进程发送请求并等待响应。
  • 主进程通过 ipcMain.handle 处理请求并返回结果。
  • 宿主渲染进程收到响应后,通过 webview.send 将结果发送回 webview。
  • 预加载脚本通过 postMessage 将响应传递给 webview 页面。
  • webview 页面中的 Promise 解析,获取响应数据。

时序图

通信流程具体实现细节

  1. webview 内部页面发送请求并等待响应

在 webview 的页面中,使用 postMessage 发送请求,并通过事件监听接收响应,利用 Promise 和 await 实现异步等待。

html

javascript 复制代码
<!-- webview.html -->
<script>
  async function sendRequest(data) {
    // 发送请求
    window.postMessage({ type: 'async-request', data }, '*');

    // 等待响应
    return new Promise((resolve) => {
      window.addEventListener('message', function handler(event) {
        if (event.data.type === 'async-response') {
          window.removeEventListener('message', handler);
          resolve(event.data.response);
        }
      });
    });
  }

  // 示例:调用并等待响应
  (async () => {
    const response = await sendRequest('一些数据');
    console.log('收到响应:', response);
  })();
</script>
  1. webview 的预加载脚本

在预加载脚本中,监听 webview 的消息并通过 ipcRenderer.sendToHost 转发给宿主渲染进程,同时接收响应并传递回 webview。

javascript 复制代码
// preload.js
const { ipcRenderer } = require('electron');

window.addEventListener('message', (event) => {
  if (event.data.type === 'async-request') {
    // 转发请求到宿主渲染进程
    ipcRenderer.sendToHost('async-request', event.data.data);
  }
});

// 接收宿主渲染进程的响应
ipcRenderer.on('async-response', (event, response) => {
  window.postMessage({ type: 'async-response', response }, '*');
});
  1. 宿主渲染进程处理请求

在宿主渲染进程中,监听 webview 的消息,使用 ipcRenderer.invoke 向主进程发送请求并等待响应,之后将结果发送回 webview。

javascript 复制代码
// renderer.js (宿主渲染进程)
const { ipcRenderer } = require('electron');
const webview = document.querySelector('webview');

webview.addEventListener('ipc-message', async (event) => {
  if (event.channel === 'async-request') {
    try {
      // 使用 await 等待主进程响应
      const response = await ipcRenderer.invoke('async-to-main', event.args[0]);
      // 将响应发送回 webview
      webview.send('async-response', response);
    } catch (error) {
      console.error('请求失败:', error);
    }
  }
});
  1. 主进程处理请求

在主进程中,使用 ipcMain.handle 处理来自渲染进程的请求,并返回一个 Promise 作为响应。

javascript 复制代码
// main.js
const { ipcMain } = require('electron');

ipcMain.handle('async-to-main', async (event, data) => {
  console.log('收到异步请求:', data);
  // 模拟异步操作,例如文件读取或延时
  await new Promise((resolve) => setTimeout(resolve, 1000));
  return '处理后的数据';
});
  1. 宿主页面配置

在宿主渲染进程的 HTML 文件中,正确加载 webview 并指定预加载脚本。

html 复制代码
<!-- index.html -->
<webview src="./webview.html" preload="./preload.js"></webview>

引入 web 服务的好处

  1. 可以将以上过程减少为一次本地的 http 请求

实现过程

  1. 项目准备

确保你已经初始化了一个 Electron 项目。如果没有,可以按照以下步骤快速创建一个:

bash 复制代码
mkdir electron-express-example
cd electron-express-example
npm init -y
npm install electron express cors

项目结构如下

bash 复制代码
electron-express-example/
├── main.js         # 主进程文件
├── index.html      # webview 页面
├── package.json
└── node_modules/

  1. 在主进程中引入并配置 Express

在 main.js 文件中,我们将引入 Express,启动一个简单的服务器,并定义一个路由来返回配置信息。同时,我们需要处理 Electron 的窗口创建。

以下是 main.js 的完整代码:

javascript 复制代码
const { app, BrowserWindow } = require('electron');
const express = require('express');
const cors = require('cors');

let mainWindow;

// 创建 Express 应用
const expressApp = express();
expressApp.use(cors()); // 解决跨域问题

// 定义一个 /config 路由,返回配置信息
expressApp.get('/config', (req, res) => {
    res.json({ config: '这是来自主进程的配置信息' });
});

// 启动 Express 服务器
expressApp.listen(3000, () => {
    console.log('Express server running on port 3000');
});

// Electron 应用启动
app.on('ready', () => {
    // 创建主窗口
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            webviewTag: true // 启用 webview 标签
        }
    });

    // 加载包含 webview 的页面
    mainWindow.loadFile('index.html');
});

// 应用退出时清理
app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

说明:

  • 我们引入了 express 和 cors,并在 3000 端口启动了一个 Express 服务器。

  • /config 路由返回一个简单的 JSON 对象,包含配置信息。

  • 使用 cors 中间件解决 webview 请求时的跨域问题。

  • Electron 的主窗口加载了一个 index.html 文件,里面会包含 webview。


  1. 创建 webview 页面

在 index.html 中,我们使用 <webview> 标签加载一个简单的页面,并在页面中通过 fetch 请求主进程的 Express 服务获取配置信息。

以下是 index.html 的代码:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>Electron Webview Example</title>
</head>
<body>
    <h1>Electron Webview 与主进程通信</h1>
    <webview id="myWebview" src="data:text/html">
        <html>
        <body>
            <h2>Webview 内容</h2>
            <p>配置信息: <span id='configDisplay'>加载中...</span></p>
            <script>
                const res = await fetch('http://localhost:3000/config')
                const data = await res.json()
            </script>
        </body>
        </html>"
        style="width: 600px; height: 400px;">
    </webview>
</body>
</html>

Web服务框架简介

  • 什么是Web服务框架?

    Web服务框架是用于构建和管理Web服务的工具或库,通常用于创建HTTP API。它提供路由管理、请求处理和响应生成的功能,帮助开发者快速搭建服务器端应用。

  • 为什么前端开发者需要了解Web服务框架?

    • 接口交互:前端界面依赖后端Web服务提供数据,理解其原理有助于优化请求设计。

    • 本地开发:运行npm run dev时启动的本地服务(如Webpack Dev Server)本质上也是Web服务,前端资源(如JS、HTML、CSS)从中加载。

    • 项目需求:在快速原型开发或Electron等场景中,前端开发者可能需要独立实现简单的后台服务。

  • Web服务框架在现代开发中的作用

    • 提供RESTful API或GraphQL服务,支撑前端应用。

    • 处理用户请求、业务逻辑和数据交互。

  1. Node.js Web服务框架
  • 为什么选择Node.js?

    • 前端开发者熟悉JavaScript,Node.js让前后端开发语言统一,降低学习成本。

    • 拥有丰富的npm生态和强大的社区支持。

  • 流行的Node.js Web服务框架

    • Express.js

      • 优点:简单易用、灵活性高、社区资源丰富。

      • 适用场景:快速原型、小型到中型项目。

    • Koa.js

      • 优点:支持async/await,代码更简洁,性能优化。

      • 适用场景:现代化项目、注重代码可维护性。

    • Hapi.js

      • 优点:强大的输入验证和配置驱动设计。

      • 适用场景:需要高安全性的API开发。

    • Nest.js

      • 优点:模块化、依赖注入,适合复杂应用。

      • 适用场景:大型项目、有Angular经验的开发者。

    • Fastify

      • 优点:高性能、低开销。

      • 适用场景:高流量服务、微服务架构。

  1. 在Electron中应用Web服务框架
  • Electron的IPC通信及其弊端

    • 机制:IPC用于主进程(Main)和渲染进程(Renderer)之间的通信。

    • 弊端:

      • 通信链路过长:从页面 → preload.js → 渲染进程 → 主进程,需要多次中转。

      • 开发效率低:反复定义事件(如ipcRenderer.send和ipcMain.on)增加代码复杂性。

      • 内存消耗高:渲染进程因事件监听和中转逻辑占用更多资源。

  • 在主进程中运行Web服务框架

    • 在主进程中启动一个本地HTTP服务器(如Express.js)。

    • 渲染进程通过HTTP请求(如fetch)与主进程通信,替代IPC。

  • HTTP请求替代IPC的优缺点

    • 优点:

      • 熟悉性:前端开发者习惯使用HTTP请求。

      • 调试方便:可通过浏览器开发者工具查看请求。

      • 逻辑清晰:主进程专注服务端,渲染进程专注UI。

    • 缺点:

      • 性能开销:HTTP比IPC多了网络层开销。

      • 复杂性:简单通信任务可能不需要服务器。

  • 代码示例

    • 主进程中设置Express服务器:
javascript 复制代码
const express = require('express');
const { app } = require('electron');
const server = express();

server.get('/api/message', (req, res) => {
  res.json({ message: 'Hello from Main Process!' });
});

app.on('ready', () => {
  server.listen(3000, () => {
    console.log('Server running on port 3000');
  });
});
  • 渲染进程中调用:
javascript 复制代码
<script>
  fetch('http://localhost:3000/api/message')
    .then(response => response.json())
    .then(data => console.log(data.message))
    .catch(error => console.error('Error:', error));
</script>
  1. 性能与安全性权衡
  • HTTP请求与IPC的性能比较

    • IPC:直接进程通信,延迟低,适合简单任务。

    • HTTP:涉及网络栈,延迟稍高,但更灵活。

  • 安全性考虑

    • CORS:需配置跨源资源共享以允许渲染进程访问。
javascript 复制代码
const cors = require('cors');
server.use(cors());
  • 验证:建议添加令牌或IP限制,确保请求来源可信。

  • 适用场景

  • IPC适合轻量级通信,HTTP适合复杂数据交互或开发者更熟悉的场景。

  1. 如何选择合适的Web服务框架
  • 选择时的考虑因素

    • 性能:Fastify适合高性能需求,Express适合一般场景。

    • 易用性:Express和Koa上手快,Nest学习曲线较陡。

    • 社区支持:Express生态最成熟,Fastify较新。

    • 项目规模:小型项目用Express,大型项目考虑Nest。

  • 在Electron中的推荐

    • Express.js因其简单性和广泛支持,适合初学者和大多数场景。

    • 根据需求权衡是否需要更高性能(如Fastify)。

  • 学习建议

    • 从Express.js入门,逐步尝试Koa或Fastify。

    • 通过实践项目(如构建一个简单的API)巩固知识。

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
八了个戒1 小时前
「数据可视化 D3系列」入门第三章:深入理解 Update-Enter-Exit 模式
开发语言·前端·javascript·数据可视化
noravinsc1 小时前
html页面打开后中文乱码
前端·html
小满zs2 小时前
React-router v7 第四章(路由传参)
前端·react.js
小陈同学呦2 小时前
聊聊双列瀑布流
前端·javascript·面试
键指江湖3 小时前
React 在组件间共享状态
前端·javascript·react.js
烛阴3 小时前
零基础必看!Express 项目 .env 配置,开发、测试、生产环境轻松搞定!
javascript·后端·express
诸葛亮的芭蕉扇3 小时前
D3路网图技术文档
前端·javascript·vue.js·microsoft
小离a_a3 小时前
小程序css实现容器内 数据滚动 无缝衔接 点击暂停
前端·css·小程序
徐小夕4 小时前
花了2个月时间研究了市面上的4款开源表格组件,崩溃了,决定自己写一款
前端·javascript·react.js
by————组态4 小时前
低代码 Web 组态
前端·人工智能·物联网·低代码·数学建模·组态