淺談Nodejs開發(fā)桌面應(yīng)用 針對(duì) Electron

electron.js 是 github 發(fā)布跨平臺(tái)桌面應(yīng)用開發(fā)工具,基于 web 技術(shù)。


基本假設(shè)

開始教程之前,請(qǐng)?jiān)试S我假設(shè)你已經(jīng)有了一個(gè)常用的的編輯器(或者 IDE),系統(tǒng)中也安裝了Node.js 和 npm,并有基礎(chǔ)的 HTML/CSS/JavaScript (對(duì) Node.js 的 CommonJS 模塊概念有所了解是最好,但不強(qiáng)求) 知識(shí)。如果以上知識(shí)你并不了解,為了防止這篇文章看到你頭昏腦脹,推薦你先看看之前我寫過的博文,補(bǔ)充一下基礎(chǔ)知識(shí)。

萬(wàn)事俱備,現(xiàn)在就把精力集中在學(xué)習(xí) Electron 上,不要再擔(dān)心界面的事情(將會(huì)構(gòu)建的界面本質(zhì)上就是普通的 Web 頁(yè)面而已)。

Electron 概覽

簡(jiǎn)而言之,Electron 提供了一個(gè)實(shí)時(shí)構(gòu)建桌面應(yīng)用的純 JavaScript 環(huán)境。Electron 可以獲取到你定義在 package.json 中 main 文件內(nèi)容,然后執(zhí)行它。通過這個(gè)文件(通常我們稱之為 main.js),可以創(chuàng)建一個(gè)應(yīng)用窗口,這個(gè)應(yīng)用窗口包含一個(gè)渲染好的 web 界面,還可以和系統(tǒng)原生的 GUI 交互。

具體來說,就是當(dāng)你啟動(dòng)了一個(gè) Electron 應(yīng)用,就有一個(gè)主進(jìn)程(main process )被創(chuàng)建了。這條進(jìn)程將負(fù)責(zé)創(chuàng)建出應(yīng)用的 GUI(也就是應(yīng)用的窗口),并處理用戶與這個(gè) GUI 之間的交互。

但直接啟動(dòng) main.js 是無(wú)法顯示應(yīng)用窗口的,在 main.js 中通過調(diào)用BrowserWindow模塊才能將使用應(yīng)用窗口。然后每個(gè)瀏覽器窗口將執(zhí)行它們各自的渲染器進(jìn)程( renderer process )。渲染器進(jìn)程將會(huì)處理一個(gè)真正的 web 頁(yè)面(HTML + CSS + JavaScript),將頁(yè)面渲染到窗口中。鑒于 Electron 使用的是基于Chrominum的瀏覽器內(nèi)核,你就不太需要考慮兼容的問題。


舉個(gè)例子,如果你只想做一個(gè)計(jì)算器,那你的 main process 只會(huì)做一件事情:實(shí)例化一個(gè)窗口,并內(nèi)置了一個(gè)計(jì)算器的界面(這個(gè)界面是你用 HTML、CSS 和 JavaScript 寫的)。

雖然理論上只有 main process 才能和原生 GUI 產(chǎn)生交互,但其實(shí)我們可以通過一些手段讓 renderer process 與原生 GUI 交互(在后文中你將學(xué)習(xí)到如何實(shí)現(xiàn))。

main process 可以通過 Electron 中的一些模塊直接和原生 GUI 交互。你的桌面應(yīng)用可以使用任意的 Node 模塊,比如用node-notifier顯示系統(tǒng)通知,用request發(fā)出 HTTP 請(qǐng)求……

Hello, world!

做好前期準(zhǔn)備,現(xiàn)在讓我們從 Hello World 開始吧!

使用的 repo

這篇教程是基于一個(gè)聲效器教程的github 倉(cāng)庫(kù),請(qǐng)使用下面的命令將它克隆到本地:

git clone https://github.com/bojzi/sound-machine-electron-guide.git

然后查看一下,你可以看看這個(gè)倉(cāng)庫(kù)中有哪些 tag:

git checkout

我們將跟隨這些 tag 將聲效器一步步構(gòu)建出來:

git checkout 00-blank-repository

拉取(checkout)目標(biāo) tag 之后,執(zhí)行:

npm install

這么做能保證項(xiàng)目所依賴的 Node 模塊都會(huì)被拉取。

如果你無(wú)法切換到某一個(gè) tag,最簡(jiǎn)單的解決方式就是重置倉(cāng)庫(kù),然后再 checkout:

git add -A

git reset --hard

開工

先把 tag 為 ‘00-blank-repository’ 拉取下拉:

git checkout 00-blank-repository

在項(xiàng)目文件夾中創(chuàng)建一個(gè) package.json 文件,并在文件中加入以下內(nèi)容:

{

"name": "sound_machine",

"version": "0.1.0",

"main": "./main.js",

"scripts": {

"start": "electron ."

}

}

這個(gè) package.json 的作用是:

確定應(yīng)用的名字和版本號(hào),

告訴 Electron main.js 是 main process 的入口,

