前面的课程已经讲述了什么是Electron,Electron的基本原理,Electron的工程化,如,怎么和React结合,怎么打包,怎么更新。Electron的基本原理,如主进程、子进程,进程间通信,Electron的窗口实现,Electron的基础结构等。
那么,今天,我们主要看看Electron的主要能力,也就是Electron能做些什么,同时对之前的课程进行加强和补充。
Electron是基于chromium的开源项目,和谷歌浏览器使用相同的内核,它的前身是Atom-shell,始于2013年,后来改名为Electron。
Electron 是一个能让你使用 JavaScript,HTML 和 CSS 来创建桌面应用程序的框架。 这些应用程序可以打包后在 macOS、Windows 和 Linux 上直接运行,或者通过 Mac App Store 或微软商店进行分发。
意思也就是,web前端开发者不仅仅可以依赖浏览器写代码,还可以将自己写的web代码打包成一个独立桌面应用,这是一个很大的进步。
上面说到web前端开发者之前只能基于浏览器进行开发,写的代码都是运行在浏览器上,那么我们是否可以脱离浏览器,把自己写的web代码打包成独立的应用呢?Electron就实现了这个想法。
Electron是怎么做到的?
说的通俗一些,Electron就是将Chrome浏览器的内核抠出来(基于chromium),当做业务代码的运行环境,最后发版的时候连同浏览器内核一起打包成一个应用,分发给用户使用。
所以,Electron应用都不会太小,因为业务代码自带浏览器内核。通常来说,至少也有四五十兆。
如果说,仅仅只是把网页打包成一个应用,那么,这种应用似乎也没有多大意义。这就引入我们下面要说的。Electron不仅自带浏览器渲染内核,同时还提供了NodeJS运行环境和底层原生能力,扩展C++的能力,Electron内置的Native API,比如,你可以在应用中操作文件,访问打印驱动,访问蓝牙等等。这才是Electron的王炸之举。Web网页是运行在沙盒环境中的(沙盒:在计算机安全领域中是一种安全机制,为运行中的程序提供的隔离环境,沙盒通常严格控制其中的程序所能访问的资源),很多原生能力访问是不允许进行的,但Electron可以。
前面讲过,Electron和Chrome浏览器很类似,都是只有一个主进程,可以有多个子进程(我们这里只讨论渲染进程),比如Chrome中,每个tab页签就是一个渲染进程,简单看几张图,大致了解一下。
在Electron中,赋予主进程和渲染进程的能力范围是不同的,渲染进程能具备完整的Node能力,但是却不具备原生底层能力,比如访问打印机。而主进程不仅具备完整的Node能力,还具备原生底层能力。那么,我们在业务需求中需要访问原生能力的时候,只能让主进程去做,这个时候,需要渲染进程通知主进程,然后由主进程完成,主进程还需要把完成的结果传递给子进程。所以,进程间通信的必要性就出现了。
下面,我们通过几幅图来了解Electron的架构和能力分布。
到底有哪些我们耳熟能详的Electron案例呢?
另外,还有Atom编辑器、微信PC客户端、美团大象等等。
通过上面的学习,我们已经知道,Electron使得web前端开发者可以开发出自己的应用,除了要要懂得纯web前端的知识,还需要学习NodeJS的知识,打包、更新等知识,另外,为了能够扩展更强的能力,有时候还需要写C++扩展,以提供业务调用。所以,对前端开发者的要求立马就提高了,所以,Electron的开发,是有一定门槛的。
1,每个应用只有一个主进程,Electron项目中,在package.json中配置,如下图所示,main.js就是主进程文件,也是整个应用的入口;
2,创建渲染进程(子进程);
3,控制应用生命周期(app);
4,管理原生GUI,如BrowserWindow,Tray,Dock,Menu等等;
5,访问系统的能力;
1,展示web页面的进程成为渲染进程;
2,通过NodeJS、Electron提供的API直接或者间接和系统底层打交道;
3,一个Electron应用可以有多个渲染进程;
4,一个渲染进程对应一个窗口;
1,callback写法:
- ipcRenderer.send
- ipcMain.on
2,Promise写法(Electron7.0之后,请求 + 响应):
- ipcRenderer.invoke
- ipcMain.handle
- ipcRenderer.on
- xxWin.webContents.send
注:主进程通知渲染进程为啥不是ipcMain.send?这里要适当注意一下,因为主进程只有一个,而渲染进程可以有多个,所以主进程通知渲染进程时,由渲染进程所在的窗口发送,渲染进程窗口在主进程中被创建和管理。
1, 通知事件
- 通过主进程转发(Electron5之前)
- ipcRenderer.sendTo(Electron5之后)
2,数据共享
- web技术(localStorage、sessionStorage、indexedDB)
- 使用remote(不推荐、不建议)
我们写一个简单的例子,来实现上面进程间通信方式
主进程main.js
const { app, BrowserWindow, ipcMain, dialog, Notification, Menu, Tray, globalShortcut } = require('electron')
let windowA = null
let windowB = null
let tray = null
// Menu的用法
const template = [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}, {
label: 'File',
submenu: [{ role: 'close' }]
}, {
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
}, {
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://www.baidu.com/')
}
}
]
}, {
label: '自定义',
submenu: [
{
label: '弹框通知',
click: () => {
let x = new Notification({ title: '我是TITLE', body: '我是BODY' })
x.show()
}
}
]
}]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
class AppWindow extends BrowserWindow {
constructor(config, htmlFile) {
const baseConfig = {
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true
}
}
const finalConfig = {...baseConfig, ...config}
super(finalConfig)
this.loadFile(htmlFile)
this.webContents.openDevTools()
}
}
// 初始化托盘Tray
const initTray = () => {
tray = new Tray('./assets/icon.png')
const contextMenu = Menu.buildFromTemplate([
{ label: 'Item1', type: 'radio' },
{ label: 'Item2', type: 'radio' },
{ label: 'Item3', type: 'radio', checked: true },
{ label: 'Item4', type: 'radio', click: () => { console.log('......点击了Item4.......') } }
])
tray.setToolTip('This is my tray')
tray.setContextMenu(contextMenu)
// 事件
tray.on('click', () => {
let x = new Notification({ body: 'Tray事件', title: '单击事件' })
x.show()
})
}
app.on('ready', () => {
initTray()
// 初始化创建视窗A(渲染进程A)
windowA = new AppWindow({}, './renderer/processA/a.html')
// 快捷键用法
globalShortcut.register('CommandOrControl+Q', () => {
windowA.webContents.toggleDevTools()
})
ipcMain.on('create-B-window', () => {
console.log('接到A进程的通知,我来创建进程B')
windowB = new AppWindow({
width: 800,
height: 600,
parent: windowA
}, './renderer/processB/b.html')
})
global.sharedObject = {
windowAWebContentsId: windowA.webContents.id
}
ipcMain.on('open-pic-file', () => {
console.log('接到B进程的通知,我来打开系统视窗')
dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif'] }]
}).then(files => {
console.log('files: ', files)
})
})
// 获取打印机列表(老方法)
ipcMain.on('b-main-pinterlist-old', () => {
// 在主线程中获取打印机列表
const list = windowB.webContents.getPrinters()
console.log('list(old): ', list)
// 通过webContents发送事件到渲染线程,同时将打印机列表页传过去
windowB.webContents.send('main-b-printerlist-old', list)
})
// 获取打印机列表(新方法)
ipcMain.handle('b-main-pinterlist-new', () => {
return new Promise(resolve => {
const list = windowB.webContents.getPrinters()
console.log('list(new): ', list)
resolve(list)
})
})
})})
看看渲染进程A:a.html/a.js
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Render-Atitle>
<link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css">
head>
<body>
<div class="container mt-4">
<h1>我是渲染进程Ah1>
<h2 id='myh2'>h2>
<button
type="button"
class="btn btn-primary btn-lg btn-block mt-4"
id="create-B-button"
>点我通知主进程去创建Bbutton>
div>
<script>
// 网页中可以使用node
require('./a.js')
script>
body>
html>
const { ipcRenderer } = require('electron')
const { $ } = require('../../helper')
$('create-B-button').addEventListener('click', () => {
console.log('通知主进程去创建B视窗')
ipcRenderer.send('create-B-window')
})
ipcRenderer.on('b-a', (e, a) => {
console.log('a ============== ', a)
$('myh2').innerHTML = a
})
看看渲染进程B:b.html/b.js
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Render-Btitle>
<link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css">
head>
<body>
<div class="container mt-4">
<h1>我是渲染进程Bh1>
<button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="select-pic">请选择图片button>
<button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="fetch-printer-list-old">获取打印机列表-老button>
<button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="fetch-printer-list-new">获取打印机列表-新button>
<button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="a-render-b">渲染进程间通信sendTobutton>
div>
<script>
// 网页中可以使用node
require('./b.js')
script>
body>
html>
const { ipcRenderer, remote } = require('electron')
const { $ } = require('../../helper')
let obj1 = remote.getGlobal('sharedObject')
console.log('obj1: ', obj1)
$('select-pic').addEventListener('click', () => {
console.log('通知主进程去打开操作系统视窗')
ipcRenderer.send('open-pic-file')
})
$('fetch-printer-list-old').addEventListener('click', () => {
console.log('通知主进程获取打印机列表--老方法')
ipcRenderer.send('b-main-pinterlist-old')
ipcRenderer.once('main-b-printerlist-old', (ev, data) => {
console.log('data(old) ==== ', data)
})
})
$('fetch-printer-list-new').addEventListener('click', async () => {
console.log('通知主进程获取打印机列表')
let res = await ipcRenderer.invoke('b-main-pinterlist-new')
console.log('打印机列表res(新): ', res)
})
$('a-render-b').addEventListener('click', async () => {
console.log('渲染进程b通知渲染进程a')
let obj = await remote.getGlobal('sharedObject')
console.log('obj: ', obj)
let windowAWebContentsId = obj.windowAWebContentsId
ipcRenderer.sendTo(windowAWebContentsId, 'b-a', '从b窗口带来的数据')
})
helper/index.js
exports.$ = id => {
return document.getElementById(id)
}
我们甚至可以在Electron的控制台上直接写代码:
const fs = require('fs')
const path = require('path')
const fileContent = fs.readFileSync(path.join(__dirname, 'a.js'), 'utf-8')
console.log(fileContent)
直接上图吧,这就是menu
如何给我们自己的应用添加Menu呢?主进程中添加如下代码!
const { app, BrowserWindow, ipcMain, Menu, Tray} = require('electron')
// Menu的用法
const template = [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}, {
label: 'File',
submenu: [{ role: 'close' }]
}, {
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
}, {
role: 'help',
submenu: [
{
label: 'Learn More',
click: async () => {
const { shell } = require('electron')
await shell.openExternal('https://www.baidu.com/')
}
}
]
}, {
label: '自定义',
submenu: [
{
label: '弹框通知',
click: () => {
let x = new Notification({ title: '我是TITLE', body: '我是BODY' })
x.show()
}
}
]
}]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
const initTray = () => {
tray = new Tray('./assets/icon.png')
const contextMenu = Menu.buildFromTemplate([
{ label: 'Item1', type: 'radio' },
{ label: 'Item2', type: 'radio' },
{ label: 'Item3', type: 'radio', checked: true },
{ label: 'Item4', type: 'radio', click: () => { console.log('......点击了Item4.......') } }
])
tray.setToolTip('This is my tray')
tray.setContextMenu(contextMenu)
// 事件
tray.on('click', () => {
let x = new Notification({ body: 'Tray事件', title: '单击事件' })
x.show()
})
}
需求:我们要用 “Command + Q” 来 打开/关闭 应用的控制台
const { globalShortcut } = require('electron')
// 快捷键用法
globalShortcut.register('CommandOrControl+Q', () => {
windowA.webContents.toggleDevTools()
})
比如,获取电脑连接的打印机列表
ipcMain.handle('b-main-pinterlist-new', () => {
return new Promise(resolve => {
const list = windowB.webContents.getPrinters()
console.log('list(new): ', list)
resolve(list)
})
})