Electron是什么,我们先看看官方https://www.electronjs.org/ 的介绍如何说的
What is Electron? Electron is a framework for building desktop applications using JavaScript, HTML, and CSS. By embedding Chromium and Node.js into its binary, Electron allows you to maintain one JavaScript codebase and create cross-platform apps that work on Windows, macOS, and Linux — no native development experience required.
简单来说,Electron是一个构建桌面应用的框架,通过它我们可以用js、html和css开发桌面应用。
我觉得最简单的理解就是它是一个特殊的浏览器,或者说是一个壳,里面加载本地或远端的web应用。
下面我们来看看它怎么用。
前提是有nodejs环境,网上有很多教程,这里不细说了。
参考官方教程http://www.electronjs.org/docs/tutorial/quick-start#prerequisites
通过下面的命令创建项目
mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron
创建并安装了electron
我的node和npm版本分别是:
bennu:~ bennu$ node -v
v14.15.5
bennu:~ bennu$ npm -v
6.14.11
是否需要完全一致并不清楚。但是我在windows上配置环境的时候npm版本不对,导致electron一直安装不成功,问题如下:
npx @electron-forge/cli@latest import
)一直失败,提示npm ERR! Maximum call stack size exceeded
超过最大栈问题npm i --save-dev electron
)不报错(当然中间很多过程其实也没执行,没有任何日志输出),但是执行npm start
的时候就会提示electron没安装成功。最终使用的是6.14.11这个版本。大家可以注意一下。
在创建的项目中新建入口文件
vim main.js
然后填入下面的内容:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
然后新建一个首页文件
vim index.html
填入下面内容
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
</head>
<body style="background: white;">
<h1>Hello World!</h1>
<p>
We are using node <script>document.write(process.versions.node)</script>,
Chrome <script>document.write(process.versions.chrome)</script>,
and Electron <script>document.write(process.versions.electron)</script>.
</p>
</body>
</html>
下一步需要修改package.json文件
{
"name": "my-electron-app",
"version": "0.1.0",
"author": "your name",
"description": "My Electron app",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
注意:
main
字段,Electron将尝试加载package.json
同级目录中的index.js
文件。author
和description
字段对于打包来说是必要的,否则运行npm run make
命令时会报错。执行npm start
即可启动这个项目,可以看到打开了一个窗口,显示index.html
的内容。
上面可以看到在main.js中是通过loadFile来加载文件的。这个是加载的本地文件,所以url都是file://xxxx。
而如果想加载url,那么使用loadURL这个函数,比如loadURL('http://www.baidu.com')
。
但是这里面存在一个问题
如果我们用的是本地文件,那么scheme默认就是file:// ,所以如果在js中做一个get或post请求时,url没有添加scheme(比如“//www.baidu.com” ),那么就是默认使用file:// ,那么这个请求就一定会失败。
所以要解决这个问题,url必须带着scheme,比如http://www.baidu.com 才行
有了项目就可以开始开发应用了。应用只是在窗口内展示,所以Electron的窗口也需要我们关注,通过Electron提供的api来定义一个合适的窗口
在上面我们创建的main.js
中,可以看到通过BrowserWindow创建的窗口
const win = new BrowserWindow({
width: 1280,
height: 744,
minWidth: 1280, //最小宽度
minHeight: 744, //最小高度
webPreferences: {
nodeIntegration: true
}
})
可以通过BrowserWindow的各个属性来调整窗口的各种参数,比如大小,最小宽度,是否全屏,是否可以最小化,是否能调整大小等等,见https://www.electronjs.org/docs/api/browser-window
注意:如果有框的话有标题栏,标题栏是24高度,这样也要考虑在height中,也就是说这里的高度是内容的高度+24
默认窗口是有菜单栏的,在mac上因为是在通知栏上而不是窗口上,所以不是很明显,但是在windows上就很明显了。
如果我们不希望展示这个菜单栏,就可以通过下面代码隐藏它
const { app, BrowserWindow, globalShortcut, Menu } = require('electron')
function createWindow () {
...
}
Menu.setApplicationMenu(null)
...
先引入menu模块,然后通过setApplicationMenu
隐藏即可。当然通过setApplicationMenu
也可以设置自定义的菜单。
但是注意,这么处理完之后应用的复制和粘贴功能也实效了,因为复制和粘贴的快捷键是设置在对应的菜单上的,如果将菜单取消,ctrl+c这类的快捷键也失效了,导致无法复制和粘贴。
所以如果你需要保留这些快捷操作,就不能完全去掉菜单栏,至少保留一些基本功能,如下:
const template = [
{
label: 'Edit',
submenu: [
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
{role: 'about'}
]
}
];
...
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
注意:这里的about菜单只显示英文名(后面会提到通过forge将应用名覆盖成中文,但是这里覆盖不到),而且在执行npm start
直接启动的时候,显示的是electron的版本和图标,但是没关系通过npm run make
打包后安装启动就会显示我们设置的应用图标和版本了。
在浏览器中,我们可以通过右键->检查来打开开发者工具,可以看到控制台输出、文件、代码、报错等信息。
但是用Electron打包后就无法看到,尤其控制台输出的日志,没有日志有问题后很难排查。
其实electron也可以开启开发者工具,在main.js文件中加入相关代码。比如我们上面写的main.js的createWindow函数的最后添加:
function createWindow () {
...
win.loadFile('index.html')
win.webContents.openDevTools() //开启开发者工具
}
注意,这行代码必须在loadFile之后才行。
然后在运行或打包运行,就可以看到右侧出现与浏览器一样的开发者工具了,可以很方便的查看控制台的日志输出。
但是上线前需要将这行代码去掉,这样太麻烦,我们可以通过快捷键来唤出开发者工具,只要将快捷键设置的足够复杂,线上用户就基本不会触发它。
修改mian.js代码如下:
const { app, BrowserWindow, globalShortcut } = require('electron')
var win;
function createWindow () {
win = new BrowserWindow({
...
})
win.loadFile('index.html')
globalShortcut.register('Alt+CommandOrControl+Shift+D', () => {
win.webContents.openDevTools({mode:'detach'}) //开启开发者工具
})
}
可以看到,使用globalShortcut
来注册快捷键,这里用Alt+CommandOrControl+Shift+D
组合(由于mac和windows的差异,所以第二个是CommandOrControl),这里因为是三个功能键+一个字母,所以一般用户不会触发。
当触发这个快捷键,就打开开发者工具。而且这里将工具的模式设置为detach
,即跟主页面分离,也就是说两个窗口,这样工具就不会占用主窗口的空间了,不会影响主窗口的内容。
但是注意:因为之前是在createWindow
中创建const win
,所以如果使用快捷键后再关闭重新打开应用,再使用快捷键时,这时候win.webContents
的win
还是之前的对象,已经销毁了,就会报错object has destroy
。所以将win
改成全局变量,如上面的代码。
以上我们仅仅是创建了一个项目并运行起来,如果要交付给用户使用,则需要将这个项目打包成可执行文件。下面看看如何进行打包。
Electron打包有几种选择
官方推荐使用Electron Forge,因为更简单易上手。
导入Electron Forge命令
npx @electron-forge/cli@latest import
这里遇到了两个问题,记录一下:
Unexpected token {
。将nodejs升级即可,我升级后的版本是v14.15.5npm ERR! cb() never called!
。再执行一次命令即可,或者像网上说的清除一下npm缓存,升级一个npm。我是重新执行一次就可以了。执行命令后会开始安装Electron Forge
✔ Checking your system
✔ Initializing Git Repository
✔ Writing modified package.json file
✔ Installing dependencies
✔ Writing modified package.json file
✔ Fixing .gitignore
We have ATTEMPTED to convert your app to be in a format that electron-forge understands.
Thanks for using "electron-forge"!!!
安装完成后执行npm run make
即可进行打包,出现下面的信息
> my-gsod-electron-app@1.0.0 make /my-electron-app
> electron-forge make
✔ Checking your system
✔ Resolving Forge Config
We need to package your application before we can make it
✔ Preparing to Package Application for arch: x64
✔ Preparing native dependencies
✔ Packaging Application
Making for the following targets: zip
✔ Making for target: zip - On platform: darwin - For arch: x64
打包完成在项目目录下的out目录下就可以看到打包好的程序
我这里是mac电脑,所以以mac为例子
其中make目录下是一个压缩文件,解压后就是可执行的app文件。
另外projectName-xxx-xxx目录下则是对应平台的可执行文件。
安装forge后打包时默认的是压缩包,make目录下是一个名为zip的文件夹,文件夹里最终是一个zip文件,解压后是app格式的mac执行文件,可以直接打开。
那么如果打一个安装包?
先看看安装forge后package.json文件的变化,增加了相关的依赖和config,如下:
{
...
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.54",
"@electron-forge/maker-deb": "^6.0.0-beta.54",
"@electron-forge/maker-rpm": "^6.0.0-beta.54",
"@electron-forge/maker-squirrel": "^6.0.0-beta.54",
"@electron-forge/maker-zip": "^6.0.0-beta.54",
"electron": "11.3.0"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
},
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
...
]
}
}
}
可以看到依赖了maker-zip,maker-deb等等,而在config
的forge
下有一个makers
属性,是一个array,其中除了@electron-forge/maker-zip
,还有@electron-forge/maker-deb
等其他maker
,这个maker
就是打包关键。
electron-forge的一个优点就是集成了大量的工具,使得打包操作更加简单方便,每个maker就是一种打包编译工具,比如maker-zip就是打包成zip格式,forge包含的所有maker如下(参考https://www.electronforge.io/config/makers ):
可以看到除了Zip,其他都依赖某个或某几个平台,甚至部分还依赖特地的环境。比如Squirrel.Windows如果在mac或linux上编译,则需要先安装mono和wine。
回到我们的问题,可以看到上面依赖了很多forge的maker其实是不用的,只有Zip可用,所以可以暂时删除。而我们要打包安装包,则需要依赖maker-dmg,执行安装命令npm i @electron-forge/maker-dmg --save-de
v,安装后在config
的maker
中添加一条maker-dmg,如下:
{
...
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.54",
"@electron-forge/maker-dmg": "^6.0.0-beta.54",
"@electron-forge/maker-zip": "^6.0.0-beta.54",
"electron": "11.3.0"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
},
"config": {
"forge": {
"packagerConfig": {
...
},
"makers": [
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-dmg",
"config": {
"name": "小盒课堂mac", //可以是中文,但只是安装包名称。
"icon": "./icon.icns",
"background": "./share_parent_bg_blue.png"
}
}
]
}
}
}
上面清理了不需要的maker,只留下zip和dmg,然后执行npm run make
打包。打包完成在/out/make/目录下看到除了之前的zip目录,多生成了一个dmg文件,这样安装包就打好了。(如果只打包dmg,则可以在maker中删除maker-zip即可)
后面可以通过maker-dmg的config进行一些设置,如安装背景background、安装包名称等。见https://js.electronforge.io/maker/dmg/interfaces/makerdmgconfig
注意: config
中的name
如果不设置,则默认应用名称,先取prodectName
,没有再取name
。而且注意如果不设置的话background
好像也不会生效,感觉是个bug。
windows上与mac类似,安装forge后也会默认安装多个maker,如zip、squirrel、rpm和deb。其中zip就是压缩包,squirrel则是安装包,另外两个用不到可以删除相关代码,如下:
{
...
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.54",
"@electron-forge/maker-squirrel": "^6.0.0-beta.54",
"@electron-forge/maker-zip": "^6.0.0-beta.54",
"electron": "12.0.1"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
},
"config": {
"forge": {
"packagerConfig": {
"icon": "./icon",
"platform": "all"
},
"makers": [
{
"name": "@electron-forge/maker-zip",
"platforms": [
...
]
},
{
"name": "@electron-forge/maker-squirrel",
"config": {}
},
...
]
}
}
}
这样打包的时候会在out/make目录下生成zip和安装包(exe)。当然还需要配置一下.
zip下的platforms可以去掉,去掉后会根据平台默认打包。
虽然我们在packagerConfig设置了图标,但是这个是应用的图标,即安装后应用入口文件的图标,而安装包文件的图标需要另外设置:
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "xxx", //不能是中文,否则安装时出错,可以不设置
"setupExe": "xxxsetup.exe",//可以是中文,可以不设置
"setupIcon": "./icon.ico", //安装包图标,可以不设置
}
}
其中setupExe
设置安装包的名字(这里可以是中文),而setupIcon
则是安装图标,这里单独设置setupIcon
好像不起作用,必须两个都设置才行。
然后在安装的过程中,会默认出现一个加载的动画,这个也可以进行替换,设置loadingGif
即可:
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "xxx", //不能是中文,否则安装时出错,可以不设置
"setupExe": "xxx.exe",//可以是中文,可以不设置
"setupIcon": "./icon.ico", //安装包图标,可以不设置
"loadingGif": "./installing.gif"
}
}
这个动画是一个gif动画。
这里有一个问题,如果将productName
设置成中文名称,那么通过squirrel打包的时候会报错Unable to load file
,反复测试发现是setupIcon
导致的,不设置这个属性就可以正常打包。
但是使用英文名称就没有问题,目前没有找到根本原因,如果要使用中文的应用名称,就不设置setupIcon
使用默认的好了。
squirrel官网介绍:https://www.electronforge.io/config/makers/squirrel.windows
相关参数
https://js.electronforge.io/maker/squirrel/classes/makersquirrel#constructor
https://js.electronforge.io/maker/squirrel/interfaces/makersquirrelconfig
最终的配置如下:
{
...
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.54",
"@electron-forge/maker-squirrel": "^6.0.0-beta.54",
"@electron-forge/maker-zip": "^6.0.0-beta.54",
"electron": "12.0.1"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
},
"config": {
"forge": {
"packagerConfig": {
"icon": "./icon",
"platform": "all"
},
"makers": [
{
"name": "@electron-forge/maker-zip",
},
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "xxx",
"setupExe": "xxx.exe", //可以不设置。如果不设置setupIcon,那么这里也可以不设置,用默认的名称就好
"setupIcon": "./icon.ico", //如果使用中文应用名称,暂时不能设置setupIcon,否则报错
"loadingGif": "./installing.gif"
}
},
...
]
}
}
}
最后在安装过程中程序会启动几次,体验很不好,要解决这个问题,需要在main.js的开头添加:
const { app } = require('electron');
if (require('electron-squirrel-startup')) return app.quit();
这样就可以防止多次启动。
安装包安装后会在桌面和开始菜单创建快捷方式。
package.json
中的第一个属性name
就是应用名称(实际上是application id)。但是这里的name
不能是中文,Electron不支持applicationId设置为中文,我们可以通过设置productName
来设置中文名称,如下:
{
"name": "my-electron-app",
"productName": "中文名称",
"version": "0.1.0",
"author": "your name",
"description": "My Electron app",
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
这样所有显示的应用名称都会换成productName
。
但是这里有一个问题,productName
改成中文后,在windows上我们通过Squirrel.Windows的方式打安装包,但是执行npm run make
到squirrel maker的时候会报错Unable to load file
。这个上面说过,就不再细说了。
修改图标则需要对electron forge设置,通过上面为项目安装使用electron forge后,在package.json中会自动添加相关的config,如下:
{
...
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
}
]
}
}
}
我们在packagerConfig
中添加图标即可,macOS上的图标必须是icns格式,而windows的图标必须是ico格式。如果只是一个平台,如mac,将图标放到项目的跟目录下,修改packagerConfig
如下:
"packagerConfig": {
"icon": "./icon.icns"
},
这样即可,注意使用npm start
直接运行的时候图标还是默认的electron图标,但是使用npm run make
打包后图标就是我们设置的图标了。
那么如果多个平台怎么办?
根据官方文档(https://electron.github.io/electron-packager/master/interfaces/electronpackager.options.html#icon ),将每个平台的图标都放到项目中,并且名称一样,这样在配置时不添加后缀名即可,打包时会根据平台自动找到相应图标。
If the file extension is omitted, it is auto-completed to the correct extension based on the platform, including when platform: 'all' is in effect.
比如将mac的图标icon.icns和windows的图标icon.ico都放在根目录下,然后修改配置
"packagerConfig": {
"icon": "./icon",
"platform": "all"
},
即可。
应用使用的过程中发现出现了跨域问题,无法访问某些地址,提示CORS(Cross-Origin Resource Sharing)
问题。
这是因为Electron的默认配置导致的,在Electron中默认是开启同源策略的,这样就导致无法访问外部的一些链接。我们需要手动进行开启,修改main.js代码如下:
const { app, BrowserWindow } = require('electron')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
webSecurity: false //禁用同源策略
}
})
win.loadFile('index.html')
}
...
相关说明如下:
我们在Electron的main.js中通过console.log
打印日志,以便查看调试应用。但是如果通过终端执行npm start
的时候,可以在终端中看到日志输出。但是打包成安装包(或可执行文件),直接打开就没法看到日志了。虽然上面提到了,我们可以通过打开开发者工具查看console,但是发现这里根本没有打印这部分日志。
这是因为Electron有两个进程:主进程 和 渲染进程,main.js是运行在主进程中的,而通过BrowserWindow装载load的文件或网站则运行在渲染进程,上面提到的开发者工具,实际上只能查看渲染进程的。
所以主进程中的日志就无法查看了,但是有几个方法可以考虑:
ipcMain
和ipcRenderer
。可以将日志传递给渲染进程,但是需要再渲染进程中实现接收消息并打印日志。const { dialog } = require('electron')
...
dialog.showMessageBox({message:"xxxxxx"})
这个涉及较多,我单独写一篇文章来讲。
使用过程中发现一个问题,因为我们是将代码放在服务端,在electron中只是加载了一个url。发现有时候虽然服务端文件更新了,但是在electron应用内(通过npm start启动)访问的还是旧的代码,这时候我们直接用浏览器访问就是新的代码。
这应该就是electron有本地缓存导致的,查了一下只要加上下面一行代码即可:
app.commandLine.appendSwitch("--disable-http-cache")