定義啟動(dòng)口令 - 在 CLI (終端或者命令行)中執(zhí)行 npm start 即可完成依賴安裝。

現(xiàn)在快把 Electron 安裝上吧。最簡(jiǎn)單的安裝方式應(yīng)該是通過 npm 安裝預(yù)構(gòu)建好的二進(jìn)制文件,然后把它作為開發(fā)依賴(development dependency)寫入 package.json 中(安裝時(shí)帶上 --save-dev 參數(shù)即可自動(dòng)寫入依賴)。在 CLI 中進(jìn)入項(xiàng)目目錄,執(zhí)行下面的命令:

npm install --save-dev electron-prebuilt

預(yù)構(gòu)建的二進(jìn)制文件會(huì)根據(jù)操作系統(tǒng)不同而不同的,通過執(zhí)行 npm start 安裝。我們以開發(fā)依賴的方式使用它,是因?yàn)樵陧?xiàng)目構(gòu)建中只有在開發(fā)階段才會(huì)使用到 Electron。

以上,就是本次 Electron 教程所需要的全部東西了。

對(duì)世界說 Hi

創(chuàng)建一個(gè) app 文件夾,在文件夾中新建 index.html 文件,并寫入以下內(nèi)容:

Hello, world!

在項(xiàng)目的根目錄創(chuàng)建 main.js 文件。Electron 主線程的入口是這個(gè) JS 文件,然后 “Hello world!” 頁(yè)面也通過它顯示出來:

'use strict';varapp=require('app');varBrowserWindow=require('browser-window');varmainWindow=null;app.on('ready',function(){mainWindow=newBrowserWindow({height:600,width:800});mainWindow.loadUrl('file://'+__dirname+'/app/index.html');});

看起來并不難吧?

app 模塊控制著應(yīng)用的生命周期(比如,當(dāng)應(yīng)用進(jìn)入準(zhǔn)備狀態(tài)(ready status)的時(shí)候要采取什么行動(dòng))。

BrowserWindow 模塊控制窗口的創(chuàng)建。

mainWindow 對(duì)象就是你的應(yīng)用窗口的主界面,當(dāng) JavaScript 垃圾回收機(jī)制被觸發(fā)時(shí)窗口就會(huì)被關(guān)閉,此時(shí)該對(duì)象的值是null。

當(dāng) app 獲取到 ready 事件后,我們通過 BrowserWindow 創(chuàng)建一個(gè) 800x600 窗口。

這個(gè) window 的渲染器線程將會(huì)渲染 index.html 文件。

執(zhí)行下面這行代碼,看看我們的應(yīng)用是什么樣的:

npm start

現(xiàn)在沐浴在這個(gè) app 的圣光中吧。

開發(fā)一個(gè)真正的應(yīng)用

華麗麗的聲效器

開始之前,我要問個(gè)問題:什么是聲效器?

聲效器是一個(gè)小設(shè)備,當(dāng)你按下不同按鍵的時(shí)候,它會(huì)發(fā)出不同聲音,比如卡通音或者效果音。在辦公室里聽到這樣有趣的聲音,好像整個(gè)人都明亮起來了呢。用這個(gè)例子作為探索如何使用 Electron 是個(gè)很棒的主意。

具體來說,我們將會(huì)實(shí)現(xiàn)以下功能,并涉及到以下知識(shí):

聲效器的基礎(chǔ)(實(shí)例化瀏覽器窗口),

關(guān)閉聲效器(主進(jìn)程和渲染器進(jìn)程之間的通信),

隨時(shí)播放聲音(全局快捷鍵),

創(chuàng)建一個(gè)快捷修飾鍵(修飾鍵,modifier keys, 指的是 Shift、Ctrl 和 Alt 鍵)設(shè)置頁(yè)面(并將用戶設(shè)置保存在主目錄下),

添加一個(gè)托盤圖標(biāo)(創(chuàng)建原生 GUI 元素、了解菜單和托盤圖標(biāo)的使用),

將應(yīng)用打包到 Mac、Windows 和 Linux 平臺(tái)。

實(shí)現(xiàn)聲效器的基本功能

開始構(gòu)建以及應(yīng)用的結(jié)構(gòu)

在開發(fā)過 “Hello World” 應(yīng)用之后,現(xiàn)在可以著手制做我們的聲效器了。

一個(gè)典型的聲效器會(huì)有很多的按鈕,你需要按下那些按鈕才能讓機(jī)器發(fā)聲,通常會(huì)是擬聲詞(比如笑聲、掌聲、打碎玻璃的聲音等等)。

響應(yīng)點(diǎn)擊 – 這是我們要做的第一件事情。

我們的應(yīng)用結(jié)構(gòu)非常簡(jiǎn)單直白。

在應(yīng)用的根目錄中,要有一個(gè) package.json、main.js 和其他全局所需的應(yīng)用文件。

app/ 目錄中要包含 HTML 文件、CSS 目錄、JS 目錄、wav 目錄還有圖片目錄。

