如何利用Electron開(kāi)發(fā)一個(gè)桌面APP
你是否曾經(jīng)想過(guò)可以用 HTML、CSS 和 JavaScript 這些前端技術(shù)來(lái)構(gòu)建跨平臺(tái)的桌面應(yīng)用?
使用 Electron 就能做到。
本文帶著你深入 Electron 的核心概念。
閱讀本文后,你會(huì)知道如何使用 Electron、HTML 和 CSS 構(gòu)建跨平臺(tái)桌面應(yīng)用。
在開(kāi)始之前,你可以提前看看我們?cè)诒窘坛讨幸獦?gòu)建的應(yīng)用。
Hear Me Type [譯者注:本文的示例應(yīng)用名為 Hear Me Type] 功能簡(jiǎn)單直接,應(yīng)用中每個(gè)鍵都會(huì)播放特有的聲音。比如我按下了 “A”,應(yīng)用會(huì)播放字母 A 特有的聲音。
該應(yīng)用目前有兩個(gè)版本可供下載:本教程的源碼,以及一個(gè)推薦給更有經(jīng)驗(yàn)的 Electron 用戶的高級(jí)版本。
因?yàn)槲疫€在為改善應(yīng)用以及添加一些新功能,所以代碼會(huì)發(fā)生變化,請(qǐng)一定注意看看有哪些更新。
就此打住,我們現(xiàn)在開(kāi)始學(xué)習(xí) Electron 并用它來(lái)創(chuàng)建***個(gè)應(yīng)用!
什么是 Electron?
Electron 是一個(gè)基于 Chrominum 和 Node.js 的跨平臺(tái)桌面應(yīng)用框架。
在這個(gè)框架中很容易構(gòu)建基于 HTML、CSS 和 JavaScript 技術(shù)的跨平臺(tái)應(yīng)用。構(gòu)建出來(lái)的應(yīng)用會(huì)很好地兼容 Mac、Windows 和 Linux 操作系統(tǒng)。
它還有其它一些特性:
- 自動(dòng)更新 —— 應(yīng)用支持自動(dòng)更新
- 原生菜單和通知 —— 可以創(chuàng)建原生應(yīng)用菜單和上下文菜單
- 應(yīng)用崩潰報(bào)告 —— 可以將崩潰報(bào)告提交到遠(yuǎn)程服務(wù)器
- 調(diào)試和分析 —— Chrominum 的內(nèi)容模塊可以發(fā)生性能瓶頸和緩慢的操作。你也可以在應(yīng)用中使用自己喜歡的 Chrome 開(kāi)發(fā)者工具。
- Windows installer —— 可以快速便捷地創(chuàng)建安裝包。
如果你對(duì) Electron 的功能感到滿意,我們就繼續(xù)深入,創(chuàng)建一個(gè)簡(jiǎn)單的 Electron 應(yīng)用。
動(dòng)手之前需要先安裝 Node.js。你還應(yīng)該申請(qǐng)一個(gè) GitHub 賬戶來(lái)保存和更新應(yīng)用。雖然這個(gè)賬戶并不是必須的,但我非常建議你去申請(qǐng)一個(gè)。Github 是一個(gè)行業(yè)標(biāo)準(zhǔn),學(xué)會(huì)使用 Github 非常重要。
我會(huì)在教程中使用 Github。
開(kāi)始
準(zhǔn)備好之后,打開(kāi)系統(tǒng)終端窗口。
按照下面的介紹將 Electron Quick Start 這個(gè) Git 庫(kù)克隆到計(jì)算機(jī)上。
我們會(huì)基于 Electron Quick Start 來(lái)創(chuàng)建自己的軟件。
- # Clone the Quick Start repository
- git clone
- # Go into the repository
- cd electron-quick-start
- # Install the dependencies and run
- npm install && npm start
完成上面的步驟之后,你會(huì)看到一個(gè)像瀏覽器窗口的應(yīng)用打開(kāi)。它確實(shí)是一個(gè)瀏覽器窗口!
這個(gè)窗口顯示的樣子在不同的操作系統(tǒng)上會(huì)有所不同。我選擇使用 Windows 的經(jīng)典樣式。非常贊!

