Electron:輕鬆實現跨平台桌面應用程序

已發表: 2022-03-11

今年早些時候,Github 發布了其著名的開源編輯器 Atom 的核心 Atom-Shell,並在特殊場合將其重命名為Electron

與基於 Node.js 的桌面應用程序類別中的其他競爭對手不同,Electron 通過將 Node.js(直到最近發布的 io.js)的強大功能與 Chromium 引擎相結合,為這個已經成熟的市場帶來了自己的變化。我們最好的服務器和客戶端 JavaScript。

想像一個世界,我們可以構建高性能的、數據驅動的、跨平台的桌面應用程序,不僅由不斷增長的 NPM 模塊存儲庫提供支持,而且由整個 Bower 註冊表提供支持,以滿足我們所有的客戶端需求。

輸入電子。

使用 Electron 構建跨平台桌面應用程序

使用 Electron 構建跨平台桌面應用程序
鳴叫

在本教程中,我們將使用 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.jsonbower.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() :顯示和隱藏窗口的便捷方法。

現在我們實際上需要從渲染進程中觸發該事件。 我們將創建一個名為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]">&bull;</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+ACtrl+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 的經驗!