出于簡(jiǎn)化這個(gè)教程的目的,所有和網(wǎng)頁(yè)設(shè)計(jì)相關(guān)的文件都已經(jīng)在一開始就放在倉(cāng)庫(kù)中了。請(qǐng)?jiān)诿钚兄休斎雊it checkout 01-start-project 獲取?,F(xiàn)在,請(qǐng)你可以輸入以下命令,重置你的倉(cāng)庫(kù)并拉取新的 tag:

If you followed along with the "Hello, world!" example:

git add -A

git reset --hard

Follow along with the tag 01-start-project:

git checkout 01-start-project

在本教程中,我們只使用兩種聲效,后面再找一些別的音效和圖標(biāo),修改* index.js *就將它們擴(kuò)展成有16種音效的聲效器。

main process 的其他內(nèi)容

首先找到 main.js 中定義聲效器外形的部分,用下面這段替換掉:

'use strict';varapp=require('app');varBrowserWindow=require('browser-window');varmainWindow=null;app.on('ready',function(){mainWindow=newBrowserWindow({frame:false,height:700,resizable:false,width:368});mainWindow.loadUrl('file://'+__dirname+'/app/index.html');});

當(dāng)窗口被定義了大小,我們也就是在自定義這個(gè)窗口,使得它不可拉伸沒有框架,讓它看起來就像一個(gè)真正的聲效器浮在桌面上。

現(xiàn)在問題來了 – 要如何移動(dòng)或者關(guān)閉一個(gè)沒有標(biāo)題欄的窗口。

很快我就會(huì)說到自定義窗口(和應(yīng)用)的關(guān)閉動(dòng)作,還會(huì)談到如何在主進(jìn)程和渲染器進(jìn)程中通信。不過現(xiàn)在讓我們先把目光聚焦到“拖拽效果”上。你可以在 app/css 目錄下找到 index.css 文件:

html,body{...-webkit-app-region:drag;...}

-webkit-app-region: drag;把整個(gè) html 都變成了一個(gè)可拖拽的對(duì)象。現(xiàn)在問題來了,在可拖拽的對(duì)象上你怎么點(diǎn)擊???!好的,可能你會(huì)想到把 html 中某個(gè)部分的這個(gè)屬性值設(shè)置為no-drag;,那就允許該元素不可拖拽(但可以點(diǎn)擊了)。讓我們想想下面這段 index.css 片段:

.button-sound {

...

-webkit-app-region: no-drag;

}

展示聲效器

現(xiàn)在通過 main.js 文件可以創(chuàng)建一個(gè)新窗口,并在窗口中顯示出聲效器的界面。如果通過npm start啟動(dòng)應(yīng)用,你將會(huì)看到一個(gè)有動(dòng)態(tài)效果的聲效器。因?yàn)槲覀兙褪菑囊粋€(gè)靜態(tài)頁(yè)面開始,所以現(xiàn)在你看到的也是不會(huì)動(dòng)的頁(yè)面:

將下面這段代碼保存到 index.js 文件中(位置在 app/js 目錄下),運(yùn)行后應(yīng)用后,你會(huì)發(fā)現(xiàn)可以與聲效器交互了:

