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

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

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

[來源: Kristian Poslek]
還有一個問題,它們能以某種方式聯(lián)系起來嗎?
這些進程都在獨立運行,但他們仍然需要通信,因為它們負責不同的任務,這尤其需要通信。
為此,存在一個進程間的通信系統(tǒng)或者 IPC。你可以使用 IPC 在主進程和渲染進程間進行通信。對于這個知識點更深入一些的解釋,請閱讀 Christian Engvall 的文章。
上面說的都是開發(fā) Electron 應用的基礎知識。
現(xiàn)在回到我們的代碼!
私有化
讓我們給應用所在的目錄起一個合適的名稱。
將目錄名從 electron-quick-start 改為 hear-me-type-tutorial。
重新在編輯器或 IDE 中打開這個目錄,我們打開 package.json 來進一步定制應用標識。
package.json 包含了至關重要的應用信息。這里定義應用的名稱、版本、主文件、作者、許可協(xié)議等。
現(xiàn)在把作者改成自己的名稱,自豪感油然而生。
找到 “author” 參數(shù),然后將值改成自己的名稱。它看起來像這樣:
- “author”: “Carol Pelu”,
我們還要改其它一些參數(shù)。在 package.json 中找到 name(名稱) 和 description(說明)并修改它們:
帥呆了!現(xiàn)在應用有了新名稱,還有簡短而清晰的說明。
記住,你可以在終端運行 npm start 來運行應用,以觀察所做的改變。
我們會繼續(xù)在應用中添加一些期望的功能。我們想在按下每個鍵的時候播放不同的聲音。
哦,有趣的功能!
沒有功能的應用是什么?什么都不是……
現(xiàn)在我們要給它添加功能。
為了讓應用響應輸入,我們必須定義一個元素來捕捉事件,然后觸發(fā)期望的動作。
為此,我們會創(chuàng)建一個具有特殊名稱的若干 audio 元素,對應于按鍵。然后我們會使用一個 switch 語句來定位按下的鍵,播放與之關聯(lián)的聲音。
如果你現(xiàn)在覺得有點復雜,不要怕,我會指引你一步步進行。
下載這個壓縮包,它包含了我們要使用的所有聲音文件。很快就會用到!
我們會打開 index.html 文件,創(chuàng)建一個<audio> 元素,在我們的應用中加入聲音。
在<body> 元素內部,創(chuàng)建一個 div 元素,將其 class 屬性設置為 audio。
在剛剛創(chuàng)建的 div 元素,創(chuàng)建<audio> 元素,將其 ID 命名為 “A”,source 屬性設置為 “sounds/A.mp3”,preload 屬性設置為 “auto”。
preload=”auto” 用于告訴應用在頁面加載的時候就加載完整的聲音文件。index.html 是應用的主文件,所有聲音都會在應用啟動的時候加載。
下面是代碼:
- <div class="audio">
- <audio id="A" src="sounds/A.mp3" preload="auto"></audio>
- </div>
你的 index.html 應該就像這樣。
現(xiàn)在 指向一個未知的文件。我們要創(chuàng)建一個名為 soudes 的目錄,并將所有聲音文件解壓到這個目錄中。
非常好!現(xiàn)在唯一缺少的是 JavaScriopt 代碼。
創(chuàng)建一個叫 functions.js 的新文件,并在 index.html 中通過 require 引用它,這樣應用運行的時候才會執(zhí)行 JS 代碼。
在示例的 require(./renderer.js') 下載添加這樣一行:
- require('./functions.js')
之后項目看起來是這樣的:
不錯!一切就緒,下面是見證奇跡的時刻。
打開 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)在是這樣:
打開 Bash 或者終端窗口,確保當前是在項目目錄下,運行 npm start 來啟動應用。
調整揚聲器的音量并敲下按鍵。
- #MindBlown
JS 代碼非常簡單明了。
我們使用了 document 對象上的 onkeydown 事件,在這里找到被訪問的元素。記住,document 對象是應用的主窗口。
我們在在匿名函數(shù)中使用了 switch 語句,根據(jù) Unicode 值來判斷按鍵。
如果找到按鍵對應的 Unicode 值,就會播放相應的聲音,否則拋出 “not found” 錯誤。這個錯誤要在控制臺中去找。
多么愉快的過程!
你可能注意到了,我們的聲音文件包含了 A-Z 和 0-9 這些鍵,把它們都用起來。
在 index.html 中為每個有對應聲音文件的鍵都創(chuà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 中加點代碼。
你可以在這個網(wǎng)站上查到字符碼(charCode 或 keyCode)。
當然,也可以使用拷貝粘貼:
- 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!");
- }
- };
大功告成!
應用的主要功能已經(jīng)完成了,但仍然還有些工作要做!
完善!
應用程序的功能已經(jīng)完成,但它尚不完善。
比如,可以在 index.html 中修應用的標題和主窗口的內容。
此外,這個應用沒有設計炫麗的色彩,也沒有使用漂亮的圖片。
充分發(fā)揮你的想像,找出改進應用設計的方法。
代碼也不***,我們有很多相同的代碼需要優(yōu)化以減少代碼行數(shù),至少看起來不那么難受。
重復代碼真不是好做法!
測試!就是測試!
好的軟件需要通過測試。
我建議你先按鍵看看,會發(fā)生什么事。
***的情況是你會聽到每個鍵對應的聲音。但如果你快速的按下多個鍵的時候會發(fā)生什么呢?如果按下了非預期的鍵,比如 Home 和 NumLock,又會發(fā)生什么呢?
如果你最小化程序再嘗試著按鈕會怎樣?能聽到聲音嗎?如果沒有選擇應用程序窗口,按下鍵盤時,還會聽到聲音嗎?
答案是否定的。
Electron 的架構決定了其行為。它允許你可以像 C# 語言那樣使用所有按鍵,但你不能注冊個性化的按鍵。這已經(jīng)超出了 Electron 應用的使用范圍。
一行行的執(zhí)行代碼,并深度中斷它,看看會發(fā)生什么,Electron 會拋出什么樣的錯誤。這一練習能幫助你更好地進行調試。如果你知道應用的缺陷,那么你就知道該如何去修復,讓應用變得更好。
我故意在 functions.js 文件中使用了一個廢棄的 JavaScript 事件,你能發(fā)現(xiàn)嗎?
如果你找到了,我希望你能在不改變應用程序功能的情況下替換它。
使用廢棄的代碼很不好,這可能會導致嚴重錯誤,你甚至可能意識不到這些錯誤的存在。關注語言的***文件,了解可能發(fā)生的變化,始終了解***狀態(tài)。