Web 服务对前端同学来说并不陌生,你们开发其他前端界面请求的后端接口就是 Web 服务,你们 npm run dev
启动的也是一个本地的 Web 服务,前端的 js,html,css 都有从这个服务上拉取到的资源。
我们在开发 Electron 时发现了 Electron 进程间通信(IPC)的弊端,弊端的主要来源是 webview 到 Main 的通信链路过长,需要从 page 发到 proload.js 文件,再从 preload 发到 render,再从 render 发送到 Main,这个过程中,需要反复定义类同命名的事件进行中转,大大降低了开发效率,还无意中增加了 render 进程的内存耗费。
官网链接:ipcRenderer | Electron
描述:从渲染进程向主进程发送异步消息。
特点:
单向通信,主进程通过 ipcMain.on 监听并处理消息。
不会自动返回响应,主进程需要额外的机制(如再次发送消息)来回复渲染进程。
使用场景:
当渲染进程需要通知主进程执行某项操作,且不需要等待结果时。
示例:
通知主进程保存数据、打开新窗口或执行某些后台任务。
代码示例:
// 渲染进程
ipcRenderer.send('save-data', { key: 'value' });
// 主进程
ipcMain.on('save-data', (event, data) => {
console.log('收到数据:', data);
});
描述:从渲染进程向主进程发送消息,并等待主进程的响应。
特点:
异步双向通信,返回一个 Promise,可以通过 await 获取主进程的处理结果。
主进程使用 ipcMain.handle 来处理请求并返回数据。
使用场景:
当渲染进程需要从主进程获取数据或等待某个操作完成时。
示例:
获取文件内容、查询数据库结果或执行耗时操作。
代码示例:
// 渲染进程
async function getFileContent() {
const content = await ipcRenderer.invoke('get-file', 'file.txt');
console.log('文件内容:', content);
}
// 主进程
ipcMain.handle('get-file', async (event, filename) => {
return '文件内容示例';
});
描述:从渲染进程向主进程发送同步消息。
特点:
会阻塞渲染进程,直到主进程通过 event.returnValue 返回结果。
同步操作会影响渲染进程的性能,可能导致 UI 卡顿。
使用场景:
当需要立即获取主进程的响应,且操作足够快时。
不推荐广泛使用,因为阻塞 UI 线程会降低用户体验。
示例:
获取简单的配置值或状态。
代码示例:
// 渲染进程
const result = ipcRenderer.sendSync('get-config', 'theme');
console.log('配置:', result);
// 主进程
ipcMain.on('get-config', (event, key) => {
event.returnValue = 'dark';
});
描述:从一个渲染进程向另一个渲染进程发送消息。
特点:
需要知道目标渲染进程的 webContentsId。
消息直接发送到指定的渲染进程,不经过主进程。
使用场景:
当应用有多个窗口或 webview 时,需要在不同渲染进程之间直接通信。
示例:
一个窗口控制另一个窗口的行为或状态。
代码示例:
// 渲染进程 A
ipcRenderer.sendTo(2, 'update-status', 'ready'); // 2 是目标窗口的 webContentsId
// 渲染进程 B
ipcRenderer.on('update-status', (event, status) => {
console.log('状态更新:', status);
});
描述:从 webview 的渲染进程向其宿主窗口的渲染进程发送消息。
特点:
特定于 webview 场景,消息发送到宿主窗口的渲染进程,而不是主进程。
宿主窗口通过 webview 标签的 on-ipc-message 事件接收消息。
使用场景:
当 webview 内部的代码需要与宿主窗口通信时。
示例:
webview 中的页面通知宿主窗口某个事件发生。
代码示例:
// 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 与宿主窗口通信 |
这个是最复杂的,但是官网却是讲得最简单的,也没有举例子,外国人做事真得没的说
必须结合 preload.js 来实现中转通信,webview 不能直接与主渲染进程通信,至少从 electron 提供的官方文档里面是没有;
需要借助 webview 注入脚本的 window.postMessage 向 preload.js 中的 window.addEventListener('message', (event) 进行中转
需要 webview dom 对象本身才能收到来自 webview 里面发过来的消息
下面是 webview 与主进程通信的整个链路,使用 await 进行阻塞式等待的解决方案
webview 页面通过 postMessage 发送请求。
预加载脚本捕获请求并通过 ipcRenderer.sendToHost 转发给宿主渲染进程。
宿主渲染进程使用 ipcRenderer.invoke 向主进程发送请求并等待响应。
主进程通过 ipcMain.handle 处理请求并返回结果。
宿主渲染进程收到响应后,通过 webview.send 将结果发送回 webview。
预加载脚本通过 postMessage 将响应传递给 webview 页面。
webview 页面中的 Promise 解析,获取响应数据。
1. webview 内部页面发送请求并等待响应
在 webview 的页面中,使用 postMessage 发送请求,并通过事件监听接收响应,利用 Promise 和 await 实现异步等待。
html
2. webview 的预加载脚本
在预加载脚本中,监听 webview 的消息并通过 ipcRenderer.sendToHost 转发给宿主渲染进程,同时接收响应并传递回 webview。
// 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 }, '*');
});
3. 宿主渲染进程处理请求
在宿主渲染进程中,监听 webview 的消息,使用 ipcRenderer.invoke 向主进程发送请求并等待响应,之后将结果发送回 webview。
// 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);
}
}
});
4. 主进程处理请求
在主进程中,使用 ipcMain.handle 处理来自渲染进程的请求,并返回一个 Promise 作为响应。
// 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 '处理后的数据';
});
5. 宿主页面配置
在宿主渲染进程的 HTML 文件中,正确加载 webview 并指定预加载脚本。
可以将以上过程减少为一次本地的 http 请求
1. 项目准备
确保你已经初始化了一个 Electron 项目。如果没有,可以按照以下步骤快速创建一个:
mkdir electron-express-example
cd electron-express-example
npm init -y
npm install electron express cors
项目结构如下
electron-express-example/
├── main.js # 主进程文件
├── index.html # webview 页面
├── package.json
└── node_modules/
2. 在主进程中引入并配置 Express
在 main.js 文件中,我们将引入 Express,启动一个简单的服务器,并定义一个路由来返回配置信息。同时,我们需要处理 Electron 的窗口创建。
以下是 main.js 的完整代码:
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。
3. 创建 webview 页面
在 index.html 中,我们使用
以下是 index.html 的代码:
Electron Webview Example
Electron Webview 与主进程通信
Webview 内容
配置信息: 加载中...
"
style="width: 600px; height: 400px;">
什么是Web服务框架?
Web服务框架是用于构建和管理Web服务的工具或库,通常用于创建HTTP API。它提供路由管理、请求处理和响应生成的功能,帮助开发者快速搭建服务器端应用。
为什么前端开发者需要了解Web服务框架?
接口交互:前端界面依赖后端Web服务提供数据,理解其原理有助于优化请求设计。
本地开发:运行npm run dev时启动的本地服务(如Webpack Dev Server)本质上也是Web服务,前端资源(如JS、HTML、CSS)从中加载。
项目需求:在快速原型开发或Electron等场景中,前端开发者可能需要独立实现简单的后台服务。
Web服务框架在现代开发中的作用
提供RESTful API或GraphQL服务,支撑前端应用。
处理用户请求、业务逻辑和数据交互。
2. 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
优点:高性能、低开销。
适用场景:高流量服务、微服务架构。
3. 在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服务器:
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');
});
});
渲染进程中调用:
4. 性能与安全性权衡
HTTP请求与IPC的性能比较
IPC:直接进程通信,延迟低,适合简单任务。
HTTP:涉及网络栈,延迟稍高,但更灵活。
安全性考虑
CORS:需配置跨源资源共享以允许渲染进程访问。
const cors = require('cors');
server.use(cors());
验证:建议添加令牌或IP限制,确保请求来源可信。
适用场景
IPC适合轻量级通信,HTTP适合复杂数据交互或开发者更熟悉的场景。
5. 如何选择合适的Web服务框架
选择时的考虑因素
性能:Fastify适合高性能需求,Express适合一般场景。
易用性:Express和Koa上手快,Nest学习曲线较陡。
社区支持:Express生态最成熟,Fastify较新。
项目规模:小型项目用Express,大型项目考虑Nest。
在Electron中的推荐
Express.js因其简单性和广泛支持,适合初学者和大多数场景。
根据需求权衡是否需要更高性能(如Fastify)。
学习建议
从Express.js入门,逐步尝试Koa或Fastify。
通过实践项目(如构建一个简单的API)巩固知识。