Десктопные приложения на JavaScript. Часть 2

от автора

Данная статья является продолжением статьи «Десктопные приложения на JavaScript. Часть 1». В предыдущей части мы рассмотрели следующее:

  • установка NW.js
  • сборка и запуск приложений на NW.js
  • основы работы с нативными контроллами (на примере создания меню)

В рамках статьи мы рассмотрим создание приложения для хранения паролей. Приложение относительно простое и является по большей части прототипом для реального. Однако при желании и наличии времени, его можно доработать и вполне использовать для повседневной работы.

Основа приложения для хранения паролей

Как известно, разработку можно вести как на чистом JavaScript, так и используя разнообразные фреймворки, которых существует такое огромное количество, что порой теряешься в их многообразии и долго не можешь решиться, что же в итоге выбрать. Для разработки приложений особенно популярны паттерны, название которых начинается с MV (MVC , MVVM , MVP ). Одним из фрейворков использующих подобный паттерн, является Angular JS, именно его мы и будем использовать при разработке нашего приложения. Если вы не знакомы с ним, советую почитать документацию (tutorial , API), также можно почерпнуть основные сведения в руководстве на русском языке.

Что же будет представлять из себя приложение? Все данные отображаются в виде таблицы, при этом логин должен быть виден, а вместо пароля должны стоять звездочки. Пользователь может добавить новый логин/пароль, а также удалить записи, ставшие ненужными. Кроме того, необходимо предусмотреть возможность редактирования.

Реализуем базовую функциональность приложения. Для этого необходимо создать папку, в которой мы будем распологать исходный код, а также поместить в нее package.json (о том как это сделать, см. Часть 1).

Создадим базовую структуру папок, состоящую из следующих директорий:

  • CSS — в этой папке будем размещать стили (Добавим сюда файл index.css, в котором будут содержаться основные стили)
  • Controller — здесь будут находится контроллеры
  • View — папка для представлений
  • Directive — папка с директивами
  • Lib — библиотеки (в эту папку необходимо скопировать angular.min.js, о том как добавить angularJS)

Кроме того в корень проекта добавим файл index.html, который будет являться точкой входа в приложение. Создадим базовую разметку:

Базовая разметка

<html ng-app="main">    <head>       <meta charset="utf-8">       <title>Password keeper</title>       <link rel="stylesheet" type="text/css" href="css/index.css">    </head>    <body>       <table>          <thead>             <tr>                <td></td>                <td>Login</td>                <td>Password</td>                <td></td>             </tr>          </thead>          <tbody>             <tr>                <td></td>                <td></td>                <td></td>                <td><a>удалить</a></td>             </tr>          </tbody>          <tfoot>             <tr>                <td></td>                <td><a>добавить</a></td>                <td></td>                <td></td>             </tr>          </tfoot>       </table>       <script type="text/javascript" src="lib/angular.min.js"></script>    </body> </html> 

Так как приложение у нас достаточно простое, создадим контроллер и в рамках него будем размещать всю основную логику приложения (по мере разрастания логики, необходимо добавить папку Service и размещать в ней сервисы, в которых и должна размещаться вся сложная логика, контроллеры по возможности необходимо оставлять «тонкими»). Назовем контроллер main, а файл контроллера main.ctrl.js. Итак, заготовка для контроллера:

(function () {     'use strict';      angular         .module('main', [])         .controller('MainCtrl', [MainCtrl]);      function MainCtrl() {       this.data = [];          return this;     }  })(); 

Данные, содержащие логины/пароли, для нашего прототипа будут размещаться в массиве data. Для упрощения реализации редактирования, создадим свой элемент EditableText и оформим его в виде директивы. Данный элемент будет работать следующим образом: элемент отображается как текст, при щелчке по элементу, текст превращается в тектовое поле input, при потере фокуса элемент вновь отображается как текст. Для этого создадим внутри папки View файл с разметкой для директивы editableText.html:

<input  ng-model="value"> <span ng-click="edit()">{{value}}</span> 

А внутри папки directive создадим файл editableText.js:

Код директивы editable-text

