Статьи

Быстрое и легкое хранилище данных для приложений Cordova с LokiJS

В этом уроке мы создадим приложение для создания заметок с Cordova и LokiJS . Мы будем использовать Ionic Framework для обработки структуры приложения и взаимодействия с пользовательским интерфейсом. Вот как будет выглядеть финальное приложение:

приложение для заметок

Что такое LokiJS?

LokiJS — это быстрое, ориентированное на память хранилище документов для node.js, браузеров и Apache Cordova, которое предоставляет комплексный API для хранения и извлечения данных. В отличие от localStorage, в LokiJS нет необходимости разбираться для хранения и доступа. LokiJS сохраняет данные в localStorage по умолчанию, но вы можете использовать другие методы хранения с «адаптерами постоянства», включая «IndexedAdapter», который использует IndexedDB для сохранения. Для этого урока мы будем использовать адаптер FileSystem, который хранит данные с использованием файла JSON, сохраненного в FileSystem.

Настроить

Сначала установите Cordova и Ionic через npm с помощью следующей команды:

npm install -g cordova ionic 

После установки создайте новый проект Ionic:

 ionic start loki-notes blank 

Это создает новый Ionic-проект с использованием пустого шаблона. Доступны и другие шаблоны, такие как «tabs» и «sidemenu», но мы будем придерживаться пустого.

Откройте каталог loki-notes и установите необходимые платформы.

 cd loki-notes ionic platform add android 

Установка зависимостей

Приложение будет зависеть от следующих библиотек, я включил инструкции по установке после каждой.

LokiJS

Используется для хранения данных приложения.

 bower install lokijs --save 

Lodash

Используется для усечения описания заметок.

 bower install lodash --save 

Локи-Cordova фс-адаптер

Используется LokiJS для хранения данных в файловой системе. К сожалению, эта библиотека не проиндексирована ни в bower, ни в npm, поэтому вам нужно скачать ее с Github и сохранить в каталоге www / js .

Кордова-Plugin-файл

Зависимость для адаптера LokiJS FileSystem.

 cordova plugin add cordova-plugin-file 

Кордова-Plugin-камера

Используется для захвата фотографий, прикрепленных к каждой заметке.

 cordova plugin add cordova-plugin-camera 

Закончив установку всех зависимостей, добавьте их в файл www / index.html после основного файла JavaScript cordova.

 <script src="js/app.js"></script><!-- the main cordova JavaScript file--> <script src="lib/lokijs/src/lokijs.js"></script> <script src="lib/lokijs/src/loki-angular.js"></script> <script src="js/loki-cordova-fs-adapter.js"></script> <script src="lib/lodash/dist/lodash.min.js"></script> 

Обратите внимание, что cordova-plugin-file и cordova-plugin-camera являются плагинами Cordova, поэтому нам не нужно связывать их в файле index.html . API-интерфейсы подключаемых модулей имеют глобальную область видимости, поэтому мы можем вызывать их из любого файла JavaScript.

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

 <script src="js/controllers/NoteController.js"></script> <script src="js/services/NoteService.js"></script> <script src="js/services/CameraService.js"></script> 

Сборка приложения

Сервисы

Сервисы инкапсулируют общую функциональность в одной функции. Это позволяет нам иметь более сжатый код при вызове различных функций из библиотек, которые мы используем. Сервисы хранятся в каталоге www / js / services .

Камера Сервис

Создайте сервис камеры ( www / js / services / CameraService.js ), отвечающий за перенос вызова в плагин камеры, и добавьте следующий код:

 (function(){ angular.module('starter') .service('CameraService', ['$q', CameraService]); function CameraService($q){ var me = this; me.options = { quality: 80, correctOrientation: true }; function getPicture(){ var q = $q.defer(); me.options.encodingType = Camera.EncodingType.JPEG; me.options.sourceType = Camera.PictureSourceType.CAMERA; navigator.camera.getPicture( function(result){ q.resolve(result); }, function(err){ q.reject(err); }, me.options ); return q.promise; } return { getPicture: getPicture } } })(); 

Разбивая вышеприведенный код, мы сначала оборачиваем все в «IIFE» (выражение для немедленного вызова функции), чтобы избежать предоставления кода глобальной области видимости.

 (function(){ ... })(); 

Затем мы создаем сервис камеры. При этом используется встроенный угловой сервис $q который позволяет вызывать функции асинхронно. Это означает, что нам не нужно ждать, пока плагин камеры выдаст результат, прежде чем приложение станет полностью отзывчивым. Как только фотография выбрана в приложении камеры по умолчанию, она возвращает управление приложению.

 angular.module('starter') .service('CameraService', ['$q', CameraService]); function CameraService($q){ ... } 

Внутри функции CameraService мы указываем параметры по умолчанию для камеры. Мы указываем два варианта. quality которое позволяет нам установить качество получаемой фотографии (меньшее качество означает меньший размер файла) и correctOrientation которая автоматически корректирует ориентацию фотографии в зависимости от ориентации камеры на момент съемки.

 var me = this; me.options = { quality: 80, correctOrientation: true }; 

CameraService имеет один метод, getPicture который запускает приложение камеры по умолчанию на устройстве. В начале функции мы запускаем службу $q , вызывая метод defer . После этого мы указываем пару вариантов. encodingType который устанавливает формат фотографии, и sourceType который позволяет нам указать источник фотографии, в данном случае камеру. После указания параметров приложение камеры по умолчанию запускается путем вызова navigator.camera.getPicture . Это принимает три аргумента: функцию обратного вызова успеха, функцию обратного вызова ошибки и параметры камеры. Затем мы открываем эту функцию для внешнего мира, используя шаблон модуля выявления.

 function getPicture(){ var q = $q.defer(); me.options.encodingType = Camera.EncodingType.JPEG; me.options.sourceType = Camera.PictureSourceType.CAMERA; navigator.camera.getPicture( function(result){ q.resolve(result); }, function(err){ q.reject(err); }, me.options ); return q.promise; } return { getPicture: getPicture } 

Примечание Сервис

Служба заметок ( www / js / services / NoteService.js ) инкапсулирует все вызовы функций LokiJS.

 (function(){ angular.module('starter') .service('NoteService', ['$q', 'Loki', NoteService]); function NoteService($q, Loki){ var db; var notes; function initialize(){ var adapter = new LokiCordovaFSAdapter({"prefix": "loki"}); db = new Loki('notes_db', { autosave: true, autosaveInterval: 1000, adapter: adapter }); } function getNotes(){ return $q(function(resolve, reject){ db.loadDatabase({}, function(){ notes = db.getCollection('notes'); if(!notes){ notes = db.addCollection('notes'); } resolve(notes.data); }); }); } function addNote(note){ notes.insert(note); } function updateNote(note){ notes.update(note); } function deleteNote(note){ notes.remove(note); } return { initialize: initialize, getNotes: getNotes, addNote: addNote, updateNote: updateNote, deleteNote: deleteNote }; } })(); 

Разбивка кода выше. Мы используем сервис Loki предоставляемый LokiJS, для взаимодействия с базой данных.

 (function(){ angular.module('starter') .service('NoteService', ['$q', 'Loki', NoteService]); function NoteService($q, Loki){ ... } })(); 

Внутри функции NoteService находятся две переменные. db которой хранится ссылка на текущую базу данных, с которой мы работаем, и notes которых хранится ссылка на текущую коллекцию. Коллекции — это просто модный термин для таблиц.

 var db; var notes; 

Функция initialize инициализирует базу данных, сначала создавая новый экземпляр адаптера файловой системы Cordova. Это принимает объект, содержащий параметры, которые мы хотим передать. В этом случае мы хотим указать используемый префикс. Далее мы создаем новый экземпляр Loki. Это принимает имя базы данных в качестве первого аргумента и параметры базы данных для второго. Для параметров мы устанавливаем для autosave значение true . Это связано с тем, что lokiJS не сохраняет данные автоматически в выбранный вами адаптер сохраняемости. Все хранится в памяти, пока вы не saveDatabase метод saveDatabase в db . Установка autosave в true означает, что он автоматически saveDatabase метод saveDatabase за кулисами. Дополнительная опция с именем autosaveInterval требуется, когда вы устанавливаете для autosave значение true . Это интервал в миллисекундах, в течение которого LokiJS должен сохранять данные. В приведенном ниже примере интервал установлен на 5000, что означает, что он будет сохраняться каждые 5 секунд.

 function initialize(){ var adapter = new LokiCordovaFSAdapter({"prefix": "loki"}); db = new Loki('notes_db', { autosave: true, autosaveInterval: 5000, adapter: adapter }); } 

Метод getNotes загружает данные, хранящиеся в коллекции notes . Он создается, если коллекция заметок еще не существует. Обратите внимание, что мы заключаем все в вызов службы $q . Синтаксис отличается от службы камеры, но идея в основном та же.

 function getNotes(){ return $q(function(resolve, reject){ db.loadDatabase({}, function(){ //retrieve data from notes collection notes = db.getCollection('notes'); if(!notes){ //create notes collection notes = db.addCollection('notes'); } resolve(notes.data); }); }); } 

Функции addNote , updateNote и deleteNote служат оболочкой для API LokiJS для вставки, обновления и удаления данных из базы данных. Все из которых принимают в качестве аргумента объект, содержащий данные заметки.

 function addNote(note){ notes.insert(note); } function updateNote(note){ notes.update(note); } function deleteNote(note){ notes.remove(note); } 

Раскрыть все для внешнего мира:

 return { initialize: initialize, getNotes: getNotes, addNote: addNote, updateNote: updateNote, deleteNote: deleteNote }; 

Контроллеры

Единственным контроллером для этого приложения является контроллер заметок, отвечающий за обработку взаимодействий пользовательского интерфейса в приложении.

 (function(){ angular.module('starter') .controller('NoteController', ['$scope', '$ionicModal', '$ionicPlatform', 'CameraService', 'NoteService', NoteController]); function NoteController($scope, $ionicModal, $ionicPlatform, CameraService, NoteService){ var me = this; me.notes = []; $ionicPlatform.ready(function(){ NoteService.initialize(); NoteService.getNotes().then(function(notes){ me.notes = notes; }); }); $ionicModal.fromTemplateUrl('new-note.html', { scope: $scope, animation: 'slide-in-up' }).then(function(modal){ $scope.new_note_modal = modal; }); $ionicModal.fromTemplateUrl('image-modal.html', { scope: $scope, animation: 'slide-in-up' }).then(function(modal){ $scope.image_modal = modal; }); me.truncate = function(string){ return _.truncate(string, {length: 35}); }; $scope.deleteNote = function(note){ NoteService.deleteNote(note); }; $scope.newNote = function(){ $scope.note = {}; $scope.isUpdate = false; $scope.new_note_modal.show(); }; $scope.viewNote = function(note){ $scope.note = {}; $scope.note = note; $scope.isUpdate = true; $scope.new_note_modal.show(); }; $scope.takePicture = function(){ CameraService.getPicture().then(function(photo){ $scope.note.photo = photo; }); }; $scope.saveNote = function(){ if($scope.isUpdate){ NoteService.updateNote($scope.note); }else{ NoteService.addNote($scope.note); } $scope.note = { title: '', text: '', photo: null }; $scope.new_note_modal.hide(); }; $scope.viewImage = function(image){ $scope.note.photo = image; $scope.image_modal.show(); }; $scope.closeModal = function(modal){ $scope[modal + '_modal'].hide(); }; } })(); 

Разбивка кода выше. Сначала мы создаем NoteController и импортируем все необходимые нам сервисы.

 (function(){ angular.module('starter') .controller('NoteController', ['$scope', '$ionicModal', '$ionicPlatform', 'CameraService', 'NoteService', NoteController]); function NoteController($scope, $ionicModal, $ionicPlatform, CameraService, NoteService){ ... })(); 

Вот краткое описание каждого из них:
$scope : позволяет получать и устанавливать данные в текущей области. Эти данные затем могут быть использованы внутри представлений.
$ionicModal : используется для создания модальных.
ionicPlatform : используется для прослушивания ready события для Ionic.
CameraService : служба камеры, созданная ранее. Это позволяет нам вызывать API, предоставляемый плагином камеры.
NoteService : служба заметок, которую мы создали ранее. Это позволяет нам совершать вызовы API, предоставляемого LokiJS.

Объявите переменную me которая будет использоваться как ссылка на контроллер.

 var me = this; 

Прикрепите пустой массив к объекту notes и прослушайте событие ready в $ionicPlatform . Когда эта функция активирована, это означает, что устройство готово. Он запускается только один раз при запуске приложения, поэтому является идеальным местом для инициализации базы данных и получения данных, хранящихся в коллекции заметок.

 me.notes = []; $ionicPlatform.ready(function(){ //when the device is ready NoteService.initialize(); //initialize the database //get all the notes from the notes collection NoteService.getNotes().then(function(notes){ me.notes = notes; //assign it to the controller }); }); 

Инициализируйте модальные поля для создания новой заметки и просмотра изображения, прикрепленного к заметке.

 //modal for creating new note $ionicModal.fromTemplateUrl('new-note.html', { scope: $scope, animation: 'slide-in-up' }).then(function(modal){ $scope.new_note_modal = modal; }); //modal for viewing image attached to note $ionicModal.fromTemplateUrl('image-modal.html', { scope: $scope, animation: 'slide-in-up' }).then(function(modal){ $scope.image_modal = modal; }); 

Добавьте метод усечения к контроллеру. Все это ограничивает количество символов в строке. При этом используется метод truncate в lodash, который принимает строку, с которой вы хотите работать, в качестве первого аргумента и объект, содержащий параметры. В этом случае мы хотим ограничить количество символов до 35 символов минус 3 символа, используемых для указания того, что строка была усечена. По умолчанию в lodash используются тройные точки ( ).

 me.truncate = function(string){ return _.truncate(string, {length: 35}); }; 

Присоедините функцию deleteNote к текущей области. Это принимает объект, содержащий данные для конкретной заметки в качестве аргумента. Все, что он делает, это вызывает метод deleteNote из NoteService .

 $scope.deleteNote = function(note){ NoteService.deleteNote(note); }; 

Функция newNote вызывается при каждом нажатии кнопки новой заметки . Это очищает заметку, хранящуюся в настоящее время в $scope и устанавливает isUpdate в false . Это будет служить переключателем для определения того, является ли текущая заметка существующей (см. Метод saveNote ). Наконец, он открывает модальные для создания новых заметок.

 $scope.newNote = function(){ $scope.note = {}; $scope.isUpdate = false; $scope.new_note_modal.show(); }; 

Функция viewNote вызывается всякий раз, когда пользователь дважды нажимает на конкретную заметку. Сначала сбросьте примечание, сохраненное в области, и назначьте ему новое значение. Он устанавливает для isUpdate значение true потому что мы пытаемся открыть существующую заметку, поэтому понятно, что пользователь пытается ее просмотреть или обновить. Наконец мы покажем модал.

 $scope.viewNote = function(note){ $scope.note = {}; $scope.note = note; $scope.isUpdate = true; $scope.new_note_modal.show(); }; 

Функция takePicture вызывается, когда пользователь takePicture значок камеры в заголовке для нового модального примечания. Он запускает приложение камеры по умолчанию и при выборе фотографии присваивает его текущей области.

 $scope.takePicture = function(){ CameraService.getPicture().then(function(photo){ $scope.note.photo = photo; }); }; 

Функция saveNote сохраняет и обновляет заметку в базе данных. Он использует свойство isUpdate для определения необходимости обновления или создания текущей заметки, хранящейся в области $scope . Как только это будет сделано, мы очищаем заметку и закрываем новую модальную заметку.

 $scope.saveNote = function(){ if($scope.isUpdate){ NoteService.updateNote($scope.note); }else{ NoteService.addNote($scope.note); } $scope.note = { title: '', text: '', photo: null }; $scope.new_note_modal.hide(); }; 

Функция viewImage показывает вложение изображения в полноэкранном режиме.

 $scope.viewImage = function(image){ $scope.note.photo = image; $scope.image_modal.show(); }; 

Функция closeModal закрывает модальный closeModal , указанный в качестве аргумента.

 $scope.closeModal = function(modal){ $scope[modal + '_modal'].hide(); }; 

Взгляды

В приложении есть одно представление — представление заметок, поэтому создайте www / templates / notes.html и добавьте следующее.

 <ion-view title="Notes" ng-controller="NoteController as note_ctrl"> <header class="bar bar-header bar-stable"> <h1 class="title">Notes</h1> <button class="button button-clear" ng-click="newNote()">New Note</button> </header> <ion-content class="has-header padding"> <div class="card" ng-repeat="note in note_ctrl.notes"> <div class="item item-text-wrap" on-double-tap="viewNote(note)" on-drag-right="deleteNote(note)"> <strong>{{note.title}}</strong> <div class="description">{{note_ctrl.truncate(note.text)}}</div> </div> </div> <div class="card" ng-if="note_ctrl.notes.length === 0"> <div class="item item-text-wrap"> No notes yet </div> </div> </ion-content> <script id="new-note.html" type="text/ng-template"> <ion-modal-view> <ion-header-bar class="bar-stable"> <button class="button button-clear icon ion-camera" ng-click="takePicture()"></button> <h1 class="title">New Note</h1> <button class="button button-clear" ng-click="closeModal('new_note')">Close</button> </ion-header-bar> <ion-content> <div class="list"> <label class="item item-input item-stacked-label"> <span class="input-label">Title</span> <input type="text" ng-model="note.title"> </label> <label class="item item-input item-stacked-label"> <span class="input-label">Note</span> <textarea ng-model="note.text"></textarea> </label> </div> <div class="padding" ng-if="note.photo"> <button class="button button-clear icon ion-image" ng-click="viewImage(note.photo)"></button> </div> <div class="padding"> <button class="button button-positive button-block" ng-click="saveNote()"> Save </button> </div> </ion-content> </ion-modal-view> </script> <script id="image-modal.html" type="text/ng-template"> <div class="modal image-modal transparent" ng-click="closeModal('image')"> <img ng-src="{{note.photo}}" class="fullscreen-image" /> </div> </script> </ion-view> 

Разбивка кода выше. Сначала мы обертываем все в <ion-view> . Это стандартный контейнер для просмотра содержимого и любой навигационной информации и информации панели заголовка. Мы указали два атрибута, title , который является именем, которое вы хотите дать представлению, и ng-controller , который позволяет нам указать контроллер, используемый для этого представления. Псевдоним note_ctrl указан так, чтобы мы могли ссылаться на любые данные или функции, подключенные к контроллеру, используя псевдоним.

 <ion-view title="Notes" ng-controller="NoteController as note_ctrl"> </ion-view> 

Тег <header> создает заголовок для приложения и содержит элемент <h1> который отображает заголовок текущей страницы и элемент <button> для создания новой заметки. Атрибут ng-click указывает, что при нажатии этой кнопки будет выполняться функция newNote прикрепленная к области $scope .

 <header class="bar bar-header bar-stable"> <h1 class="title">Notes</h1> <button class="button button-clear" ng-click="newNote()">New Note</button> </header> 

<ion-content> используется для указания основного содержимого представления. В этом случае основным контентом является список заметок, сохраненных в данный момент в базе данных.

Для этого мы создаем <div> с классом card и используем директиву ng-repeat чтобы повторять элемент для каждой заметки, хранящейся в массиве notes . Внутри каждой карты есть <div> с классом item . Это служит контейнером для любого контента, который вы хотите добавить внутри карты. Здесь мы просто выводим название заметки и ее описание.

Атрибут on-double-tap указывает функцию, выполняемую каждый раз, когда пользователь дважды нажимает на элемент. Это вызывает функцию viewNote которая открывает модальное viewNote для просмотра заметки. Атрибут on-drag-right указывает функцию, выполняемую при перетаскивании элемента вправо. Это обычный жест для удаления чего-либо, поэтому мы прикрепили к deleteNote функцию deleteNote . Если в настоящее время в базе данных нет заметок, мы заявляем, что заметок еще нет.

 <ion-content class="has-header padding"> <div class="card" ng-repeat="note in note_ctrl.notes"> <div class="item item-text-wrap" on-double-tap="viewNote(note)" on-drag-right="deleteNote(note)"> <strong>{{note.title}}</strong> <div class="description">{{note_ctrl.truncate(note.text)}}</div> </div> </div> <div class="card" ng-if="note_ctrl.notes.length === 0"> <div class="item item-text-wrap"> No notes yet </div> </div> </ion-content> 

В Ionic модалы создаются с использованием <script> который имеет тип text/ng-template и обязательный атрибут id . Затем содержимое помещается в <ion-modal-view> . Внутри находится заголовок и основной контент. Заголовок содержит кнопку для запуска приложения камеры по умолчанию, заголовок модального окна и кнопку для закрытия модального окна. Основным содержанием является форма, содержащая текстовые поля для заголовка и описания, а также кнопка для сохранения заметки. Если к заметке прикреплена фотография, добавляется кнопка со значком. Это откроет фотографию в полноэкранном режиме при нажатии пользователем.

 <script id="new-note.html" type="text/ng-template"> <ion-modal-view> <ion-header-bar class="bar-stable"> <button class="button button-clear icon ion-camera" ng-click="takePicture()"></button> <h1 class="title">New Note</h1> <button class="button button-clear" ng-click="closeModal('new_note')">Close</button> </ion-header-bar> <ion-content> <div class="list"> <label class="item item-input item-stacked-label"> <span class="input-label">Title</span> <input type="text" ng-model="note.title"> </label> <label class="item item-input item-stacked-label"> <span class="input-label">Note</span> <textarea ng-model="note.text"></textarea> </label> </div> <div class="padding" ng-if="note.photo"> <button class="button button-clear icon ion-image" ng-click="viewImage(note.photo)"></button> </div> <div class="padding"> <button class="button button-positive button-block" ng-click="saveNote()"> Save </button> </div> </ion-content> </ion-modal-view> </script> 

Модальное изображение отображает прикрепленное изображение в полноэкранном режиме. Это показано, когда viewImage функция viewImage .

 <script id="image-modal.html" type="text/ng-template"> <div class="modal image-modal transparent" ng-click="closeModal('image')"> <img ng-src="{{note.photo}}" class="fullscreen-image" /> </div> </script> 

стайлинг

Поскольку Ionic Framework обрабатывает большинство стилей для нас, все, что нам нужно сделать, это добавить стили для описания заметки и полноэкранного изображения:

Добавьте следующее в www / css / style.css

 .description { color: #737373; font-size: 15px; } .fullscreen-image { max-width: 100%; max-height: 100%; bottom: 0; left: 0; margin: auto; overflow: auto; position: fixed; right: 0; top: 0; } 

Бутстрапирование

Чтобы собрать все вместе, откройте файл www / js / app.js и зарегистрируйте lokijs сразу после ionic .

 angular.module('starter', ['ionic', 'lokijs']) 

Затем добавьте конфигурацию для состояния приложения. Поскольку у нас есть одна страница, у нас есть только одно состояние, называемое notes . Используя метод state мы указываем имя состояния, URL, к которому он может быть получен, и путь к представлению (относительно каталога www ). Наконец, мы указываем URL по умолчанию, по которому нужно перейти.

 .config(function($stateProvider, $urlRouterProvider) { $stateProvider .state('notes', { url: '/notes', templateUrl: 'templates/notes.html' }); // if none of the above states are matched, use this as the fallback $urlRouterProvider.otherwise('/notes'); }); 

Вывод

Это оно! Из этого руководства вы узнали, как использовать LokiJS для сохранения данных в приложениях Cordova. Используя LokiJS, мы выполнили основные операции CRUD (создание, чтение, обновление, удаление) в базе данных. Мы едва коснулись поверхности этого урока, поэтому вот несколько тем для изучения:

  • Find : в этом руководстве мы не выполняли никаких сложных запросов, поэтому ознакомьтесь с документацией по методу find .
  • API изменений : если вам нужно сохранить данные на сервере, API изменений будет полезен. Это позволяет синхронизировать изменения, сделанные в локальной копии базы данных, на удаленном сервере.

Полный код этого руководства можно найти на Github. Если у вас есть вопросы или комментарии, сообщите мне об этом ниже.