Quick-Start Electron 應(yīng)用的主窗口
正如我之前所說(shuō),你可以在應(yīng)用中使用 Chrome 開(kāi)發(fā)者工具,這個(gè)工具的用法跟在瀏覽器中的開(kāi)發(fā)者工具一樣,再贊一個(gè)!
Electron 應(yīng)用程序架構(gòu)
我們來(lái)看看這個(gè)應(yīng)用的源代碼及其文件結(jié)構(gòu)。可以使用你自己喜歡的編輯器或者 IDE 打開(kāi)項(xiàng)目,我使用 Atom …… 你猜到了 …… 它就是用 Electron 構(gòu)建的![譯者注:我比較喜歡 VSCode,也是基于 Electron 構(gòu)建的]
新應(yīng)用的目錄和文件結(jié)構(gòu)。
我們有一個(gè)基本的文件結(jié)構(gòu):
electron-quick-start
- - index.html
- - main.js
- - package.json
- - render.js
文件結(jié)構(gòu)類似于我們創(chuàng)建網(wǎng)頁(yè)的結(jié)構(gòu)。
我們有:
- index.html 是一個(gè) HTML5 頁(yè)面,它具有很重要的作用:提供畫布
- main.js 創(chuàng)建窗口并處理系統(tǒng)事件
- package.json 是應(yīng)用的啟動(dòng)腳本。它包含了應(yīng)用的信息,在主進(jìn)行中運(yùn)行
- render.js 處理應(yīng)用的渲染進(jìn)程
也許你對(duì)主進(jìn)程和渲染進(jìn)程存有疑問(wèn)。它們到底是什么,用來(lái)干什么?
很高興你有此疑問(wèn)。注意了,如果你來(lái)自瀏覽器的 JavaScript 領(lǐng)域,這對(duì)你來(lái)說(shuō)可能是一塊新的領(lǐng)域!
什么是進(jìn)程?
看到“進(jìn)程”這個(gè)詞的時(shí)候,想像一下操作系統(tǒng)級(jí)的進(jìn)程。那是運(yùn)行在系統(tǒng)中的計(jì)算機(jī)程序?qū)嵗?/p>
啟動(dòng) Electron 應(yīng)用之后,查看 Windows 的任務(wù)管理器或者 macOS 的活動(dòng)監(jiān)視器,就可以看到與這個(gè)應(yīng)用相關(guān)的進(jìn)程。

這些進(jìn)程都是并行運(yùn)行的,為每個(gè)進(jìn)程分配的內(nèi)存和資源相互隔離。
如果我想創(chuàng)建一個(gè) for 循環(huán)在渲染進(jìn)程中逐步處理一些事件。
- var a = 1;
- for ( a = 1; a < 10; a ++) {
- console.log('This is a for loop');
- }
這些改變只在渲染進(jìn)程中有效,根本不會(huì)對(duì)主進(jìn)行造成影響。“This is a for loop”消息只會(huì)出現(xiàn)在渲染模塊中。
主進(jìn)程
主進(jìn)程控制著應(yīng)用的生命周期。它內(nèi)置了完整的 Node.js API,可以打開(kāi)對(duì)話框,創(chuàng)建渲染進(jìn)程,還可以處理其它其它與操作系統(tǒng)的交互操作,包括啟動(dòng)和退出應(yīng)用。
按照慣例,這個(gè)進(jìn)程寫在名為 main.js 的文件中。不過(guò)你想使用其它名字也沒(méi)有問(wèn)題。
你可以在 package.json 文件中配置主進(jìn)程文件的名稱。
試驗(yàn)一下,打開(kāi) package.json 并將
- “main”: “main.js”,
修改為
- “main”: “mainTest.js”,
啟動(dòng)應(yīng)用看看它是否仍然正常運(yùn)行。
注意,主進(jìn)程只有一個(gè)。
渲染進(jìn)程
應(yīng)用中的渲染進(jìn)程是一個(gè)瀏覽器窗口。與主進(jìn)程不同,可以存在多個(gè)獨(dú)立的渲染進(jìn)程。
因?yàn)殇秩具M(jìn)程是各自獨(dú)立的,如果其中一個(gè)崩潰了并不會(huì)影響到其它進(jìn)程,這得益于 Chrominum 的多進(jìn)程架構(gòu)。
這些瀏覽器窗口就像演示網(wǎng)頁(yè)一樣,也可以被隱藏或自定義。
不過(guò) Electron 內(nèi)置了完整的 Node.js API,也就是說(shuō)我們可以打開(kāi)對(duì)話框或進(jìn)行其它與操作系統(tǒng)的交互。
這樣考慮;

