Статьи

Отслеживание расходов с CouchDB и Angular

В этом руководстве мы создадим приложение, используя CouchDB в качестве нашего бэкэнда и Angular в качестве предпочтительной технологии интерфейса. CouchDB — это база данных NoSQL, а Angular — одна из более новых платформ JavaScript MVC. Удивительно и удивительно то, что CouchDB — это база данных с HTTP API — наше клиентское приложение будет напрямую взаимодействовать с базой данных: CouchDB будет действовать как единственный бэкэнд, который нам нужен для нашего клиентского приложения!

Мы сосредоточимся на небольшом приложении для отслеживания наших расходов. Для каждого шага будет выполняться коммит, а иногда коммит также включает тесты. Тесты не будут темой в этом уроке, но если вы заинтересованы в них, вы должны взглянуть! Весь код, который используется в этом руководстве, вы найдете в репозитории на GitHub .

Почему CouchDB?

Некоторые из вас могут сказать, что мы могли бы использовать альтернативы на стороне клиента. IndexedDB или Local Storage — это технологии, которые локально работают на клиенте для сохранения данных. Но использование сервера базы данных имеет несколько преимуществ: мы можем подключиться к нашему приложению со многими клиентами. Ваш партнер может обновить список расходов, пока вы находитесь в одиночестве в другом супермаркете, также добавив расходы.

Использование CouchDB дает преимущества: CouchDB «говорит» по HTTP изначально, поэтому нам не понадобится еще один слой между нашей базой данных и приложением. Наше приложение JavaScript может напрямую взаимодействовать с базой данных CouchDB с помощью интерфейса RESTful, предоставляемого CouchDB!

И, если бы мы захотели использовать репликацию для нашей базы данных, это было бы так же просто, как нарезать хлеб: CouchDB предназначен для создания распределенных систем баз данных.

Требования

Для этого урока вам потребуется установить последнюю версию CouchDB (1.6) и последнюю стабильную версию Node.js (в настоящее время 0.10.x).

Установка Node.js & Yo

Как пользователь Mac вы можете получить официальный установщик на домашней странице Node . Еще один способ управления установками Node.js в Linux и OSX — это замечательный nvm от Tim Caswell.

Мы установим Йо, чтобы подмостить наше приложение. Йо задаст нам несколько вопросов в процессе создания нашего скелета. Йо спрашивает, хотим ли мы использовать SASS, и если вы не уверены, просто ответьте «нет», но мы определенно хотим включить Bootstrap и предварительно выбранные угловые модули.

В нашей оболочке мы набираем:

npm install -g yo generator-angular grunt-cli couchapp mkdir expenses && cd expenses yo angular expenses 

Как часть наших лесов, Йо создал для нас Gruntfile (Gruntfile.js). Grunt — это исполнитель задач в JavaScript с множеством уже написанных плагинов для автоматизации задач и облегчения вашей жизни.

С помощью команды grunt serve сервер разработки запускается, и http://127.0.0.1:9000 должен открываться в браузере после завершения задач grunt. Пример этого показан на следующем рисунке.

подмости

Установка CouchDB

Существуют отличные документы по установке CouchDB на многих платформах — есть пакеты для всех основных операционных систем, а в OSX вы можете использовать brew для установки CouchDB.

Первые шаги с CouchDB

Давайте запустим наш первый экземпляр CouchDB и создадим базу данных:

 couchdb & # start a CouchDB curl -X PUT http://127.0.0.1:5984/expenses # create the database expenses 

CouchDB отвечает:

 {"ok":true} 

Мы только что создали нашу первую базу данных, используя HTTP!

Давайте подробнее рассмотрим HTTP API CouchDB: теперь мы можем вставить первый документ, скажем, мы хотим отследить какой-то попкорн, который мы купили (нам понадобятся эти вызовы CouchDB позже для нашего приложения).

 curl -X POST http://127.0.0.1:5984/expenses -H "Content-Type: application/json" -d '{"name": "Popcorn", "price": "0.99"}' 

