Electron入门

前言

Electron 可以让你使用纯 JavaScript 调用丰富的原生(操作系统) APIs 来创造桌面应用。 你可以把它看作一个 Node. js 的变体,它专注于桌面应用而不是 Web 服务器端。

这不意味着 Electron 是某个图形用户界面(GUI)库的 JavaScript 版本。 相反,Electron 使用 web 页面作为它的 GUI,所以你能把它看作成一个被 JavaScript 控制的,精简版的 Chromium 浏览器。

起步

1
2
3
4
git clone https://gitee.com/psvmc/electron-quick-start.git
cd electron-quick-start
npm install
npm start

创建窗口

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
const { app, BrowserWindow } = require('electron')

// 保持对window对象的全局引用,如果不这么做的话,当JavaScript对象被
// 垃圾回收的时候,window对象将会自动的关闭
let win

function createWindow () {
// 创建浏览器窗口。
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})

// 加载index.html文件
win.loadFile('index.html')

// 打开开发者工具
win.webContents.openDevTools()

// 当 window 被关闭,这个事件会被触发。
win.on('closed', () => {
// 取消引用 window 对象,如果你的应用支持多窗口的话,
// 通常会把多个 window 对象存放在一个数组里面,
// 与此同时,你应该删除相应的元素。
win = null
})
}

// Electron 会在初始化后并准备
// 创建浏览器窗口时,调用这个函数。
// 部分 API 在 ready 事件触发后才能使用。
app.on('ready', createWindow)

// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
// 否则绝大部分应用及其菜单栏会保持激活。
if (process.platform !== 'darwin') {
app.quit()
}
})

app.on('activate', () => {
// 在macOS上,当单击dock图标并且没有其他窗口打开时,
// 通常在应用程序中重新创建一个窗口。
if (win === null) {
createWindow()
}
})

// 在这个文件中,你可以续写应用剩下主进程代码。
// 也可以拆分成几个文件,然后用 require 导入。

常用路径

程序安装目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const {app} = window.require("electron").remote;
const path = window.require("path");
const fs = window.require("fs");

let homeDir = path.dirname(app.getPath("exe"));

let mypath = path.join(
homeDir,
"_temp",
"个人空间"
);

try{
if (!fs.existsSync(mypath)) {
fs.mkdirSync(mypath, { recursive: true });
}
}catch (e) {}

其中homeDir的路径为:

  • 开发环境 D:\Project\Electron\school_live_client\node_modules\electron\dist\
  • 打包环境 安装目录中exe的同级目录

EXE路径

可执行文件路径

1
app.getPath("exe")

结果

app.getPath(“exe”): D:\Project\Electron\school_live_client\node_modules\electron\dist\electron.exe

注意:

这里是开发时打印的路径,发布后是不一样的,是安装后的文件夹路径+exe文件名称

用户相关的路径

  • home 用户的 home 文件夹(主目录)
  • documents 用户文档目录的路径
  • downloads 用户下载目录的路径
  • music 用户音乐目录的路径
  • pictures 用户图片目录的路径
  • videos 用户视频目录的路径
1
2
3
4
5
6
app.getPath("home");
app.getPath("documents");
app.getPath("downloads");
app.getPath("music");
app.getPath("pictures");
app.getPath("videos");

结果

app.getPath(“home”): C:\Users\18351
app.getPath(“documents”): C:\Users\18351\Documents
app.getPath(“downloads”): C:\Users\18351\Downloads
app.getPath(“music”): C:\Users\18351\Music
app.getPath(“pictures”): C:\Users\18351\Pictures
app.getPath(“videos”): C:\Users\18351\Videos

注意:

这些路径中包含用户名,如果计算机的用户名是中文的话,路径中就会包含中文,有些第三方的库使用的路径可能不支持中文,一定要注意!!!

桌面路径

1
app.getPath("desktop")

结果

app.getPath(“desktop”): C:\Users\18351\Desktop

注意:

桌面路径中包含用户名,如果计算机的用户名是中文的话,路径中就会包含中文,有些第三方的库使用的路径可能不支持中文,一定要注意!!!

AppData下

  • appData 每个用户的应用程序数据目录,默认情况下指向

    • %APPDATA% Windows 中
    • $XDG_CONFIG_HOME or ~/.config Linux 中
    • ~/Library/Application Support macOS 中
  • userData 储存你应用程序设置文件的文件夹,默认是 appData 文件夹附加应用的名称
  • logs应用程序的日志文件夹
  • cache缓存路径
  • temp 临时文件夹

代码

1
2
3
4
5
app.getPath("appData");
app.getPath("userData");
app.getPath("logs");
app.getPath("cache");
app.getPath("temp");