[來(lái)源: Kristian Poslek]
還有一個(gè)問(wèn)題,它們能以某種方式聯(lián)系起來(lái)嗎?
這些進(jìn)程都在獨(dú)立運(yùn)行,但他們?nèi)匀恍枰ㄐ?,因?yàn)樗鼈冐?fù)責(zé)不同的任務(wù),這尤其需要通信。
為此,存在一個(gè)進(jìn)程間的通信系統(tǒng)或者 IPC。你可以使用 IPC 在主進(jìn)程和渲染進(jìn)程間進(jìn)行通信。對(duì)于這個(gè)知識(shí)點(diǎn)更深入一些的解釋,請(qǐng)閱讀 Christian Engvall 的文章。
上面說(shuō)的都是開(kāi)發(fā) Electron 應(yīng)用的基礎(chǔ)知識(shí)。
現(xiàn)在回到我們的代碼!
私有化
讓我們給應(yīng)用所在的目錄起一個(gè)合適的名稱。
將目錄名從 electron-quick-start 改為 hear-me-type-tutorial。
重新在編輯器或 IDE 中打開(kāi)這個(gè)目錄,我們打開(kāi) package.json 來(lái)進(jìn)一步定制應(yīng)用標(biāo)識(shí)。
package.json 包含了至關(guān)重要的應(yīng)用信息。這里定義應(yīng)用的名稱、版本、主文件、作者、許可協(xié)議等。
現(xiàn)在把作者改成自己的名稱,自豪感油然而生。
找到 “author” 參數(shù),然后將值改成自己的名稱。它看起來(lái)像這樣:
- “author”: “Carol Pelu”,
我們還要改其它一些參數(shù)。在 package.json 中找到 name(名稱) 和 description(說(shuō)明)并修改它們:
帥呆了!現(xiàn)在應(yīng)用有了新名稱,還有簡(jiǎn)短而清晰的說(shuō)明。
記住,你可以在終端運(yùn)行 npm start 來(lái)運(yùn)行應(yīng)用,以觀察所做的改變。
我們會(huì)繼續(xù)在應(yīng)用中添加一些期望的功能。我們想在按下每個(gè)鍵的時(shí)候播放不同的聲音。
哦,有趣的功能!
沒(méi)有功能的應(yīng)用是什么?什么都不是……
現(xiàn)在我們要給它添加功能。
為了讓應(yīng)用響應(yīng)輸入,我們必須定義一個(gè)元素來(lái)捕捉事件,然后觸發(fā)期望的動(dòng)作。
為此,我們會(huì)創(chuàng)建一個(gè)具有特殊名稱的若干 audio 元素,對(duì)應(yīng)于按鍵。然后我們會(huì)使用一個(gè) switch 語(yǔ)句來(lái)定位按下的鍵,播放與之關(guān)聯(lián)的聲音。
如果你現(xiàn)在覺(jué)得有點(diǎn)復(fù)雜,不要怕,我會(huì)指引你一步步進(jìn)行。
下載這個(gè)壓縮包,它包含了我們要使用的所有聲音文件。很快就會(huì)用到!
我們會(huì)打開(kāi) index.html 文件,創(chuàng)建一個(gè)<audio> 元素,在我們的應(yīng)用中加入聲音。
在<body> 元素內(nèi)部,創(chuàng)建一個(gè) div 元素,將其 class 屬性設(shè)置為 audio。
在剛剛創(chuàng)建的 div 元素,創(chuàng)建<audio> 元素,將其 ID 命名為 “A”,source 屬性設(shè)置為 “sounds/A.mp3”,preload 屬性設(shè)置為 “auto”。
preload=”auto” 用于告訴應(yīng)用在頁(yè)面加載的時(shí)候就加載完整的聲音文件。index.html 是應(yīng)用的主文件,所有聲音都會(huì)在應(yīng)用啟動(dòng)的時(shí)候加載。
下面是代碼:
- <div class="audio">
- <audio id="A" src="sounds/A.mp3" preload="auto"></audio>
- </div>
你的 index.html 應(yīng)該就像這樣。
現(xiàn)在 指向一個(gè)未知的文件。我們要?jiǎng)?chuàng)建一個(gè)名為 soudes 的目錄,并將所有聲音文件解壓到這個(gè)目錄中。
非常好!現(xiàn)在唯一缺少的是 JavaScriopt 代碼。
創(chuàng)建一個(gè)叫 functions.js 的新文件,并在 index.html 中通過(guò) require 引用它,這樣應(yīng)用運(yùn)行的時(shí)候才會(huì)執(zhí)行 JS 代碼。
在示例的 require(./renderer.js') 下載添加這樣一行:
- require('./functions.js')
之后項(xiàng)目看起來(lái)是這樣的:
不錯(cuò)!一切就緒,下面是見(jiàn)證奇跡的時(shí)刻。
打開(kāi) functions.js 文件并將下面的代碼添加到文件中。我稍后解釋這段代碼。
- document.onkeydown = function(e) {
- switch (e.keyCode) {
- case 65:
- document.getElementById('A').play();
- break;
- default:
- console.log("Key is not found!");
- }
- };
代碼現(xiàn)在是這樣:
打開(kāi) Bash 或者終端窗口,確保當(dāng)前是在項(xiàng)目目錄下,運(yùn)行 npm start 來(lái)啟動(dòng)應(yīng)用。
調(diào)整揚(yáng)聲器的音量并敲下按鍵。
- #MindBlown
JS 代碼非常簡(jiǎn)單明了。
我們使用了 document 對(duì)象上的 onkeydown 事件,在這里找到被訪問(wèn)的元素。記住,document 對(duì)象是應(yīng)用的主窗口。
我們?cè)谠谀涿瘮?shù)中使用了 switch 語(yǔ)句,根據(jù) Unicode 值來(lái)判斷按鍵。
如果找到按鍵對(duì)應(yīng)的 Unicode 值,就會(huì)播放相應(yīng)的聲音,否則拋出 “not found” 錯(cuò)誤。這個(gè)錯(cuò)誤要在控制臺(tái)中去找。
多么愉快的過(guò)程!
你可能注意到了,我們的聲音文件包含了 A-Z 和 0-9 這些鍵,把它們都用起來(lái)。
在 index.html 中為每個(gè)有對(duì)應(yīng)聲音文件的鍵都創(chuàng)建一個(gè) 元素。
之后代碼就像這樣:

