使用 Electron 構建桌面應用
轉載 https://zhuanlan.zhihu.com/p/20225295?columnSlug=FrontendMagazine
譯者注:前一段時間有過關於NW.js 的翻譯,有興趣的朋友可以一起讀來看看。另,譯者尚未實踐本教程,預計將在下周使用新Mac實踐。若在此之前有朋友踩坑,請通知譯者,譯者將在實踐后更改之。
使用 JavaScript、Node.js 和 Electron 創造專屬於你的聲效器
JavaScript 桌面應用是什么
即使在移動端和雲端大行其道而,桌面端日漸落末的現在,我的心中仍然為桌面應用留有一個特殊的位置。和 Web 應用比起來桌面應用的優點還是很多的:只要把它們放在開始菜單欄或者 dock 上,你就能隨時打開它們;還可以通過 alt-tab 或者 cmd-tab 切換應用;和操作系統之間的交互更良好(快捷鍵,通知欄等)。
我將會在這篇文章中指導構建一個簡單的桌面應用。當然,你也將了解到在使用 JavaScript 構建桌面應用的時候要哪些重要的概念。
使用 JavaScript 開發桌面應用意味着在打包(package application)的時候你會需要根據操作系統的不同發出不同的命令。這一行為是將原生桌面應用兼容不同平台的概念抽象出來,方便維護應用。現在,我們可以借助 Electron 或者 NW.js 開發一個桌面應用。其實這兩者提供的或多或少差不多的特性,但對於我來說,還是更偏向於 Electron。在做出選擇之前,先詳細了解它們並考慮各種情況,就不會選錯的。

基本假設
開始教程之前,請允許我假設你已經有了一個常用的的編輯器(或者 IDE),系統中也安裝了 Node.js 和 npm,並有基礎的 HTML/CSS/JavaScript (對 Node.js 的 CommonJS 模塊概念有所了解是最好,但不強求) 知識。如果以上知識你並不了解,為了防止這篇文章看到你頭昏腦脹,推薦你先看看之前我寫過的博文,補充一下基礎知識。
萬事俱備,現在就把精力集中在學習 Electron 上,不要再擔心界面的事情(將會構建的界面本質上就是普通的 Web 頁面而已)。
Electron 概覽
簡而言之,Electron 提供了一個實時構建桌面應用的純 JavaScript 環境。Electron 可以獲取到你定義在 package.json 中 main 文件內容,然后執行它。通過這個文件(通常我們稱之為 main.js),可以創建一個應用窗口,這個應用窗口包含一個渲染好的 web 界面,還可以和系統原生的 GUI 交互。
具體來說,就是當你啟動了一個 Electron 應用,就有一個主進程(main process )被創建了。這條進程將負責創建出應用的 GUI(也就是應用的窗口),並處理用戶與這個 GUI 之間的交互。
但直接啟動 main.js 是無法顯示應用窗口的,在 main.js 中通過調用BrowserWindow模塊才能將使用應用窗口。然后每個瀏覽器窗口將執行它們各自的渲染器進程( renderer process )。渲染器進程將會處理一個真正的 web 頁面(HTML + CSS + JavaScript),將頁面渲染到窗口中。鑒於 Electron 使用的是基於 Chrominum 的瀏覽器內核,你就不太需要考慮兼容的問題。

