Electron:輕鬆實現跨平台桌面應用程序
已發表: 2022-03-11今年早些時候,Github 發布了其著名的開源編輯器 Atom 的核心 Atom-Shell,並在特殊場合將其重命名為Electron 。
與基於 Node.js 的桌面應用程序類別中的其他競爭對手不同,Electron 通過將 Node.js(直到最近發布的 io.js)的強大功能與 Chromium 引擎相結合,為這個已經成熟的市場帶來了自己的變化。我們最好的服務器和客戶端 JavaScript。
想像一個世界,我們可以構建高性能的、數據驅動的、跨平台的桌面應用程序,不僅由不斷增長的 NPM 模塊存儲庫提供支持,而且由整個 Bower 註冊表提供支持,以滿足我們所有的客戶端需求。
輸入電子。
在本教程中,我們將使用 Electron、Angular.js 和 Loki.js 構建一個簡單的密碼鑰匙串應用程序,這是一個輕量級的內存數據庫,具有 MongoDB 開發人員熟悉的語法。
此應用程序的完整源代碼可在此處獲得。
本教程假設:
- 讀者的機器上安裝了 Node.js 和 Bower。
- 他們熟悉 Node.js、Angular.js 和類似 MongoDB 的查詢語法。
獲取貨物
首先,我們需要獲取 Electron 二進製文件以便在本地測試我們的應用程序。 我們可以全局安裝它並將其用作 CLI,或者將其安裝在本地應用程序的路徑中。 我建議在全球範圍內安裝它,這樣我們就不必為我們開發的每個應用程序一遍又一遍地這樣做。
稍後我們將學習如何使用 Gulp 打包我們的應用程序以進行分發。 這個過程涉及復制 Electron 二進製文件,因此在我們的應用程序路徑中手動安裝它幾乎沒有意義。
要安裝 Electron CLI,我們可以在終端中輸入以下命令:
$ npm install -g electron-prebuilt
要測試安裝,請鍵入electron -h
,它應該會顯示 Electron CLI 的版本。
在撰寫本文時,Electron 的版本是0.31.2
。
設置項目
讓我們假設以下基本文件夾結構:
my-app |- cache/ |- dist/ |- src/ |-- app.js | gulpfile.js
… 其中: - cache/將用於在構建應用程序時下載 Electron 二進製文件。 - dist/將包含生成的分發文件。 - src/將包含我們的源代碼。 - src/app.js將是我們應用程序的入口點。
接下來,我們將導航到終端中的src/
文件夾並為我們的應用程序創建package.json
和bower.json
文件:
$ npm init $ bower init
我們將在本教程後面安裝必要的軟件包。
了解電子過程
Electron 區分兩種類型的過程:
- 主進程:我們應用程序的入口點,每當我們運行應用程序時都會執行的文件。 通常,此文件聲明應用程序的各種窗口,並且可以選擇用於使用 Electron 的 IPC 模塊定義全局事件偵聽器。
- 渲染器進程:我們應用程序中給定窗口的控制器。 每個窗口都創建自己的渲染器進程。
為了代碼清晰,應該為每個渲染器進程使用一個單獨的文件。 要為我們的應用程序定義主進程,我們將打開
src/app.js
並包含app
模塊來啟動應用程序,以及browser-window
模塊來創建我們應用程序的各種窗口(都是 Electron 核心的一部分),因此:
var app = require('app'), BrowserWindow = require('browser-window');
當應用程序實際啟動時,它會觸發一個ready
事件,我們可以綁定到該事件。 此時,我們可以實例化我們應用程序的主窗口:
var mainWindow = null; app.on('ready', function() { mainWindow = new BrowserWindow({ width: 1024, height: 768 }); mainWindow.loadUrl('file://' + __dirname + '/windows/main/main.html'); mainWindow.openDevTools(); });
關鍵點:
- 我們通過創建
BrowserWindow
對象的新實例來創建新窗口。 - 它將一個對像作為單個參數,允許我們定義各種設置,其中包括窗口的默認寬度和高度。
- 窗口實例有一個
loadUrl()
方法,允許我們在當前窗口中加載實際 HTML 文件的內容。 HTML 文件可以是本地的或遠程的。 - 窗口實例有一個可選的
openDevTools()
方法,允許我們在當前窗口中打開 Chrome 開發工具的實例以進行調試。
接下來,我們應該稍微組織一下我們的代碼。 我建議在我們的src/
文件夾中創建一個windows/
文件夾,我們可以在其中為每個窗口創建一個子文件夾,如下所示:
my-app |- src/ |-- windows/ |--- main/ |---- main.controller.js |---- main.html |---- main.view.js
... 其中main.controller.js
將包含我們應用程序的“服務器端”邏輯,而main.view.js
將包含我們應用程序的“客戶端”邏輯。
main.html
文件只是一個 HTML5 網頁,所以我們可以像這樣簡單地啟動它:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Password Keychain</title> </head> <body> <h1>Password Keychain</h1> </body> </html>
此時,我們的應用程序應該可以運行了。 為了測試它,我們可以在終端中的src
文件夾的根目錄中簡單地鍵入以下內容:
$ electron .
我們可以通過定義 package.son 文件的
start
腳本來自動化這個過程。
構建密碼鑰匙串桌面應用程序
要構建密碼鑰匙串應用程序,我們需要: - 一種添加、生成和保存密碼的方法。 - 複製和刪除密碼的便捷方式。
生成和保存密碼
一個簡單的表格就足以插入新密碼。 為了演示 Electron 中多個窗口之間的通信,首先在我們的應用程序中添加第二個窗口,它將顯示“插入”表單。 由於我們將多次打開和關閉此窗口,因此我們應該將邏輯包裝在一個方法中,以便我們可以在需要時簡單地調用它:
function createInsertWindow() { insertWindow = new BrowserWindow({ width: 640, height: 480, show: false }); insertWindow.loadUrl('file://' + __dirname + '/windows/insert/insert.html'); insertWindow.on('closed',function() { insertWindow = null; }); }
關鍵點:
- 我們需要在 BrowserWindow 構造函數的 options 對像中將show屬性設置為false ,以防止應用程序啟動時默認打開窗口。
- 每當窗口觸發關閉事件時,我們都需要銷毀 BrowserWindow 實例。
打開和關閉“插入”窗口
這個想法是能夠在最終用戶單擊“主”窗口中的按鈕時觸發“插入”窗口。 為此,我們需要從主窗口向主進程發送一條消息,指示它打開插入窗口。 我們可以使用 Electron 的 IPC 模塊來實現這一點。 IPC 模塊實際上有兩種變體:
- 一個用於主進程,允許應用訂閱從窗口發送的消息。
- 一個用於渲染器進程,允許應用程序向主進程發送消息。
儘管 Electron 的通信通道大多是單向的,但可以通過使用遠程模塊訪問渲染器進程中的主進程的 IPC 模塊。 此外,主進程可以使用 Event.sender.send() 方法將消息發送回事件起源的渲染器進程。
要使用 IPC 模塊,我們只需像主進程腳本中的任何其他 NPM 模塊一樣需要它:
var ipc = require('ipc');
…然後使用on()
方法綁定到事件:
ipc.on('toggle-insert-view', function() { if(!insertWindow) { createInsertWindow(); } return (!insertWindow.isClosed() && insertWindow.isVisible()) ? insertWindow.hide() : insertWindow.show(); });
關鍵點:
- 我們可以隨意命名事件,這個例子是任意的。
- 不要忘記檢查 BrowserWindow 實例是否已經創建,如果沒有,則實例化它。
- BrowserWindow 實例有一些有用的方法:
- isClosed()返回一個布爾值,無論窗口當前是否處於
closed
狀態。 - isVisible() :返回一個布爾值,無論窗口當前是否可見。
- show() / hide() :顯示和隱藏窗口的便捷方法。
- isClosed()返回一個布爾值,無論窗口當前是否處於
現在我們實際上需要從渲染進程中觸發該事件。 我們將創建一個名為main.view.js
的新腳本文件,並將其添加到我們的 HTML 頁面,就像使用任何普通腳本一樣:
<script src="./main.view.js"></script>
通過 HTML
script
標籤加載腳本文件會在客戶端上下文中加載該文件。 這意味著,例如,全局變量可通過window.<varname>
獲得。 要在服務器端上下文中加載腳本,我們可以直接在 HTML 頁面中使用require()
方法:require('./main.controller.js');
.
即使腳本是在客戶端上下文中加載的,我們仍然可以像訪問主進程一樣訪問渲染進程的 IPC 模塊,然後這樣發送我們的事件:
var ipc = require('ipc'); angular .module('Utils', []) .directive('toggleInsertView', function() { return function(scope, el) { el.bind('click', function(e) { e.preventDefault(); ipc.send('toggle-insert-view'); }); }; });
還有一個 sendSync() 方法可用,以防我們需要同步發送事件。
現在,要打開“插入”窗口,我們剩下要做的就是創建一個帶有匹配 Angular 指令的 HTML 按鈕:
<div ng-controller="MainCtrl as vm"> <button toggle-insert-view class="mdl-button"> <i class="material-icons">add</i> </button> </div>
並將該指令添加為主窗口的 Angular 控制器的依賴項:
angular .module('MainWindow', ['Utils']) .controller('MainCtrl', function() { var vm = this; });
生成密碼
為簡單起見,我們可以使用 NPM uuid
模塊生成唯一 ID,作為本教程的密碼。 我們可以像安裝任何其他 NPM 模塊一樣安裝它,在我們的“Utils”腳本中使用它,然後創建一個簡單的工廠來返回一個唯一的 ID:
var uuid = require('uuid'); angular .module('Utils', []) ... .factory('Generator', function() { return { create: function() { return uuid.v4(); } }; })
現在,我們剩下要做的就是在插入視圖中創建一個按鈕,並為其附加一個指令,該指令將監聽按鈕上的點擊事件並調用 create() 方法:
<!-- in insert.html --> <button generate-password class="mdl-button">generate</button>
// in Utils.js angular .module('Utils', []) ... .directive('generatePassword', ['Generator', function(Generator) { return function(scope, el) { el.bind('click', function(e) { e.preventDefault(); if(!scope.vm.formData) scope.vm.formData = {}; scope.vm.formData.password = Generator.create(); scope.$apply(); }); }; }])
保存密碼
此時,我們要存儲我們的密碼。 我們的密碼條目的數據結構相當簡單:
{ "id": String "description": String, "username": String, "password": String }
所以我們真正需要的是某種內存數據庫,可以選擇同步到文件進行備份。 為此,Loki.js 似乎是理想的候選者。 它完全滿足了我們應用程序的目的,並在其之上提供了動態視圖功能,允許我們做類似於 MongoDB 的聚合模塊的事情。
動態視圖不提供 MongodDB 的聚合模塊提供的所有功能。 請參閱文檔以獲取更多信息。
讓我們從創建一個簡單的 HTML 表單開始:
<div class="insert" ng-controller="InsertCtrl as vm"> <form name="insertForm" no-validate> <fieldset ng-disabled="!vm.loaded"> <div class="mdl-textfield"> <input class="mdl-textfield__input" type="text" ng-model="vm.formData.description" required /> <label class="mdl-textfield__label" for="description">Description...</label> </div> <div class="mdl-textfield"> <input class="mdl-textfield__input" type="text" ng-model="vm.formData.username" /> <label class="mdl-textfield__label" for="username">Username...</label> </div> <div class="mdl-textfield"> <input class="mdl-textfield__input" type="password" ng-model="vm.formData.password" required /> <label class="mdl-textfield__label" for="password">Password...</label> </div> <div class=""> <button generate-password class="mdl-button">generate</button> <button toggle-insert-view class="mdl-button">cancel</button> <button save-password class="mdl-button" ng-disabled="insertForm.$invalid">save</button> </div> </fieldset> </form> </div>
現在,讓我們添加 JavaScript 邏輯來處理表單內容的發布和保存:
var loki = require('lokijs'), path = require('path'); angular .module('Utils', []) ... .service('Storage', ['$q', function($q) { this.db = new loki(path.resolve(__dirname, '../..', 'app.db')); this.collection = null; this.loaded = false; this.init = function() { var d = $q.defer(); this.reload() .then(function() { this.collection = this.db.getCollection('keychain'); d.resolve(this); }.bind(this)) .catch(function(e) { // create collection this.db.addCollection('keychain'); // save and create file this.db.saveDatabase(); this.collection = this.db.getCollection('keychain'); d.resolve(this); }.bind(this)); return d.promise; }; this.addDoc = function(data) { var d = $q.defer(); if(this.isLoaded() && this.getCollection()) { this.getCollection().insert(data); this.db.saveDatabase(); d.resolve(this.getCollection()); } else { d.reject(new Error('DB NOT READY')); } return d.promise; }; }) .directive('savePassword', ['Storage', function(Storage) { return function(scope, el) { el.bind('click', function(e) { e.preventDefault(); if(scope.vm.formData) { Storage .addDoc(scope.vm.formData) .then(function() { // reset form & close insert window scope.vm.formData = {}; ipc.send('toggle-insert-view'); }); } }); }; }])
關鍵點:

- 我們首先需要初始化數據庫。 此過程包括創建 Loki 對象的新實例,提供數據庫文件的路徑作為參數,查找該備份文件是否存在,在需要時創建它(包括“鑰匙串”集合),然後加載這個文件在內存中。
- 我們可以使用
getCollection()
方法檢索數據庫中的特定集合。 - 一個集合對象公開了幾個方法,包括一個
insert()
方法,允許我們向集合中添加一個新文檔。 - 為了將數據庫內容保存到文件中,Loki 對象公開了一個
saveDatabase()
方法。 - 我們需要重置表單的數據並發送一個 IPC 事件來告訴主進程在文檔保存後關閉窗口。
我們現在有一個簡單的表格,允許我們生成和保存新密碼。 讓我們回到主視圖來列出這些條目。
列出密碼
這裡需要發生一些事情:
- 我們需要能夠獲取我們集合中的所有文檔。
- 每當保存新密碼時,我們都需要通知主視圖,以便它可以刷新視圖。
我們可以通過調用 Loki 對象的getCollection()
方法來檢索文檔列表。 此方法返回一個對象,其屬性名為data ,它只是該集合中所有文檔的數組:
this.getCollection = function() { this.collection = this.db.getCollection('keychain'); return this.collection; }; this.getDocs = function() { return (this.getCollection()) ? this.getCollection().data : null; };
然後我們可以在我們的 Angular 控制器中調用 getDocs() 並檢索存儲在數據庫中的所有密碼,在我們初始化它之後:
angular .module('MainView', ['Utils']) .controller('MainCtrl', ['Storage', function(Storage) { var vm = this; vm.keychain = null; Storage .init() .then(function(db) { vm.keychain = db.getDocs(); }); });
一些 Angular 模板,我們有一個密碼列表:
<tr ng-repeat="item in vm.keychain track by $index" class="item--{{$index}}"> <td class="mdl-data-table__cell--non-numeric">{{item.description}}</td> <td>{{item.username || 'n/a'}}</td> <td> <span ng-repeat="n in [1,2,3,4,5,6]">•</span> </td> <td> <a href="#" copy-password="{{$index}}">copy</a> <a href="#" remove-password="{{item}}">remove</a> </td> </tr>
一個不錯的附加功能是在插入新密碼後刷新密碼列表。 為此,我們可以使用 Electron 的 IPC 模塊。 如前所述,可以在渲染器進程中調用主進程的 IPC 模塊,通過使用遠程模塊將其變為偵聽器進程。 這是一個關於如何在main.view.js
中實現它的示例:
var remote = require('remote'), remoteIpc = remote.require('ipc'); angular .module('MainView', ['Utils']) .controller('MainCtrl', ['Storage', function(Storage) { var vm = this; vm.keychain = null; Storage .init() .then(function(db) { vm.keychain = db.getDocs(); remoteIpc.on('update-main-view', function() { Storage .reload() .then(function() { vm.keychain = db.getDocs(); }); }); }); }]);
關鍵點:
- 我們需要通過它自己的
require()
方法使用遠程模塊來從主進程請求遠程 IPC 模塊。 - 然後我們可以通過
on()
方法將我們的渲染進程設置為事件監聽器,並將回調函數綁定到這些事件。
每當保存新文檔時,插入視圖將負責調度此事件:
Storage .addDoc(scope.vm.formData) .then(function() { // refresh list in main view ipc.send('update-main-view'); // reset form & close insert window scope.vm.formData = {}; ipc.send('toggle-insert-view'); });
複製密碼
以純文本形式顯示密碼通常不是一個好主意。 相反,我們將隱藏並提供一個方便的按鈕,允許最終用戶直接複製特定條目的密碼。
在這裡,Electron 再次拯救了我們,它為我們提供了一個剪貼板模塊,該模塊不僅可以復制和粘貼文本內容,還可以復制和粘貼圖像和 HTML 代碼:
var clipboard = require('clipboard'); angular .module('Utils', []) ... .directive('copyPassword', [function() { return function(scope, el, attrs) { el.bind('click', function(e) { e.preventDefault(); var text = (scope.vm.keychain[attrs.copyPassword]) ? scope.vm.keychain[attrs.copyPassword].password : ''; // atom's clipboard module clipboard.clear(); clipboard.writeText(text); }); }; }]);
由於生成的密碼將是一個簡單的字符串,我們可以使用writeText()
方法將密碼複製到系統的剪貼板。 然後我們可以更新我們的主視圖 HTML,並添加帶有copy-password
指令的複制按鈕,提供密碼數組的索引:
<a href="#" copy-password="{{$index}}">copy</a>
刪除密碼
我們的最終用戶可能還希望能夠刪除密碼,以防它們過時。 為此,我們需要做的就是在鑰匙串集合上調用remove()
方法。 我們需要將整個文檔提供給“remove()”方法,如下所示:
this.removeDoc = function(doc) { return function() { var d = $q.defer(); if(this.isLoaded() && this.getCollection()) { // remove the doc from the collection & persist changes this.getCollection().remove(doc); this.db.saveDatabase(); // inform the insert view that the db content has changed ipc.send('reload-insert-view'); d.resolve(true); } else { d.reject(new Error('DB NOT READY')); } return d.promise; }.bind(this); };
Loki.js 文檔指出,我們也可以通過 id 刪除文檔,但它似乎沒有按預期工作。
創建桌面菜單
Electron 與我們的操作系統桌面環境無縫集成,為我們的應用程序提供“原生”用戶體驗外觀。 因此,Electron 捆綁了一個 Menu 模塊,專門為我們的應用程序創建複雜的桌面菜單結構。
菜單模塊是一個龐大的主題,幾乎值得擁有自己的教程。 我強烈建議您閱讀 Electron 的桌面環境集成教程,以了解該模塊的所有功能。
在本教程的範圍內,我們將了解如何創建自定義菜單、向其中添加自定義命令以及實現標準退出命令。
為我們的應用創建和分配自定義菜單
通常,Electron 菜單的 JavaScript 邏輯屬於我們應用程序的主腳本文件,其中定義了我們的主進程。 但是,我們可以將其抽象為一個單獨的文件,並通過遠程模塊訪問 Menu 模塊:
var remote = require('remote'), Menu = remote.require('menu');
要定義一個簡單的菜單,我們需要使用buildFromTemplate()
方法:
var appMenu = Menu.buildFromTemplate([ { label: 'Electron', submenu: [{ label: 'Credits', click: function() { alert('Built with Electron & Loki.js.'); } }] } ]);
數組中的第一項始終用作“默認”菜單項。
label
屬性的值對於默認菜單項並不重要。 在開發模式下,它將始終顯示Electron
。 稍後我們將看到如何在構建階段為默認菜單項分配自定義名稱。
最後,我們需要使用setApplicationMenu()
方法將此自定義菜單指定為我們應用程序的默認菜單:
Menu.setApplicationMenu(appMenu);
映射鍵盤快捷鍵
Electron 提供“加速器”,一組預定義的字符串映射到實際的鍵盤組合,例如: Command+A
或Ctrl+Shift+Z
。
Command
加速器在 Windows 或 Linux 上不起作用。 對於我們的密碼鑰匙串應用程序,我們應該添加一個File
菜單項,提供兩個命令:
- 創建密碼:使用Cmd(或 Ctrl)+ N打開插入視圖
- 退出:使用Cmd(或 Ctrl)+ Q完全退出應用程序
... { label: 'File', submenu: [ { label: 'Create Password', accelerator: 'CmdOrCtrl+N', click: function() { ipc.send('toggle-insert-view'); } }, { type: 'separator' // to create a visual separator }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', selector: 'terminate:' // OS X only!!! } ] } ...
關鍵點:
- 我們可以通過將
type
屬性設置為separator
的項目添加到數組中來添加可視分隔符。 -
CmdOrCtrl
加速器與 Mac 和 PC 鍵盤兼容 selector
屬性僅與 OSX 兼容!
設計我們的應用程序
您可能注意到在各種代碼示例中對以mdl-
開頭的類名的引用。 出於本教程的目的,我選擇使用 Material Design Lite UI 框架,但您可以隨意使用您選擇的任何 UI 框架。
我們可以用 HTML5 做的任何事情都可以在 Electron 中完成; 請記住應用程序二進製文件的大小不斷增長,以及如果您使用太多第三方庫可能會出現的性能問題。
打包電子應用程序以供分發
你製作了一個 Electron 應用程序,它看起來很棒,你用 Selenium 和 WebDriver 編寫了你的 e2e 測試,你準備將它分發給全世界!
但是您仍然想對其進行個性化設置,給它一個自定義名稱而不是默認的“Electron”,並且可能還為 Mac 和 PC 平台提供自定義應用程序圖標。
使用 Gulp 構建
這些天來,我們可以想到的任何東西都有一個 Gulp 插件。 我所要做的就是在 Google 中輸入gulp electron
,果然有一個 gulp-electron 插件!
只要保持本教程開頭詳述的文件夾結構,這個插件就相當容易使用。 如果沒有,您可能需要稍微移動一下。
這個插件可以像任何其他 Gulp 插件一樣安裝:
$ npm install gulp-electron --save-dev
然後我們可以這樣定義我們的 Gulp 任務:
var gulp = require('gulp'), electron = require('gulp-electron'), info = require('./src/package.json'); gulp.task('electron', function() { gulp.src("") .pipe(electron({ src: './src', packageJson: info, release: './dist', cache: './cache', version: 'v0.31.2', packaging: true, platforms: ['win32-ia32', 'darwin-x64'], platformResources: { darwin: { CFBundleDisplayName: info.name, CFBundleIdentifier: info.bundle, CFBundleName: info.name, CFBundleVersion: info.version }, win: { "version-string": info.version, "file-version": info.version, "product-version": info.version } } })) .pipe(gulp.dest("")); });
關鍵點:
-
src/
文件夾不能與 Gulpfile.js 所在的文件夾相同,也不能與分發文件夾相同。 - 我們可以通過
platforms
數組定義我們希望導出到的平台。 - 我們應該定義一個
cache
文件夾,將下載 Electron 二進製文件,以便將它們與我們的應用程序打包在一起。 - 應用程序的 package.json 文件的內容需要通過
packageJson
屬性傳遞給 gulp 任務。 - 有一個可選的
packaging
屬性,允許我們創建生成的應用程序的 zip 存檔。 - 對於每個平台,可以定義一組不同的“平台資源”。
添加應用程序圖標
platformResources
屬性之一是icon
屬性,它允許我們為我們的應用程序定義一個自定義圖標:
"icon": "keychain.ico"
OS X 需要帶有
.icns
文件擴展名的圖標。 有多種在線工具允許我們免費將.png
文件轉換為.ico
和.icns
。
結論
在本文中,我們只觸及了 Electron 實際可以做的事情的皮毛。 將 Atom 或 Slack 等出色的應用程序視為您可以使用此工具的靈感來源。
我希望您發現本教程對您有用,請隨時留下您的評論並分享您使用 Electron 的經驗!