结果

app.getPath(“appData”): C:\Users\18351\AppData\Roaming
app.getPath(“userData”): C:\Users\18351\AppData\Roaming\应用名称
app.getPath(“logs”): C:\Users\18351\AppData\Roaming\应用名称\Electron\logs
app.getPath(“cache”): C:\Users\18351\AppData\Roaming
app.getPath(“temp”): C:\Users\18351\AppData\Local\Temp

注意:

注意用户名应用名为中文的时候,logsuserData中会包含中文,有些第三方的库使用的路径可能不支持中文,一定要注意!!!

程序根目录

不建议使用

1
2
const {app} = window.require("electron").remote;
app.getAppPath();

结果

app.getAppPath(): D:\Project\Electron\school_live_client

注意:

这个在开发时时源代码所在路径。

这是应用的安装路径和应用名称没有必然关系,一定要注意。

打包后的路径为

假如程序的安装路径为D:\MyApp

D:\MyApp\resources\app.asar

这个路径是在压缩文件内不建议使用。

不建议使用的路径

这两个路径会报错

  • recent 用户最近文件的目录 (仅限 Windows)。
  • crashDumps 崩溃转储文件存储的目录。

返回 String - 一个与 name相关的特殊目录或文件的路径。 失败会抛出一个Error

自动重新加载页面

1
npm install --save-dev electron-reloader

添加下面代码到main.js的最下面

1
2
3
4
5
6
7
8
const {app} = require("electron");
const isDevelopment = !app.isPackaged;
if (isDevelopment) {
try {
require('electron-reloader')(module);
} catch (err) {
}
}

前端页面自动刷新

添加依赖

1
npm install --save-dev electron-reload

添加下面代码到main.js的最下面

1
2
3
4
5
const {app} = require("electron");
const isDevelopment = !app.isPackaged;
if (isDevelopment) {
require('electron-reload')(path.join(__dirname, "build"));
}

这个插件跟上面的区别在于我们可以指定自动刷新所监听的文件夹

主进程和渲染进程

Electron 运行 package.jsonmain.js 脚本的进程被称为主进程

在主进程中运行的脚本通过创建web页面来展示用户界面。

一个 Electron 应用总是有且只有一个主进程。

每个 Electron 中的 web 页面运行在它自己的渲染进程中。

主进程管理所有的web页面和它们对应的渲染进程。 每个渲染进程都是独立的,它只关心它所运行的 web 页面。

Electron同时在主进程和渲染进程中对Node.js 暴露了所有的接口。

主进程和渲染进程模块

两种进程都可用的模块

  • clipboard 在系统剪贴板上执行复制和粘贴操作。
  • crashReporter 将崩溃日志提交给远程服务器。
  • nativeImage 使用 PNG 或 JPG 文件创建托盘、dock和应用程序图标。
  • shell 使用默认应用程序管理文件和 url。

Main Process 模块

  • app 控制你的应用程序的事件生命周期。
  • autoUpdater 使应用程序能够自动更新
  • BrowserView 创建和控制视图,相当于Android中的Fragment。
  • BrowserWindow 创建和控制浏览器窗口,相当于Android中的Activity。
  • contentTracing 从Chromium的内容模块收集跟踪数据,以查找性能瓶颈和缓慢的操作。
  • dialog 显示用于打开和保存文件、警报等的本机系统对话框。
  • globalShortcut 在应用程序没有键盘焦点时,监听键盘事件。
  • inAppPurchase Mac App Store中的应用内购买。
  • ipcMain 从主进程到渲染进程的异步通信。
  • Menu 创建原生应用菜单和上下文菜单。
  • MenuItem 添加菜单项到应用程序菜单和上下文菜单中。
  • net 使用Chromium的原生网络库发出HTTP / HTTPS请求。
  • netLog 网络请求日志。
  • powerMonitor 监视电源状态的改变。
  • powerSaveBlocker 阻止系统进入低功耗 (休眠) 模式。
  • protocol 注册自定义协议并拦截基于现有协议的请求。
  • screen 检索有关屏幕大小、显示器、光标位置等的信息。
  • session 管理浏览器会话、cookie、缓存、代理设置等。
  • systemPreferences 获取system preferences。
  • 触控板 为原生macOS应用创建TouchBar布局。
  • Tray 添加图标和上下文菜单到系统通知区。
  • webContents 渲染以及控制 web 页面。

Renderer Process 模块

  • desktopCapturer 从桌面上捕获音频和视频的媒体源信息。
  • ipcRenderer 从渲染器进程到主进程的异步通信。
  • remote 在渲染进程中使用主进程模块。
  • webFrame 自定义渲染当前网页。