舉個例子,如果你只想做一個計算器,那你的 main process 只會做一件事情:實例化一個窗口,並內置了一個計算器的界面(這個界面是你用 HTML、CSS 和 JavaScript 寫的)。
雖然理論上只有 main process 才能和原生 GUI 產生交互,但其實我們可以通過一些手段讓 renderer process 與原生 GUI 交互(在后文中你將學習到如何實現)。
main process 可以通過 Electron 中的一些模塊直接和原生 GUI 交互。你的桌面應用可以使用任意的 Node 模塊,比如用 node-notifier 顯示系統通知,用 request 發出 HTTP 請求……
Hello, world!
做好前期准備,現在讓我們從 Hello World 開始吧!
使用的 repo
這篇教程是基於一個聲效器教程的 github 倉庫,請使用下面的命令將它克隆到本地:
git clone https://github.com/bojzi/sound-machine-electron-guide.git
然后查看一下,你可以看看這個倉庫中有哪些 tag:
git checkout <tag-name>
我們將跟隨這些 tag 將聲效器一步步構建出來:
git checkout 00-blank-repository
拉取(checkout)目標 tag 之后,執行:
npm install
這么做能保證項目所依賴的 Node 模塊都會被拉取。
如果你無法切換到某一個 tag,最簡單的解決方式就是重置倉庫,然后再 checkout:
git add -A
git reset --hard
開工
先把 tag 為 ‘00-blank-repository’ 拉取下拉:
git checkout 00-blank-repository
在項目文件夾中創建一個 package.json 文件,並在文件中加入以下內容:
{
"name": "sound_machine",
"version": "0.1.0",
"main": "./main.js",
"scripts": {
"start": "electron ."
}
}
這個 package.json 的作用是:
- 確定應用的名字和版本號,
- 告訴 Electron main.js 是 main process 的入口,
- 定義啟動口令 - 在 CLI (終端或者命令行)中執行 npm start 即可完成依賴安裝。
現在快把 Electron 安裝上吧。最簡單的安裝方式應該是通過 npm 安裝預構建好的二進制文件,然后把它作為開發依賴(development dependency)寫入 package.json 中(安裝時帶上 --save-dev 參數即可自動寫入依賴)。在 CLI 中進入項目目錄,執行下面的命令:
npm install --save-dev electron-prebuilt
預構建的二進制文件會根據操作系統不同而不同的,通過執行 npm start 安裝。我們以開發依賴的方式使用它,是因為在項目構建中只有在開發階段才會使用到 Electron。
以上,就是本次 Electron 教程所需要的全部東西了。
對世界說 Hi
創建一個 app 文件夾,在文件夾中新建 index.html 文件,並寫入以下內容:
<h1>Hello, world!</h1>
在項目的根目錄創建 main.js 文件。Electron 主線程的入口是這個 JS 文件,然后 “Hello world!” 頁面也通過它顯示出來:
'use strict'; var app = require('app'); var BrowserWindow = require('browser-window'); var mainWindow = null; app.on('ready', function() { mainWindow = new BrowserWindow({ height: 600, width: 800 }); mainWindow.loadUrl('file://' + __dirname + '/app/index.html'); });
看起來並不難吧?
app 模塊控制着應用的生命周期(比如,當應用進入准備狀態(ready status)的時候要采取什么行動)。
BrowserWindow 模塊控制窗口的創建。
mainWindow 對象就是你的應用窗口的主界面,當 JavaScript 垃圾回收機制被觸發時窗口就會被關閉,此時該對象的值是null。
當 app 獲取到 ready 事件后,我們通過 BrowserWindow 創建一個 800x600 窗口。
這個 window 的渲染器線程將會渲染 index.html 文件。
執行下面這行代碼,看看我們的應用是什么樣的:
npm start
現在沐浴在這個 app 的聖光中吧。

開發一個真正的應用
華麗麗的聲效器
開始之前,我要問個問題:什么是聲效器?
聲效器是一個小設備,當你按下不同按鍵的時候,它會發出不同聲音,比如卡通音或者效果音。在辦公室里聽到這樣有趣的聲音,好像整個人都明亮起來了呢。用這個例子作為探索如何使用 Electron 是個很棒的主意。

具體來說,我們將會實現以下功能,並涉及到以下知識:
- 聲效器的基礎(實例化瀏覽器窗口),
- 關閉聲效器(主進程和渲染器進程之間的通信),
- 隨時播放聲音(全局快捷鍵),
- 創建一個快捷修飾鍵(修飾鍵,modifier keys, 指的是 Shift、Ctrl 和 Alt 鍵)設置頁面(並將用戶設置保存在主目錄下),
- 添加一個托盤圖標(創建原生 GUI 元素、了解菜單和托盤圖標的使用),
- 將應用打包到 Mac、Windows 和 Linux 平台。
實現聲效器的基本功能
開始構建以及應用的結構
在開發過 “Hello World” 應用之后,現在可以着手制做我們的聲效器了。
一個典型的聲效器會有很多的按鈕,你需要按下那些按鈕才能讓機器發聲,通常會是擬聲詞(比如笑聲、掌聲、打碎玻璃的聲音等等)。
響應點擊 – 這是我們要做的第一件事情。
我們的應用結構非常簡單直白。