CouchDB отвечает:

 {"ok":true,"id":"39414de82e814b6e1ca754c61b000efe","rev":"1-2b0a863dc254239204aa5b132fda8f58"}`` 

Теперь мы можем получить доступ к документу, используя запрос GET и идентификатор, который CouchDB присвоил нашему документу, поскольку мы не предоставили конкретный идентификатор:

 curl -X GET http://127.0.0.1:5984/expenses/39414de82e814b6e1ca754c61b000efe 

CouchDB отвечает:

 {"_id":"39414de82e814b6e1ca754c61b000efe","_rev":"1-2b0a863dc254239204aa5b132fda8f58","name":"Popcorn","price":"0.99"} 

После этого мы вставляем другой документ:

 curl -X POST http://127.0.0.1:5984/expenses -H "Content-Type: application/json" -d '{"name": "Washing powder", "price": "2.99"}' 

Конфигурация: CORS с CouchDB

Наш клиент будет связываться через HTTP из другого места, чем сам CouchDB. Чтобы сделать это в нашем браузере, нам нужно включить CORS (Cross-Origin Resource Sharing) в CouchDB.

В этом случае мы хотим изменить local.ini для наших локальных пользовательских изменений. Можно изменить конфигурацию через HTTP. В разделе https мы включаем CORS, а затем настраиваем наши источники с подстановочным знаком:

 curl -X PUT http://localhost:5984/_config/httpd/enable_cors -d '"true"' curl -X PUT http://localhost:5984/_config/cors/origins -d '"*"' 

local.ini двумя командами мы меняем local.ini CouchDB. Вы можете узнать, где находится local.ini используя couchdb -c .

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

Угловая и зависимая инъекция

В app/scripts/app.js мы найдем основной файл JavaScript нашего приложения, который на самом деле является так называемым угловым модулем. Этот модуль загружает некоторые другие модули как зависимости (например, ngCookies ). В этом файле мы также находим клиентскую маршрутизацию для нашего приложения с использованием $routeprovider .

$routeprovider в этом файле является хорошим примером внедрения зависимостей Angular (DI). Определяя имя службы, которую вы хотите использовать, Angular вводит ее в заданную область действия функции. Вы можете найти дополнительную информацию о внедрении зависимостей Angular в документах .

Поскольку мы хотим иметь данные, необходимые для подключения к нашей CouchDB, в одном центральном месте, давайте попробуем использовать DI с константой. Мы используем цепочку, чтобы добавить их в наш модуль:

 .constant('appSettings', { db: 'http://127.0.0.1:5984/expenses' }); 

Единственный контроллер, который у нас есть, который был создан во время первоначального скаффолда, это MainCtrl расположенный в app/scripts/controllers/main.js MainCtrl определен, и $scope MainCtrl . Мы увидим, как использовать область позже.

Теперь мы можем добавить appSettings к аргументам функции, чтобы внедрить их, как мы видели ранее с $routeprovider :

 .controller('MainCtrl', function ($scope, appSettings) { console.log(appSettings); }); 

Теперь вы должны иметь возможность регистрировать вывод на отладочной консоли вашего браузера. Поздравляем! Вы успешно использовали внедрение зависимостей. Полный коммит можно найти по адресу: https://github.com/robertkowalski/couchdb-workshop/commit/d6b635a182df78bc22a2e93af86162f479d8b351 .

Выборка результатов

На следующем шаге мы добавим сервис $http для извлечения данных из нашей CouchDB и обновления представления. В то время как традиционные базы данных работают с данными, которые разлагаются на таблицы, CouchDB использует неструктурированные документы, которые можно объединять, фильтровать и объединять с помощью карты и сокращать функции с помощью концепции, называемой представлениями. Представление определяется проектным документом, особым видом документа.

Вы можете написать представление по своему усмотрению и отправить его в CouchDB с помощью curl, использовать графический интерфейс по адресу http://localhost:5984/_utils или с помощью такого инструмента, как CouchApp — существует множество инструментов, таких как CouchApp ( npm install -g couchapp ), чтобы упростить разработку и развертывание представлений.