'use strict';varsoundButtons=document.querySelectorAll('.button-sound');for(vari=0;i

通過上面這段代碼,我們:

獲取聲音按鈕,

迭代訪問按鈕的data-sound屬性值,

給每個(gè)按鈕加上背景圖,

通過HTMLAudioElement 接口給每個(gè)按鈕都添加一個(gè)點(diǎn)擊事件,使之可以播放音頻,

通過下面這行命令運(yùn)行你的應(yīng)用吧:

npm start

通過遠(yuǎn)程事件從瀏覽窗口中關(guān)閉應(yīng)用

接著拉取02-basic-sound-machine的內(nèi)容:

git checkout 02-basic-sound-machine

簡(jiǎn)單來說 - 應(yīng)用窗口(渲染器進(jìn)程)不應(yīng)該和 GUI 發(fā)生交互(也就是不應(yīng)該和“關(guān)閉窗口”有關(guān)聯(lián)),Electron 的官方教程上說了:

考慮到在網(wǎng)頁(yè)中直接調(diào)用原生的 GUI 容易造成資源溢出,這很危險(xiǎn),開發(fā)者不能這么使用。如果開發(fā)者想要在網(wǎng)頁(yè)上執(zhí)行 GUI 操作,必須要通過渲染器進(jìn)程和主進(jìn)程的通信實(shí)現(xiàn)。

Electron 為主進(jìn)程和渲染器進(jìn)程提供了 ipc (跨進(jìn)程通信)模塊,ipc 模塊允許接收和發(fā)送通信頻道的信息。頻道由字符串表示(比如“channel-1”,“channel-2”這樣),可以用于區(qū)分不同的信息接收者。傳遞的信息中也可以包含數(shù)據(jù)。根據(jù)接收到的信息,訂閱者可以做出響應(yīng)。信息傳遞的最大好處就是做到分離任務(wù) – 主進(jìn)程不需要知道是哪些渲染器進(jìn)程發(fā)送了信息。

這正是我們想要做的 – 將主進(jìn)程(main.js)訂閱到“關(guān)閉主窗口”頻道中,當(dāng)用戶點(diǎn)擊關(guān)閉按鈕時(shí),從渲染器進(jìn)程(index.js)向該頻道發(fā)送信息。

Add the following to main.js to subscribe to a channel:

將下面的代碼實(shí)現(xiàn)了頻道訂閱,將它添加到 main.js 中:

varipc=require('ipc');ipc.on('close-main-window',function(){app.quit();});

把 ipc 模塊包含進(jìn)來之后,從頻道中訂閱信息就非常簡(jiǎn)單了:過 on() 方法和頻道名稱,再加上一個(gè)回調(diào)函數(shù)就行了。

要向該頻道發(fā)送信息,就要把下面的代碼加入 index.js 中:

varipc=require('ipc');varcloseEl=document.querySelector('.close');closeEl.addEventListener('click',function(){ipc.send('close-main-window');});

我們依然需要把 ipc 模塊引入到文件中,給關(guān)閉按鈕綁定點(diǎn)擊事件。當(dāng)點(diǎn)擊了關(guān)閉按鈕時(shí),通過 send() 方法發(fā)送一條信息到“關(guān)閉主窗口”頻道。

不要忘記在在 index.css 中將關(guān)閉按鈕設(shè)置為不可拖拽:

.settings{...-webkit-app-region:no-drag;}

就這樣,我們的應(yīng)用現(xiàn)在可以通過按鈕關(guān)掉了。ipc 的通信可以通過事件和參數(shù)的傳遞變得很復(fù)雜,在后文中會(huì)有傳遞參數(shù)的例子。

通過全局快捷鍵播放聲音

拉取03-closable-sound-machine:

git checkout 03-closable-sound-machine

聲效器的地基已經(jīng)打的不錯(cuò)。但是我們還面臨著使用性的問題 – 這個(gè)應(yīng)用要始終保持在桌面最前方,且可以被重復(fù)點(diǎn)擊。

這就是全局快捷鍵要介入的地方。Electron 提供了全局快捷模塊(global shortcut module)允許開發(fā)者捕獲組合鍵并做出相應(yīng)的反應(yīng)。在 Eelctron 中組合鍵被稱為加速器,它以字符串的形式被記錄下(比如 “Ctrl+Shift+1”)。

因?yàn)槲覀兿胍东@到原生的 GUI 事件(全局快捷鍵),并執(zhí)行應(yīng)用窗口事件(播放聲音),我們將使用 ipc 模塊從主進(jìn)程發(fā)送信息到渲染器進(jìn)程。

在看代碼之前,還有兩件事情要我們考慮:

全局快捷鍵會(huì)在 app 的 ready 事件被觸發(fā)后注冊(cè)(相關(guān)代碼片段要被包含在 ‘ready’ 中)

通過 ipc 模塊從主進(jìn)程向渲染器進(jìn)程發(fā)送信息,你必須使用窗口對(duì)象的引用(類似于createdWindow.webContents.send(‘channel’))。

記住上面的兩點(diǎn)了嗎?現(xiàn)在讓我們來改寫* main.js *吧:

var globalShortcut = require('global-shortcut');

app.on('ready', function() {

... // 之前寫過的代碼

globalShortcut.register('ctrl+shift+1', function () {

mainWindow.webContents.send('global-shortcut', 0);

});

globalShortcut.register('ctrl+shift+2', function () {

mainWindow.webContents.send('global-shortcut', 1);

});

});

首先,要先引入 global-shortcut 模塊,當(dāng)應(yīng)用進(jìn)入ready狀態(tài)之時(shí),我們將會(huì)注冊(cè)兩個(gè)快捷鍵 – ‘Ctrl+Shift+1’ 和 ‘Ctrl+Shift+2’。這兩個(gè)快捷鍵可以通過不同的參數(shù)向“全局快捷鍵”頻道( “global-shortcut”channel)發(fā)送信息。通過參數(shù)匹配到到底要播放哪種聲音,將下面的代碼加入 index.js 中:

ipc.on('global-shortcut', function (arg) {

var event = new MouseEvent('click');

soundButtons[arg].dispatchEvent(event);

});

為了保證整個(gè)架構(gòu)足夠簡(jiǎn)單,我們將會(huì)用 soundButtons 選擇器模擬按鈕的點(diǎn)擊播放聲音。當(dāng)發(fā)送的信息是“1”時(shí),我們將會(huì)獲取 soundButtons[1] 元素,觸發(fā)鼠標(biāo)點(diǎn)擊事件(注意:在生產(chǎn)環(huán)境中的應(yīng)用,你需要封裝好播放聲音的代碼,然后執(zhí)行它)。

在新窗口中通過用戶設(shè)置配置 modifier keys

下面請(qǐng)拉取04-global-shortcuts-bound:

