作者:ivyhaswell
关于它的介绍,我想从最近发生的一个实际例子说起:
最近组内开始给项目写文档,需要找一款录屏工具做视频和gif图。于是我们开始寻找录屏的工具,它们或截不到状态栏,或视频体积太大,或windows平台没有等等。最终找到一款能用的花了不少时间。我正好在做electron的项目,于是就寻思:用electron做一个简单的,跨平台的录屏工具,需要多久呢?
答案是二十分钟,数十行代码。
有兴趣可以跟着下面的步骤尝试一下:
(一) 创建一个录屏工具
- 首先创建一个目录,目录下安装electron;
yarn add -D electron
- 创建
main.js
,这里是主进程的代码;
const { app, BrowserWindow, globalShortcut } = require("electron");
const path = require("path");
app.on("ready", () => {
const browserWindow = new BrowserWindow({
webPreferences: { nodeIntegration: true, enableRemoteModule: true },
});
browserWindow.loadFile(path.resolve(__dirname, "index.html"));
globalShortcut.register('CommandOrControl+Shift+R', () => browserWindow.webContents.send("StartRecording"))
globalShortcut.register('CommandOrControl+Shift+S', () => browserWindow.webContents.send("StopRecording"))
});
app.on('will-quit', () => globalShortcut.unregisterAll())
- 创建
index.html
,用来引入渲染进程脚本
<h1>当前状态:<span id="status">空闲</span></h1>
<ol>
<li>macOS下快捷键: Command+Shift+R 开始录制, Command+Shift+S停止录制;</li>
<li>Windows下快捷键: Ctrl+Shift+R 开始录制, Ctrl+Shift+S停止录制;</li>
</ol>
<script src="./renderer.js"></script>
- 创建
renderer.js
,这里有主要的业务代码
const { desktopCapturer, remote, shell, ipcRenderer } = require("electron");
const path = require("path");
const fs = require("fs");
let mediaRecorder = null;
let chunks = []
async function start() {
if (mediaRecorder) return;
const sources = await desktopCapturer.getSources({ types: ["screen"] });
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: sources[0].id,
},
},
});
mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm; codecs=vp9" });
mediaRecorder.ondataavailable = (event) => {
event.data.size > 0 && chunks.push(event.data);
};
mediaRecorder.start();
updateStatusText('录制中...')
}
function stop(){
if(!mediaRecorder) return
mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: "video/webm" });
const buffer = Buffer.from(await blob.arrayBuffer());
const filePath = path.resolve(remote.app.getPath("downloads"), `${Date.now()}.webm`);
fs.writeFile(filePath, buffer, () => {
shell.openPath(filePath);
mediaRecorder = null;
chunks = []
});
};
mediaRecorder.stop();
updateStatusText('空闲')
}
function updateStatusText(text){
const $statusElement = document.querySelector('#status')
$statusElement.textContent = text
}
ipcRenderer.on("StartRecording", start);
ipcRenderer.on("StopRecording", stop);
然后就可以命令行启动应用
./node_modules/.bin/electron main.js
使用也非常简单:
- Mac下使用快捷键
Command+Shift+R
开始录制,Command+Shift+S
停止录制;Windows的快捷键则为Control+Shift+R
和Control+Shift+S
; - 停止录制后会自动打开录制好的视频(视频默认保存在下载目录);
应用启动后长这样:
下面的动图就是使用这个demo录屏然后转换而成的:
(二) 工具代码解析
二十分钟和数十行代码的说法可能有些夸张,毕竟还有许多功能,如录制参数配置、格式转换、多平台打包等等都还没有实现;但并不妨碍能看出来,electron开发上手,确实挺简单。
在分析代码之前先来看看electron的一个基本概念:主进程和渲染进程。
- 主进程通过
BrowserWindow
创建窗口,每个窗口对应一个渲染进程; - 渲染进程管理对应的web页面,
BrowserWindow
销毁后,相应的渲染进程也会终止; - 一个渲染进程的崩溃不会影响其他渲染进程;
为了理解这个概念,我们直接动手尝试一下:
首先在根目录添加一个main-process.js
const { app, BrowserWindow, dialog } = require("electron");
const path = require("path");
app.on("ready", () => {
const win1 = new BrowserWindow({ x: 20, y: 20 });
win1.loadURL("https://github.com");
const win2 = new BrowserWindow({ x: 500, y: 20 });
win2.loadURL("https://stackoverflow.com");
const win3 = new BrowserWindow({
x: 20,
y: 500,
webPreferences: { nodeIntegration: true },
});
win3.loadFile(path.resolve(__dirname, "crash-renderer.html"));
win3.webContents.on('render-process-gone', async () => {
await dialog.showMessageBoxSync(win3, {message: '进程已崩溃.', buttons: ['关闭']})
win3.close()
})
});
再添加一个crash-renderer.html
<script>
setTimeout(() => {
process.crash()
}, 2000);
</script>
启动应用试试:./node_modules/.bin/electron main-process.js
在这里main-process.js
创建了三个窗口,第一个打开了github,第二个打开了stackoverflow,第三个打开了本地的html文件。三个窗口分别对应三个渲染进程。
在crash-renderer.html
中我们执行了process.crash()
,因此可以发现第三个窗口的进程在2秒后崩溃,而另外两个窗口github和stackoverflow依然正常。
回到录屏应用,它的基本功能结构设计是这样的:
反映到实现上,我们从main.js看起
先是注册了应用准备好之后和退出之前的方法
app.on('ready', () => {...});
app.on('will-quit', () => {...});
在ready事件中做了两件事,首先是创建窗口:
const browserWindow = new BrowserWindow({
webPreferences: { nodeIntegration: true, enableRemoteModule: true },
});
browserWindow.loadFile(path.resolve(__dirname, "index.html"));
nodeIntegration
和enableRemoteModule
设置为true以便在渲染进程中使用node和remote模块,有时为了应用安全我们需要禁用这两个属性;
然后通过loadFile
方法加载index.html
;
接下来是注册全局快捷键:
globalShortcut.register('CommandOrControl+Shift+R', () => browserWindow.webContents.send("StartRecording"))
globalShortcut.register('CommandOrControl+Shift+S', () => browserWindow.webContents.send("StopRecording"))
监听到快捷键按下后,通过ipc向渲染进程发送消息;
在renderer.js中,则会接收对应的ipc消息执行对应的方法:
ipcRenderer.on("StartRecording", start);
ipcRenderer.on("StopRecording", stop);
其中start方法用于执行录制:
- 通过desktopCapturer.getSources获取屏幕源,这里取其中第一个,通常为主屏幕;
- 通过navigator.mediaDevices.getUserMedia获取视频流;
- 创建mediaRecorder,通过mediaRecorder记录视频数据;
async function start() {
// 已经录制中
if (mediaRecorder) return;
// 这里取屏幕源的第一个,通常为主屏幕
const sources = await desktopCapturer.getSources({ types: ["screen"] });
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: sources[0].id,
},
},
});
mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm; codecs=vp9" });
mediaRecorder.ondataavailable = (event) => {
event.data.size > 0 && chunks.push(event.data);
};
mediaRecorder.start();
updateStatusText('录制中...')
}
end方法结束录制并保存文件:
- 给mediaRecorder添加结束方法,调用mediaRecorder.stop结束录制;
- 结束后,取录制的数据chunks创建Blob对象;
- 将Blob对象转换成arrayBuffer,然后转换成buffer;
- 将buffer写入本地新建视频文件;
- 打开视频文件;
- 重置mediaRecorder和chunks;
function stop(){
if(!mediaRecorder) return
mediaRecorder.onstop = async () => {
const blob = new Blob(chunks, { type: "video/webm" });
const buffer = Buffer.from(await blob.arrayBuffer());
const filePath = path.resolve(remote.app.getPath("downloads"), `${Date.now()}.webm`);
fs.writeFile(filePath, buffer, () => {
shell.openPath(filePath);
mediaRecorder = null;
chunks = []
});
};
mediaRecorder.stop();
updateStatusText('空闲')
}
大功告成。
(三) 一探Electron
我们再来回顾一下electron的官方自我介绍:用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。
通过上面的例子也能看的出来,编写的代码确实都没有超出这三驾马车(甚至没用到CSS)。对于开发过nodejs的前端同学来说,不需要看解析都能很容易理解这些代码。
是不是感觉,抛掉主进程和通信模块的话,开发体验就像是在开发一个集成了node环境的网页?
而electron本身也提供了快速打开一个链接或html文件的方法,甚至不需要写主进程的代码,比如:
electron https://github.com
electron index.html
electron /Users/username/Projects/electron-demo/index.html
...
这些方式都会让electron启动应用后打开一个窗口,并加载对应的网页链接或文件。
但如果只是单纯的网页套壳,我们用PWA不就好了吗?
因为electron能够提供更多的东西。
我们来看看electron的主体,它包括三个部分:
- Chromium:用于web内容的显示;
- Node.js:用于文件读写,操作系统等底层api交互;
- 自定义API:用来提供常用的系统操作需要的方法,比如设置菜单和托盘,控制窗口等;
官方文档自己也吐槽:使用Electron开发应用程序,就像使用Web界面构建Node.js应用程序,或通过无缝集成的Node.js构建网页一样。 其中web开发和Node.js想必很多前端同学已经很熟悉了,因此上手electron开发,更多的就是需要熟悉electron的开发模式及其提供的API。另外如果想要搭建一个大型一些,在公司使用的项目,还需要一些electron开发的工程化经验。
官方文档是一个很好的学习对象,但无法作为唯一的参考。从官方文档到周边库文档,文档说不清的自己去试,试不全的打开vscode源码参考考;遇着bug到github找issue,有时需要一路追溯到electron源码和chromium源码......
在其中学过的一些知识,踩过的一些坑,我们总结出了一个开源项目:
项目中有我们总结了自己学习和踩坑的经验,参考了一些开源社区比较优秀的方案,做了这个项目,作为electron的快速学习和踩坑用。
目前最主要的功能,一是文档中内嵌代码都能直接运行,也可以直接在界面上修改代码运行,方便调整参数看效果;二是演练场,用于编写一些小的功能模块直接运行,目前只有基础模板,我们的目标是以后增加许多常用的功能模块,比如截图,比如消息通知,比如文件上传下载......等等等等,请拭目以待。
对项目有什么建议、想要什么功能、发现什么bug,都欢迎到Github提issue,我们的回复速度,超快的。
为了能更好学习electron,我们目前创作了一个系列,有兴趣可以看看
- 【Electron-playground系列】菜单篇
- 【Electron-Playground系列】Dialog与文件选择篇
- 【Electron-playground系列】协议篇
- 【Electron-Playground系列】托盘篇
如果想看更完整的文档,请参考下面文档
github地址传送门:github.com/tal-tech/el…