作者:ivyhaswell[1]
关于它的介绍,我想从最近发生的一个实际例子说起:
最近组内开始给项目写文档,需要找一款录屏工具做视频和gif图。于是我们开始寻找录屏的工具,它们或截不到状态栏,或视频体积太大,或windows平台没有等等。最终找到一款能用的花了不少时间。我正好在做electron的项目,于是就寻思:用electron做一个简单的,跨平台的录屏工具,需要多久呢?
答案是二十分钟,数十行代码。
有兴趣可以跟着下面的步骤尝试一下:
(一) 创建一个录屏工具
1.首先创建一个目录,目录下安装electron;
yarn add -D electron
1.创建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())
1.创建index.html
,用来引入渲染进程脚本
当前状态:空闲
macOS下快捷键:Command+Shift+R 开始录制, Command+Shift+S停止录制; Windows下快捷键:Ctrl+Shift+R 开始录制, Ctrl+Shift+S停止录制;
1.创建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
使用也非常简单:
1.Mac下使用快捷键 Command+Shift+R
开始录制,Command+Shift+S
停止录制;Windows的快捷键则为 Control+Shift+R
和 Control+Shift+S
;2.停止录制后会自动打开录制好的视频(视频默认保存在下载目录);
应用启动后长这样:
下面的动图就是使用这个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
setTimeout(() => { process.crash() }, 2000);
启动应用试试:./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方法用于执行录制:
1.通过desktopCapturer.getSources获取屏幕源,这里取其中第一个,通常为主屏幕;2.通过navigator.mediaDevices.getUserMedia获取视频流;3.创建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方法结束录制并保存文件:
1.给mediaRecorder添加结束方法,调用mediaRecorder.stop结束录制;2.结束后,取录制的数据chunks创建Blob对象;3.将Blob对象转换成arrayBuffer,然后转换成buffer;4.将buffer写入本地新建视频文件;5.打开视频文件;6.重置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.comelectron index.htmlelectron /Users/username/Projects/electron-demo/index.html...
这些方式都会让electron启动应用后打开一个窗口,并加载对应的网页链接或文件。
但如果只是单纯的网页套壳,我们用PWA不就好了吗?
因为electron能够提供更多的东西。
我们来看看electron的主体,它包括三个部分:
1.Chromium:用于web内容的显示;2.Node.js:用于文件读写,操作系统等底层api交互;3.自定义API:用来提供常用的系统操作需要的方法,比如设置菜单和托盘,控制窗口等;
官方文档自己也吐槽:使用Electron开发应用程序,就像使用Web界面构建Node.js应用程序,或通过无缝集成的Node.js构建网页一样。其中web开发和Node.js想必很多前端同学已经很熟悉了,因此上手electron开发,更多的就是需要熟悉electron的开发模式及其提供的API。另外如果想要搭建一个大型一些,在公司使用的项目,还需要一些electron开发的工程化经验。
官方文档是一个很好的学习对象,但无法作为唯一的参考。从官方文档到周边库文档,文档说不清的自己去试,试不全的打开vscode源码参考考;遇着bug到github找issue,有时需要一路追溯到electron源码和chromium源码......
在其中学过的一些知识,踩过的一些坑,我们总结出了一个开源项目:
https://github.com/tal-tech/electron-playground
项目中有我们总结了自己学习和踩坑的经验,参考了一些开源社区比较优秀的方案,做了这个项目,作为electron的快速学习和踩坑用。
目前最主要的功能,一是文档中内嵌代码都能直接运行,也可以直接在界面上修改代码运行,方便调整参数看效果;二是演练场,用于编写一些小的功能模块直接运行,目前只有基础模板,我们的目标是以后增加许多常用的功能模块,比如截图,比如消息通知,比如文件上传下载......等等等等,请拭目以待。
对项目有什么建议、想要什么功能、发现什么bug,都欢迎到Github提issue,我们的回复速度,超快的。
为了能更好学习electron,我们目前创作了一个系列,有兴趣可以看看
•【Electron-playground系列】菜单篇[2]•【Electron-Playground系列】Dialog与文件选择篇[3]•【Electron-playground系列】协议篇[4]•【Electron-Playground系列】托盘篇[5]
如果想看更完整的文档,请参考下面文档
Electron-Playground官方文档[6]
github地址传送门:https://github.com/tal-tech/electron-playground
References
[1]
ivyhaswell: https://juejin.im/user/1644525124323880[2]
【Electron-playground系列】菜单篇: https://juejin.im/post/6887808911831728141[3]
【Electron-Playground系列】Dialog与文件选择篇: https://juejin.im/post/6887845363340804104[4]
【Electron-playground系列】协议篇: https://juejin.im/post/6887845625447055367[5]
【Electron-Playground系列】托盘篇: https://juejin.im/post/6887845739432771591[6]
Electron-Playground官方文档: https://www.yuque.com/ezg6c4/op375w