electron
常見需求
-
在 Renderer 端創(chuàng)建菜單
相關(guān) issue: https://github.com/electron/electron/issues/7455
在 Electron 中,無(wú)論是應(yīng)用程序的主菜單(macOS 頂部的菜單)、窗口菜單(Windows/Linux)的窗口菜單酵幕、Tray 菜單届良、還是 Renderer 端的 Context 菜單等翘贮,凡是和菜單掛鉤的功能都是通過 Menu.buildFromTemplate 進(jìn)行創(chuàng)建的迂曲,他們只是被掛載到了不同的實(shí)體上,比如被掛載到了頁(yè)面上的叫做 Context 菜單佳遂,被 setApplicationMenu 掛載的成為了主菜單营袜。
Electron 的菜單其實(shí)限制也非常之大,其菜單在創(chuàng)建完成后便不能在運(yùn)行時(shí)被直接修改丑罪,需要修改時(shí)荚板,必須對(duì)整個(gè)菜單重新創(chuàng)建凤壁,從而達(dá)到動(dòng)態(tài)菜單的目的。這也是非常不友好的跪另。
另一方面拧抖,一個(gè)菜單項(xiàng)被點(diǎn)擊后,只接受三個(gè)參數(shù)免绿,也就是說 click 屬性的回調(diào)只接收: menuItem(被點(diǎn)擊的item)唧席、focusedWindow(點(diǎn)擊按鈕時(shí)focus的窗口) 以及毫無(wú)用途的 event (提供鍵盤是否被按下的信息),這也就造成的諸多的不便针姿。
例如袱吆,當(dāng)我們希望一個(gè)按鈕被點(diǎn)擊后厌衙,其他的菜單項(xiàng)的 enbalbed 屬性設(shè)置為 false距淫,將無(wú)從下手。
為了解決這一問題婶希,這時(shí)候我們可以巧妙的使用 Electron 的 ipc 機(jī)制在 renderer 端創(chuàng)建應(yīng)用菜單榕暇,在 renderer 端創(chuàng)建的菜單便可以在內(nèi)部使用 ipcRenderer.send() 這個(gè)方法,讓 ipcMain 來(lái)處理其他其他菜單項(xiàng)的操作喻杈。
不僅如此彤枢,甚至于可以讓整個(gè)菜單的 click 邏輯都交付于 main 端進(jìn)行管理,例如下面這樣的 menu template:
{
type: 'separator',
visible: (()=>{
if (process.platfrom == 'darwin') return true;
else return false;
})()
},
{
label: `Current Version: ${app.getVersion()}`,
enabled: false
},
{
type: 'separator'
},
{
label: 'Logout',
click: () => { ipcRenderer.send('application', 'logout'); }
},
{
type: 'separator'
},
{
label: 'Exit ${app.getAppName()}',
accelerator: 'CmdOrCtrl+Q',
click: () => { ipcRenderer.send('application', 'quit'); }
}
而在 main 端則可以:
ipcMain.on('application', (event, args) => {
switch(args){
case: 'logout':
....; break
case: 'quit':
...; break
default: ...
}
})
-
后臺(tái)網(wǎng)絡(luò)狀態(tài)監(jiān)測(cè)
相關(guān) issue: https://github.com/electron/electron/issues/6633
問題: Electron 的官方文檔其實(shí)就提供了網(wǎng)絡(luò)檢測(cè)的方法筒饰,思路是通過一個(gè)隱藏的后臺(tái)窗口缴啡,檢測(cè)網(wǎng)絡(luò)網(wǎng)絡(luò)狀態(tài),通過網(wǎng)絡(luò)狀態(tài)產(chǎn)生變化后瓷们,通過 ipcRenderer 發(fā)送消息給 ipcMain业栅,然后響應(yīng)所需的操作,見這里谬晕。
但事實(shí)上這個(gè)方法是基于 online 和 offline 事件的碘裕,換句話說這個(gè)方法只能檢測(cè)到當(dāng)系統(tǒng)網(wǎng)絡(luò)連接被切斷物理連接后的狀態(tài)變化,無(wú)法檢測(cè)網(wǎng)絡(luò)本身攒钳。
這個(gè)問題可以參考issue帮孔,但其實(shí)現(xiàn)思路就是向蘋果的 hotspot detect 頁(yè)面發(fā)起請(qǐng)求,當(dāng)未超時(shí)狀態(tài)下有 Success 返回時(shí)不撑,便說明網(wǎng)絡(luò)狀態(tài)正常文兢。在實(shí)際應(yīng)用編寫過程中,我們也并不需要為了這樣一個(gè)簡(jiǎn)單的功能而引入框架焕檬,只需定時(shí)向服務(wù)器發(fā)起任意一個(gè)能夠判斷網(wǎng)絡(luò)狀態(tài)的請(qǐng)求即可禽作,與心跳連接殊途同歸。
-
preload 執(zhí)行階段和使用場(chǎng)景
相關(guān) issue: https://github.com/electron/electron/issues/7455
在前面創(chuàng)建基本應(yīng)用中我們已經(jīng)談到了關(guān)于 preload 腳本用來(lái)引入 jQeury 的使用揩页,事實(shí)上我們可以用 preload 做更多的事情旷偿。preload 腳本會(huì)在整個(gè)頁(yè)面開始加載之前被執(zhí)行烹俗,所以如果我們直接執(zhí)行一些當(dāng)整個(gè) DOM 加載完成才能被執(zhí)行的操作,是必定會(huì)失效的萍程,因此這樣的兩個(gè)事件是非常有用的:DOMNodeInserted幢妄、DOMContentLoaded。
為此茫负,我們可以把 preload 腳本大致分為三塊區(qū)域:
// ---------------------------------------------------
// 在頁(yè)面加載之前需要執(zhí)行的相關(guān)代碼
// ...
// ---------------------------------------------------
// -------------------------------------------------------
document.addEventListener('DOMNodeInserted', (event) => {
// 頁(yè)面內(nèi)容加載之前需要引入的一些代碼
// ...
})
// -------------------------------------------------------
// -------------------------------------------------------
document.addEventListener('DOMContentLoaded', (event) => {
// 頁(yè)面內(nèi)容加載之后需要引入的一些操作
// ...
})
// -------------------------------------------------------
preload 腳本的作用非常大蕉鸳,有時(shí)候會(huì)有這樣的需求:當(dāng)我們加載一個(gè)網(wǎng)絡(luò)上的頁(yè)面時(shí),我們不能控制從網(wǎng)絡(luò)中讀取到的頁(yè)面內(nèi)容忍法,但 preload 提供了這樣的可能性潮尝,使得我們能夠向頁(yè)面 注入 一些代碼,滿足一些神奇的需求饿序,比如對(duì)網(wǎng)絡(luò)加載頁(yè)面增加 Context Menu勉失。但也有使用時(shí)值得注意的地方:
Electron 的 main 進(jìn)程、preload 腳本原探、renderer 進(jìn)程乱凿、以及 document 對(duì)象分別有彼此的創(chuàng)建和執(zhí)行順序。首先 main 進(jìn)程會(huì)優(yōu)先被創(chuàng)建毫無(wú)疑問咽弦,preload 會(huì)在 document 對(duì)象被創(chuàng)建之前優(yōu)先加載(但能夠使用 document)徒蟆,而 renderer 進(jìn)程會(huì)在 document 創(chuàng)建之后被創(chuàng)建,而他們?nèi)哂质遣l(fā)創(chuàng)建的型型,如下圖所示段审。
那么,如果我們不小心在 preload 腳本中直接引入 ipcRenderer 發(fā)送一條消息給 ipcMain闹蒜,那么 ipcMain 可能不能收到這條早期消息寺枉。為了保證我們能夠收到這條消息,最好的方式就是:
// preload.js
// 不要再外面這么干
// ipcRenderer.send(...)
document.addEventListener('DOMContentLoaded', (event) => {
// 頁(yè)面內(nèi)容加載之后需要引入的一些操作
// ...
// 正確的做法
ipcRenderer.send(...)
})
-
下載
下載也是一個(gè)常見的需求嫂用,比如型凳,你正在基于 Electron 實(shí)現(xiàn)一個(gè) Web 文本應(yīng)用,用戶可能需要下載保存在服務(wù)器上的一個(gè)編輯好的文件嘱函,這時(shí)候當(dāng)點(diǎn)擊 Web 界面中的下載時(shí)甘畅,Electron 并不需要專門針對(duì)這個(gè)下載行為進(jìn)行單獨(dú)的處理,Electron 會(huì)想瀏覽器那樣直接跳出一個(gè)保存的文件選擇器往弓,讓用戶獲得下一步的操作疏唾。當(dāng)我們真正需要處理一些特殊的下載操作時(shí),同樣可以用 electron 的 DownloadItem 來(lái)實(shí)現(xiàn)函似,但其接口設(shè)計(jì)著實(shí)有點(diǎn)讓筆者難以接受槐脏,這里推薦可以嘗試 electron-userland/electron-download 這個(gè)庫(kù),雖然其本質(zhì)也是 DownloadItem撇寞,但其接口相比之下友善許多顿天,因?yàn)閹?kù)本身也并不復(fù)雜堂氯,也可以在項(xiàng)目中自行實(shí)現(xiàn)這部分邏輯。
-
軟件更新
軟件的日后更新一直都是產(chǎn)品日后迭代的殺手牌废,一個(gè)需要被分發(fā)的桌面應(yīng)用咽白,在沒有確定的更新機(jī)制之前,切忌發(fā)布鸟缕。
Electron 雖然本身自帶 audoUpdater 模塊晶框,但作為框架的使用者來(lái)說,筆者很難說它做得優(yōu)秀懂从,因?yàn)樾枰渲玫膬?nèi)容相較于其他功能來(lái)說略加繁瑣授段。因此這里推薦使用 electron-updater。下面的代碼相當(dāng)于一個(gè)純粹的更新功能的封裝番甩,使用成本非常簡(jiǎn)單侵贵,只需根據(jù) electron-builder wiki 的說明配置好 publisher 即可實(shí)現(xiàn)更新功能:
// updater.js
const { dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
let updater
// 禁用自動(dòng)下載,給用戶選擇余地
autoUpdater.autoDownload = false
autoUpdater.on('error', (event, error) => {
dialog.showErrorBox('Error: ', error)
})
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
title: 'Found Updates',
message: 'Found updates, do you want update now?',
buttons: ['Sure', 'No']
}, (buttonIndex) => {
if (buttonIndex === 0) {
autoUpdater.downloadUpdate()
} else {
updater.enabled = true
updater = null
}
})
})
autoUpdater.on('update-not-available', () => {
dialog.showMessageBox({
title: 'No Updates',
message: 'Current version is up-to-date.'
})
updater.enabled = true
updater = null
})
// 下載完成時(shí)对室,提醒用戶
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
title: 'Install Updates',
message: 'Updates downloaded, application will be quit for update...'
}, () => {
autoUpdater.quitAndInstall()
})
})
// 將這個(gè)回調(diào)輸出給更新功能所在的菜單項(xiàng)的 click 回調(diào)
function checkForUpdates (menuItem, focusedWindow, event) {
updater = menuItem
updater.enabled = false
autoUpdater.checkForUpdates()
}
module.exports.checkForUpdates = checkForUpdates
-
發(fā)布
打包工具
期初的 Electron 打包是一個(gè)比較惱人的問題模燥,因?yàn)榭捎玫墓ぞ咂鋵?shí)不多咖祭,electron-packager 是一個(gè)很原始的打包工具掩宜,雖然具備打包的功能,但是其提供以開發(fā) API 為藍(lán)本的入口使得構(gòu)建還需要額外編寫腳本進(jìn)行么翰,而它其實(shí)只具備將應(yīng)用進(jìn)行打包編譯的功能牺汤,這與最終發(fā)布的 Installer 還有一步之遙,所以使用 electron-packager 是非常消耗開發(fā)成本的一件事情浩嫌。好在有一個(gè)取而代之的工具 electron-builder檐迟。它的好處在本文前面的部分也已經(jīng)多次提及,它不僅擁有方便的配置 protocol 的功能码耐、內(nèi)置的 Auto Update追迟、簡(jiǎn)單的配置 package.json 便能完成整個(gè)打包工作,用戶體驗(yàn)是相當(dāng)優(yōu)秀的骚腥。
代碼簽名
代碼簽名對(duì)于發(fā)布作為正式商業(yè)產(chǎn)品應(yīng)用來(lái)說是非常重要的敦间。如今的 electron-builder 對(duì)于代碼簽名已經(jīng)做得相當(dāng)友好,對(duì)于 macOS 來(lái)說束铭,它能夠自動(dòng)獲取系統(tǒng)中 Keychain 中的開發(fā)者證書廓块,自動(dòng)對(duì)代碼進(jìn)行簽名,而 Windows 也可以通過配置一個(gè) .p12 證書來(lái)達(dá)到簽名的目的契沫。
因此带猴,到目前為止,代碼的簽名成本已經(jīng)非常低懈万,只需購(gòu)買好證書拴清,基本上沒有什么煩心的事情靶病,唯一一個(gè)值得注意的事情是:如果要分發(fā) macOS 上的應(yīng)用,那么構(gòu)建平臺(tái)將只有 macOS 是被推薦的口予,因?yàn)樗俏ㄒ灰粋€(gè)能夠同時(shí)構(gòu)建 macOS/Linux/Windows 三平臺(tái)應(yīng)用的平臺(tái)嫡秕。但是,如果使用 CSC_LINK 將會(huì)出現(xiàn)沖突苹威,因?yàn)?CSC_LINK 已被用于 macOS 平臺(tái)的簽名昆咽,因此額外在 package.json 中配置 Windows 平臺(tái)的證書。
由于證書最終不會(huì)被分發(fā)牙甫,可以在簽名時(shí)使用一個(gè)移除密碼的證書掷酗;亦或者對(duì)兩個(gè)平臺(tái)的證書使用相同的密碼,方便最終的簽名和打包窟哺。