git checkout 04-global-shortcuts-bound

通常我們會(huì)同時(shí)運(yùn)行好多個(gè)應(yīng)用,聲效器中設(shè)置的快捷鍵很可能已經(jīng)被占用了。所以現(xiàn)在要引入一個(gè)設(shè)置界面,允許用戶更改修飾鍵(modifier keys)的原因(Ctrl、Alt 和 Shift)。

要完成這一個(gè)功能,我們需要做下面這些事情:

在主界面上添加設(shè)置按鈕,

實(shí)現(xiàn)一個(gè)設(shè)置窗口(設(shè)置頁(yè)面上有對(duì)應(yīng)的HTML、CSS 和 JS),

開啟和關(guān)閉設(shè)置窗口,以及更新全局快捷鍵的 ipc 信息,

從用戶的系統(tǒng)中讀寫存儲(chǔ)設(shè)置信息的 JSON 文件。

piu~ 以上就是我們要做的。

設(shè)置按鈕和設(shè)置窗口

和關(guān)閉主窗口類似,我們將會(huì)把事件綁定到設(shè)置按鈕上,(settings button),在* index.js *中加入發(fā)送給頻道的信息:

var settingsEl = document.querySelector('.settings');

settingsEl.addEventListener('click', function () {

ipc.send('open-settings-window');

});

當(dāng)點(diǎn)擊了設(shè)置按鈕,將會(huì)有一條信息向“打開設(shè)置窗口”這個(gè)頻道發(fā)送。* main.js 可以響應(yīng)這個(gè)事件,并打開一個(gè)新窗口,將以下代碼加入 main.js *中:

var settingsWindow = null;

ipc.on('open-settings-window', function () {

if (settingsWindow) {

return;

}

settingsWindow = new BrowserWindow({

frame: false,

height: 200,

resizable: false,

width: 200

});

settingsWindow.loadUrl('file://' + __dirname + '/app/settings.html');

settingsWindow.on('closed', function () {

settingsWindow = null;

});

});

這一步和之前的類似,我們將會(huì)打開一個(gè)新的窗口。唯一的不同點(diǎn)就是,為了防止實(shí)例化兩個(gè)一樣的對(duì)象,我們將會(huì)檢查設(shè)置窗口是否已經(jīng)被打開了。

當(dāng)上述代碼成功執(zhí)行之后,我們需要再添加一個(gè)關(guān)閉設(shè)置窗口的動(dòng)作。類似的,我們需要向頻道中發(fā)送一條信息,但這次是從* settings.js 中發(fā)送(關(guān)閉按鈕的事件是在 settings.js 中)。新建 settings.js *文件,并添加以下代碼(如果已經(jīng)有該文件,就直接在原文件中添加):

'use strict';

var ipc = require('ipc');

var closeEl = document.querySelector('.close');

closeEl.addEventListener('click', function (e) {

ipc.send('close-settings-window');

});

在 main.js 中監(jiān)聽該頻道:

ipc.on('close-settings-window', function () {

if (settingsWindow) {

settingsWindow.close();

}

});

現(xiàn)在,設(shè)置窗口已經(jīng)可以實(shí)現(xiàn)我們的邏輯了。

用戶設(shè)置的讀寫

執(zhí)行05-settings-window-working:

git checkout 05-settings-window-working

設(shè)置窗口的交互過程是,存儲(chǔ)設(shè)置信息以及刷新應(yīng)用:

創(chuàng)建一個(gè) JSON 文件用于讀寫用戶設(shè)置,

用這個(gè)設(shè)置初始化設(shè)置窗口,

通過用戶的操作更新這個(gè)設(shè)置文檔,

通知主進(jìn)程要更新設(shè)置頁(yè)面。

我們可以把實(shí)現(xiàn)讀寫設(shè)置的部分直接寫進(jìn) main.js 中,但是如果把這部分獨(dú)立成模塊,可以隨處引用這樣不是更好嗎?

使用 JSON 做配置文件

現(xiàn)在我們要?jiǎng)?chuàng)建一個(gè) configuration.js 文件,再將這個(gè)文件引入到項(xiàng)目中。Node.js 使用了CommonJS作為編寫模塊的規(guī)范,也就是說你需要將你的 API 和這個(gè) API 中可用的函數(shù)都要暴露出來。

為了更簡(jiǎn)單地讀寫文件,我們將會(huì)使用 nconf 模塊,這個(gè)模塊封裝了 JSON 文件的讀寫。但首先,我們需要將這個(gè)模塊包含到項(xiàng)目中來:

npm install --save nconf

這行命令意味著 nconf 模塊將會(huì)作為應(yīng)用依賴被安裝到項(xiàng)目中,當(dāng)我們要發(fā)布應(yīng)用的時(shí)候,這個(gè)模塊會(huì)被一起打包給用戶(save-dev 參數(shù)會(huì)使安裝的模塊只出現(xiàn)在開發(fā)階段,發(fā)布應(yīng)用的時(shí)候不會(huì)被包含進(jìn)去)。

