导读
运用Electron
开辟客户端顺序已有一段时间了,团体以为照样异常不错的,个中也遇到了一些坑点,本文是从【运转道理】到【现实运用】对Electron
举行一次体系性的总结。【多图,长文预警~】
本文一切实例代码均在我的github electron-react上,连系代码浏览文章结果更佳。别的electron-react
还可作为运用Electron + React + Mobx + Webpack
手艺栈的脚手架工程。
一、桌面运用顺序
桌面运用顺序,又称为 GUI 顺序(Graphical User Interface),然则和 GUI 顺序也有一些辨别。桌面运用顺序 将 GUI 顺序从GUI 细致为“桌面”,使冷冰冰的像块木头一样的电脑观点更具有 人性化,更活泼和富有生机。
我们电脑上运用的种种客户端顺序都属于桌面运用顺序,近年来WEB
和挪动端的鼓起让桌面顺序逐渐昏暗,然则在某些一样平常功用或许行业运用中桌面运用顺序依旧是必不可少的。
传统的桌面运用开辟体式格局,平常是下面两种:
1.1 原生开辟
直接将言语编译成可实行文件,直接挪用体系API
,完成UI绘制等。这类开辟手艺,有着较高的运转效力,但平常来讲,开辟速率较慢,手艺要求较高,比方:
- 运用
C++ / MFC
开辟Windows
运用 - 运用
Objective-C
开辟MAC
运用
1.2 托管平台
一最先就有当地开辟和UI开辟。一次编译后,获得中心文件,经由历程平台或虚机完成二次加载编译或诠释运转。运转效力低于原生编译,但平台优化后,其效力也是比较可观的。就开辟速率方面,比原生编译手艺要快一些。比方:
- 运用
C# / .NET Framework
(只能开辟Windows运用
) Java / Swing
不过,上面两种对前端开辟职员太不友爱了,基础是前端职员不会触及的范畴,然则在这个【大前端😅】的时期,前端开辟者正在千方百计涉足各个范畴,运用WEB
手艺开辟客户端的体式格局横空出世。
1.3 WEB开辟
运用WEB
手艺举行开辟,应用浏览器引擎完成UI
衬着,应用Node.js
完成服务器端JS
编程并能够挪用体系API
,能够把它想像成一个套了一个客户端外壳的WEB
运用。
在界面上,WEB
的壮大生态为UI
带来了无穷能够,而且开辟、庇护本钱相对较低,有WEB
开辟履历的前端开辟者很轻易上手举行开辟。
本文就来偏重引见运用WEB
手艺开辟客户端顺序的手艺之一【electron
】
二、Electron
Electron
是由Github
开辟,用HTML,CSS
和JavaScript
来构建跨平台桌面运用顺序的一个开源库。 Electron
经由历程将Chromium
和Node.js
合并到同一个运转时环境中,并将其打包为Mac,Windows
和Linux
体系下的运用来完成这一目标。
2.1 运用Electron开辟的来由:
- 1.运用具有壮大生态的
Web
手艺举行开辟,开辟本钱低,可扩大性强,更炫酷的UI
- 2.跨平台,一套代码可打包为
Windows、Linux、Mac
三套软件,且编译疾速 - 3.可直接在现有
Web
运用上举行扩大,供应浏览器不具有的才 - 4.你是一个前端👨💻~
固然,我们也要认清它的瑕玷:机能比原生桌面运用要低,终究打包后的运用比原生运用大很多。
2.2 开辟体验
兼容性
虽然你还在用WEB
手艺举行开辟,然则你没必要再斟酌兼容性题目了,你只须要体贴你当前运用Electron
的版本对应Chrome
的版本,平常情况下它已充足新来让你运用最新的API
和语法了,你还能够手动晋级Chrome
版本。一样的,你也没必要斟酌差异浏览器带的款式和代码兼容题目。
Node环境
这多是很多前端开辟者曾梦想过的功用,在WEB
界面中运用Node.js
供应的壮大API
,这意味着你在WEB
页面直接能够操纵文件,挪用体系API
,以至操纵数据库。固然,除了完整的 Node API
,你还能够运用分外的几十万个npm
模块。
跨域
你能够直接运用Node
供应的request
模块举行收集要求,这意味着你无需再被跨域所搅扰。
壮大的扩大性
借助node-ffi
,为运用顺序供应壮大的扩大性(背面的章节会细致引见)。
2.3 谁在用Electron
如今市面上已有异常多的运用在运用Electron
举行开辟了,包括我们熟习的VS Code
客户端、GitHub
客户端、Atom
客户端等等。印象很深的,客岁迅雷在宣布迅雷X10.1
时的案牍:
从迅雷X 10.1版本最先,我们采纳Electron软件框架完整重写了迅雷主界面。运用新框架的迅雷X能够圆满支撑2K、4K等高清显现屏,界面中的笔墨衬着也越发清楚锋利。从手艺层面来讲,新框架的界面绘制、事宜处置惩罚等方面比老框架越发天真高效,因而界面的流通度也明显优于老框架的迅雷。至于细致提拔有多大?您一试便知。
你能够翻开VS Code
,点击【协助】【切换开辟职员东西】来调试VS Code
客户端的界面。
三、Electron运转道理
Electron
连系了 Chromium
、Node.js
和用于挪用操纵体系当地功用的API
。
3.1 Chromium
Chromium
是 Google
为生长 Chrome
浏览器而启动的开源项目,Chromium
相当于 Chrome
的工程版或称试验版,新功用会率先在 Chromium
上完成,待考证后才会运用在Chrome
上,故 Chrome
的功用会相对落伍但较稳固。
Chromium
为Electron
供应壮大的UI
才,能够在不斟酌兼容性的情况下开辟界面。
3.2 Node.js
Node.js
是一个让 JavaScript
运转在服务端的开辟平台,Node
运用事宜驱动,非壅塞I/O
模子而得以轻量和高效。
单单靠Chromium
是不能具有直接操纵原生GUI
才的,Electron
内集成了Nodejs
,这让其在开辟界面的同时也有了操纵体系底层 API
的才,Nodejs
中经常使用的 Path、fs、Crypto
等模块在 Electron
能够直接运用。
3.3 体系API
为了供应原生体系的GUI
支撑,Electron
内置了原生运用顺序接口,对挪用一些体系功用,如挪用体系关照、翻开体系文件夹供应支撑。
在开辟情势上,Electron
在挪用体系API
和绘制界面上是星散开辟的,下面我们来看看Electron
关于历程怎样离别。
3.4 主历程
Electron
辨别了两种历程:主历程和衬着历程,二者各自担任本身的职能。
Electron
运转 package.json
的 main
剧本的历程被称为主历程。一个 Electron
运用总是有且只要一个主历程。
职责:
- 竖立衬着历程(可多个)
- 控制了运用生命周期(启动、退出
APP
以及对APP
做一些事宜监听) - 挪用体系底层功用、挪用原生资本
可挪用的API:
Node.js API
-
Electron
供应的主历程API
(包括一些体系功用和Electron
附加功用)
3.5 衬着历程
由于 Electron
运用了 Chromium
来展现 web
页面,所以 Chromium
的多历程架构也被运用到。 每一个 Electron
中的 web
页面运转在它本身的衬着历程中。
主历程运用 BrowserWindow 实例竖立页面。 每一个 BrowserWindow 实例都在本身的衬着历程里运转页面。 当一个 BrowserWindow 实例被烧毁后,相应的衬着历程也会被停止。
你能够把衬着历程想像成一个浏览器窗口,它能存在多个而且相互自力,不过和浏览器差异的是,它能挪用Node API
。
职责:
- 用
HTML
和CSS
衬着界面 - 用
JavaScript
做一些界面交互
可挪用的API:
DOM API
Node.js API
-
Electron
供应的衬着历程API
四、Electron基础
4.1 Electron API
在上面的章节我们提到,衬着进和主历程离别可挪用的Electron API
。一切Electron
的API
都被指派给一种历程范例。 很多API
只能被用于主历程中,有些API
又只能被用于衬着历程,又有一些主历程和衬着历程中都能够运用。
你能够经由历程以下体式格局猎取Electron API
const { BrowserWindow, ... } = require('electron')
下面是一些经常使用的Electron API
:
在背面的章节我们会挑选个中经常使用的模块举行细致引见。
4.2 运用 Node.js 的 API
你能够同时在Electron
的主历程和衬着历程运用Node.js API
,)一切在Node.js
能够运用的API
,在Electron
中一样能够运用。
import {shell} from 'electron';
import os from 'os';
document.getElementById('btn').addEventListener('click', () => {
shell.showItemInFolder(os.homedir());
})
有一个异常主要的提醒: 原生Node.js模块 (即指,须要编译源码事后才被运用的模块) 须要在编译后才和Electron一同运用。
4.3 历程通信
主历程和衬着历程虽然具有差异的职责,然是他们也须要相互协作,相互通信。
比方:在
web
页面治理原生
GUI
资本是很风险的,会很轻易泄漏资本。所以在
web
页面,不允许直接挪用原生
GUI
相干的
API
。衬着历程假如想要举行原生的
GUI
操纵,就必须和主历程通信,要求主历程来完成这些操纵。
4.4 衬着历程向主历程通信
ipcRenderer
是一个 EventEmitter
的实例。 你能够运用它供应的一些要领,从衬着历程发送同步或异步的音讯到主历程。 也能够吸收主历程复兴的音讯。
在衬着历程引入ipcRenderer
:
import { ipcRenderer } from 'electron';
异步发送:
经由历程 channel
发送同步音讯到主历程,能够照顾恣意参数。
在内部,参数会被序列化为
JSON
,因而参数对象上的函数和原型链不会被发送。
ipcRenderer.send('sync-render', '我是来自衬着历程的异步音讯');
同步发送:
const msg = ipcRenderer.sendSync('async-render', '我是来自衬着历程的同步音讯');
注重: 发送同步音讯将会壅塞悉数衬着历程,直到收到主历程的相应。
主历程监听音讯:
ipcMain
模块是EventEmitter
类的一个实例。 当在主历程中运用时,它处置惩罚从衬着器历程(网页)发送出来的异步和同步信息。 从衬着器历程发送的音讯将被发送到该模块。
ipcMain.on
:监听 channel
,当吸收到新的音讯时 listener
会以 listener(event, args...)
的情势被挪用。
ipcMain.on('sync-render', (event, data) => {
console.log(data);
});
4.5 主历程向衬着历程通信
在主历程中能够经由历程BrowserWindow
的webContents
向衬着历程发送音讯,所以,在发送音讯前你必须先找到对应衬着历程的BrowserWindow
对象。:
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.send('main-msg', `ConardLi]`)
依据音讯来源发送:
在ipcMain
接收音讯的回调函数中,经由历程第一个参数event
的属性sender
能够拿到音讯来源衬着历程的webContents
对象,我们能够直接用此对象回应音讯。
ipcMain.on('sync-render', (event, data) => {
console.log(data);
event.sender.send('main-msg', '主历程收到了衬着历程的【异步】音讯!')
});
衬着历程监听:
ipcRenderer.on
:监听 channel
, 当新音讯抵达,将经由历程 listener(event, args...)
挪用 listener
。
ipcRenderer.on('main-msg', (event, msg) => {
console.log(msg);
})
4.6 通信道理
ipcMain
和 ipcRenderer
都是 EventEmitter
类的一个实例。EventEmitter
类是 NodeJS
事宜的基础,它由 NodeJS
中的 events
模块导出。
EventEmitter
的中心就是事宜触发与事宜监听器功用的封装。它完成了事宜模子须要的接口, 包括 addListener,removeListener
, emit
及别的东西要领. 同原生 JavaScript
事宜相似, 采纳了宣布/定阅(观察者)的体式格局, 运用内部 _events
列表来纪录注册的事宜处置惩罚器。
我们经由历程 ipcMain
和ipcRenderer
的 on、send
举行监听和发送音讯都是 EventEmitter
定义的相干接口。
4.7 remote
remote
模块为衬着历程(web页面)和主历程通信(IPC
)供应了一种简朴要领。 运用 remote
模块, 你能够挪用 main
历程对象的要领, 而没必要显式发送历程间音讯, 相似于 Java
的 RMI
。
import { remote } from 'electron';
remote.dialog.showErrorBox('主历程才有的dialog模块', '我是运用remote挪用的')
但现实上,我们在挪用长途对象的要领、函数或许经由历程长途组织函数竖立一个新的对象,现实上都是在发送一个同步的历程间音讯。
在上面经由历程 remote
模块挪用 dialog
的例子里。我们在衬着历程中竖立的 dialog
对象实在并不在我们的衬着历程中,它只是让主历程竖立了一个 dialog
对象,并返回了这个相对应的长途对象给了衬着历程。
4.8 衬着历程间通信
Electron
并没有供应衬着历程之间相互通信的体式格局,我们能够在主历程中竖立一个音讯中转站。
衬着历程之间通信起首发送音讯到主历程,主历程的中转站吸收到音讯后依据前提举行分发。
4.9 衬着历程数据同享
在两个衬着历程间同享数据最简朴的要领是运用浏览器中已完成的 HTML5 API
。 个中比较好的计划是用 Storage API
, localStorage,sessionStorage
或许 IndexedDB。
就像在浏览器中运用一样,这类存储相当于在运用顺序中永远存储了一部份数据。偶然你并不须要如许的存储,只须要在当前运用顺序的生命周期内举行一些数据的同享。这时刻你能够用 Electron
内的 IPC
机制完成。
将数据存在主历程的某个全局变量中,然后在多个衬着历程中运用 remote
模块来访问它。
在主历程中初始化全局变量:
global.mainId = ...;
global.device = {...};
global.__dirname = __dirname;
global.myField = { name: 'ConardLi' };
在衬着历程中读取:
import { ipcRenderer, remote } from 'electron';
const { getGlobal } = remote;
const mainId = getGlobal('mainId')
const dirname = getGlobal('__dirname')
const deviecMac = getGlobal('device').mac;
在衬着历程中转变:
getGlobal('myField').name = 'code隐秘花圃';
多个衬着历程同享同一个主历程的全局变量,如许即可到达衬着历程数据同享和通报的结果。
五、窗口
5.1 BrowserWindow
主历程模块BrowserWindow
用于竖立和控制浏览器窗口。
mainWindow = new BrowserWindow({
width: 1000,
height: 800,
// ...
});
mainWindow.loadURL('http://www.conardli.top/');
你能够在这里检察它一切的组织参数。
5.2 无框窗口
无框窗口是没有镶边的窗口,窗口的部份(如东西栏)不属于网页的一部份。
在BrowserWindow
的组织参数中,将frame
设置为false
能够指定窗口为无边框窗口,将东西栏隐蔽后,就会发作两个题目:
- 1.窗口控制按钮(最小化、全屏、封闭按钮)会被隐蔽
- 2.没法拖拽挪动窗口
能够经由历程指定titleBarStyle
选项来再将东西栏按钮显现出来,将其设置为hidden
示意返回一个隐蔽题目栏的全尺寸内容窗口,在左上角依旧有规范的窗口控制按钮。
new BrowserWindow({
width: 200,
height: 200,
titleBarStyle: 'hidden',
frame: false
});
5.3 窗口拖拽
默许情况下, 无边框窗口是不可拖拽的。我们能够在界面中经由历程CSS
属性-webkit-app-region: drag
手动制订拖拽地区。
在无框窗口中, 拖动行动能够与挑选文本争执,能够经由历程设定-webkit-user-select: none;
禁用文本挑选:
.header {
-webkit-user-select: none;
-webkit-app-region: drag;
}
相反的,在可拖拽地区内部设置
-webkit-app-region: no-drag
则能够指定特定不可拖拽地区。
5.4 通明窗口
经由历程将transparent
选项设置为true
, 还能够使无框窗口通明:
new BrowserWindow({
transparent: true,
frame: false
});
5.5 Webview
运用 webview
标签在Electron
运用中嵌入 “外来” 内容。外来内容包括在 webview
容器中。 运用中的嵌入页面能够控制外来内容的规划和重绘。
与 iframe
差异, webview
在与运用顺序差异的历程中运转。它与您的网页没有雷同的权限, 运用顺序和嵌入内容之间的一切交互都将是异步的。
六、对话框
dialog
模块供应了api
来展现原生的体系对话框,比方翻开文件框,alert
框,所以web
运用能够给用户带来跟体系运用雷同的体验。
注重:dialog是主历程模块,想要在衬着历程挪用能够运用remote
6.1 毛病提醒
dialog.showErrorBox
用于显现一个显现毛病音讯的模态对话框。
remote.dialog.showErrorBox('毛病', '这是一个毛病弹框!')
6.2 对话框
dialog.showErrorBox
用于挪用体系对话框,能够为指定几种差异的范例: “none
“, “info
“, “error
“, “question
” 或许 “warning
“。
在 Windows 上, “question” 与”info”显现雷同的图标, 除非你运用了 “icon” 选项设置图标。 在 macOS 上, “warning” 和 “error” 显现雷同的正告图标
remote.dialog.showMessageBox({
type: 'info',
title: '提醒信息',
message: '这是一个对话弹框!',
buttons: ['肯定', '作废']
}, (index) => {
this.setState({ dialogMessage: `【你点击了${index ? '作废' : '肯定'}!!】` })
})
6.3 文件框
dialog.showOpenDialog
用于翻开或挑选体系目次。
remote.dialog.showOpenDialog({
properties: ['openDirectory', 'openFile']
}, (data) => {
this.setState({ filePath: `【挑选途径:${data[0]}】 ` })
})
6.4 信息框
这里引荐直接运用HTML5 API
,它只能在衬着器历程中运用。
let options = {
title: '信息框题目',
body: '我是一条信息~~~',
}
let myNotification = new window.Notification(options.title, options)
myNotification.onclick = () => {
this.setState({ message: '【你点击了信息框!!】' })
}
七、体系
7.1 猎取体系信息
经由历程remote
猎取到主历程的process
对象,能够猎取到当前运用的各个版本信息:
-
process.versions.electron
:electron
版本信息 -
process.versions.chrome
:chrome
版本信息 -
process.versions.node
:node
版本信息 -
process.versions.v8
:v8
版本信息
猎取当前运用根目次:
remote.app.getAppPath()
运用node
的os
模块猎取当前体系根目次:
os.homedir();
7.2 复制粘贴
Electron
供应的clipboard
在衬着历程和主历程都可运用,用于在体系剪贴板上实行复制和粘贴操纵。
以纯文本的情势写入剪贴板:
clipboard.writeText(text[, type])
以纯文本的情势猎取剪贴板的内容:
clipboard.readText([type])
7.3 截图
desktopCapturer
用于从桌面捕捉音频和视频的媒体源的信息。它只能在衬着历程中被挪用。
下面的代码是一个猎取屏幕截图并保存的实例:
getImg = () => {
this.setState({ imgMsg: '正在截取屏幕...' })
const thumbSize = this.determineScreenShotSize()
let options = { types: ['screen'], thumbnailSize: thumbSize }
desktopCapturer.getSources(options, (error, sources) => {
if (error) return console.log(error)
sources.forEach((source) => {
if (source.name === 'Entire screen' || source.name === 'Screen 1') {
const screenshotPath = path.join(os.tmpdir(), 'screenshot.png')
fs.writeFile(screenshotPath, source.thumbnail.toPNG(), (error) => {
if (error) return console.log(error)
shell.openExternal(`file://${screenshotPath}`)
this.setState({ imgMsg: `截图保存到: ${screenshotPath}` })
})
}
})
})
}
determineScreenShotSize = () => {
const screenSize = screen.getPrimaryDisplay().workAreaSize
const maxDimension = Math.max(screenSize.width, screenSize.height)
return {
width: maxDimension * window.devicePixelRatio,
height: maxDimension * window.devicePixelRatio
}
}
八、菜单
运用顺序的菜单能够协助我们快速的抵达某一功用,而不借助客户端的界面资本,平常菜单分为两种:
- 运用顺序菜单:位于运用顺序顶部,在全局范围内都能运用
- 上下文菜单:可自定义恣意页面显现,自定义挪用,如右键菜单
Electron
为我们供应了Menu
模块用于竖立本机运用顺序菜单和上下文菜单,它是一个主历程模块。
你能够经由历程Menu
的静态要领buildFromTemplate(template)
,运用自定义菜单模版来组织一个菜单对象。
template
是一个MenuItem
的数组,我们来看看MenuItem
的几个主要参数:
-
label
:菜单显现的笔墨 -
click
:点击菜单后的事宜处置惩罚函数 -
role
:体系预定义的菜单,比方copy
(复制)、paste
(粘贴)、minimize
(最小化)… -
enabled
:指导是不是启用该项目,此属机能够动态变动 -
submenu
:子菜单,也是一个MenuItem
的数组
引荐:最好指定role与规范角色相匹配的任何菜单项,而不是尝试手动完成click函数中的行动。内置role行动将供应最好的当地体验。
下面的实例是一个简朴的额菜单template
。
const template = [
{
label: '文件',
submenu: [
{
label: '新建文件',
click: function () {
dialog.showMessageBox({
type: 'info',
message: '嘿!',
detail: '你点击了新建文件!',
})
}
}
]
},
{
label: '编辑',
submenu: [{
label: '剪切',
role: 'cut'
}, {
label: '复制',
role: 'copy'
}, {
label: '粘贴',
role: 'paste'
}]
},
{
label: '最小化',
role: 'minimize'
}
]
8.1 运用顺序菜单
运用Menu
的静态要领setApplicationMenu
,可竖立一个运用顺序菜单,在 Windows
和 Linux
上,menu
将被设置为每一个窗口的顶层菜单。
注重:必须在模块ready事宜后挪用此 API app。
我们能够依据运用顺序差异的的生命周期,差异的体系对菜单做差异的处置惩罚。
app.on('ready', function () {
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
})
app.on('browser-window-created', function () {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = false
})
app.on('window-all-closed', function () {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = true
})
if (process.platform === 'win32') {
const helpMenu = template[template.length - 1].submenu
addUpdateMenuItems(helpMenu, 0)
}
8.2 上下文菜单
运用Menu
的实例要领menu.popup
可自定义弹出上下文菜单。
let m = Menu.buildFromTemplate(template)
document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e) => {
e.preventDefault()
m.popup({ window: remote.getCurrentWindow() })
})
8.3 快速键
在菜单选项中,我们能够指定一个accelerator
属性来指定操纵的快速键:
{
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}
别的,我们还能够运用globalShortcut
来注册全局快速键。
globalShortcut.register('CommandOrControl+N', () => {
dialog.showMessageBox({
type: 'info',
message: '嘿!',
detail: '你触发了手动注册的快速键.',
})
})
CommandOrControl代表在macOS上为Command键,以及在Linux和Windows上为Control键。
九、打印
很多情况下顺序中运用的打印都是用户无感知的。而且想要天真的控制打印内容,每每须要借助打印机给我们供应的api
再举行开辟,这类开辟体式格局异常烦琐,而且开辟难度较大。第一次在营业中用到Electron
实在就是用到它的打印功用,这里就多引见一些。
Electron
供应的打印api能够异常天真的控制打印设置的显现,而且能够经由历程html来誊写打印内容。Electron
供应了两种体式格局举行打印,一种是直接挪用打印机打印,一种是打印到pdf
。
而且有两种对象能够挪用打印:
- 经由历程
window
的webcontent
对象,运用此种体式格局须要零丁开出一个打印的窗口,能够将该窗口隐蔽,然则通信挪用相对庞杂。 - 运用页面的
webview
元素挪用打印,能够将webview
隐蔽在挪用的页面中,通信体式格局比较简朴。
上面两种体式格局同时具有print
和printToPdf
要领。
9.1 挪用体系打印
contents.print([options], [callback]);
打印设置(options)中只要简朴的三个设置:
-
silent
:打印时是不是不展现打印设置(是不是寂静打印) -
printBackground
:是不是打印背景 -
deviceName
:打印机装备称号
起首要将我们运用的打印机称号设置好,而且要在挪用打印前起首要推断打印机是不是可用。
运用webContents
的getPrinters
要领可猎取当前装备已设置的打印机列表,注重设置过不是可用,只是在此装备上安装过驱动。
经由历程getPrinters
猎取到的打印机对象:https://electronjs.org/docs/a…
我们这里尽管体贴两个,name
和status
,status
为0
时示意打印机可用。
print
的第二个参数callback
是用于推断打印使命是不是发出的回调,而不是打印使命完成后的回调。所以平常打印使命发出,回调函数即会挪用并返回参数true
。这个回调并不能推断打印是不是真的胜利了。
if (this.state.curretnPrinter) {
mainWindow.webContents.print({
silent: silent, printBackground: true, deviceName: this.state.curretnPrinter
}, () => { })
} else {
remote.dialog.showErrorBox('毛病', '请先挑选一个打印机!')
}
9.2 打印到PDF
printToPdf
的用法基础和print
雷同,然则print
的设置项异常少,而printToPdf
则扩大了很多属性。这里翻了一下源码发明另有很多没有被贴进api的,大概有三十几个包括能够对打印的margin,打印页眉页脚等举行设置。
contents.printToPDF(options, callback)
callback
函数在打印失利或打印胜利后挪用,可猎取打印失利信息或包括PDF
数据的缓冲区。
const pdfPath = path.join(os.tmpdir(), 'webviewPrint.pdf');
const webview = document.getElementById('printWebview');
const renderHtml = '我是被暂时插进去webview的内容...';
webview.executeJavaScript('document.documentElement.innerHTML =`' + renderHtml + '`;');
webview.printToPDF({}, (err, data) => {
console.log(err, data);
fs.writeFile(pdfPath, data, (error) => {
if (error) throw error
shell.openExternal(`file://${pdfPath}`)
this.setState({ webviewPdfPath: pdfPath })
});
});
这个例子中的打印是运用
webview
完成的,经由历程挪用
executeJavaScript
要领可动态向
webview
插进去打印内容。
9.3 两种打印计划的挑选
上面提到,运用webview
和webcontent
都能够挪用打印功用,运用webcontent
打印,起首要有一个打印窗口,这个窗口不能随时打印随时竖立,比较消耗机能。能够将它在顺序运转时启动好,并做好事宜监听。
此历程需和挪用打印的举行做好通信,大抵历程以下:
可见通信异常烦琐,运用webview
举行打印可完成一样的结果然则通信体式格局会变得简朴,由于衬着历程和webview
通信不须要经由主历程,经由历程以下体式格局即可:
const webview = document.querySelector('webview')
webview.addEventListener('ipc-message', (event) => {
console.log(event.channel)
})
webview.send('ping');
const {ipcRenderer} = require('electron')
ipcRenderer.on('ping', () => {
ipcRenderer.sendToHost('pong')
})
之前特地为ELectron
打印写过一个DEMO
:electron-print-demo有兴致能够clone
下来看一下。
9.4 打印功用封装
下面是几个针对经常使用打印功用的东西函数封装。
/**
* 猎取体系打印机列表
*/
export function getPrinters() {
let printers = [];
try {
const contents = remote.getCurrentWindow().webContents;
printers = contents.getPrinters();
} catch (e) {
console.error('getPrintersError', e);
}
return printers;
}
/**
* 猎取体系默许打印机
*/
export function getDefaultPrinter() {
return getPrinters().find(element => element.isDefault);
}
/**
* 检测是不是安装了某个打印驱动
*/
export function checkDriver(driverMame) {
return getPrinters().find(element => (element.options["printer-make-and-model"] || '').includes(driverMame));
}
/**
* 依据打印机称号猎取打印机对象
*/
export function getPrinterByName(name) {
return getPrinters().find(element => element.name === name);
}
十、顺序庇护
10.1 崩溃
崩溃监控是每一个客户端顺序必备的庇护功用,当顺序崩溃时我们平常希冀做到两件事:
- 1.上传崩溃日记,实时报警
- 2.监控顺序崩溃,提醒用户重启顺序
electron
为我们供应给了crashReporter
来协助我们纪录崩溃日记,我们能够经由历程crashReporter.start
来竖立一个崩溃报告器:
const { crashReporter } = require('electron')
crashReporter.start({
productName: 'YourName',
companyName: 'YourCompany',
submitURL: 'https://your-domain.com/url-to-submit',
uploadToServer: true
})
当顺序发作崩溃时,崩溃报日记将被储存在暂时文件夹中名为YourName Crashes
的文件文件夹中。submitURL
用于指定你的崩溃日记上传服务器。 在启动崩溃报告器之前,您能够经由历程挪用app.setPath('temp', 'my/custom/temp')
API来自定义这些暂时文件的保存途径。你还能够经由历程crashReporter.getLastCrashReport()
来猎取上次崩溃报告的日期和ID
。
我们能够经由历程webContents
的crashed
来监听衬着历程的崩溃,别的经测试有些主历程的崩溃也会触发该事宜。所以我们能够依据主window
是不是被烧毁来推断举行差异的重启逻辑,下面使悉数崩溃监控的逻辑:
import { BrowserWindow, crashReporter, dialog } from 'electron';
// 开启历程崩溃纪录
crashReporter.start({
productName: 'electron-react',
companyName: 'ConardLi',
submitURL: 'http://xxx.com', // 上传崩溃日记的接口
uploadToServer: false
});
function reloadWindow(mainWin) {
if (mainWin.isDestroyed()) {
app.relaunch();
app.exit(0);
} else {
// 烧毁其他窗口
BrowserWindow.getAllWindows().forEach((w) => {
if (w.id !== mainWin.id) w.destroy();
});
const options = {
type: 'info',
title: '衬着器历程崩溃',
message: '这个历程已崩溃.',
buttons: ['重载', '封闭']
}
dialog.showMessageBox(options, (index) => {
if (index === 0) mainWin.reload();
else mainWin.close();
})
}
}
export default function () {
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.webContents.on('crashed', () => {
const errorMessage = crashReporter.getLastCrashReport();
console.error('顺序崩溃了!', errorMessage); // 可零丁上传日记
reloadWindow(mainWindow);
});
}
10.2 最小化到托盘
有的时刻我们并不想让用户经由历程点封闭按钮的时刻就封闭顺序,而是把顺序最小化到托盘,在托盘上做真正的退出操纵。
起首要监听窗口的封闭事宜,阻挠用户封闭操纵的默许行动,将窗口隐蔽。
function checkQuit(mainWindow, event) {
const options = {
type: 'info',
title: '封闭确认',
message: '确认要最小化顺序到托盘吗?',
buttons: ['确认', '封闭顺序']
};
dialog.showMessageBox(options, index => {
if (index === 0) {
event.preventDefault();
mainWindow.hide();
} else {
mainWindow = null;
app.exit(0);
}
});
}
function handleQuit() {
const mainWindow = BrowserWindow.fromId(global.mainId);
mainWindow.on('close', event => {
event.preventDefault();
checkQuit(mainWindow, event);
});
}
这时刻顺序就再也找不到了,使命托盘中也没有我们的顺序,所以我们要先竖立好使命托盘,并做好事宜监听。
windows平台运用
ico
文件能够到达更好的结果
export default function createTray() {
const mainWindow = BrowserWindow.fromId(global.mainId);
const iconName = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
tray = new Tray(path.join(global.__dirname, iconName));
const contextMenu = Menu.buildFromTemplate([
{
label: '显现主界面', click: () => {
mainWindow.show();
mainWindow.setSkipTaskbar(false);
}
},
{
label: '退出', click: () => {
mainWindow.destroy();
app.quit();
}
},
])
tray.setToolTip('electron-react');
tray.setContextMenu(contextMenu);
}
十一、扩大才
在很多情况下,你的运用顺序要和外部装备举行交互,平常情况下厂商会为你供应硬件装备的开辟包,这些开辟包基础上都是经由历程C++
编写,在运用electron
开辟的情况下,我们并不具有直接挪用C++
代码的才,我们能够应用node-ffi
来完成这一功用。
node-ffi
供应了一组壮大的东西,用于在Node.js
环境中运用纯JavaScript
挪用动态链接库接口。它能够用来为库构建接口绑定,而不须要运用任何C++
代码。
注重
node-ffi
并不能直接挪用
C++
代码,你须要将
C++
代码编译为动态链接库:在
Windows
下是
Dll
,在
Mac OS
下是
dylib
,Linux
是
so
。
node-ffi
加载Library
是有限定的,只能处置惩罚C
作风的Library
。
下面是一个简朴的实例:
const ffi = require('ffi');
const ref = require('ref');
const SHORT_CODE = ref.refType('short');
const DLL = new ffi.Library('test.dll', {
Test_CPP_Method: ['int', ['string',SHORT_CODE]],
})
testCppMethod(str: String, num: number): void {
try {
const result: any = DLL.Test_CPP_Method(str, num);
return result;
} catch (error) {
console.log('挪用失利~',error);
}
}
this.testCppMethod('ConardLi',123);
上面的代码中,我们用ffi
包装C++
接口天生的动态链接库test.dll
,并运用ref
举行一些范例映照。
运用JavaScript
挪用这些映照要领时,引荐运用TypeScript
来商定参数范例,由于弱范例的JavaScript
在挪用强范例言语的接口时能够会带来意想不到的风险。
借助这一才,前端开辟工程师也能够在IOT
范畴一展技艺了😎~
十二、环境挑选
平常情况下,我们的运用顺序能够运转在多套环境下(production
、beta
、uat
、moke
、development
…),差异的开辟环境能够对应差异的后端接口或许其他设置,我们能够在客户端顺序中内置一个简朴的环境挑选功用来协助我们更高效的开辟。
细致战略以下:
- 在开辟环境中,我们直接进入环境挑选页面,读取到挑选的环境后举行相应的重定向操纵
- 在菜单保存环境挑选进口,以便在开辟历程当中切换
const envList = ["moke", "beta", "development", "production"];
exports.envList = envList;
const urlBeta = 'https://wwww.xxx-beta.com';
const urlDev = 'https://wwww.xxx-dev.com';
const urlProp = 'https://wwww.xxx-prop.com';
const urlMoke = 'https://wwww.xxx-moke.com';
const path = require('path');
const pkg = require(path.resolve(global.__dirname, 'package.json'));
const build = pkg['build-config'];
exports.handleEnv = {
build,
currentEnv: 'moke',
setEnv: function (env) {
this.currentEnv = env
},
getUrl: function () {
console.log('env:', build.env);
if (build.env === 'production' || this.currentEnv === 'production') {
return urlProp;
} else if (this.currentEnv === 'moke') {
return urlMoke;
} else if (this.currentEnv === 'development') {
return urlDev;
} else if (this.currentEnv === "beta") {
return urlBeta;
}
},
isDebugger: function () {
return build.env === 'development'
}
}
十三、打包
末了也是最主要的一步,将写好的代码打包成可运转的.app
或.exe
可实行文件。
这里我把打包气氛两部份来做,衬着历程打包和主历程打包。
13.1 衬着历程打包和晋级
平常情况下,我们的大部份营业逻辑代码是在衬着历程完成的,在大部份情况下我们仅仅须要对衬着历程举行更新和晋级而不须要修改主历程代码,我们衬着历程的打包现实上和平常的web
项目打包没有太大差异,运用webpack
打包即可。
这里我说说衬着历程零丁打包的优点:
打包完成的html
和js
文件,我们平常要上传到我们的前端静态资本服务器下,然后示知服务端我们的衬着历程有代码更新,这里能够说成衬着历程零丁的晋级。
注重,和壳的晋级差异,衬着历程的晋级仅仅是静态资本服务器上html
和js
文件的更新,而不须要从新下载更新客户端,如许我们每次启动顺序的时刻检测到离线包有更新,即可直接革新读取最新版本的静态资本文件,纵然在顺序运转历程当中要强迫更新,我们的顺序只须要强迫革新页面读取最新的静态资本即可,如许的晋级对用户是异常友爱的。
这里注重,一旦我们如许设置,就意味着衬着历程和主历程打包晋级的完整星散,我们在启动主窗口时读取的文件就不应该再是当地文件,而是打包完成后放在静态资本服务器的文件。
为了轻易开辟,这里我们能够辨别当地和线上加载差异的文件:
function getVersion (mac,current){
// 依据装备mac和当前版本猎取最新版本
}
export default function () {
if (build.env === 'production') {
const version = getVersion (mac,current);
return 'https://www.xxxserver.html/electron-react/index_'+version+'.html';
}
return url.format({
protocol: 'file:',
pathname: path.join(__dirname, 'env/environment.html'),
slashes: true,
query: { debugger: build.env === "development" }
});
}
细致的webpack
设置这里就不再贴出,能够到我的github
electron-react
的/scripts
目次下检察。
这里须要注重,在开辟环境下我们能够连系webpack
的devServer
和electron
敕令来启动app
:
devServer: {
contentBase: './assets/',
historyApiFallback: true,
hot: true,
port: PORT,
noInfo: false,
stats: {
colors: true,
},
setup() {
spawn(
'electron',
['.'],
{
shell: true,
stdio: 'inherit',
}
)
.on('close', () => process.exit(0))
.on('error', e => console.error(e));
},
},//...
13.2 主历程打包
主历程,行将悉数顺序打包成可运转的客户端顺序,经常使用的打包计划平常有两种,electron-packager
和electron-builder
。
electron-packager
在打包设置上我以为有些烦琐,而且它只能将运用直接打包为可实行顺序。
这里我引荐运用electron-builder
,它不仅具有轻易的设置 protocol
的功用、内置的 Auto Update
、简朴的设置 package.json
便能完成悉数打包事情,用户体验异常不错。而且electron-builder
不仅能直接将运用打包成exe app
等可实行顺序,还能打包成msi dmg
等安装包花样。
你能够在package.json
轻易的举行种种设置:
"build": {
"productName": "electron-react", // app中文称号
"appId": "electron-react",// app标识
"directories": { // 打包后输出的文件夹
"buildResources": "resources",
"output": "dist/"
}
"files": [ // 打包后依旧保存的源文件
"main_process/",
"render_process/",
],
"mac": { // mac打包设置
"target": "dmg",
"icon": "icon.ico"
},
"win": { // windows打包设置
"target": "nsis",
"icon": "icon.ico"
},
"dmg": { // dmg文件打包设置
"artifactName": "electron_react.dmg",
"contents": [
{
"type": "link",
"path": "/Applications",
"x": 410,
"y": 150
},
{
"type": "file",
"x": 130,
"y": 150
}
]
},
"nsis": { // nsis文件打包设置
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"shortcutName": "electron-react"
},
}
实行electron-builder
打包敕令时,可指定参数举行打包。
--mac, -m, -o, --macos macOS打包
--linux, -l Linux打包
--win, -w, --windows Windows打包
--mwl 同时为macOS,Windows和Linux打包
--x64 x64 (64位安装包)
--ia32 ia32(32位安装包)
关于主历程的更新你能够运用electron-builder
自带的Auto Update
模块,在electron-react
也完成了手动更新的模块,由于篇幅缘由这里就不再赘述,假若有兴致能够到我的github
检察main
下的update
模块。
13.3 打包优化
electron-builder
打包出来的App
要比雷同功用的原生客户端运用体积大很多,纵然是空的运用,体积也要在100mb
以上。缘由有很多:
第一点;为了到达跨平台的结果,每一个Electron
运用都包括了悉数V8
引擎和Chromium
内核。
第二点:打包时会将悉数node_modules
打包进去,人人都晓得一个运用的node_module
体积是异常巨大的,这也是使得Electron
运用打包后的体积较大的缘由。
第一点我们没法转变,我们能够从第二点对运用体积举行优化:Electron
在打包时只会将denpendencies
的依靠打包进去,而不会将 devDependencies
中的依靠举行打包。所以我们应尽能够的削减denpendencies
中的依靠。在上面的历程中,我们运用webpack
对衬着历程举行打包,所以衬着历程的依靠悉数都能够移入devDependencies
。
别的,我们还能够运用双packajson.json
的体式格局来举行优化,把只在开辟环境中运用到的依靠放在悉数项目标根目次的package.json
下,将与平台相干的或许运转时须要的依靠装在app
目次下。细致详见two-package-structure。
参考
- https://electronjs.org/docs
- http://jlord.us/essential-ele…
- https://imweb.io/topic/5b9f50…
- https://www.jianshu.com/p/1ec…
- https://zhuanlan.zhihu.com/p/…
本项目源码地点:
https://github.com/ConardLi/e…
小结
愿望你浏览本篇文章后能够到达以下几点:
- 相识
Electron
的基础运转道理 - 控制
Electron
开辟的中心基础知识 - 相识
Electron
关于弹框、打印、庇护、打包等功用的基础运用
文中若有毛病,迎接在批评区斧正,假如这篇文章协助到了你,迎接点赞和关注。
想浏览更多优良文章、可关注我的github
博客,你的star✨、点赞和关注是我延续创作的动力!
引荐关注我的微信民众号【code隐秘花圃】,天天推送高质量文章,我们一同交换生长。
关注民众号后复兴【加群】拉你进入优良前端交换群。