进程间通讯

渲染进程=>主进程=>渲染进程

异步

在渲染器进程 (网页) 中

1
2
3
4
5
6
const { ipcRenderer } = require("electron");

ipcRenderer.send('downloadfile', 'http://www.psvmc.cn/favicon.ico')
ipcRenderer.on('downloadfile-result', (event, arg) => {
console.log(arg)
})

在主进程中

1
2
3
4
5
6
const { ipcMain } = require("electron")

ipcMain.on("downloadfile", (event, arg) => {
console.log(arg); // prints "ping"
event.reply("downloadfile-result", "E://1.jpg");
});

同步

在渲染器进程 (网页) 中

1
2
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"

在主进程中

1
2
3
4
5
const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})

主进程=>渲染进程

在主进程中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const { ipcMain } = require('electron')

let rtmpWindow = new BrowserWindow({
width: 400,
height: 600,
frame: false,
hasShadow: false,
show: false,
title: "",
webPreferences: {
nodeIntegration: true
}
});
rtmpWindow.loadFile("views/rtmp.html");
rtmpWindow.webContents.openDevTools()


rtmpWindow.webContents.on('did-finish-load', () => {
//下面的这行代码要在上面的BrowserWindow加载完成之后调用
rtmpWindow.webContents.send('asynchronous-msg',"test");
})

在渲染器进程 (网页) 中

1
2
3
4
5
const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-msg', (event, arg) => {
console.log(arg) // prints "pong"
})

渲染进程=>渲染进程

使用全局共享属性

使用全局共享属性或者用 Storage API( localStoragesessionStorage 或者 IndexedDB)。

但不具备事件机制,没有实质的通信功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 主进程中在global上自定义对象
global.saveDefault= {
token: 'default value',
name: 'default value',
password: 'default value',
}

// 在登录页 In page 1
require('electron').remote.getGlobal('saveDefault').token= 'token'
require('electron').remote.getGlobal('saveDefault').name= 'name'


// 在主页 In page 2 就可以获取到
console.log(require('electron').remote.getGlobal('saveDefault').name)
console.log(require('electron').remote.getGlobal('saveDefault').token)
console.log(require('electron').remote.getGlobal('saveDefault').password)

利用主进程做消息中转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 渲染进程1
ipcRenderer.send('ping-event', 'ping');

// 在主进程中
ipcMain.on(
'ping-event',
(event, arg) => {
yourWindow.webContents.send('pong-event', 'something');
}
);

// 渲染进程2
ipcRenderer.on(
'pong-event',
(event, arg) => {
// do something
}
);

使用 ipcRenderer.sendTo()

1
2
3
// 渲染进程
ipcRenderer.sendTo(webContentsId, channel, [, arg1][, arg2][, ...]);
ipcRenderer.sendTo(winId, 'ping', 'someThing');

利用 remote 接口直接获取渲染进程发送消息

1
2
3
// 渲染进程
// 获取窗口的id
remote.BrowserWindow.fromId(winId).webContents.send('ping', 'someThing');

获取进程id的方法

第一种: 通过 global 设置和获取

1
2
3
4
5
6
7
8
9
10
11
// 主进程中在global上自定义对象
global.winIds= {
win1Id : win1.id,
win2Id : win2.id
}

// 主进程获取
global.winIds["win1Id"];

// 渲染进程获取
require('electron').remote.getGlobal('winIds').win1Id

第二种是: 主进程创建事件,发送信息(不推荐)

1
2
3
4
5
6
7
8
// 主进程中
win1.webContents.send('distributeIds',{
win2Id : win2.id
});

win2.webContents.send('distributeIds',{
win1Id : win1.id
});

BrowserView与BrowserWindow

先说结论如果你的窗口内只加载一个页面就不要用BrowserView了,因为它不会带来任何好处。

本来我以为使用BrowserWindow创建窗口每一个窗口都会创建一个Electron进程,如果页面使用Node也会同事创建一个Node进程,好浪费资源啊,那么是不是使用BrowserView就会减少呢,毕竟它是依存于BrowserWindow而存在的(也就是说BrowserWindow隐藏,里面的BrowserView都会隐藏),但实际上压根没有减少资源的消耗,还增大了代码复杂度,还有很多坑。

