使用Electron
开发客户端程序已经有一段时间了,总体感受仍是很是不错的,其中也遇到了一些坑点,本文是从【运行原理】到【实际应用】对Electron
进行一次系统性的总结。【多图,长文预警~】css
本文全部实例代码均在个人github electron-react上,结合代码阅读文章效果更佳。另外electron-react
还可做为使用Electron + React + Mobx + Webpack
技术栈的脚手架工程。html
桌面应用程序,又称为 GUI 程序(Graphical User Interface),可是和 GUI 程序也有一些区别。桌面应用程序 将 GUI 程序从GUI 具体为“桌面”,使冷冰冰的像块木头同样的电脑概念更具备 人性化,更生动和富有活力。
咱们电脑上使用的各类客户端程序都属于桌面应用程序,近年来WEB
和移动端的兴起让桌面程序渐渐暗淡,可是在某些平常功能或者行业应用中桌面应用程序仍然是必不可少的。前端
传统的桌面应用开发方式,通常是下面两种:node
直接将语言编译成可执行文件,直接调用系统API
,完成UI绘制等。这类开发技术,有着较高的运行效率,但通常来讲,开发速度较慢,技术要求较高,例如:react
C++ / MFC
开发Windows
应用Objective-C
开发MAC
应用一开始就有本地开发和UI开发。一次编译后,获得中间文件,经过平台或虚机完成二次加载编译或解释运行。运行效率低于原生编译,但平台优化后,其效率也是比较可观的。就开发速度方面,比原生编译技术要快一些。例如:linux
C# / .NET Framework
(只能开发Windows应用
)Java / Swing
不过,上面两种对前端开发人员太不友好了,基本是前端人员不会涉及的领域,可是在这个【大前端😅】的时代,前端开发者正在千方百计涉足各个领域,使用WEB
技术开发客户端的方式横空出世。webpack
使用WEB
技术进行开发,利用浏览器引擎完成UI
渲染,利用Node.js
实现服务器端JS
编程并能够调用系统API
,能够把它想像成一个套了一个客户端外壳的WEB
应用。git
在界面上,WEB
的强大生态为UI
带来了无限可能,而且开发、维护成本相对较低,有WEB
开发经验的前端开发者很容易上手进行开发。github
本文就来着重介绍使用WEB
技术开发客户端程序的技术之一【electron
】web
Electron
是由Github
开发,用HTML,CSS
和JavaScript
来构建跨平台桌面应用程序的一个开源库。 Electron
经过将Chromium
和Node.js
合并到同一个运行时环境中,并将其打包为Mac,Windows
和Linux
系统下的应用来实现这一目的。
Web
技术进行开发,开发成本低,可扩展性强,更炫酷的UI
Windows、Linux、Mac
三套软件,且编译快速Web
应用上进行扩展,提供浏览器不具有的能力固然,咱们也要认清它的缺点:性能比原生桌面应用要低,最终打包后的应用比原生应用大不少。
兼容性
虽然你还在用WEB
技术进行开发,可是你不用再考虑兼容性问题了,你只须要关心你当前使用Electron
的版本对应Chrome
的版本,通常状况下它已经足够新来让你使用最新的API
和语法了,你还能够手动升级Chrome
版本。一样的,你也不用考虑不一样浏览器带的样式和代码兼容问题。
Node环境
这多是不少前端开发者曾经梦想过的功能,在WEB
界面中使用Node.js
提供的强大API
,这意味着你在WEB
页面直接能够操做文件,调用系统API
,甚至操做数据库。固然,除了完整的 Node API
,你还可使用额外的几十万个npm
模块。
跨域
你能够直接使用Node
提供的request
模块进行网络请求,这意味着你无需再被跨域所困扰。
强大的扩展性
借助node-ffi
,为应用程序提供强大的扩展性(后面的章节会详细介绍)。
如今市面上已经有很是多的应用在使用Electron
进行开发了,包括咱们熟悉的VS Code
客户端、GitHub
客户端、Atom
客户端等等。印象很深的,去年迅雷在发布迅雷X10.1
时的文案:
从迅雷X 10.1版本开始,咱们采用Electron软件框架彻底重写了迅雷主界面。使用新框架的迅雷X能够完美支持2K、4K等高清显示屏,界面中的文字渲染也更加清晰锐利。从技术层面来讲,新框架的界面绘制、事件处理等方面比老框架更加灵活高效,所以界面的流畅度也显著优于老框架的迅雷。至于具体提高有多大?您一试便知。
你能够打开VS Code
,点击【帮助】【切换开发人员工具】来调试VS Code
客户端的界面。
Electron
结合了 Chromium
、Node.js
和用于调用操做系统本地功能的API
。
Chromium
是 Google
为发展 Chrome
浏览器而启动的开源项目,Chromium
至关于 Chrome
的工程版或称实验版,新功能会率先在 Chromium
上实现,待验证后才会应用在Chrome
上,故 Chrome
的功能会相对落后但较稳定。
Chromium
为Electron
提供强大的UI
能力,能够在不考虑兼容性的状况下开发界面。
Node.js
是一个让 JavaScript
运行在服务端的开发平台,Node
使用事件驱动,非阻塞I/O
模型而得以轻量和高效。
单单靠Chromium
是不能具有直接操做原生GUI
能力的,Electron
内集成了Nodejs
,这让其在开发界面的同时也有了操做系统底层 API
的能力,Nodejs
中经常使用的 Path、fs、Crypto
等模块在 Electron
能够直接使用。
为了提供原生系统的GUI
支持,Electron
内置了原生应用程序接口,对调用一些系统功能,如调用系统通知、打开系统文件夹提供支持。
在开发模式上,Electron
在调用系统API
和绘制界面上是分离开发的,下面咱们来看看Electron
关于进程如何划分。
Electron
区分了两种进程:主进程和渲染进程,二者各自负责本身的职能。
Electron
运行 package.json
的 main
脚本的进程被称为主进程。一个 Electron
应用老是有且只有一个主进程。
职责:
APP
以及对APP
作一些事件监听)可调用的API:
Node.js API
Electron
提供的主进程API
(包括一些系统功能和Electron
附加功能)因为 Electron
使用了 Chromium
来展现 web
页面,因此 Chromium
的多进程架构也被使用到。 每一个 Electron
中的 web
页面运行在它本身的渲染进程中。
主进程使用 BrowserWindow 实例建立页面。 每一个 BrowserWindow 实例都在本身的渲染进程里运行页面。 当一个 BrowserWindow 实例被销毁后,相应的渲染进程也会被终止。
你能够把渲染进程想像成一个浏览器窗口,它能存在多个而且相互独立,不过和浏览器不一样的是,它能调用Node API
。
职责:
HTML
和CSS
渲染界面JavaScript
作一些界面交互可调用的API:
DOM API
Node.js API
Electron
提供的渲染进程API
在上面的章节咱们提到,渲染进和主进程分别可调用的Electron API
。全部Electron
的API
都被指派给一种进程类型。 许多API
只能被用于主进程中,有些API
又只能被用于渲染进程,又有一些主进程和渲染进程中均可以使用。
你能够经过以下方式获取Electron API
const { BrowserWindow, ... } = require('electron')
下面是一些经常使用的Electron 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一块儿使用。
主进程和渲染进程虽然拥有不一样的职责,然是他们也须要相互协做,互相通信。
例如:在web
页面管理原生GUI
资源是很危险的,会很容易泄露资源。因此在web
页面,不容许直接调用原生GUI
相关的API
。渲染进程若是想要进行原生的GUI
操做,就必须和主进程通信,请求主进程来完成这些操做。
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); });
在主进程中能够经过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); })
ipcMain
和 ipcRenderer
都是 EventEmitter
类的一个实例。EventEmitter
类是 NodeJS
事件的基础,它由 NodeJS
中的 events
模块导出。
EventEmitter
的核心就是事件触发与事件监听器功能的封装。它实现了事件模型须要的接口, 包括 addListener,removeListener
, emit
及其它工具方法. 同原生 JavaScript
事件相似, 采用了发布/订阅(观察者)的方式, 使用内部 _events
列表来记录注册的事件处理器。
咱们经过 ipcMain
和ipcRenderer
的 on、send
进行监听和发送消息都是 EventEmitter
定义的相关接口。
remote
模块为渲染进程(web页面)和主进程通讯(IPC
)提供了一种简单方法。 使用 remote
模块, 你能够调用 main
进程对象的方法, 而没必要显式发送进程间消息, 相似于 Java
的 RMI
。
import { remote } from 'electron'; remote.dialog.showErrorBox('主进程才有的dialog模块', '我是使用remote调用的')
但实际上,咱们在调用远程对象的方法、函数或者经过远程构造函数建立一个新的对象,实际上都是在发送一个同步的进程间消息。
在上面经过 remote
模块调用 dialog
的例子里。咱们在渲染进程中建立的 dialog
对象其实并不在咱们的渲染进程中,它只是让主进程建立了一个 dialog
对象,并返回了这个相对应的远程对象给了渲染进程。
Electron
并无提供渲染进程之间相互通讯的方式,咱们能够在主进程中创建一个消息中转站。
渲染进程之间通讯首先发送消息到主进程,主进程的中转站接收到消息后根据条件进行分发。
在两个渲染进程间共享数据最简单的方法是使用浏览器中已经实现的 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秘密花园';
多个渲染进程共享同一个主进程的全局变量,这样便可达到渲染进程数据共享和传递的效果。
主进程模块BrowserWindow
用于建立和控制浏览器窗口。
mainWindow = new BrowserWindow({ width: 1000, height: 800, // ... }); mainWindow.loadURL('http://www.conardli.top/');
你能够在这里查看它全部的构造参数。
无框窗口是没有镶边的窗口,窗口的部分(如工具栏)不属于网页的一部分。
在BrowserWindow
的构造参数中,将frame
设置为false
能够指定窗口为无边框窗口,将工具栏隐藏后,就会产生两个问题:
能够经过指定titleBarStyle
选项来再将工具栏按钮显示出来,将其设置为hidden
表示返回一个隐藏标题栏的全尺寸内容窗口,在左上角仍然有标准的窗口控制按钮。
new BrowserWindow({ width: 200, height: 200, titleBarStyle: 'hidden', frame: false });
默认状况下, 无边框窗口是不可拖拽的。咱们能够在界面中经过CSS
属性-webkit-app-region: drag
手动制定拖拽区域。
在无框窗口中, 拖动行为可能与选择文本冲突,能够经过设定-webkit-user-select: none;
禁用文本选择:
.header { -webkit-user-select: none; -webkit-app-region: drag; }
相反的,在可拖拽区域内部设置
-webkit-app-region: no-drag
则能够指定特定不可拖拽区域。
经过将transparent
选项设置为true
, 还可使无框窗口透明:
new BrowserWindow({ transparent: true, frame: false });
使用 webview
标签在Electron
应用中嵌入 "外来" 内容。外来内容包含在 webview
容器中。 应用中的嵌入页面能够控制外来内容的布局和重绘。
与 iframe
不一样, webview
在与应用程序不一样的进程中运行。它与您的网页没有相同的权限, 应用程序和嵌入内容之间的全部交互都将是异步的。
dialog
模块提供了api
来展现原生的系统对话框,例如打开文件框,alert
框,因此web
应用能够给用户带来跟系统应用相同的体验。
注意:dialog是主进程模块,想要在渲染进程调用可使用remote
dialog.showErrorBox
用于显示一个显示错误消息的模态对话框。
remote.dialog.showErrorBox('错误', '这是一个错误弹框!')
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 ? '取消' : '肯定'}!!】` }) })
dialog.showOpenDialog
用于打开或选择系统目录。
remote.dialog.showOpenDialog({ properties: ['openDirectory', 'openFile'] }, (data) => { this.setState({ filePath: `【选择路径:${data[0]}】 ` }) })
这里推荐直接使用HTML5 API
,它只能在渲染器进程中使用。
let options = { title: '信息框标题', body: '我是一条信息~~~', } let myNotification = new window.Notification(options.title, options) myNotification.onclick = () => { this.setState({ message: '【你点击了信息框!!】' }) }
经过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();
Electron
提供的clipboard
在渲染进程和主进程均可使用,用于在系统剪贴板上执行复制和粘贴操做。
以纯文本的形式写入剪贴板:
clipboard.writeText(text[, type])
以纯文本的形式获取剪贴板的内容:
clipboard.readText([type])
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' } ]
使用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) }
使用Menu
的实例方法menu.popup
可自定义弹出上下文菜单。
let m = Menu.buildFromTemplate(template) document.getElementById('menuDemoContainer').addEventListener('contextmenu', (e) => { e.preventDefault() m.popup({ window: remote.getCurrentWindow() }) })
在菜单选项中,咱们能够指定一个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
方法。
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('错误', '请先选择一个打印机!') }
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
插入打印内容。
上面提到,使用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
下来看一下。
下面是几个针对经常使用打印功能的工具函数封装。
/** * 获取系统打印机列表 */ 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); }
崩溃监控是每一个客户端程序必备的保护功能,当程序崩溃时咱们通常指望作到两件事:
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); }); }
有的时候咱们并不想让用户经过点关闭按钮的时候就关闭程序,而是把程序最小化到托盘,在托盘上作真正的退出操做。
首先要监听窗口的关闭事件,阻止用户关闭操做的默认行为,将窗口隐藏。
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
可执行文件。
这里我把打包氛围两部分来作,渲染进程打包和主进程打包。
通常状况下,咱们的大部分业务逻辑代码是在渲染进程完成的,在大部分状况下咱们仅仅须要对渲染进程进行更新和升级而不须要改动主进程代码,咱们渲染进程的打包实际上和通常的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)); }, },//...
主进程,即将整个程序打包成可运行的客户端程序,经常使用的打包方案通常有两种,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
模块。
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://github.com/ConardLi/e...
但愿你阅读本篇文章后能够达到如下几点:
Electron
的基本运行原理Electron
开发的核心基础知识Electron
关于弹框、打印、保护、打包等功能的基本使用文中若有错误,欢迎在评论区指正,若是这篇文章帮助到了你,欢迎点赞和关注。
想阅读更多优质文章、可关注个人github
博客,你的star✨、点赞和关注是我持续创做的动力!
推荐关注个人微信公众号【code秘密花园】,天天推送高质量文章,咱们一块儿交流成长。
关注公众号后回复【加群】拉你进入优质前端交流群。