Electron开发小结

最近基于Electron开发了一个音视频应用,遇到了一些坑,特此记录下,希望可以帮助后续的同学。

Electron下载慢

安装electron时会自动最新的Electron二进制文件,由于文件比较大还容易墙,所以我们可以先配置好环境变量,再运行yarn或者npm install

1
2
export ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"
yarn install

ELECTRON_MIRROR表示Electron镜像地址,切换到国内淘宝源,速度会飞快

Electron 截图

Electron里面其实有专门的截屏函数capturePage, 但必须在主进程进行调用,可以在渲染进程通过remote模快来调用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const takeScreenshot = ( savedFolderName = 'screenShoots' ) => {
const isNative = typeof require === 'function' && !!require('electron')
return new Promise((resolve, reject) => {
if(isNative) {
const fs = require('fs')
const path = require('path')
const { remote } = require('electron')
const screenshotsDir = path.join(process.env.HOME || process.env.USERPROFILE, savedFolderName)
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir)
}
remote.getCurrentWindow().capturePage().then(img => {
const name = Date.now()
fs.writeFile(`${screenshotsDir}${path.sep}${name}.png`, img.toPNG(), err => {
if (err != null) {
reject(err)
} else {
resolve(screenshotsDir)
}
})
})

} else {
reject('should be invoked in electron environment')
}
})
}

Electron 桌面分享

大家知道,在Google Chrome中可以调用MediaDevices.getDisplayMedia()来进行分享,不过很可惜, Electron没法使用该方法,具体issues可以参考 getDisplayMedia with Chrome 72 throwing Not Allowed。不过Electron中提供了desktopCapturer来获取桌面视频流,这样也可以进行屏幕共享。

为了保证代码兼容性,并且在Electron进行屏幕分享,我们可以重写window.navigator.mediaDevices方法,保证代码无缝迁移。代码放在preload.js中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const { desktopCapturer } = require('electron')
window.navigator.mediaDevices = window.navigator.mediaDevices || {}
window.navigator.mediaDevices.getDisplayMedia = async () => {
// 获取所有可以分享的桌面或者窗口
const sources = await desktopCapturer.getSources({ types: ['screen', 'window'] })
// 为了方便,我们只选择全屏进行分享
const source = sources.filter(source => source.name === 'Entire Screen' || source.name === 'Electron')[0]
if (source) {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: source.id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720
}
}
})
return stream
} catch (e) {
console.error(e)
}
return
}
}

上面的例子只是进行全屏分享,如果想基于某个单独窗口分享,可以遍历desktopCapturer.getSources({ types: ['screen', 'window'] })返回的数据,弹出窗口,用户选择后进行分享。

Electron 全屏

在网页中,我们可以通过requestFullscreen进入全屏,通过Document.exitFullscreen来退出全屏,不过在Electron,如果应用点击最大化按钮进入全屏后,上面的方法就失效了,我们可以在渲染进程中调用Electron的setFullScreen方法来控制全屏进入和退出 ,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const utils = {
isNative() {
return typeof require === 'function' && !!require('electron')
}
}

const toggleFullScreen = () => {
if (utils.isNative()) {
const win = require('electron').remote.getCurrentWindow()
// 检测当前Electron是否为全屏状态
const isFullScreen = win.fullScreen
win.setFullScreen(!isFullScreen)
} else {
const isFullScreen = document.webkitIsFullScreen
if (isFullScreen) {
document.webkitExitFullscreen()
} else {
document.body.requestFullscreen()
}
}
}

关闭应用

这个比较简单,直接调用Electron中的close方法即可。

1
2
3
const close = () => {
utils.isNative() && require('electron').remote.getCurrentWindow().close()
}

electron-builder 打包后,input输入框粘贴、剪切失效的问题

这个问题很奇怪,直接通过electron src/main/index.js 运行时不会存在该问题,但是通过 electron-builder 构建后会存在该问题。我的Mac上就存在该问题。

解决方案有两个,第一个方案是给应用增加菜单,比如网上的解决方案:

https://github.com/onmyway133/blog/issues/67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const {app} = require('electron')
const Menu = require('electron').Menu

app.on('ready', () => {
createWindow()
createMenu()
})

function createMenu() {
const application = {
label: "Application",
submenu: [
{
label: "About Application",
selector: "orderFrontStandardAboutPanel:"
},
{
type: "separator"
},
{
label: "Quit",
accelerator: "Command+Q",
click: () => {
app.quit()
}
}
]
}

const edit = {
label: "Edit",
submenu: [
{
label: "Undo",
accelerator: "CmdOrCtrl+Z",
selector: "undo:"
},
{
label: "Redo",
accelerator: "Shift+CmdOrCtrl+Z",
selector: "redo:"
},
{
type: "separator"
},
{
label: "Cut",
accelerator: "CmdOrCtrl+X",
selector: "cut:"
},
{
label: "Copy",
accelerator: "CmdOrCtrl+C",
selector: "copy:"
},
{
label: "Paste",
accelerator: "CmdOrCtrl+V",
selector: "paste:"
},
{
label: "Select All",
accelerator: "CmdOrCtrl+A",
selector: "selectAll:"
}
]
}

const template = [
application,
edit
]

Menu.setApplicationMenu(Menu.buildFromTemplate(template))
}