BrowserView的劣势

  • 必须配置下方代码才能使用Node环境,并不是只配置BrowserWindow就可以。

    1
    2
    3
    webPreferences: {
    nodeIntegration: true
    }
  • 必须先把BrowserView添加到BrowserWindow后再加载页面,否则页面添加的事件不生效。

    1
    2
    classMainWin.addBrowserView(blackboardView);
    blackboardView.webContents.loadURL(blackboardURL);
  • BrowserView初始化设置窗口大小无效,必须初始化后调用下面方法设置窗口大小。

    1
    2
    3
    4
    5
    6
    pptView.setBounds({
    width: winW,
    height: winH,
    x: 0,
    y: 0
    });
  • 可以添加多个子窗口,但是无法设置子窗口的显示隐藏。

  • 添加的子窗口也要新开进程,并没有节省资源。

  • 是实验性方法,后期可能被删除。

  • 添加的窗口可以是多标签形式,但是支持Mac。

BrowserView什么时候用

  • 想在一个窗口同时显示多个页面。
  • 只用适配Mac,并想实现多标签页。

总而言之强烈不推荐使用BrowserView。

页面数据共享

渲染进程之间

在两个网页(渲染进程)间共享数据最简单的方法是使用浏览器中已经实现的 HTML5 API。 其中比较好的方案是用 Storage API( localStoragesessionStorage 或者 IndexedDB)。

所有进程间

但是如果要想在主进程和渲染进程之间共享数据,就不能用上面所说的方式了。

可以用 IPC 机制global.sharedObject

IPC

Electron 内的 IPC 机制实现。

global.sharedObject

将数据存在主进程的某个全局变量中,然后在多个渲染进程中使用 remote 模块来访问它。

在主进程中

1
2
3
global.sharedObject = {
username: 'default value'
}

在第一个页面中

1
2
const remote = window.require("electron").remote;
remote.getGlobal('sharedObject').username = '000';

在第二个页面中

1
2
const remote = window.require("electron").remote;
console.log(remote.getGlobal('sharedObject').username);

主进程

1
2
3
global.sharedObject.username = "123";

console.info(global.sharedObject.username);

常见问题

jQuery/RequireJS/Meteor/AngularJS 的问题

jQuery 等新版本的框架,在 Electron 中使用普通的引入的办法会引发异常,原因是 Electron 默认启用了 Node.js 的 require 模块,而这些框架为了支持 commondJS 标准,当 Window 中存在 require 时,会启用模块引入的方式。

分别有以下几种解决方案:

方式一:

使用 Electron 官方论坛提供的方法,改变require的写法。下面的代码各个框架通用:

1
2
3
4
5
6
7
//在引入框架之前先输入下面的代码
<script>
window.nodeRequire = require;
delete window.require;
delete window.exports;
delete window.module;
</script>

方式二:

禁用Node.js的require模块化引入(如果你不想使用 Node.js 模块):

1
2
3
4
5
6
// In the main process.
let win = new BrowserWindow({
webPreferences: {
nodeIntegration: false
}
});

方式三:

为使 web 项目正常浏览,在引入 jquery 后进行判断:

1
2
3
4
//置于引入 jQuery 之后
<script>
if (typeof module === 'object') {window.jQuery = window.$ = module.exports;};
</script>

关于页面跳转 的问题

在接收到命令后创建下一个窗口(创建窗口需要时间,期间可能出现空白):

1
2
3
4
5
6
7
8
9
10
//在main.js中::
const ipc = require('electron').ipcMain;
//进行监控,如果有new-window 发送过来,则重新创建一个窗口,文件是list.html
ipc.on('new-window', function () {
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, '/views/list.html'),
protocol: 'file:',
slashes: true
}))
});

关于无边框窗口 的问题

为了使窗口无边框,使得在某些时候让项目看起来更美观,所以在创建窗口的时候通过设置 frame 属性的值为 false 来创建无边框窗口。

但是无边框窗口会产生无法移动的问题,对于这个问题我们可以在渲染进程中通过编辑 css 文件来解决。

设置 -webkit-app-region: no-drag 禁止拖拽:

1
2
3
body {
-webkit-app-region: no-drag
}

需要拖拽的地方设置:

1
2
3
section {
-webkit-app-region: drag;
}

electron报错:fs.existsSync is not a function

使用vue-cli-plugin-electron-builder创建electron项目,在渲染进程中require(‘electron’)会报错:

fs.existsSync is not a function

解决办法:
需要修改两个地方
渲染进程中引入模块,改成使用window.require引入

1
const electron = window.require('electron');

主进程中,加上nodeIntegration: true

1
2
3
4
5
6
7
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})

图标显示不完整

使用下面软件制作ICO图标

链接:https://pan.baidu.com/s/1FAYikTjto2SbobuwxNFkKg
提取码:641i

图标建议尺寸256x256

获取窗口句柄

1
2
3
const win = window.require("electron").remote.getCurrentWindow(); 
let hwnd = win.getNativeWindowHandle();
let winId = hwnd.readUInt32LE(0);