主进程和渲染进程
主进程使用 BrowserWindow 创建实例,主进程销毁后,对应的渲染进程回被终止。主进程与渲染进程通过 IPC 方式(事件驱动)进行通信。
-
主进程: 启动项目时运行的 main.js 脚本就是我们说的主进程。在主进程运行的脚本可以以创建 Web 页面的形式展示 GUI。主进程只有一个。
-
渲染进程: 每个 Electron 的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。

预加载脚本
Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境;渲染进程默认跑在网页页面上,而并非 Node.js里。
Electron 的主进程和渲染进程有着清楚的分工并且不可互换,为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。
使用 Electron 的 ipcMain
模块和 ipcRenderer
模块来进行进程间通信。 为了从你的网页向主进程发送消息,你可以使用 ipcMain.handle
设置一个主进程处理程序(handler),然后在预处理脚本中暴露一个被称为 ipcRenderer.invoke
的函数来触发该处理程序(handler)。
单向通信
从渲染器进程到主进程,也可以理解为:在UI界面向主进程发送消息
可以使用 ipcRenderer.send
API 发送消息,然后使用 ipcMain.on
API 接收。
1、先在预加载脚本里定义方法,暴露全局对象
在 src-electron 目录下定义一个名为 preload.js的脚本文件
使用 contextBridge.exposeInMainWorld
向window暴露一个名为 electronAPI
的对象,然后定义一个changeTitle
的方法,在这里使用ipcRenderer.send
向主进程发送一个消息
js
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title)
})

2、在主进程中监听方法,并处理
在主进程main.js 文件中引入预加载脚本,然后使用ipcMain.on
进行监听
js
const win = new BrowserWindow({
width: 1000,
height: 800,
title:'董员外',
icon: join(__dirname, '../public/logo.ico'),
webPreferences: {
nodeIntegration: true,
contextIsolation: false
// 引入预加载脚本
preload: join(__dirname, 'preload.js')
}
})
ipcMain.on('set-title', (event, title) => {
// 获取当前的窗口
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
// 设置窗口标题 setTitle是electron内置的方法
win.setTitle(title)
})
3、在UI页面调用暴露的方法
js
const changeTitle = ()=>{
window?.electronAPI.setTitle(title.value)
}

小结
通过这个demo可以了解到,单向通信无非就三步
1、在预加载文件里暴露方法,使用ipcRenderer.send
向主进程发送消息
2、 在主进程中用 ipcMain.on
接收消息,进行操作
3、 调用全局方法
至此形成一个闭环
双向通信
ipcRenderer.send
和 ipcRenderer.invoke
都是 Electron 渲染进程中用于发送 IPC(进程间通信)消息的方法,他们在用法上有一些区别
ipcRenderer.send
: 发送消息后不返回任何值,它是单向的,发送消息到主进程后不会等待主进程的响应。ipcRenderer.invoke
: 发送消息后会返回一个 Promise,该 Promise 在主进程处理完成后被解析,可以获取到主进程返回的结果。- 对于
ipcRenderer.send
,主进程通过ipcMain.on
来监听并处理消息。 - 对于
ipcRenderer.invoke
,主进程通过ipcMain.handle
来注册处理程序,处理程序返回的 Promise 在处理完成后被解析,将结果返回给渲染进程。
1、预加载暴露全局对象
在 preload.js 文件 定义并暴露方法
js
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
setTitle: (title) => ipcRenderer.send('set-title', title),
postMessage: () => ipcRenderer.invoke('post-message')
})
2、在主进程中监听
ipcMain.handle
返回一个 Promise
js
ipcMain.handle('post-message', async ()=>{
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('支付宝到账100万元');
}, 2000);
});
})
3、调用方法
js
const sentMessage = async ()=>{
message.value = await window.electronAPI.postMessage()
}

全局注入ipcRender的方法
无论是单向通信还是双向通信,都需要在预加载脚本里加入一个方法,然后在主进程中监听。这样当方法数量比较多时,代码添加量也批量上升
可以将ipcRender的send,on,once,removeListener,sendSync,invoke方法注入到全局中,这样直接可以在应用层直接进行使用,而不需要再次在预加载脚本中添加代码
先在预加载脚本中,注入ipcRenderer
的各类方法
js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, ...args) => {
if (args?.length > 0) {
ipcRenderer.send(channel, ...args)
} else {
ipcRenderer.send(channel)
}
},
on: (channel, func) => {
ipcRenderer.on(channel, func)
},
once: (channel, func) => {
ipcRenderer.once(channel, func)
},
removeListener: (channel, func) => {
ipcRenderer.removeListener(channel, func)
},
sendSync: (channel, ...args) => {
if (args?.length > 0) {
return ipcRenderer.sendSync(channel, ...args)
} else {
return ipcRenderer.sendSync(channel)
}
},
invoke: (channel, ...args) => {
try {
return ipcRenderer.invoke(channel, ...args)
} catch (error) {
console.error(`Error invoking API: ${channel}`, error)
}
},
})
以后就可以直接在应用层进行调用方法
js
const print = ()=>{
// (window as any).ipcRenderer?.send('方法名');
(window as any).ipcRenderer?.send('consolelog');
}
const print2 = ()=>{
(window as any).ipcRenderer?.send('consolelog_2',222);
}
const print3 = ()=>{
(window as any).ipcRenderer?.send('consolelog_3',333);
}
然后在主进程中添加监听方法就行
js
ipcMain.on('consolelog', () => {
console.log("打印 this is 111111")
})
ipcMain.on('consolelog_2', (event, data) => {
console.log("打印 this is ",data)
})
ipcMain.on('consolelog_3', (event, data) => {
console.log("打印 this is ",data)
})

