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
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
});

页面数据共享

渲染进程之间

在两个网页(渲染进程)间共享数据最简单的方法是使用浏览器中已经实现的 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);

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。

常见问题

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);