(function () {     'use strict';      angular         .module('main')         .directive('editableText', [editableText]);      function editableText() {         var directive = {             restrict: 'E',             scope: {                 value: "="             },             templateUrl: 'view/editableText.html',             link: function ( $scope, element, attrs ) {                 // получаем ссылку на внутренний input нашей директивы                 var inputElement = angular.element( element.children()[0] );                 element.addClass( 'editable-text' );                  // функция, вызываемая при щелчке на элементе, когда он отображается                 // в режиме для чтения                 $scope.edit = function () {                     element.addClass( 'active' );                     inputElement[0].focus();                 };                  // при потере фокуса, т.е. когда пользователь закончил редактирование                 inputElement.prop( 'onblur', function() {                     element.removeClass( 'active' );                 });             }         };          return directive;     } })(); 

Для работы директивы необходимы также некоторые стили, которые можно разместить внутри index.css:

.editable-text span {    cursor: pointer; }  .editable-text input {    display: none; }  .editable-text.active span {    display: none; }  .editable-text.active input {    display: inline-block; } 

Использование директивы происходит следующим образом:

<editable-text value="variable"></editable-text> 

Для логина все в порядке — отображаем либо текст, либо текстовое поле, но как быть с паролем, ведь мы не должны его показывать. Добавим в scope нашей директивы поле crypto следующим образом:

scope: {    value: "=",    crypto: "=" } 

А также изменим разметку директивы:

<input  ng-model="value"> <span ng-click="edit()">{{crypto?'***':value}}</span> 

Кроме того, необходимо не забывать добавлять скрипты в index.html:

<script type="text/javascript" src="lib/angular.min.js"></script> <script type="text/javascript" src="controller/main.ctrl.js"></script> <script type="text/javascript" src="directive/editableText.js"></script> 

Пришло время добавить функциональность. Изменим контроллер следующим образом:

Код контроллера

function MainCtrl() {    var self = this;    this.data = [];     this.remove = remove;    this.copy = copy;    this.add = add;     return this;     function remove(ind){       self.data.splice(ind, 1);    };     function copy(ind){       // добавим позже    };     function add(){       self.data.push({login: "login", password: "password"});    }; } 

Кроме того необходимы изменения в разметке:

Итоговая разметка

<body ng-controller="MainCtrl as ctrl">    <table>       <thead>          <tr>             <td></td>             <td>Login</td>             <td>Password</td>             <td></td>          </tr>       </thead>       <tbody>          <tr ng-repeat="record in ctrl.data track by $index">             <td><a ng-click="ctrl.copy($index)">{{$index}}</a></td>             <td><editable-text value="record.login"></editable-text></td>             <td><editable-text value="record.password" crypto="true"></editable-text></td>             <td><a ng-click="ctrl.remove($index)">удалить</a></td>          </tr>       </tbody>       <tfoot>          <tr>             <td></td>             <td><a ng-click="ctrl.add()">добавить</a></td>             <td></td>             <td></td>          </tr>       </tfoot>    </table>    <script type="text/javascript" src="lib/angular.min.js"></script>    <script type="text/javascript" src="controller/main.ctrl.js"></script>    <script type="text/javascript" src="directive/editableText.js"></script> </body> 

На данном этапе можно заняться стилизацией. Пример простой стилизации (напоминаю, что добавляем стили в index.css, однако если стилей будет достаточно много, можно разбить стили по файлам или даже использовать препроцессор, например LESS):

Пример стилизации приложения

table {    border-collapse: collapse;    margin: auto;    width: calc(100% - 40px); }  table, table thead, table tfoot, table tbody tr td:first-child, table tbody tr td:nth-child(2), table tbody tr td:nth-child(3), table thead tr td:nth-child(2), table thead tr td:nth-child(3) {    border: 1px solid #000; }  table td {    padding: 5px; }  table thead {    background: #EEE; }  table tbody tr td:first-child {    background: #CCC; }  table tbody tr td:nth-child(2) {    background: #777;    color: #FFF; }  table tbody tr td:nth-child(3) {    background: #555;    color: #FFF; }  table thead tr td:nth-child(2),table thead tr td:nth-child(3) {    text-align: center; }  table a {    font-size: smaller;    cursor: pointer; } 

Приложение выглядит следующим образом:

Работа с буфером обмена

Итак, основа приложения готова, но оно пока не реализует основное назначение, мы не можем копировать пароли (вернее можем, но достаточно неудобно). Для начала рассмотрим работу с буфером обмена в NW.js
Существует специальный объект — Clipboard, который используется как абстракция для буфера обмена Windows и GTK, а также для pasteboard (Mac). На момент написания статьи осуществляется поддержка записи и чтения только текста.
Для работы с объектом нам понадобится знакомый нам модуль nw.gui:

var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); 

Обратите внимание, что мы не может создать свой объект, мы можем получить лишь системный. Поддерживаются три метода:

  • get ([type]) — получить объект из буфера обмена с указанием типа данного объекта, по умолчанию text, однако пока это единственный поддерживаемый тип
  • set (data, [type]) — отправить объект в буфер обмена (также поддерживается лишь type — «text»)
  • clear — очистить буфер обмена

Теперь можно доделать функциональность приложения, и контроллер будет выглядеть следующим образом:

Код контроллера

function MainCtrl() {      var self = this;      var gui = require('nw.gui');      var clipboard = gui.Clipboard.get();      this.data = [];       this.remove = remove;      this.copy = copy;      this.add = add;       return this;       function remove(ind){          self.data.splice(ind, 1);      };       function copy(ind){          clipboard.set(self.data[ind].password);      };       function add(){          self.data.push({login: "login", password: "password"});      };  } 

Хранение паролей

После того, как приложение было запущено, пользователь занес на хранение несколько паролей, закрыл приложение. На следующий день оказывается, что пароли пропали. Проблема в том, что мы держали их в обычной локальной переменной, которая при закрытии удалилась.
В третьей части мы рассмотрим работу NW.js с базами данных, а пока будем хранить пароли в localStorage. Прежде чем приступить к созданию функционала, (хотя приложение у нас пока лишь только прототип) необходимо позаботиться о безопасности. Для этого мы не должны хранить пароли в открытом виде.
Для шифрования/дешифрования существуют различные библиотеки на JavaScript. Одной из таких библиотек является crypto-js. Установим ее как модуль для node.js. Библиотека поддерживает большое количество стандартов, полный список которых можно найти в документации. При этом, можно подключить как все модули, так и отдельный модуль:

// подключаем все модули, доступ к отдельному модулю можно получить например так CryptoJS.HmacSHA1 var CryptoJS = require("crypto-js");   // подключаем отдельный модуль, например AES var AES = require("crypto-js/aes"); 

Для того, чтобы зашифровать сообщение используется метод encrypt:

var ciphertext = CryptoJS.AES.encrypt('сообщение', 'секретный ключ'); 

Расшифровка происходит немного сложнее:

var bytes  = CryptoJS.AES.decrypt(ciphertext.toString(), 'секретный ключ'); var plaintext = bytes.toString(CryptoJS.enc.Utf8); 

Давайте модифицируем наше приложение для того, чтобы мы могли сохранять пароли при закрытии приложения и загружать их при запуске.
Создадим сервис crypto.svc и поместим в папку service (если вы еще не создали данную папку, то создайте ее в корне приложения):

Код сервиса

(function () {    'use strict';     angular       .module('main')       .factory('CryptoService', [CryptoService]);     function CryptoService() {       var CryptoJS = require("crypto-js");       var secretKey = "secretKey";        var service = {          encrypt: encrypt,          decrypt: decrypt       };        return service;        function encrypt(data) {          return CryptoJS.AES.encrypt(JSON.stringify(data), secretKey);       }        function decrypt(text) {          var bytes  = CryptoJS.AES.decrypt(text.toString(), secretKey);          var decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));           return decryptedData;       }    }  })(); 

Для использования нашего сервиса модернизируем контроллер:

Код контроллера

(function () {    'use strict';     angular       .module('main', [])       .controller('MainCtrl', ['$scope', 'CryptoService', MainCtrl]);     function MainCtrl($scope, CryptoService) {       var self = this;       var gui = require('nw.gui');       var clipboard = gui.Clipboard.get();       var localStorageKey = "loginPasswordData"       this.data = [];        this.remove = remove;       this.copy = copy;       this.add = add;        load();        $scope.$watch("ctrl.data", save, true);        return this;        function remove(ind){          self.data.splice(ind, 1);       };        function copy(ind){          clipboard.set(self.data[ind].password);       };        function add(){          self.data.push({login: "login", password: "password"});       };        function load(){          var text = localStorage.getItem(localStorageKey);          if(text) {             self.data = CryptoService.decrypt(text);          }       }        function save(){          if(self.data) {             localStorage.setItem(localStorageKey, CryptoService.encrypt(self.data));          }       }    }  })(); 

Кроме подключения сервиса, нам так же понадобился уже существующий в AngularJS сервис $scope. Мы используем метод $watch для отслеживания момента изменения данных, для того, чтобы вовремя сохранить их (обратите внимание, что третьим аргументом мы передаем true для того, чтобы отслеживались изменения не только в массиве, т.е. вставка/удаление, но и изменения элементов массива, т.е. изменение логина или пароля отдельного элемента массива). Загрузка данных происходит при открытии представления.

Сворачиваем в трей

Основа приложения готова, но как известно подобные программы зачастую сворачиваются в системный трей, чтобы не перегружать пользователя обилием открытых окон.
Еще одна абстракция, которую ввели в NW.js — это трей: System Tray Icon для Windows, Status Icon для GTK, Status Item для OSX. Данный объект создается с помощью известного нам модуля gui:

var gui = require("nw.gui"); var tray = new gui.Tray({ title: 'Tray', icon: 'img/icon.png' });  

При работе с данным объектом необходимо заботиться об области видимости переменной, если создать внутри функции, то вскоре он будет удален GC. При создании объекта можно сразу же создать свойства, как мы это сделали в примере, а можно позаботиться об этом несколько позже. Следующие свойства можно определить для данного объекта:

  • Title — будет показываться только в Mac OSX
  • Tooltip — подсказка, доступная на всех платформах
  • Icon — иконка, отображаемая в трее, также доступна на всех платформах
  • Menu — меню, которое в Mac OS X будет появляться по щелчку, для Windows и Linux — будет реагировать на одиночный щелчок и щелчок правой кнопкой (о том, как создать меню, см первую часть цикла статей)

Для того, чтобы использовать трей в нашем приложении, необходимо создать любой элемент разметки, наиболее очевидным вариантом является кнопка button. Далее необходимо подписаться на событие click, и использовать методы объекта window, с которым мы сейчас и познакомимся.

Работаем с окном

Для работы с окном, необходимо либо получить существующее окно, либо создать новое. Итак, для того, чтобы получить текущее окно, в котором и отображается наше приложение, необходимо выполнить команду:

var win = gui.Window.get(); 

А для создания нового окна, необходимо указать адрес, по которому располагается страница, для открытия в данном окне, а также параметры для открытия (данные параметры соотвествуют тем, что мы указываем при создании манифеста, см. первую часть из цикла статей):

var win = gui.Window.open ('https://myurl', {   position: 'center',   width: 901,   height: 127 }); 

Также в параметрах можно передать специальное свойство focus: true, при указании которого, только что созданное окно сразу же получит фокус, в противном случае фокус останется на нашем текущем окне.
Если мы создаем новое окно и хотим что-то с ним сделать после того, как оно будет создано, необходимо подписаться на соответствующее событие:

win.on ('loaded', function(){   // получим элемент document текущего окна, с которым впоследствии мы можем работать   var document = win.window.document;      // логика для работы с созданным окном... }); 

Как видно из примера, одним из свойств окна является объект window, из которого мы можем получить остальные элементы, включая document. Кроме данного свойства окно также поддерживает множество других:

  • x, y — координаты окна
  • width, height — размеры окна
  • title — заголовок окна
  • menu — главное меню приложения, которое будет располагаться в верхней части окна

Данные свойства можно не только читать, но и изменять. Кроме них также есть свойства, доступные только для чтения (все они логические и могут принимать значения true или false)

  • isTransparent — является ли окно прозрачным
  • isFullscreen — открыто ли окно на полный экран
  • isKioskMode — открыто ли приложение в киоск моде

Помимо свойств, объект поддерживает большое количество методов. Основные методы приводятся ниже и для удобства объединены по группам.
Методы для изменения позиции и размеров окна:

  • moveTo — переместить окно на позицию, переданную в параметрах в виде координаты x и координаты y
  • moveBy — переместить окно на определенное количество пикселей вправо и вниз (в случае задания отрицательных аргументов влево и вверх)
  • resizeTo — изменить размеры окна: первый аргумент указывает ширину, второй — высоту окна
  • resizeBy — изменить размеры окна на определенное количество пикселей вправо и вниз (в случае задания отрицательных аргументов влево и вверх)
  • setPosition — задать специфическую позицию окна, переданную в качестве аргумента (на данный момент поддерживается только ‘center’)

Методы для работы с фокусом и видимостью:

  • focus — метод без параметров для передачи фокуса окну
  • blur — метод без параметров для того, чтобы сделать окно не активным
  • hide — скрыть окно
  • show — показать окно, однако если передать в качестве аргумента false, то метод будет работать как hide

Методы для управления свертыванием/развертыванием, закрытием окна:

  • close — закрытие окна, при этом возникает событие close, однако если передать в качестве аргумента true, то событие возникать не будет
  • reload — перезагрузить окно
  • reloadDev — перезагрузить окно, но с элементами разработчика
  • maximize — распахнуть окно на весь экран
  • unmaximize — вернуть окно в исходный размер после того, как окно распахнули
  • minimize — свернуть окно
  • restore — развернуть окно, противоположно minimize
  • setShowInTaskbar — показывать ли окно на панели задач
  • setAlwaysOnTop — показывать ли окно поверх других

Методы для управления состоянием:

  • enterFullscreen, leaveFullscreen, toggleFullscreen — управление полноэкранным режимом
  • enterKioskMode, leaveKioskMode, toggleKioskMode — управление киоск режимом
  • setTransparent — установить/сбросить прозрачность окна, в зависимости от переданного аргумента
  • showDevTools — показать инструменты разработчика
  • closeDevTools — скрыть инструменты разработчика
  • isDevToolsOpen — проверка: открыты ли инструменты разработчика

Методы управления возможностью изменять размеры окна

  • setResizable — установить/сбросить возможность изменения размера экрана
  • setMaximumSize — задать ограничения на максимальный размер экрана (первый аргумент — ширина, второй — высота)
  • setMinimumSize — задать ограничения на минимальный размер экрана (первый аргумент — ширина, второй — высота)

Итак, познакомившись с объектами tray и window, напишем функциональность сворачивания в трей. Для этого в разметку необходимо (как говорилось выше) добавить элемент, например кнопку или ссылку:

<a ng-click="ctrl.toTray()">В трей</a> 

И изменить контроллер следующим образом:

Код контроллера

(function () {    'use strict';     angular       .module('main', [])       .controller('MainCtrl', ['$scope', 'CryptoService', MainCtrl]);     function MainCtrl($scope, CryptoService) {       var self = this;       var localStorageKey = "loginPasswordData"       this.data = [];       var gui = require('nw.gui');       var clipboard = gui.Clipboard.get();       var win = gui.Window.get();       var tray =  new gui.Tray({ title: 'Tray', icon: 'img/test.png' });       tray.on("click", restoreFromTray);        this.remove = remove;       this.copy = copy;       this.add = add;       this.toTray = toTray;        load();        $scope.$watch("ctrl.data", save, true);        return this;        function remove(ind){          self.data.splice(ind, 1);       };        function copy(ind){          clipboard.set(self.data[ind].password);       };        function add(){          self.data.push({login: "login", password: "password"});       };        function load(){          var text = localStorage.getItem(localStorageKey);          if(text) {             self.data = CryptoService.decrypt(text);          }       }        function save(){          if(self.data) {             localStorage.setItem(localStorageKey, CryptoService.encrypt(self.data));          }       }        function toTray(){         win.minimize();         win.setShowInTaskbar(false);       } 	       function restoreFromTray(){         win.restore();         win.setShowInTaskbar(true);       }    }  })(); 

Также для работы данного примера необходимо создать папку img и поместить туда иконку трея (в данном примере это img/test.png).

Заключение

В рамках статьи мы написали прототип приложения, который вы можете улучшить различным образом: начиная от стилей и заканчивая улучшениями в функционале. Например:

  • можно подписаться на событие keydown и для первых 10 паролей, при нажатии на кнопку от 0 до 9, копировать пароль в буфер обмена, это упростит и ускорит работу с программой
  • добавить способ копирования не только пароля, но и логина

Успехов в программировании!

ссылка на оригинал статьи https://habrahabr.ru/post/275615/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *