个性化阅读
专注于IT技术分析

Electron:跨平台桌面应用程序变得轻松

本文概述

今年早些时候, 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 CLI, 我们可以在终端中键入以下命令:

$ npm install -g electron-prebuilt

要测试安装, 请键入electron -h, 它应该显示Electron CLI的版本。

在撰写本文时, Electron的版本为0.31.2。

设置项目

假设以下基本文件夹结构:

my-app
|- cache/
|- dist/
|- src/
|-- app.js
| gulpfile.js

…其中:-构建应用程序时, 将使用cache /下载电子二进制文件。 -dist /将包含生成的分发文件。 -src /将包含我们的源代码。 -src / app.js将是我们应用程序的入口。

接下来, 我们将导航到终端中的src /文件夹, 并为我们的应用程序创建package.json和bower.json文件:

$ npm init
$ bower init

我们将在本教程的后面部分安装必要的软件包。

了解电子过程

电子区分两种类型的过程:

  • 主要流程:应用程序的入口点, 即每当我们运行该应用程序时将执行的文件。通常, 该文件声明应用程序的各个窗口, 并且可以选择使用Electron的IPC模块来定义全局事件监听器。
  • 渲染器进程:我们应用程序中给定窗口的控制器。每个窗口都会创建自己的渲染器进程。

为了使代码清晰, 应为每个渲染器进程使用一个单独的文件。为了定义我们的应用程序的主流程, 我们将打开src / app.js并包括应用程序模块以启动应用程序, 并包含浏览器窗口模块以创建应用程序的各个窗口(都是Electron核心的一部分), 因此:

var app = require('app'), BrowserWindow = require('browser-window');

当应用实际启动时, 它会触发一个就绪事件, 我们可以绑定该事件。此时, 我们可以实例化应用程序的主窗口:

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文件的启动脚本来自动执行此过程。

Electron:跨平台桌面应用程序变得轻松2

构建密码钥匙串桌面应用

要构建密码钥匙串应用程序, 我们需要:-一种添加, 生成和保存密码的方法。 -复制和删除密码的便捷方法。

生成和保存密码

一个简单的表格就足以插入新密码。为了演示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模块实际上有两种变体:

  • 一个用于主进程, 允许应用程序订阅从Windows发送的消息。
  • 一个用于Renderer Process, 允许应用程序将消息发送到主进程。

尽管Electron的通信通道大部分是单向的, 但可以通过使用远程模块访问Renderer Process中Main Process的IPC模块。而且, 主进程可以使用Event.sender.send()方法将消息发送回事件源所在的渲染器进程。

要使用IPC模块, 我们只需要像Main Process脚本中的任何其他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()返回一个布尔值。
    • isVisible():返回一个布尔值, 无论该窗口当前是否可见。
    • show()/ hide():显示和隐藏窗口的便捷方法。

现在, 我们实际上需要从”渲染器进程”中触发该事件。我们将创建一个名为main.view.js的新脚本文件, 并将其添加到HTML页面中, 就像使用普通脚本一样:

<script src="./main.view.js"></script>

通过HTML脚本标记加载脚本文件会在客户端上下文中加载此文件。这意味着, 例如, 可以通过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;
    });
Electron:跨平台桌面应用程序变得轻松3

产生密码

为简单起见, 我们仅可以使用NPM uuid模块生成唯一的ID, 该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的Aggregation模块的功能。

动态视图不提供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" id="description" 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" id="username" 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" id="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对象的新实例, 提供数据库文件的路径作为参数, 查找该备份文件是否存在, 如果需要(包括” Keychain”集合)创建该文件, 然后加载其中的内容。该文件在内存中。
  • 我们可以使用getCollection()方法检索数据库中的特定集合。
  • 集合对象公开了几种方法, 包括insert()方法, 使我们可以向集合中添加新文档。
  • 为了将数据库内容持久保存到文件中, Loki对象公开了一个saveDatabase()方法。
  • 保存文档后, 我们将需要重置表单的数据并发送IPC事件以通知Main Process关闭窗口。

