В этом руководстве мы создадим приложение, используя 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