因为我们Electron不需要菜单, 所以第一种方式不合适; 所以我换了一种方式,直接在document中监听onkeydown事件来手动处理,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

const fixInputEvent = () => {
const { clipboard } = require('electron')
const keyCodes = {
V: 86,
C: 67,
X: 88
}
document.onkeydown = function(event){
let activeElement = event.target
if (activeElement.tagName !== 'INPUT') return

const startOffset = activeElement.selectionStart
const endOffset = activeElement.selectionEnd
const clipboardText = clipboard.readText()

if(event.ctrlKey || event.metaKey){ // detect ctrl or cmd
switch(event.which) {
case keyCodes.V: {
activeElement.setRangeText(clipboardText, startOffset, endOffset, 'end')
activeElement.dispatchEvent(new Event('input', { bubbles: true}))
break
}
case keyCodes.C: {
const text = activeElement.value.substring(startOffset, endOffset)
clipboard.writeText(text)
break
}
case keyCodes.X: {
const text = activeElement.value.substring(startOffset, endOffset)
clipboard.writeText(text)
activeElement.setRangeText('', startOffset, endOffset )
break
}
}
}
}
}

上面的代码处理了 CTRL + CCTRL + V已经CTRL + X 三种情况,也就是复制、粘贴和剪切三种场景, 用到了HTMLInputElement.setRangeText() 这个函数,解决了在指定光标处复制、粘贴以及剪切等问题。大家感兴趣可以看看。

electron-webpack 构建问题

开发用到了electron-webpack 构建工具,由于我们渲染进程的代码(前端HTML、JavaScript等代码)是其它项目已经写好的,所以不需要进行渲染进程打包,可以在package.json中关闭渲染进程打包。

1
2
3
4
5
6
{
"electronWebpack": {
// 渲染进程无需打包
"renderer": null
}
}

既然关闭了渲染进程打包,那么我们已经写好的前端代码放在哪里呢?需要放在static文件夹, 并且在主进程中可以这样访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const mainWindow = new BrowserWindow({
width: 1280,
height: 840,
center: true,
autoHideMenuBar: true,
webPreferences: {
webSecurity: false,
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: true,
enableRemoteModule: true,
},
title: 'XXX视频会议'
})

mainWindow.loadFile(path.join(__static, 'index.html'))

通过electron-webpack打包后,默认引入的第三方库都会默认设置为external, 所以如果引入了第三方库, 需要改下package.json, 增加whiteListedModules配置项,这样webpack才会把第三方法代码一起打包压缩。

1
2
3
4
5
6
"electronWebpack": {
"renderer": null,
"whiteListedModules": [
"open"
]
}

electron-webpack打包会默认生成SourceMap文件, 我们一般生产环境不需要SourceMap,程序运行时还会报Uncaught Exception: Error: Cannot find module source-map-support/source-map-support.js 错误, 原因是electron-webpack使用了BannerPlugin插件,默认会在打包后的文件中加入一句require("source-map-support/source-map-support.js").install()

我们可以修改electron-webpack的配置文件来删除BannerPlugin插件, 在package.json中增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
  "electronWebpack": {
"main": {
"extraEntries": [
"@/preload.js"
],
"webpackConfig": "electron.config.js"
},
"renderer": null,
"whiteListedModules": [
"open"
]
}

electron.config.js内容如下所示:

1
2
3
4
5
6

module.exports = function (config) {
config.devtool = false
config.plugins = config.plugins.filter(plugin => plugin.constructor.name !== 'BannerPlugin')
return config
}

这样就可以解决SourceMap问题了。

MAC及Windows打包

打包主要使用 electron-builder来进行打包,我的package.json配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26


{
"scripts": {
"dev": "cross-env NODE_ENV=development electron src/main/index.js",
"compile": "cross-env NODE_ENV=production electron-webpack",
"mac": "rm -rf release dist && yarn compile && electron-builder --mac --x64",
"windows": "rm -rf release dist && yarn compile && electron-builder --win --x64"
},

"build": {
"appId": "larry.asyncoder.com",
"mac": {
"category": "asyncoder-web",
"target": "dmg",
"icon": "./icon.icns"
},
"win": {
"target": "portable",
"icon": "./icon.png"
},
"directories": {
"output": "release/${platform}"
}
}
}

运行yarn macyarn windows就可以生成Mac上的dmg和windows上的exe文件。

值得说明的是, 在Mac上是没法直接对Windows环境进行打包的, 所以推荐使用docker来进行打包。安装好Docker, 命令行进入工程目录,运行如下代码

1
2
3
4
5
6
7
8
9
docker run --rm -ti \
--env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \
--env ELECTRON_CACHE="/root/.cache/electron" \
--env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \
-v ${PWD}:/project \
-v ${PWD##*/}-node-modules:/project/node_modules \
-v ~/.cache/electron:/root/.cache/electron \
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
electronuserland/builder:wine

进入以后,先运行yarn, 再运行yarn windows即可。