在應用的根目錄中,要有一個 package.json、main.js 和其他全局所需的應用文件。
app/ 目錄中要包含 HTML 文件、CSS 目錄、JS 目錄、wav 目錄還有圖片目錄。
出於簡化這個教程的目的,所有和網頁設計相關的文件都已經在一開始就放在倉庫中了。請在命令行中輸入git checkout 01-start-project 獲取。現在,請你可以輸入以下命令,重置你的倉庫並拉取新的 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
在本教程中,我們只使用兩種聲效,后面再找一些別的音效和圖標,修改* index.js *就將它們擴展成有16種音效的聲效器。
main process 的其他內容
首先找到 main.js 中定義聲效器外形的部分,用下面這段替換掉:
'use strict'; var app = require('app'); var BrowserWindow = require('browser-window'); var mainWindow = null; app.on('ready', function() { mainWindow = new BrowserWindow({ frame: false, height: 700, resizable: false, width: 368 }); mainWindow.loadUrl('file://' + __dirname + '/app/index.html'); });
當窗口被定義了大小,我們也就是在自定義這個窗口,使得它不可拉伸沒有框架,讓它看起來就像一個真正的聲效器浮在桌面上。
現在問題來了 – 要如何移動或者關閉一個沒有標題欄的窗口。
很快我就會說到自定義窗口(和應用)的關閉動作,還會談到如何在主進程和渲染器進程中通信。不過現在讓我們先把目光聚焦到“拖拽效果”上。你可以在 app/css 目錄下找到 index.css 文件:
html, body { ... -webkit-app-region: drag; ... }
-webkit-app-region: drag;把整個 html 都變成了一個可拖拽的對象。現在問題來了,在可拖拽的對象上你怎么點擊啊?!好的,可能你會想到把 html 中某個部分的這個屬性值設置為no-drag;,那就允許該元素不可拖拽(但可以點擊了)。讓我們想想下面這段 index.css 片段:
.button-sound {
...
-webkit-app-region: no-drag;
}
展示聲效器
現在通過 main.js 文件可以創建一個新窗口,並在窗口中顯示出聲效器的界面。如果通過npm start啟動應用,你將會看到一個有動態效果的聲效器。因為我們就是從一個靜態頁面開始,所以現在你看到的也是不會動的頁面:
將下面這段代碼保存到 index.js 文件中(位置在 app/js 目錄下),運行后應用后,你會發現可以與聲效器交互了:
'use strict'; var soundButtons = document.querySelectorAll('.button-sound'); for (var i = 0; i < soundButtons.length; i++) { var soundButton = soundButtons[i]; var soundName = soundButton.attributes['data-sound'].value; prepareButton(soundButton, soundName); } function prepareButton(buttonEl, soundName) { buttonEl.querySelector('span').style.backgroundImage = 'url("img/icons/' + soundName + '.png")'; var audio = new Audio(__dirname + '/wav/' + soundName + '.wav'); buttonEl.addEventListener('click', function () { audio.currentTime = 0; audio.play(); }); }
通過上面這段代碼,我們:
- 獲取聲音按鈕,
- 迭代訪問按鈕的data-sound屬性值,
- 給每個按鈕加上背景圖,
- 通過 HTMLAudioElement 接口給每個按鈕都添加一個點擊事件,使之可以播放音頻,
通過下面這行命令運行你的應用吧:
npm start

通過遠程事件從瀏覽窗口中關閉應用
接着拉取02-basic-sound-machine的內容:
git checkout 02-basic-sound-machine
簡單來說 - 應用窗口(渲染器進程)不應該和 GUI 發生交互(也就是不應該和“關閉窗口”有關聯),Electron 的官方教程上說了:
考慮到在網頁中直接調用原生的 GUI 容易造成資源溢出,這很危險,開發者不能這么使用。如果開發者想要在網頁上執行 GUI 操作,必須要通過渲染器進程和主進程的通信實現。
Electron 為主進程和渲染器進程提供了 ipc (跨進程通信)模塊,ipc 模塊允許接收和發送通信頻道的信息。頻道由字符串表示(比如“channel-1”,“channel-2”這樣),可以用於區分不同的信息接收者。傳遞的信息中也可以包含數據。根據接收到的信息,訂閱者可以做出響應。信息傳遞的最大好處就是做到分離任務 – 主進程不需要知道是哪些渲染器進程發送了信息。