在根目錄下創(chuàng)建 configuration.js 文件,它的內(nèi)容非常簡(jiǎn)單:

'use strict';varnconf=require('nconf').file({file:getUserHome()+'/sound-machine-config.json'});functionsaveSettings(settingKey,settingValue){nconf.set(settingKey,settingValue);nconf.save();}functionreadSettings(settingKey){nconf.load();returnnconf.get(settingKey);}functiongetUserHome(){returnprocess.env[(process.platform=='win32')?'USERPROFILE':'HOME'];}module.exports={saveSettings:saveSettings,readSettings:readSettings};

我們要把文件位置和文件名傳 nconf 模塊(用 Node.js 的 process.env 獲取到文件的位置),具體路徑會(huì)根據(jù)平臺(tái)而異。

通過 nconf 模塊的 set() 和 get() 方法結(jié)合文件操作的 save() 和 load(),我們可以實(shí)現(xiàn)設(shè)置文件的讀寫操作,然后通過 module.exports 將接口暴露到外部。

初始化默認(rèn)的快捷鍵設(shè)置

在講設(shè)置交互之前,為了避免用戶是第一次打開這個(gè)應(yīng)用,要先初始化一個(gè)設(shè)置文件。我們將會(huì)以數(shù)組的形式儲(chǔ)存熱鍵,對(duì)應(yīng)的鍵是 “shortcutKeys”,儲(chǔ)存在 main.js 中,我們需要把 configuration 模塊包含到項(xiàng)目中:

