使用Electron
開發客戶端程序已經有一段時間了,總體感受仍是很是不錯的,其中也遇到了一些坑點,本文是從【運行原理】到【實際應用】對Electron
進行一次系統性的總結。【多圖,長文預警~】css
本文全部實例代碼均在個人github electron-react上,結合代碼閱讀文章效果更佳。另外electron-react
還可做爲使用Electron + React + Mobx + Webpack
技術棧的腳手架工程。html
桌面應用程序,又稱爲 GUI 程序(Graphical User Interface),可是和 GUI 程序也有一些區別。桌面應用程序 將 GUI 程序從GUI 具體爲「桌面」,使冷冰冰的像塊木頭同樣的電腦概念更具備 人性化,更生動和富有活力。前端
咱們電腦上使用的各類客戶端程序都屬於桌面應用程序,近年來WEB
和移動端的興起讓桌面程序漸漸暗淡,可是在某些平常功能或者行業應用中桌面應用程序仍然是必不可少的。node
傳統的桌面應用開發方式,通常是下面兩種:react
直接將語言編譯成可執行文件,直接調用系統API
,完成UI繪製等。這類開發技術,有着較高的運行效率,但通常來講,開發速度較慢,技術要求較高,例如:linux
C++ / MFC
開發Windows
應用Objective-C
開發MAC
應用一開始就有本地開發和UI開發。一次編譯後,獲得中間文件,經過平臺或虛機完成二次加載編譯或解釋運行。運行效率低於原生編譯,但平臺優化後,其效率也是比較可觀的。就開發速度方面,比原生編譯技術要快一些。例如:webpack
C# / .NET Framework
(只能開發Windows應用
)Java / Swing
不過,上面兩種對前端開發人員太不友好了,基本是前端人員不會涉及的領域,可是在這個【大前端😅】的時代,前端開發者正在千方百計涉足各個領域,使用WEB
技術開發客戶端的方式橫空出世。git
使用WEB
技術進行開發,利用瀏覽器引擎完成UI
渲染,利用Node.js
實現服務器端JS
編程並能夠調用系統API
,能夠把它想像成一個套了一個客戶端外殼的WEB
應用。github
在界面上,WEB
的強大生態爲UI
帶來了無限可能,而且開發、維護成本相對較低,有WEB
開發經驗的前端開發者很容易上手進行開發。web
本文就來着重介紹使用WEB
技術開發客戶端程序的技術之一【electron
】
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('async-render', '我是來自渲染進程的異步消息');
複製代碼
同步發送:
const msg = ipcRenderer.sendSync('sync-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
獲取到的打印機對象:electronjs.org/docs/api/st…
咱們這裏只管關心兩個,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
則擴展了不少屬性。這裏翻了一下源碼發現還有不少沒有被貼進文檔的,大概有三十幾個,包括能夠對打印的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。
本項目源碼地址:github.com/ConardLi/el…
但願你閱讀本篇文章後能夠達到如下幾點:
Electron
的基本運行原理Electron
開發的核心基礎知識Electron
關於彈框、打印、保護、打包等功能的基本使用文中若有錯誤,歡迎在評論區指正,若是這篇文章幫助到了你,歡迎點贊和關注。
想閱讀更多優質文章、可關注個人github
博客,你的star✨、點贊和關注是我持續創做的動力!
推薦關注個人微信公衆號【code祕密花園】,天天推送高質量文章,咱們一塊兒交流成長。
關注公衆號後回覆【加羣】拉你進入優質前端交流羣。