這正是我們想要做的 – 將主進程(main.js)訂閱到“關閉主窗口”頻道中,當用戶點擊關閉按鈕時,從渲染器進程(index.js)向該頻道發送信息。
Add the following to main.js to subscribe to a channel:
將下面的代碼實現了頻道訂閱,將它添加到 main.js 中:
var ipc = require('ipc'); ipc.on('close-main-window', function () { app.quit(); });
把 ipc 模塊包含進來之后,從頻道中訂閱信息就非常簡單了:過 on() 方法和頻道名稱,再加上一個回調函數就行了。
要向該頻道發送信息,就要把下面的代碼加入 index.js 中:
var ipc = require('ipc'); var closeEl = document.querySelector('.close'); closeEl.addEventListener('click', function () { ipc.send('close-main-window'); });
我們依然需要把 ipc 模塊引入到文件中,給關閉按鈕綁定點擊事件。當點擊了關閉按鈕時,通過 send() 方法發送一條信息到“關閉主窗口”頻道。
不要忘記在在 index.css 中將關閉按鈕設置為不可拖拽:
.settings { ... -webkit-app-region: no-drag; }
就這樣,我們的應用現在可以通過按鈕關掉了。ipc 的通信可以通過事件和參數的傳遞變得很復雜,在后文中會有傳遞參數的例子。
通過全局快捷鍵播放聲音
拉取03-closable-sound-machine:
git checkout 03-closable-sound-machine
聲效器的地基已經打的不錯。但是我們還面臨着使用性的問題 – 這個應用要始終保持在桌面最前方,且可以被重復點擊。
這就是全局快捷鍵要介入的地方。Electron 提供了全局快捷模塊(global shortcut module)允許開發者捕獲組合鍵並做出相應的反應。在 Eelctron 中組合鍵被稱為加速器,它以字符串的形式被記錄下(比如 “Ctrl+Shift+1”)。
因為我們想要捕獲到原生的 GUI 事件(全局快捷鍵),並執行應用窗口事件(播放聲音),我們將使用 ipc 模塊從主進程發送信息到渲染器進程。

在看代碼之前,還有兩件事情要我們考慮:
- 全局快捷鍵會在 app 的 ready 事件被觸發后注冊(相關代碼片段要被包含在 ‘ready’ 中)
- 通過 ipc 模塊從主進程向渲染器進程發送信息,你必須使用窗口對象的引用(類似於createdWindow.webContents.send(‘channel’))。
記住上面的兩點了嗎?現在讓我們來改寫* 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 模塊,當應用進入ready狀態之時,我們將會注冊兩個快捷鍵 – ‘Ctrl+Shift+1’ 和 ‘Ctrl+Shift+2’。這兩個快捷鍵可以通過不同的參數向“全局快捷鍵”頻道( “global-shortcut”channel)發送信息。通過參數匹配到到底要播放哪種聲音,將下面的代碼加入 index.js 中:
ipc.on('global-shortcut', function (arg) {
var event = new MouseEvent('click');
soundButtons[arg].dispatchEvent(event);
});
為了保證整個架構足夠簡單,我們將會用 soundButtons 選擇器模擬按鈕的點擊播放聲音。當發送的信息是“1”時,我們將會獲取 soundButtons[1] 元素,觸發鼠標點擊事件(注意:在生產環境中的應用,你需要封裝好播放聲音的代碼,然后執行它)。
在新窗口中通過用戶設置配置 modifier keys
下面請拉取04-global-shortcuts-bound:
git checkout 04-global-shortcuts-bound
通常我們會同時運行好多個應用,聲效器中設置的快捷鍵很可能已經被占用了。所以現在要引入一個設置界面,允許用戶更改修飾鍵(modifier keys)的原因(Ctrl、Alt 和 Shift)。
要完成這一個功能,我們需要做下面這些事情:
- 在主界面上添加設置按鈕,
- 實現一個設置窗口(設置頁面上有對應的HTML、CSS 和 JS),
- 開啟和關閉設置窗口,以及更新全局快捷鍵的 ipc 信息,
- 從用戶的系統中讀寫存儲設置信息的 JSON 文件。
piu~ 以上就是我們要做的。
設置按鈕和設置窗口
和關閉主窗口類似,我們將會把事件綁定到設置按鈕上,(settings button),在* index.js *中加入發送給頻道的信息:
var settingsEl = document.querySelector('.settings');
settingsEl.addEventListener('click', function () {
ipc.send('open-settings-window');
});
當點擊了設置按鈕,將會有一條信息向“打開設置窗口”這個頻道發送。* main.js 可以響應這個事件,並打開一個新窗口,將以下代碼加入 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;
});
});
這一步和之前的類似,我們將會打開一個新的窗口。唯一的不同點就是,為了防止實例化兩個一樣的對象,我們將會檢查設置窗口是否已經被打開了。
當上述代碼成功執行之后,我們需要再添加一個關閉設置窗口的動作。類似的,我們需要向頻道中發送一條信息,但這次是從* settings.js 中發送(關閉按鈕的事件是在 settings.js 中)。新建 settings.js *文件,並添加以下代碼(如果已經有該文件,就直接在原文件中添加):
'use strict';
var ipc = require('ipc');
var closeEl = document.querySelector('.close');
closeEl.addEventListener('click', function (e) {
ipc.send('close-settings-window');
});
在 main.js 中監聽該頻道:
ipc.on('close-settings-window', function () {
if (settingsWindow) {
settingsWindow.close();
}
});
現在,設置窗口已經可以實現我們的邏輯了。
用戶設置的讀寫
執行05-settings-window-working:
git checkout 05-settings-window-working
設置窗口的交互過程是,存儲設置信息以及刷新應用:
- 創建一個 JSON 文件用於讀寫用戶設置,
- 用這個設置初始化設置窗口,
- 通過用戶的操作更新這個設置文檔,
- 通知主進程要更新設置頁面。
我們可以把實現讀寫設置的部分直接寫進 main.js 中,但是如果把這部分獨立成模塊,可以隨處引用這樣不是更好嗎?
使用 JSON 做配置文件
現在我們要創建一個 configuration.js 文件,再將這個文件引入到項目中。Node.js 使用了 CommonJS 作為編寫模塊的規范,也就是說你需要將你的 API 和這個 API 中可用的函數都要暴露出來。