'use strict';varconfiguration=require('./configuration');app.on('ready',function(){if(!configuration.readSettings('shortcutKeys')){configuration.saveSettings('shortcutKeys',['ctrl','shift']);}...}

我們需要先檢測(cè)鍵 ‘shortcutKeys’ 是否已經(jīng)有對(duì)應(yīng)的值了,如果沒有我們需要初始化一個(gè)值。

在 main.js 中,我們將重寫全局快捷鍵的注冊(cè)方法,在之后我們更新設(shè)置的時(shí)候,會(huì)直接調(diào)用這個(gè)方法。將原來的注冊(cè)代碼改成以下內(nèi)容:

app.on('ready', function () {

...

setGlobalShortcuts();

}

function setGlobalShortcuts() {

globalShortcut.unregisterAll();

var shortcutKeysSetting = configuration.readSettings('shortcutKeys');

var shortcutPrefix = shortcutKeysSetting.length === 0 ? '' : shortcutKeysSetting.join('+') + '+';

globalShortcut.register(shortcutPrefix + '1', function () {

mainWindow.webContents.send('global-shortcut', 0);

});

globalShortcut.register(shortcutPrefix + '2', function () {

mainWindow.webContents.send('global-shortcut', 1);

});

}

上述方法重置了全局快捷鍵的值,從設(shè)置中讀取熱鍵的數(shù)組,將它傳入加速器兼容字符串(Accelerator-compatible)并注冊(cè)新鍵。

設(shè)置窗口的交互

回到 settings.js 文件,我們需要綁定點(diǎn)擊事件來改變我們的全局快捷鍵。首先,我們將會(huì)遍歷復(fù)選框,記錄下被勾選的選項(xiàng)(從 configuration 模塊中讀值):

varconfiguration=require('../configuration.js');varmodifierCheckboxes=document.querySelectorAll('.global-shortcut');for(vari=0;i

現(xiàn)在我們需要綁定復(fù)選框的行為??紤]到設(shè)置窗口(和它的渲染器進(jìn)程)是不允許改變 GUI 綁定的。這說明我們需要從 setting.js 中發(fā)送信息(之后會(huì)處理這個(gè)信息的):

for (var i = 0; i < modifierCheckboxes.length; i++) {

...

modifierCheckboxes[i].addEventListener('click', function (e) {

bindModifierCheckboxes(e);

});

}

function bindModifierCheckboxes(e) {

var shortcutKeys = configuration.readSettings('shortcutKeys');

var modifierKey = e.target.attributes['data-modifier-key'].value;

if (shortcutKeys.indexOf(modifierKey) !== -1) {

var shortcutKeyIndex = shortcutKeys.indexOf(modifierKey);

shortcutKeys.splice(shortcutKeyIndex, 1);

}

else {

shortcutKeys.push(modifierKey);

}

configuration.saveSettings('shortcutKeys', shortcutKeys);

ipc.send('set-global-shortcuts');

}

這段代碼看起來比較長(zhǎng),但事實(shí)上它很簡(jiǎn)單。我們將會(huì)遍歷所有的復(fù)選框,并綁定點(diǎn)擊事件,在每次點(diǎn)擊的時(shí)候檢查設(shè)置數(shù)組中是否包含有熱鍵。根據(jù)檢查結(jié)果,更改數(shù)組,將結(jié)果保存到設(shè)置中,并向主進(jìn)程發(fā)送信息,更新我們的全局快捷鍵。

現(xiàn)在的工作就是在 main.js 中將 ipc 信息訂閱到“設(shè)置全局快捷鍵”頻道,并更新我們的全局快捷鍵:

ipc.on('set-global-shortcuts',function(){setGlobalShortcuts();});

就這么簡(jiǎn)單,我們的全局快捷鍵已經(jīng)可配置了!

菜單中要放什么?

接下來拉取 06-shortcuts-configurable:

git checkout 06-shortcuts-configurable

另一個(gè)在桌面應(yīng)用中的重要概念就是“菜單”,比如右鍵菜單(點(diǎn)擊右鍵出現(xiàn)的菜單)、托盤菜單(通常會(huì)有一個(gè)托盤 icon)和應(yīng)用菜單(在 OS X 中)等等。

在這一節(jié)中,我們將會(huì)添加一個(gè)托盤菜單。我們也將會(huì)借此機(jī)會(huì)嘗試在remote 模塊中使用別的進(jìn)程間的通信方式。

remote 模塊從渲染器進(jìn)程到主進(jìn)程完成 RPC 類型的調(diào)用。將模塊引入的時(shí)候,這個(gè)模塊是在主進(jìn)程中被實(shí)例化的,所以它們的方法也會(huì)在主進(jìn)程中被執(zhí)行。實(shí)際開發(fā)中,這個(gè)行為是在遠(yuǎn)程地請(qǐng)求 index.js 中的原生 GUI 模塊,然后又在 main.js 中調(diào)用 GUI 的方法。這么做的話,你需要在 index.js 中將 BrowserWindow 模塊引入,然后實(shí)例化一個(gè)新的瀏覽器窗口。其實(shí)在主進(jìn)程中有一個(gè)同步的調(diào)用,實(shí)際上是這個(gè)調(diào)用創(chuàng)建了新的瀏覽器窗口。

現(xiàn)在讓我們來看看要怎么樣創(chuàng)建一個(gè)菜單,并在渲染器進(jìn)程中將它綁定到一個(gè)托盤圖標(biāo)上。將下面這段代碼加入 index.js 中:

varremote=require('remote');varTray=remote.require('tray');varMenu=remote.require('menu');varpath=require('path');vartrayIcon=null;if(process.platform==='darwin'){trayIcon=newTray(path.join(__dirname,'img/tray-iconTemplate.png'));}else{trayIcon=newTray(path.join(__dirname,'img/tray-icon-alt.png'));}vartrayMenuTemplate=[{label:'Sound machine',enabled:false},{label:'Settings',click:function(){ipc.send('open-settings-window');}},{label:'Quit',click:function(){ipc.send('close-main-window');}}];vartrayMenu=Menu.buildFromTemplate(trayMenuTemplate);trayIcon.setContextMenu(trayMenu);

原生的 GUI 模塊(菜單和托盤)通過remote模塊包含進(jìn)來比較安全。

OS X 支持圖片模板(將圖片文件名以 ‘Template’ 結(jié)尾,就會(huì)被定義成為圖片模板),托盤圖標(biāo)可以通過模板來定義,這樣我們的圖標(biāo)就會(huì)有“暗黑”和“光明”兩個(gè)主題了。其他的操作系統(tǒng)用正常的圖標(biāo)就行。

在 Electron 中有很多綁定菜單的方法。這里介紹的方法只是創(chuàng)建了一個(gè)菜單模板(將菜單項(xiàng)用數(shù)組的方式存儲(chǔ)),然后通過這個(gè)模板創(chuàng)建菜單,托盤 icon 再綁定上這個(gè)菜單,就實(shí)現(xiàn)了我們的菜單功能。

應(yīng)用打包

接下來拉取 07-ready-for-packaging:

git checkout 07-ready-for-packaging

如果你做了一個(gè)應(yīng)用結(jié)果人們連下載都下載不了,怎么會(huì)有人用呢?

通過electron-packager你可以將應(yīng)用打包到全平臺(tái)。這一步驟在 shell 中就可以完成,將應(yīng)用打包好以后就能發(fā)布了。

它可以作為一個(gè)命令行應(yīng)用或者作為開發(fā)應(yīng)用過程中的一步,構(gòu)建一個(gè)更復(fù)雜的開發(fā)場(chǎng)景不是這篇文章要談的內(nèi)容,不過我們將通過 npm 腳本讓應(yīng)用打包更簡(jiǎn)單一點(diǎn)。用 electron-packager 打包的命令是這樣的:

electron-packager

以上命令:

將目錄切換到項(xiàng)目所在路徑,

參數(shù) ‘name of project’ 是你的項(xiàng)目名,參數(shù) ‘plateform’ 確定了你要構(gòu)建哪個(gè)平臺(tái)的應(yīng)用(Windows、Mac 還是 Linux),

參數(shù) ‘a(chǎn)rchitecture’ 決定了使用 x86 還是 x64 還是兩個(gè)架構(gòu)都用,

決定了使用的 Electron 版本。

第一次打包應(yīng)用需要比較久的時(shí)間,因?yàn)樗衅脚_(tái)的二進(jìn)制文件都需要下載,之后打包應(yīng)用會(huì)比較快了。

在 Mac 上我是這么做的:

electron-packager ~/Projects/sound-machine SoundMachine --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/sound-machine/app/img/app-icon.icns

首先你要將圖標(biāo)的格式轉(zhuǎn)換成 .icns(在 Mac 上)或者 .ico(在 Windows 上),網(wǎng)絡(luò)上有工具可以把 PNG 做這樣的轉(zhuǎn)換(確保下載的圖片的擴(kuò)展名是 .icns 而不是 .hqx)。如果從非 Windows 的系統(tǒng)上打包了 Windows 的應(yīng)用,你應(yīng)該需要處理一下路徑(Mac 用戶可以用 brew,Linux 用戶可以用 apt-get)。

每次都要執(zhí)行這么長(zhǎng)的一句命令一點(diǎn)都不合理。所以你可以在 package.json 中添加另一個(gè)腳本。首先,將electron-packager 作為 development dependency 安裝:

npm install --save-dev electron-packager

然后在 package.json 中添加以下內(nèi)容:

"scripts":{"start":"electron .","package":"electron-packager ./ SoundMachine --all --out ~/Desktop/SoundMachine --version 0.30.2 --overwrite --icon=./app/img/app-icon.icns"}

接著執(zhí)行:

npm run-script package

打包命令啟動(dòng)了 electron-packager,在當(dāng)前目錄中查看項(xiàng)目,在 Desktop 目錄中構(gòu)建。如果你使用的是 Windows,腳本內(nèi)容需要一些細(xì)微的更新。

聲效器目前是 100MB 大小,不要擔(dān)心,當(dāng)你壓縮它之后,所占空間會(huì)減半。

如果你對(duì)此還有更大的計(jì)劃,可以看看electron-builder,它是根據(jù) electron-packager 構(gòu)建出的應(yīng)用打包再做自動(dòng)安裝的處理。

添加其他的特性

現(xiàn)在你可以嘗試開發(fā)別的功能了。

這里有一些方案,可以啟發(fā)你的靈感:

應(yīng)用的使用手冊(cè),說明了有那些快捷鍵和應(yīng)用作者,

在應(yīng)用中給使用手冊(cè)添加一個(gè)圖標(biāo)和菜單入口,

構(gòu)建一個(gè)打包腳本,用于快速構(gòu)建和分發(fā),

使用* node-notifier *添加一個(gè)提示系統(tǒng),告訴用戶正在播放哪一個(gè)聲音,

使用* lodash *讓你的代碼更加干凈、具有更好的擴(kuò)展性,

在打包之前不要忘了壓縮你的 CSS 和 JavaScript,

結(jié)合上文提到的* node-notifier *和一個(gè)服務(wù)器端的調(diào)用,通知用戶是否需要更新版本……

還有一個(gè)值得一試的東西 – 將代碼中關(guān)于瀏覽器窗口的邏輯抽離出來,通過類似 browserify 的工具創(chuàng)建一個(gè)和聲效器一樣的網(wǎng)頁(yè)。一份代碼,兩個(gè)產(chǎn)品(桌面端和 Web 引用)??釘懒?!

更深入研究 Electron

我們只是嘗試了 Electron 的冰山一角,想要知道監(jiān)控主機(jī)的電源情況、獲取當(dāng)前窗口的信息(比如光標(biāo)的位置)等,Eletron 都能幫你做到。

對(duì)于所有的內(nèi)置工具(通常在開發(fā) Electron 應(yīng)用時(shí)使用),查看Electron API 文檔。

這些文檔在 Electron 的 github 倉(cāng)庫(kù)中都能找到。

Sindre Sorhus 正在維護(hù)一份Electron 資源清單,在那個(gè)上面你可以看到很多非??岬捻?xiàng)目,還能了解到一些系統(tǒng)架構(gòu)做的很好的 Electron 應(yīng)用,這些都能給你的開發(fā)帶來靈感。

Electron 是基于 io.js 的,大部分 Node.js 模塊都可以兼容,可以使用它們擴(kuò)展你的應(yīng)用。去npmjs.com上看看有沒有合適的。

這樣就夠了嗎?

當(dāng)然不。

現(xiàn)在,可以來構(gòu)建一個(gè)更大型的應(yīng)用了。在這篇文章中,我?guī)缀鯖]有說到如何使用外部的庫(kù)或者構(gòu)建工具來構(gòu)建一個(gè)應(yīng)用,不過用 ES6 和 Typescript 的語(yǔ)法結(jié)合 Angular 和 React 來構(gòu)建 Electron 應(yīng)用也很簡(jiǎn)單,還可以用 gulp 或 grunt 構(gòu)建流程。

干嘛不用你最喜歡的語(yǔ)言,框架和工具,來試試構(gòu)建一個(gè) Filckr 同步工具(借助 Filckr API 和 node-filckrapi)或者一個(gè) Gmail 客戶端(使用 Google 的官方 Node.JS 客戶端庫(kù)?)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容