现在, 我们有一个简单的表单, 允许我们生成和保存新密码。让我们回到主视图以列出这些条目。

列出密码

这里需要发生一些事情:

  • 我们需要能够获取集合中的所有文档。
  • 每当保存新密码时, 我们都需要通知主视图, 以便刷新视图。

我们可以通过调用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:跨平台桌面应用程序变得轻松4

一个不错的附加功能是在插入新密码后刷新密码列表。为此, 我们可以使用Electron的IPC模块。如前所述, 可以在Renderer进程中调用Main Process的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()方法使用远程模块, 以从Main Process中请求远程IPC模块。
  • 然后, 我们可以通过on()方法将Renderer Process设置为事件侦听器, 并将回调函数绑定到这些事件。

每当保存新文档时, 插入视图将负责调度此事件:

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与我们的OS桌面环境无缝集成, 为我们的应用程序提供”原生”的用户体验外观。因此, Electron与菜单模块捆绑在一起, 专门用于为我们的应用程序创建复杂的桌面菜单结构。

菜单模块是一个广泛的主题, 几乎值得一提。我强烈建议你通读Electron的桌面环境集成教程, 以发现该模块的所有功能。

对于本教程的范围, 我们将看到如何创建自定义菜单, 向其添加自定义命令以及实现标准quit命令的方法。

为我们的应用程序创建和分配自定义菜单

通常, Electron菜单的JavaScript逻辑属于我们的应用程序的主脚本文件, 该文件在其中定义了Main Process。但是, 我们可以将其抽象为一个单独的文件, 然后通过远程模块访问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。

命令加速器在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!!!
        }
    ]
}
...

关键点:

  • 我们可以通过将类型属性设置为分隔符的项添加到数组中来添加可视分隔符。
  • CmdOrCtrl加速器与Mac和PC键盘兼容
  • 选择器属性仅与OSX兼容!

造型我们的应用程序

你可能在整个代码示例中都注意到了对以mdl-开头的类名的引用。就本教程而言, 我选择使用Material Design Lite UI框架, 但可以随意使用你选择的任何UI框架。

我们可以使用HTML5进行的所有操作都可以在Electron中完成;只需记住应用程序二进制文件的大小不断增长, 以及如果使用过多的第三方库可能会导致的性能问题。

打包电子应用进行分发

你制作了一个Electron应用程序, 它看起来很棒, 你使用Selenium和WebDriver编写了e2e测试, 并准备将其发布给全世界!

但是你仍然要对其进行个性化设置, 给它一个默认的” Electron”以外的其他自定义名称, 并可能同时为Mac和PC平台提供自定义应用程序图标。

用Gulp构建

这些天来, 有一个我们可以想到的任何东西的Gulp插件。我要做的就是在Google中键入gulp electronic, 并且肯定有一个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数组定义要导出到的平台。
  • 我们应该定义一个缓存文件夹, 将在其中下载电子二进制文件, 以便可以将其与我们的应用程序打包在一起。
  • 应用程序的package.json文件的内容需要通过packageJson属性传递到gulp任务。
  • 有一个可选的包装属性, 使我们还可以为生成的应用程序创建zip存档。
  • 对于每个平台, 可以定义一组不同的”平台资源”。

添加应用程序图标

platformResources属性之一是icon属性, 它允许我们为应用定义自定义图标:

"icon": "keychain.ico"

OS X需要带有.icns文件扩展名的图标。有多种在线工具可让我们免费将.png文件转换为.ico和.icns。

总结

在本文中, 我们仅介绍了Electron实际可以做的事情。可以将Atom或Slack之类的出色应用视为灵感的来源, 你可以在此工具中使用它。

希望本教程对你有所帮助, 请随时发表评论并与Electron分享你的经验!

赞(0)
未经允许不得转载:srcmini » Electron:跨平台桌面应用程序变得轻松

评论 抢沙发

评论前必须登录!