Вот как будет выглядеть наш взгляд:

 { "_id":"_design/expenses", "views": { "byName": { "map": "function (doc) { emit(doc.name, doc.price); }" } } } 

_id важен для нас, так как он определяет путь, по которому мы будем запрашивать представление позже. Свойство _design получает префикс _design когда мы создаем проектный документ. Мы byName наше представление по byName и оно просто включает в себя базовую функцию карты, которая будет генерировать свойство name каждого документа в нашей базе данных в качестве ключа и цену в качестве значения.

Давайте отправим его в CouchDB, используя curl:

 curl -X POST http://127.0.0.1:5984/expenses -H "Content-Type: application/json" -d '{"_id":"_design/expenses","views": {"byName": {"map": "function (doc) {emit(doc.name, doc.price);}"}}}' 

CouchDB отвечает:

 {"ok":true,"id":"_design/expenses","rev":"1-71127e7155cf2f780cae2f9fff1ef3bc"} 

Теперь у нас есть представление, которое мы можем запросить по адресу:

 http://localhost:5984/expenses/_design/expenses/_view/byName 

Если вас интересуют такие инструменты, как CouchApp (подсказка: вы должны будете использовать его позже), вот коммит, который показывает, как его использовать (используйте npm run bootstrap для развертывания проектного документа).