接下来就用一个截屏demo实战一下
实现截屏
需要先安装一个截屏插件
js
npm install js-web-screen-shot -D
js-web-screen-shot
提供了两种截图模式: webrtc 和 html2canvas,如果不开启 enableWebRtc
那么就会使用html2canvas
截图模式
html2canvas截图
新建一个截屏页面
js
<template>
<h1>
这是 截屏 页面
</h1>
<div>
<img src="/public/logo.png" alt="">
</div>
<button @click="begainScreen">截屏</button>
<img class="box-img" :src="imgSrc" alt="">
</template>
<script setup lang="ts">
import ScreenShot from "js-web-screen-shot";
import { ref } from "vue";
const imgSrc = ref("")
const begainScreen = () => {
console.log("开始截屏")
new ScreenShot ({
enableWebRtc: false,
level: 9999, //层级级别
completeCallback: callback
});
}
const callback = (base64data:any)=>{
console.log(base64data);
imgSrc.value = base64data.base64
}
</script>
<style scoped>
.box-img{
width: 200px;
position: fixed;
right: 10px;
top: 10px;
border: 2px solid red;
}
</style>
点击 '截屏' 会初始化截屏方法,在callback
回调方法里会获取到图片的base64数据,可以作为图片的src。

此时内嵌一个 iframe
页面,就会出现一个大bug,截屏时截取不到iframe的内容
js
<iframe
style="width: 600px;height: 400px;border: 1px solid red;"
src="https://www.juejin.cn"
disablewebsecurity
allowpopups
webpreferences="nodeIntegration, contextIsolation=no"
/>

这是因为 html2canvas
截图是遍历页面的所有dom节点,然后转化为canvas。但是iframe内的dom节点无法获取
WebRtc 截图
因为electron环境下无法直接调用webrtc来获取屏幕流,所以需要获取设备的窗口,然后主线程发送一个IPC消息
1、全局注入ipcRenderer
在之前的通信中已经介绍了如何全局注入 ipcRenderer
了,只需要在预加载脚本中添加代码即可
js
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, ...args) => {
if (args?.length > 0) {
ipcRenderer.send(channel, ...args)
} else {
ipcRenderer.send(channel)
}
},
on: (channel, func) => {
ipcRenderer.on(channel, func)
},
once: (channel, func) => {
ipcRenderer.once(channel, func)
},
removeListener: (channel, func) => {
ipcRenderer.removeListener(channel, func)
},
sendSync: (channel, ...args) => {
if (args?.length > 0) {
return ipcRenderer.sendSync(channel, ...args)
} else {
return ipcRenderer.sendSync(channel)
}
},
invoke: (channel, ...args) => {
try {
return ipcRenderer.invoke(channel, ...args)
} catch (error) {
console.error(`Error invoking API: ${channel}`, error)
}
},
})
2、封装注入Api
在 渲染线程 封装发送消息的方法 和 获取指定窗口的媒体流 的方法
utils/tool.ts
js
// 发送消息
export const getDesktopCapturerSource = async () => {
return await (window as any).ipcRenderer.invoke("ev:send-desktop-capturer_source", []);
};
// 获取指定id设备的视频流
export const getInitStream = async (
source: { id: string },
audio?: boolean
): Promise<MediaStream | null> =>{
return new Promise((resolve, _reject) => {
// 获取指定窗口的媒体流
// 此处遵循的是webRTC的接口类型 暂时TS类型没有支持 只能断言成any
(navigator.mediaDevices as any)
.getUserMedia({
audio: audio
? {
mandatory: {
chromeMediaSource: "desktop",
},
}
: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: source.id,
},
},
})
.then((stream: MediaStream) => {
resolve(stream);
})
.catch((error: any) => {
console.log(error);
resolve(null);
});
});
}
3.主线程获取媒体流
在主线程 添加获取 窗口信息和媒体流的代码
js
const selfWindws = async () =>
await Promise.all(
webContents
.getAllWebContents()
.filter((item) => {
const win = BrowserWindow.fromWebContents(item);
return win && win.isVisible();
})
.map(async (item) => {
const win = BrowserWindow.fromWebContents(item);
const thumbnail = await win?.capturePage();
// 当程序窗口打开DevTool的时候 也会计入
return {
name:
win?.getTitle() + (item.devToolsWebContents === null ? "" : "-dev"), // 给dev窗口加上后缀
id: win?.getMediaSourceId(),
thumbnail,
display_id: "",
appIcon: null,
};
})
);
// 获取设备窗口信息
ipcMain.handle(
'ev:send-desktop-capturer_source',
async (_event, _args) => {
console.log('222')
return [
...(await desktopCapturer.getSources({ types: ["window", "screen"] })),
...(await selfWindws()),
];
}
);
4、调用webrtc截图
在页面中截图时初始化 截图事件参数,获取传入屏幕流数据,主要依靠前两步封装的方法
js
import ScreenShot from "js-web-screen-shot";
import { getDesktopCapturerSource,getInitStream } from '../../utils/tool'
js
const begainScreen = async () => {
// 下面这两块自己考虑
const sources = await getDesktopCapturerSource(); // 这里返回的是设备上的所有窗口信息
// 这里可以对`sources`数组下面id进行判断 找到当前的electron窗口 这里为了简单直接拿了第一个
console.log("sources",sources);
const stream = await getInitStream(sources.filter(e => e.name == "董员外")[0]);
console.log("🚀 ~ doScreenShot ~ stream:", stream)
new ScreenShot({
enableWebRtc: true, // 启用webrtc
screenFlow: stream!, // 传入屏幕流数据
level: 999,
completeCallback: callback,
});
}
const callback = (base64data:any) => {
console.log("base64data",base64data);
imgSrc.value = base64data.base64
}
可以看到 iframe也可以截取到
完整的代码放到github上了:electron桌面端工具