Данная статья является продолжением статьи «Десктопные приложения на 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:
(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/
Добавить комментарий