為了更簡單地讀寫文件,我們將會使用 nconf 模塊,這個模塊封裝了 JSON 文件的讀寫。但首先,我們需要將這個模塊包含到項目中來:
npm install --save nconf
這行命令意味着 nconf 模塊將會作為應用依賴被安裝到項目中,當我們要發布應用的時候,這個模塊會被一起打包給用戶(save-dev 參數會使安裝的模塊只出現在開發階段,發布應用的時候不會被包含進去)。
在根目錄下創建 configuration.js 文件,它的內容非常簡單:
'use strict'; var nconf = require('nconf').file({file: getUserHome() + '/sound-machine-config.json'}); function saveSettings(settingKey, settingValue) { nconf.set(settingKey, settingValue); nconf.save(); } function readSettings(settingKey) { nconf.load(); return nconf.get(settingKey); } function getUserHome() { return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; } module.exports = { saveSettings: saveSettings, readSettings: readSettings };
我們要把文件位置和文件名傳 nconf 模塊(用 Node.js 的 process.env 獲取到文件的位置),具體路徑會根據平台而異。
通過 nconf 模塊的 set() 和 get() 方法結合文件操作的 save() 和 load(),我們可以實現設置文件的讀寫操作,然后通過 module.exports 將接口暴露到外部。
初始化默認的快捷鍵設置
在講設置交互之前,為了避免用戶是第一次打開這個應用,要先初始化一個設置文件。我們將會以數組的形式儲存熱鍵,對應的鍵是 “shortcutKeys”,儲存在 main.js 中,我們需要把 configuration 模塊包含到項目中:
'use strict'; var configuration = require('./configuration'); app.on('ready', function () { if (!configuration.readSettings('shortcutKeys')) { configuration.saveSettings('shortcutKeys', ['ctrl', 'shift']); } ... }
我們需要先檢測鍵 ‘shortcutKeys’ 是否已經有對應的值了,如果沒有我們需要初始化一個值。
在 main.js 中,我們將重寫全局快捷鍵的注冊方法,在之后我們更新設置的時候,會直接調用這個方法。將原來的注冊代碼改成以下內容:
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);
});
}
上述方法重置了全局快捷鍵的值,從設置中讀取熱鍵的數組,將它傳入加速器兼容字符串(Accelerator-compatible)並注冊新鍵。
設置窗口的交互
回到 settings.js 文件,我們需要綁定點擊事件來改變我們的全局快捷鍵。首先,我們將會遍歷復選框,記錄下被勾選的選項(從 configuration 模塊中讀值):
var configuration = require('../configuration.js'); var modifierCheckboxes = document.querySelectorAll('.global-shortcut'); for (var i = 0; i < modifierCheckboxes.length; i++) { var shortcutKeys = configuration.readSettings('shortcutKeys'); var modifierKey = modifierCheckboxes[i].attributes['data-modifier-key'].value; modifierCheckboxes[i].checked = shortcutKeys.indexOf(modifierKey) !== -1; ... // Binding of clicks comes here }
現在我們需要綁定復選框的行為。考慮到設置窗口(和它的渲染器進程)是不允許改變 GUI 綁定的。這說明我們需要從 setting.js 中發送信息(之后會處理這個信息的):
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');
}
這段代碼看起來比較長,但事實上它很簡單。我們將會遍歷所有的復選框,並綁定點擊事件,在每次點擊的時候檢查設置數組中是否包含有熱鍵。根據檢查結果,更改數組,將結果保存到設置中,並向主進程發送信息,更新我們的全局快捷鍵。
現在的工作就是在 main.js 中將 ipc 信息訂閱到“設置全局快捷鍵”頻道,並更新我們的全局快捷鍵:
ipc.on('set-global-shortcuts', function () { setGlobalShortcuts(); });
就這么簡單,我們的全局快捷鍵已經可配置了!
菜單中要放什么?
接下來拉取 06-shortcuts-configurable:
git checkout 06-shortcuts-configurable
另一個在桌面應用中的重要概念就是“菜單”,比如右鍵菜單(點擊右鍵出現的菜單)、托盤菜單(通常會有一個托盤 icon)和應用菜單(在 OS X 中)等等。
在這一節中,我們將會添加一個托盤菜單。我們也將會借此機會嘗試在 remote 模塊中使用別的進程間的通信方式。

remote 模塊從渲染器進程到主進程完成 RPC 類型的調用。將模塊引入的時候,這個模塊是在主進程中被實例化的,所以它們的方法也會在主進程中被執行。實際開發中,這個行為是在遠程地請求 index.js 中的原生 GUI 模塊,然后又在 main.js 中調用 GUI 的方法。這么做的話,你需要在 index.js 中將 BrowserWindow 模塊引入,然后實例化一個新的瀏覽器窗口。其實在主進程中有一個同步的調用,實際上是這個調用創建了新的瀏覽器窗口。
現在讓我們來看看要怎么樣創建一個菜單,並在渲染器進程中將它綁定到一個托盤圖標上。將下面這段代碼加入 index.js 中:
var remote = require('remote'); var Tray = remote.require('tray'); var Menu = remote.require('menu'); var path = require('path'); var trayIcon = null; if (process.platform === 'darwin') { trayIcon = new Tray(path.join(__dirname, 'img/tray-iconTemplate.png')); } else { trayIcon = new Tray(path.join(__dirname, 'img/tray-icon-alt.png')); } var trayMenuTemplate = [ { label: 'Sound machine', enabled: false }, { label: 'Settings', click: function () { ipc.send('open-settings-window'); } }, { label: 'Quit', click: function () { ipc.send('close-main-window'); } } ]; var trayMenu = Menu.buildFromTemplate(trayMenuTemplate); trayIcon.setContextMenu(trayMenu);
原生的 GUI 模塊(菜單和托盤)通過remote模塊包含進來比較安全。
OS X 支持圖片模板(將圖片文件名以 ‘Template’ 結尾,就會被定義成為圖片模板),托盤圖標可以通過模板來定義,這樣我們的圖標就會有“暗黑”和“光明”兩個主題了。其他的操作系統用正常的圖標就行。
在 Electron 中有很多綁定菜單的方法。這里介紹的方法只是創建了一個菜單模板(將菜單項用數組的方式存儲),然后通過這個模板創建菜單,托盤 icon 再綁定上這個菜單,就實現了我們的菜單功能。
應用打包
接下來拉取 07-ready-for-packaging:
git checkout 07-ready-for-packaging
如果你做了一個應用結果人們連下載都下載不了,怎么會有人用呢?
通過 electron-packager 你可以將應用打包到全平台。這一步驟在 shell 中就可以完成,將應用打包好以后就能發布了。

它可以作為一個命令行應用或者作為開發應用過程中的一步,構建一個更復雜的開發場景不是這篇文章要談的內容,不過我們將通過 npm 腳本讓應用打包更簡單一點。用 electron-packager 打包的命令是這樣的:
electron-packager <location of project> <name of project> <platform> <architecture> <electron version> <optional options>
以上命令:
- 將目錄切換到項目所在路徑,
- 參數 ‘name of project’ 是你的項目名,參數 ‘plateform’ 確定了你要構建哪個平台的應用(Windows、Mac 還是 Linux),
- 參數 ‘architecture’ 決定了使用 x86 還是 x64 還是兩個架構都用,
- 決定了使用的 Electron 版本。
- 第一次打包應用需要比較久的時間,因為所有平台的二進制文件都需要下載,之后打包應用會比較快了。
在 Mac 上我是這么做的:
electron-packager ~/Projects/sound-machine SoundMachine --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/sound-machine/app/img/app-icon.icns
首先你要將圖標的格式轉換成 .icns(在 Mac 上)或者 .ico(在 Windows 上),網絡上有工具可以把 PNG 做這樣的轉換(確保下載的圖片的擴展名是 .icns 而不是 .hqx)。如果從非 Windows 的系統上打包了 Windows 的應用,你應該需要處理一下路徑(Mac 用戶可以用 brew,Linux 用戶可以用 apt-get)。
每次都要執行這么長的一句命令一點都不合理。所以你可以在 package.json 中添加另一個腳本。首先,將electron-packager 作為 development dependency 安裝:
npm install --save-dev electron-packager
然后在 package.json 中添加以下內容:
"scripts": { "start": "electron .", "package": "electron-packager ./ SoundMachine --all --out ~/Desktop/SoundMachine --version 0.30.2 --overwrite --icon=./app/img/app-icon.icns" }
接着執行:
npm run-script package
打包命令啟動了 electron-packager,在當前目錄中查看項目,在 Desktop 目錄中構建。如果你使用的是 Windows,腳本內容需要一些細微的更新。
聲效器目前是 100MB 大小,不要擔心,當你壓縮它之后,所占空間會減半。
如果你對此還有更大的計划,可以看看 electron-builder,它是根據 electron-packager 構建出的應用打包再做自動安裝的處理。
添加其他的特性
現在你可以嘗試開發別的功能了。
這里有一些方案,可以啟發你的靈感:
- 應用的使用手冊,說明了有那些快捷鍵和應用作者,
- 在應用中給使用手冊添加一個圖標和菜單入口,
- 構建一個打包腳本,用於快速構建和分發,
- 使用* node-notifier *添加一個提示系統,告訴用戶正在播放哪一個聲音,
- 使用* lodash *讓你的代碼更加干凈、具有更好的擴展性,
- 在打包之前不要忘了壓縮你的 CSS 和 JavaScript,
- 結合上文提到的* node-notifier *和一個服務器端的調用,通知用戶是否需要更新版本……
還有一個值得一試的東西 – 將代碼中關於瀏覽器窗口的邏輯抽離出來,通過類似 browserify 的工具創建一個和聲效器一樣的網頁。一份代碼,兩個產品(桌面端和 Web 引用)。酷斃了!
更深入研究 Electron
我們只是嘗試了 Electron 的冰山一角,想要知道監控主機的電源情況、獲取當前窗口的信息(比如光標的位置)等,Eletron 都能幫你做到。
對於所有的內置工具(通常在開發 Electron 應用時使用),查看 Electron API 文檔。
這些文檔在 Electron 的 github 倉庫中都能找到。
Sindre Sorhus 正在維護一份 Electron 資源清單,在那個上面你可以看到很多非常酷的項目,還能了解到一些系統架構做的很好的 Electron 應用,這些都能給你的開發帶來靈感。
Electron 是基於 io.js 的,大部分 Node.js 模塊都可以兼容,可以使用它們擴展你的應用。去 npmjs.com 上看看有沒有合適的。
這樣就夠了嗎?
當然不。
現在,可以來構建一個更大型的應用了。在這篇文章中,我幾乎沒有說到如何使用外部的庫或者構建工具來構建一個應用,不過用 ES6 和 Typescript 的語法結合 Angular 和 React 來構建 Electron 應用也很簡單,還可以用 gulp 或 grunt 構建流程。
干嘛不用你最喜歡的語言,框架和工具,來試試構建一個 Filckr 同步工具(借助 Filckr API 和 node-filckrapi)或者一個 Gmail 客戶端(使用 Google 的官方 Node.JS 客戶端庫?)
選一個自己感興趣的項目,開工吧!