當(dāng)然你可以用拷貝粘貼:
- <audio id="B" src="sounds/B.mp3" preload="auto"></audio>
- <audio id="C" src="sounds/C.mp3" preload="auto"></audio>
- <audio id="D" src="sounds/D.mp3" preload="auto"></audio>
- <audio id="E" src="sounds/E.mp3" preload="auto"></audio>
- <audio id="F" src="sounds/F.mp3" preload="auto"></audio>
- <audio id="G" src="sounds/G.mp3" preload="auto"></audio>
- <audio id="H" src="sounds/H.mp3" preload="auto"></audio>
- <audio id="I" src="sounds/I.mp3" preload="auto"></audio>
- <audio id="J" src="sounds/J.mp3" preload="auto"></audio>
- <audio id="K" src="sounds/K.mp3" preload="auto"></audio>
- <audio id="L" src="sounds/L.mp3" preload="auto"></audio>
- <audio id="M" src="sounds/M.mp3" preload="auto"></audio>
- <audio id="N" src="sounds/N.mp3" preload="auto"></audio>
- <audio id="O" src="sounds/O.mp3" preload="auto"></audio>
- <audio id="P" src="sounds/P.mp3" preload="auto"></audio>
- <audio id="Q" src="sounds/Q.mp3" preload="auto"></audio>
- <audio id="R" src="sounds/R.mp3" preload="auto"></audio>
- <audio id="S" src="sounds/S.mp3" preload="auto"></audio>
- <audio id="T" src="sounds/T.mp3" preload="auto"></audio>
- <audio id="U" src="sounds/U.mp3" preload="auto"></audio>
- <audio id="V" src="sounds/V.mp3" preload="auto"></audio>
- <audio id="W" src="sounds/W.mp3" preload="auto"></audio>
- <audio id="X" src="sounds/X.mp3" preload="auto"></audio>
- <audio id="Y" src="sounds/Y.mp3" preload="auto"></audio>
- <audio id="Z" src="sounds/Z.mp3" preload="auto"></audio>
- <audio id="0" src="sounds/0.mp3" preload="auto"></audio>
- <audio id="1" src="sounds/1.mp3" preload="auto"></audio>
- <audio id="2" src="sounds/2.mp3" preload="auto"></audio>
- <audio id="3" src="sounds/3.mp3" preload="auto"></audio>
- <audio id="4" src="sounds/4.mp3" preload="auto"></audio>
- <audio id="5" src="sounds/5.mp3" preload="auto"></audio>
- <audio id="6" src="sounds/6.mp3" preload="auto"></audio>
- <audio id="7" src="sounds/7.mp3" preload="auto"></audio>
- <audio id="8" src="sounds/8.mp3" preload="auto"></audio>
- <audio id="9" src="sounds/9.mp3" preload="auto"></audio>
現(xiàn)在在 functions.js 中加點(diǎn)代碼。
你可以在這個(gè)網(wǎng)站上查到字符碼(charCode 或 keyCode)。
當(dāng)然,也可以使用拷貝粘貼:
- document.onkeydown = function(e) {
- switch (e.keyCode) {
- case 48:
- document.getElementById('0').play();
- break;
- case 49:
- document.getElementById('1').play();
- break;
- case 50:
- document.getElementById('2').play();
- break;
- case 51:
- document.getElementById('3').play();
- break;
- case 52:
- document.getElementById('4').play();
- break;
- case 53:
- document.getElementById('5').play();
- break;
- case 54:
- document.getElementById('6').play();
- break;
- case 55:
- document.getElementById('7').play();
- break;
- case 56:
- document.getElementById('8').play();
- break;
- case 57:
- document.getElementById('9').play();
- break;
- case 65:
- document.getElementById('A').play();
- break;
- case 66:
- document.getElementById('B').play();
- break;
- case 67:
- document.getElementById('C').play();
- break;
- case 68:
- document.getElementById('D').play();
- break;
- case 69:
- document.getElementById('E').play();
- break;
- case 70:
- document.getElementById('F').play();
- break;
- case 71:
- document.getElementById('G').play();
- break;
- case 72:
- document.getElementById('H').play();
- break;
- case 73:
- document.getElementById('I').play();
- break;
- case 74:
- document.getElementById('J').play();
- break;
- case 75:
- document.getElementById('K').play();
- break;
- case 76:
- document.getElementById('L').play();
- break;
- case 77:
- document.getElementById('M').play();
- break;
- case 78:
- document.getElementById('N').play();
- break;
- case 79:
- document.getElementById('O').play();
- break;
- case 80:
- document.getElementById('P').play();
- break;
- case 81:
- document.getElementById('Q').play();
- break;
- case 82:
- document.getElementById('R').play();
- break;
- case 83:
- document.getElementById('S').play();
- break;
- case 84:
- document.getElementById('T').play();
- break;
- case 85:
- document.getElementById('U').play();
- break;
- case 86:
- document.getElementById('V').play();
- break;
- case 87:
- document.getElementById('W').play();
- break;
- case 88:
- document.getElementById('X').play();
- break;
- case 89:
- document.getElementById('Y').play();
- break;
- case 90:
- document.getElementById('Z').play();
- break;
- default:
- console.log("Key is not found!");
- }
- };
大功告成!
應(yīng)用的主要功能已經(jīng)完成了,但仍然還有些工作要做!
完善!
應(yīng)用程序的功能已經(jīng)完成,但它尚不完善。
比如,可以在 index.html 中修應(yīng)用的標(biāo)題和主窗口的內(nèi)容。
此外,這個(gè)應(yīng)用沒(méi)有設(shè)計(jì)炫麗的色彩,也沒(méi)有使用漂亮的圖片。
充分發(fā)揮你的想像,找出改進(jìn)應(yīng)用設(shè)計(jì)的方法。
代碼也不***,我們有很多相同的代碼需要優(yōu)化以減少代碼行數(shù),至少看起來(lái)不那么難受。
重復(fù)代碼真不是好做法!
測(cè)試!就是測(cè)試!
好的軟件需要通過(guò)測(cè)試。
我建議你先按鍵看看,會(huì)發(fā)生什么事。
***的情況是你會(huì)聽(tīng)到每個(gè)鍵對(duì)應(yīng)的聲音。但如果你快速的按下多個(gè)鍵的時(shí)候會(huì)發(fā)生什么呢?如果按下了非預(yù)期的鍵,比如 Home 和 NumLock,又會(huì)發(fā)生什么呢?
如果你最小化程序再嘗試著按鈕會(huì)怎樣?能聽(tīng)到聲音嗎?如果沒(méi)有選擇應(yīng)用程序窗口,按下鍵盤時(shí),還會(huì)聽(tīng)到聲音嗎?
答案是否定的。
Electron 的架構(gòu)決定了其行為。它允許你可以像 C# 語(yǔ)言那樣使用所有按鍵,但你不能注冊(cè)個(gè)性化的按鍵。這已經(jīng)超出了 Electron 應(yīng)用的使用范圍。
一行行的執(zhí)行代碼,并深度中斷它,看看會(huì)發(fā)生什么,Electron 會(huì)拋出什么樣的錯(cuò)誤。這一練習(xí)能幫助你更好地進(jìn)行調(diào)試。如果你知道應(yīng)用的缺陷,那么你就知道該如何去修復(fù),讓應(yīng)用變得更好。
我故意在 functions.js 文件中使用了一個(gè)廢棄的 JavaScript 事件,你能發(fā)現(xiàn)嗎?
如果你找到了,我希望你能在不改變應(yīng)用程序功能的情況下替換它。
使用廢棄的代碼很不好,這可能會(huì)導(dǎo)致嚴(yán)重錯(cuò)誤,你甚至可能意識(shí)不到這些錯(cuò)誤的存在。關(guān)注語(yǔ)言的***文件,了解可能發(fā)生的變化,始終了解***狀態(tài)。