背景
Web 服务对前端同学来说并不陌生,你们开发其他前端界面请求的后端接口就是 Web 服务,你们 npm run dev
启动的也是一个本地的 Web 服务,前端的 js,html,css 都有从这个服务上拉取到的资源。
我们在开发 Electron 时发现了 Electron 进程间通信(IPC)的弊端,弊端的主要来源是 webview 到 Main 的通信链路过长,需要从 page 发到 proload.js 文件,再从 preload 发到 render,再从 render 发送到 Main,这个过程中,需要反复定义类同命名的事件进行中转,大大降低了开发效率,还无意中增加了 render 进程的内存耗费。
ipcRenderer 通信知识补充
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 详解
这个是最复杂的,但是官网却是讲得最简单的,也没有举例子,外国人做事真得没的说

-
必须结合 preload.js 来实现中转通信,webview 不能直接与主渲染进程通信,至少从 electron 提供的官方文档里面是没有;
-
需要借助 webview 注入脚本的 window.postMessage 向 preload.js 中的 window.addEventListener('message', (event) 进行中转
-
需要 webview dom 对象本身才能收到来自 webview 里面发过来的消息
下面是 webview 与主进程通信的整个链路,使用 await 进行阻塞式等待的解决方案
webview 实现对 Main 进程的 await 请求
通信流程概述
- webview 页面通过 postMessage 发送请求。
- 预加载脚本捕获请求并通过 ipcRenderer.sendToHost 转发给宿主渲染进程。
- 宿主渲染进程使用 ipcRenderer.invoke 向主进程发送请求并等待响应。
- 主进程通过 ipcMain.handle 处理请求并返回结果。
- 宿主渲染进程收到响应后,通过 webview.send 将结果发送回 webview。
- 预加载脚本通过 postMessage 将响应传递给 webview 页面。
- webview 页面中的 Promise 解析,获取响应数据。
时序图

通信流程具体实现细节
- 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>
- 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 }, '*');
});
- 宿主渲染进程处理请求
在宿主渲染进程中,监听 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);
}
}
});
- 主进程处理请求
在主进程中,使用 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 '处理后的数据';
});
- 宿主页面配置
在宿主渲染进程的 HTML 文件中,正确加载 webview 并指定预加载脚本。
html
<!-- index.html -->
<webview src="./webview.html" preload="./preload.js"></webview>
引入 web 服务的好处
- 可以将以上过程减少为一次本地的 http 请求
实现过程
- 项目准备
确保你已经初始化了一个 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/
- 在主进程中引入并配置 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。
- 创建 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服务,支撑前端应用。
-
处理用户请求、业务逻辑和数据交互。
-
- 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
-
优点:高性能、低开销。
-
适用场景:高流量服务、微服务架构。
-
-
- 在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>
- 性能与安全性权衡
-
HTTP请求与IPC的性能比较
-
IPC:直接进程通信,延迟低,适合简单任务。
-
HTTP:涉及网络栈,延迟稍高,但更灵活。
-
-
安全性考虑
- CORS:需配置跨源资源共享以允许渲染进程访问。
javascript
const cors = require('cors');
server.use(cors());
-
验证:建议添加令牌或IP限制,确保请求来源可信。
-
适用场景
-
IPC适合轻量级通信,HTTP适合复杂数据交互或开发者更熟悉的场景。
- 如何选择合适的Web服务框架
-
选择时的考虑因素
-
性能:Fastify适合高性能需求,Express适合一般场景。
-
易用性:Express和Koa上手快,Nest学习曲线较陡。
-
社区支持:Express生态最成熟,Fastify较新。
-
项目规模:小型项目用Express,大型项目考虑Nest。
-
-
在Electron中的推荐
-
Express.js因其简单性和广泛支持,适合初学者和大多数场景。
-
根据需求权衡是否需要更高性能(如Fastify)。
-
-
学习建议
-
从Express.js入门,逐步尝试Koa或Fastify。
-
通过实践项目(如构建一个简单的API)巩固知识。
-