Вы помните наши запросы керлинга в начале? Теперь мы реализуем их в JavaScript. Angular предоставляет сервис $http , который можно внедрить, как показано ниже:

 .controller('MainCtrl', function ($scope, $http, appSettings) { 

Затем мы добавляем функцию для извлечения наших товаров с помощью службы $http :

 function getItems () { $http.get(appSettings.db + '/_design/expenses/_view/byName') .success(function (data) { $scope.items = data.rows; }); } getItems(); 

Служба $http возвращает обещание, которое предоставит нам данные JSON из представления CouchDB. Мы добавляем данные в $scope.items . Используя $scope мы можем устанавливать и обновлять значения в нашем представлении. Если значение изменяется в нашей модели, представление автоматически обновляется. Двухстороннее связывание Angular синхронизирует наши данные между представлением и моделью. Он немедленно обновит представление после того, как контроллер изменит модель, а также обновит модель, когда значения в представлении изменятся.

Давайте добавим немного HTML с выражением для отображения наших элементов в app/views/main.html после того, как мы удалили большую часть шаблонной разметки:

 <div>{{ item[0].key }}</div> <div>{{ item[0].value }}</div> 

Мы увидим первый элемент, который мы добавили в раздел «Первые шаги с CouchDB»:

Вид приложения

Коммит для этой части доступен на GitHub.

Использование директив: ng-repeat

Теперь мы должны увидеть первый элемент, но что за остальные элементы?

Здесь мы можем использовать директиву ng-repeat , которая создаст для нас разметку из более длинных списков. В целом можно сказать, что директива в Angular придает поведение элементу DOM. В Angular существует множество других предопределенных директив, и вы также можете определять свои собственные директивы. В этом случае мы добавляем ng-repeat="item in items" во внешний div , который затем будет перебирать items нашего массива из $scope.items .

Классы pull-left и pull-right являются частью Bootstrap CSS и предоставляют нам плавающие элементы. Поскольку элементы плавают, мы применяем clearfix который также включен в Bootstrap:

 <div ng-repeat="item in items"> <div class="clearfix"> <div class="pull-left">{{ item.key }}</div> <div class="pull-right">{{ item.value }}</div> </div> </div> 

Если вы обновите страницу, элементы будут отображаться в вашем DOM-инспекторе как:

 <!-- ngRepeat: item in items --> <div ng-repeat="item in items" class="ng-scope"> <div class="clearfix"> <div class="pull-left ng-binding">Popcorn</div> <div class="pull-right ng-binding">0.99</div> </div> </div> <!-- end ngRepeat: item in items --> <div ng-repeat="item in items" class="ng-scope"> <div class="clearfix"> <div class="pull-left ng-binding">Washing powder</div> <div class="pull-right ng-binding">2.99</div> </div> </div> <!-- end ngRepeat: item in items --> 

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

Посмотреть список

Создание формы для отправки товаров

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

div с class="row" из Bootstrap используются для адаптивного оформления нашего приложения. Классы Bootstrap, такие как form-control и btn btn-primary , используются для btn btn-primary кнопки и входных данных.

Форма также получает атрибут novalidate : она отключает встроенную проверку формы браузера, поэтому мы можем проверить нашу форму позже, используя Angular:

 <form class="form-inline" role="form" novalidate> <div class="row"> <div class="form-group"> <label class="sr-only" for="item-name">Your item</label> <input class="form-control" id="item-name" name="item-name" placeholder="Your item" /> </div> <div class="form-group"> <label class="sr-only" for="item-price">Price</label> <input class="form-control" id="item-price" name="item-price" placeholder="Price" /> </div> </div> <div class="row"> <button class="btn btn-primary pull-right" type="submit">Save</button> </div> </form> 

Зафиксировать форму можно по адресу https://github.com/robertkowalski/couchdb-workshop/commit/d678c51dfff16210f1cd8843fbe55c97dc25a408 .

Сохранение данных в CouchDB

Используя ng-model мы можем наблюдать и получать доступ к значениям входов в нашем контроллере, а затем отправлять их в CouchDB. Для нашего ввода цены мы добавим атрибут ng-model="price" :

 <input class="form-control" ng-model="price" id="item-price" name="item-price" placeholder="Price" /> 

Ввод для имени получит атрибут ng-model="name" . Это выглядит так:

 <input class="form-control" ng-model="price" id="item-price" name="item-price" placeholder="Price" /> 

Мы также добавляем небольшое поле статуса под нашим последним элементом. Нам это нужно для отображения ошибок.

 <div class="status"> {{ status }} </div> 

Теперь мы можем получить доступ к значениям в нашем контроллере с помощью $scope.price и $scope.name . Объем соединяет вид с нашим контроллером. Глядя на паттерн Model-View-Controller (MVC), область действия будет нашей моделью. Angular иногда также называют MVVM (Model-View-View-Model) Framework — все эти JavaScript MVC-фреймворки часто называют MVW (Model-View-Wh независимо), поскольку между ними существует множество небольших различий.

Но как мы можем отправить форму?

Распространенным способом отправки формы является определение функции в $scope сочетании с директивой ng-submit в представлении. Наша функция создаст JSON, который мы хотим отправить в CouchDB. После создания JSON processForm вызовет postItem который отправит JSON в CouchDB:

 $scope.processForm = function () { var item = { name: $scope.name, price: $scope.price }; postItem(item); }; 
 function postItem (item) { // optimistic ui update $scope.items.push({key: $scope.name, value: $scope.price}); // send post request $http.post(appSettings.db, item) .success(function () { $scope.status = ''; }).error(function (res) { $scope.status = 'Error: ' + res.reason; // refetch items from server getItems(); }); } 

Многое происходит в нашей функции postItem :

Перед отправкой HTTP-запроса в базу данных мы делаем оптимистичное обновление пользовательского интерфейса, поэтому пользователь сразу же видит это обновление, и наше приложение чувствует себя быстрее. Для этого мы добавляем элемент к другим элементам области. Angular обновит представление для нас.

Затем мы выполняем POST-запрос для нашего элемента в фоновом режиме, и в случае успеха мы удаляем любые (предыдущие) сообщения об ошибках из нашего поля состояния.

В случае ошибки мы пишем сообщение об ошибке в представление. CouchDB сообщит нам, почему произошла ошибка в свойстве reason возвращаемого JSON. Чтобы снова получить согласованное представление, мы обновляем список наших товаров после того, как получили ошибку.

В нашу форму мы теперь можем добавить директиву ng-submit которая будет вызывать нашу функцию в области видимости при отправке формы:

 <form class="form-inline" role="form" novalidate ng-submit="processForm()"> 

Вот и все! Angular очень помогает нам в обновлении нашего обзора! Проверьте последний коммит .

Добавление проверки

Вы могли заметить, что мы можем поместить все виды ценностей в нашу заявку на расходы. Люди могут добавлять в цены недопустимые строки, такие как foo и отправлять их на сервер. Итак, давайте добавим некоторую проверку на стороне сервера: CouchDB может проверять документы при их обновлении. Нам просто нужно добавить поле validate_doc_update с функцией в наш проектный документ. Эта функция должна выдавать исключение в случае неверных данных.

Функция имеет четыре аргумента, как показано ниже:

 validate_doc_update: function (newDoc, oldDoc, userCtx, secObj) { // ... } 

newDoc — это документ, который будет создан или использован для обновления. Есть также аргументы oldDoc , userCtx и secObj для более сложных secObj , но мы будем просто использовать newDoc для проверки:

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

 var ddoc = { _id: '_design/expenses', views: {}, lists: {}, shows: {}, validate_doc_update: function (newDoc, oldDoc, userCtx, secObj) { if (newDoc._deleted === true) { return; } if (!newDoc.name) { throw({forbidden: 'Document must have an item name.'}); } if (!newDoc.price) { throw({forbidden: 'Document must have a price.'}); } if (!/\d+\.\d\d/.test(newDoc.price)) { throw({forbidden: 'Price must be a number and have two decimal places after a dot.'}); } } }; // _design/expenses/_view/byName ddoc.views.byName = { map: function (doc) { emit(doc.name, doc.price); } }; module.exports = ddoc; 

Имя поля и price не могут быть undefined в нашей проверке. Кроме того, мы проверяем формат цены с помощью регулярного выражения. Если мы просто хотим удалить документ, нам не нужны какие-либо проверки. Мы обновляем наш проектный документ, используя следующую команду:

 couchapp push couchdb/views.js http://localhost:5984/expenses 

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

Обработка ошибок

Вот соответствующий коммит .

Добавление проверки в интерфейс

Удивительно, что у нас сейчас есть какая-то проверка на сервере, но разве не будет еще лучше, если нам не понадобится запрос на проверку нашего документа? Давайте добавим немного проверки с использованием Angular.

Оба наших ввода являются обязательными, поэтому они получают required атрибут. Вы помните наше регулярное выражение в функции проверки нашего конструкторского документа? Директива ng-pattern проверяет наш ввод с помощью регулярного выражения:

 <input class="form-control" ng-model="price" id="item-price" name="item-price" placeholder="Price" required ng-pattern="/\d+\.\d\d$/"/> 

Используя name-of-the-form.$invalid мы можем проверить, является ли один из наших входов недействительным. Поскольку наша форма имеет форму атрибута name, мы будем использовать form.$invalid . Мы можем объединить это значение с директивой типа ng-disabled , которая отключит нашу кнопку отправки в случае формы, которая имеет недопустимые или отсутствующие значения:

 <button class="btn btn-primary pull-right" type="submit" ng-disabled="form.$invalid">Save</button> 

Это оно! Всего за несколько строк HTML мы получили отличные проверки. Ознакомьтесь с последним коммитом , включая тесты.

Вывод

Мы узнали, как создать небольшое приложение, используя CouchDB и Angular. Angular и CouchDB сделали для нас много тяжелой работы. Мы посмотрели на:

  • HTTP-интерфейс CouchDB
  • CouchDB просмотры и проверки
  • Внедрение зависимостей Angular
  • Двухстороннее связывание данных Angular
  • Директивы в угловых
  • Использование проверки в Angular

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

  • Размещение приложения на самой CouchDB
  • Обновление документов
  • Написание ваших собственных директив
  • копирование
  • Используя редукторные функции на наш взгляд
  • Тестирование приложений Angular