Клиентские базы данных остаются больным местом в кросс-браузерной автономной разработке приложений В одном углу Safari и Opera ≤ 12. Оба этих браузера поддерживают исключительно Web SQL. В другом углу у нас есть Firefox и Internet Explorer (10+), которые поддерживают исключительно IndexedDB. Chrome (и Opera 15+), несмотря ни на что, поддерживает оба.
Теперь этот раскол не был бы таким ужасным, если бы Web SQL и IndexedDB не были радикально отличающимися базами данных с разными моделями для хранения данных. Поддержка обоих — нетривиальная задача. К счастью для нас, PouchDB существует.
PouchDB — это клиентский API базы данных. Он написан на JavaScript и создан по образцу API CouchDB . Он даже способен синхронизироваться с экземпляром CouchDB. Однако нам интересен PouchDB, поскольку он абстрагирует различия между Web SQL и IndexedDB и объединяет их в единый интерфейс.
В этой статье мы познакомимся с PouchDB, создав простое приложение для создания заметок, которое работает в автономном режиме. Здесь будут рассмотрены только части кода. Многие функции были упрощены для удобства чтения. Вы можете скачать все это с GitHub.
Что вам нужно
Для этого проекта вам понадобится следующее.
- Копия скрипта PouchDB
- Веб-браузер, который поддерживает IndexedDB или Web SQL. Текущие версии Opera, Safari, Internet Explorer, Chrome и Firefox отвечают всем требованиям.
- HTTP-сервер, такой как Nginx, Lighttpd или Apache HTTP.
В этом нет необходимости, но если вы хотите просмотреть данные, хранящиеся в вашей локальной базе данных, используйте браузер с инструментами инспектора базы данных. Chrome, Opera и Safari поддерживают проверку баз данных с помощью собственных инструментов разработчика. На следующем рисунке показана база данных PouchDB в Chrome.
Из-за ограничений источника, встроенных в IndexedDB и Web SQL, вам также потребуется использовать HTTP-сервер для разработки. Используйте любой сервер — Apache , Nginx и Lighttpd — три надежных варианта. Или вы можете использовать такие пакеты, как MAMP для Mac OS X, WAMP для Windows или XAMPP для Mac, Windows и Linux.
Добавьте PouchDB в ваш HTML-документ, как и любой другой файл JavaScript:
<script src="pouchdb-nightly.min.js"></script>
Создание базы данных PouchDB
Все базы данных PouchDB или соединения с базой данных создаются с PouchDB
конструктора PouchDB
:
var pdb = new PouchDB('pouchnotes');
Это создает базу данных с именем _pouch_pouchnotes
. PouchDB префикс каждого имени базы данных с _pouch_
. Если вы также используете «сырую» IndexedDB или Web SQL для других областей вашего веб-сайта, не используйте префикс _pouch_
для этих баз данных.
Планирование нашего приложения
Так как же может выглядеть приложение для создания заметок? Что ж, мы, вероятно, хотим, чтобы каждая заметка имела заголовок. Каждая заметка также будет иметь текст, который составляет тело заметки. Возможно, мы тоже захотим пометить наши заметки, поэтому у нас будет поле для этого. И было бы неплохо, если бы мы смогли прикрепить файл? Мы будем использовать форму HTML, такую как приведенная ниже.
Мы будем основывать структуру нашей базы данных на этой форме.
Разработка схемы (сортов)
Что приятно в PouchDB, так это то, что он имеет гибкую схему. Каждый объект в базе данных является действительно отдельным документом. PouchDB не использует реляционную модель организации данных, поэтому мы можем просто добавлять поля или свойства в документ по мере необходимости.
Вместо синтаксиса SELECT * FROM tablename
SQL / реляционных баз данных, запросы PouchDB используют MapReduce. Вы пишете функции для фильтрации и сортировки ваших данных. Это требует небольшого умственного сдвига по сравнению с SQL, но это легко, когда вы освоите его. Мы увидим пример этого чуть позже.
Добавление и обновление заметок
Мы добавим нашу заметку в базу данных при отправке формы. PouchDB предлагает два способа сохранения документа: отправка и put
. Каждый метод принимает два аргумента.
-
document
(обязательно): объект, содержащий свойства и их значения. В этом случае это будут поля формы и их значения. -
callback
(необязательно): функция, вызываемая по завершении операции. Он принимает два параметра:error
иresponse
.
Основное отличие заключается в следующем: post
добавляет новый документ и генерирует идентификатор ( _id
); с put
, мы должны поставить один. Это означает, что вы можете использовать put
для добавления или обновления документов. Но post
строго для добавления новых документов в базу. Теперь давайте рассмотрим пример использования put
.
var form, savenote; form = document.getElementById('addnote'); savenote = function(event) { var o = {}; o.notetitle = form.notetitle.value; o.note = form.note.value; o.tags = form.tags.value; /* Generate an _id if we don't have one. It should be a string, which is why we're adding '' to it. */ if (event.target._id.value == '') { o._id = new Date().getTime() + ''; } else { o._id = event.target._id.value; } pdb.put(o, function(error, response) { if (error) { console.log(error); return; } else if(response && response.ok) { /* Do something with the response. */ } }); } /* Add the event handler */ form.addEventListener('submit', savenote);
Если в нашей форме нет значения _id
, мы сгенерируем временную метку для его использования. В противном случае мы будем использовать значение form._id
. Другие наши поля формы станут свойствами и значениями для нашего объекта документа. Используя put
а не post
, мы можем использовать нашу функцию savenote
как для добавления, так и для обновления заметок.
Если все пойдет хорошо, наш обратный вызов получит ответ в формате JSON. Пример успешного ответа показан ниже.
{ok: true, id: "1391406871281", rev: "1-1d95025598a94304a87ef14c108db7be"}
Мы ничего не сделали с нашим ответом. В зависимости от вашего приложения, вы можете не захотеть. Но для нашего приложения для создания заметок нам нужна возможность связать файл с заметкой. PouchDB называет эти файлы вложениями .
Сохранение вложений
Сохранение вложения немного сложнее, чем сохранение текста. Мы не можем просто запросить атрибут value
поля input type="file"
. Вместо этого мы должны прочитать данные файла, используя File API, а затем сохранить их, используя метод putAttachment
. Давайте добавим к нашему методу savenote
из предыдущего раздела.
savenote = function(event) { var o = {}; o.notetitle = form.notetitle.value; o.note = form.note.value; o.tags = form.tags.value; /* Generate an _id if we don't have one. It should be a string, which is why we're adding '' to it. */ if (event.target._id.value == '') { o._id = new Date().getTime() + ''; } else { o._id = event.target._id.value; } pdb.put(o, function(error, response) { if (error) { console.log(error); return; } /* New code for saving attachments */ if (response && response.ok) { if (form.attachment.files.length) { var reader = new FileReader(); /* Using a closure so that we can extract the File's attributes in the function. */ reader.onload = (function(file) { return function(e) { pdb.putAttachment(response.id, file.name, response.rev, e.target.result, file.type); }; })(form.attachment.files.item(0)); reader.readAsDataURL(form.attachment.files.item(0)); } } }); }
Каждый тип ввода файла также имеет атрибут files
который возвращает объект FileList
. В данном случае это form.attachment.files
. Как следует из его названия, объект FileList
— это массив, содержащий файл или файлы, отправленные с использованием этого поля. Мы можем определить количество файлов в списке с помощью свойства length
. На каждый файл в списке можно ссылаться, используя его индекс и метод item
, как мы сделали здесь ( form.attachment.files.item(0)
). Кроме того, вы можете использовать синтаксис в квадратных скобках ( form.attachment.files[0]
).
Если заметка добавлена успешно, мы получим response.id
. Затем мы можем проверить, есть ли файл для сохранения в качестве вложения. Если есть, мы будем читать его с помощью объекта FileReader
( var reader = new FileReader()
). Вложения PouchDB должны быть в кодировке base64. Самый простой способ кодировать файлы — это использовать readAsDataURL()
. После загрузки файла мы можем сохранить его в базе данных, используя putAttachment
.
Метод putAttachment putAttachment
принимает до шести аргументов. Пять обязательны, один необязательный.
-
docID
(обязательно): идентификатор документа, с которым будет связано это вложение. В данном случае этоresponse.id
. -
Attachment ID
(обязательно): название вложения. Здесь мы используем имя файла. -
rev
(обязательно): номер редакции родительского документа. -
attachment_doc
(обязательно): данные файла в кодировке base64. В этом случае свойствоresult
нашего объектаFileReader
. -
type
(обязательный): тип MIME для этих данных. Например,image/png
илиapplication/pdf
. -
callback
(необязательно): функция, которая вызывается после завершения операции. Как и во всех функциях обратного вызова PouchDB, он принимает два аргумента:error
иresponse
. Мы оставили это в нашем примере.
В этом примере мы также обернули наш обработчик события onload
в замыкание. Закрытие позволяет получить доступ к нашим свойствам файла из нашего обработчика событий (например, с помощью file.name
и file.type
).
Теперь, когда мы рассмотрели сохранение заметок и вложений, давайте рассмотрим извлечение записей как по отдельности, так и в наборах.
Получение всех заметок
Что если мы хотим просмотреть список заметок в нашей базе данных? Вот где полезны allDocs
. PouchDB.allDocs
позволяет нам получать пакет документов одновременно.
Название allDocs
вводит в заблуждение. Мы, безусловно, можем использовать его для получения всех наших документов. Однако мы также можем использовать его для извлечения документов, попадающих в определенный диапазон, или извлечения документов, соответствующих определенным ключам. Этот метод принимает два аргумента, ни один из которых не требуется.
-
options
(необязательно): объект, содержащий одно или несколько из следующих свойств.-
include_docs
(Boolean): включить весь документ для каждой строки. Приfalse
возвращает толькоid
документа и номер версии.
*conflicts
(логическое значение): включить конфликты. -
startkey
иendkey
: включить документы с ключами в этот диапазон. -
descending
(логическое значение): вместо этого сортируйте результаты по убыванию.
*options.keys
(массив): возвращать только документы, соответствующие указанным ключам.
*options.attachments
(Boolean): возврат вложений с документами.
*callback
(необязательно): функция, которая вызывается при завершении поиска. Как и в случае других обратных вызовов PouchDB, он получает аргументerror
и аргументresponse
.
-
В приведенном ниже упрощенном примере мы получили все документы в нашей базе данных. Чтобы получить заголовок документа, дату создания и дату изменения, нам нужно установить значение include_docs
в true
. Вот наша функция viewnoteset
.
var viewnoteset = function() { var df = document.createDocumentFragment(), options = {}, nl = document.querySelector('#notelist tbody'); options.include_docs = true; this.pdb.allDocs(options, function(error, response) { var row = response.rows.map(addrow); // Calls an addrow() function row.map(function(f) { if (f) { df.appendChild(f); } }); nl.appendChild(df); }); };
Значением response
является объект, содержащий три свойства: total_rows
, offset
и total_rows
. Мы больше всего заинтересованы в response.rows
, так как это массив объектов документа. Здесь мы использовали map
, один из встроенных в JavaScript методов массива, в response.rows
. Использование map
вызывает нашу функцию addrow
для каждой заметки и добавляет ее в таблицу, в которой перечислены наши заметки.
Получение отдельных заметок
Извлечь отдельную заметку немного проще, так как мы можем использовать метод get
PouchDB. Единственным обязательным аргументом является идентификатор документа. Однако мы можем включить аргумент options
и функцию обратного вызова для обработки результатов.
Наш аргумент опций {attachments: true}
гарантирует, что если в конкретной заметке есть какие-либо вложения, она будет показана вместе с заметкой при просмотре. Здесь наша функция обратного вызова берет наши данные заметок и использует их для заполнения полей формы и отображения любого вложения.
var viewnote = function(noteid) { var noteform = document.querySelector('#noteform'); pdb.get(noteid, {attachments: true}, function(error, response) { var fields = Object.keys(response), o, link, attachments, li; if (error) { return; } else { /* Since our note field names and form field names match, We can just iterate over them. */ fields.map(function(f) { if (noteform[f] !== undefined && noteform[f].type != 'file') { noteform[f].value = response[f]; } if (f == '_attachments') { attachments = response[f]; for (o in attachments) { li = document.createElement('li'); link = document.createElement('a'); link.href = 'data:' + attachments[o].content_type + ';base64,' + attachments[o].data; link.target = "_blank"; link.appendChild(document.createTextNode(o)); li.appendChild(link); } document.getElementById('attachmentlist').appendChild(li); } }); } }); }
В нашем демонстрационном приложении мы передаем id
для каждой заметки, используя ссылку. Каждый href
указывает на /#/view/xxxxx
где xxxxx
— это id
заметки. Нажатие на ссылку запускает событие hashchange
, и hashchange
события hashchange
(показанный ниже) — это место, где мы передаем id
в viewnote
.
window.addEventListener('hashchange', function(e) { var noteid; /* Replacing # for compatibility with IE */ if (window.location.hash.replace(/#/,'')) { noteid = window.location.hash.match(/\d/g).join(''); viewnote(noteid); } });
Делать заметки для поиска
Примечания особенно полезны, когда они доступны для поиска. Итак, давайте добавим функцию поиска в наше приложение. Мы возьмем входные данные из нашей формы поиска и будем использовать ее в качестве основы для нашего поискового запроса. На следующем рисунке показано, как будет выглядеть наше приложение при использовании функции поиска.
Запросы PouchDB сильно отличаются от запросов SQL. С помощью SQL вы указываете, что выбрать, из какой таблицы и по каким критериям. Например, простой поисковый запрос к заметке может выглядеть следующим образом: SELECT * FROM notes WHERE title, text, tags LIKE %interview%
. Но с PouchDB мы выполняем запросы, используя функции.
Чтобы выполнить запрос, мы будем использовать метод query
PouchDB. Он принимает три аргумента.
-
fun
(обязательно): название функции. -
options
(необязательно): объект, содержащий параметры для результатов поиска. Вы можете указать функцию уменьшения или ограничить результаты определенной клавишей или диапазоном клавиш. -
callback
(необязательно): функция, вызываемая по завершении запроса.
Давайте посмотрим на нашу функцию поиска ниже.
var search = function(searchkey) { var map = function(doc) { var searchkey, regex; /* Escape characters with special RegExp meaning */ searchkey = document.getElementById('q').value.replace(/[$-\/?[-^{|}]/g, '\\$&'); regex = new RegExp(searchkey,'i'); /* If the notetitle, note, or tags fields match, return only the fields we need to create the result list. */ if (doc.notetitle.match(regex) || doc.note.match(regex) || doc.tags.match(regex)) { emit(doc._id, {notetitle: doc.notetitle, note: doc.note, tags: doc.tags}); } } db.query(map, function(err, response) { if (err) { console.log(err); } if (response) { var df, rows, nl, results; /* Rewrite the response so that our object has the correct structure for our addrow function. */ results = response.rows.map(function(r) { r.doc = r.value; delete r.value; return r; }); nl = document.querySelector('#notelist tbody'); df = document.createDocumentFragment(), rows = results.map(addrow, that); rows.map(function(f) { if (f) { df.appendChild(f); } }); nl.innerHTML = ''; nl.appendChild(df); } }); }
В нашей функции поиска мы определили функцию map
которая позволяет нам находить и фильтровать наши записи. Функция map
всегда получает документ PouchDB в качестве единственного аргумента. Нам не нужно называть эту map
функций, но это должен быть первый аргумент.
В рамках map
мы создали объект регулярного выражения из нашей формы поиска. Мы протестируем наши notetitle
, note
и tags
, чтобы увидеть, соответствует ли какое-либо из этих полей нашему регулярному выражению. Если они это сделают, мы вернем notetitle
, id
(который является notetitle
времени) и измененные свойства, используя метод notetitle
. Метод emit встроен в PouchDB. Как следует из его названия, он выбирает и возвращает указанные свойства в указанном формате. Первый аргумент emit
становится ключом к нашим результатам.
Наша функция map
становится первым аргументом для query
. И второй аргумент для query
— как вы уже, наверное, догадались — функция обратного вызова. Предполагая, что все прошло нормально, наш response
аргумент будет объектом, содержащим три свойства: total_rows
, offset
и total_rows
. Мы хотим rows
. Это массив, содержащий примечания, которые соответствуют нашему поисковому запросу. В следующем примере кода показано, как может выглядеть ответ.
[{ value: { id: "1388703769529", modified: 1391742787884, notetitle: "Fluffernutter sandwich recipe" }, id:"1388703769529", key:"1388703769529" }, { value: { id: "1391656570611", modified: 1391656570611, notetitle: "Browned-butter Rice Krispie Treats recipe" }, id:"1391656570611", key:"1391656570611" }]
Поскольку наш ответ является массивом, мы можем использовать собственные методы Array.prototype
для манипулирования результатами. В этом случае мы использовали Array.prototype.map
чтобы переписать каждый объект заметки, чтобы вместо него нашим value
стало doc
, и снова вызывать addrow
для каждого результата.
Работа в автономном режиме с кэшем приложений
Чтобы это приложение работало в автономном режиме, нам также нужно сохранять HTML, CSS и JavaScript в автономном режиме с помощью кеша приложений . Кэш приложения представляет собой простой текстовый файл, который обслуживается заголовком Content-type: text/cache-manifest
который сообщает браузеру, какие ресурсы следует хранить локально. Здесь мы не будем pouchnotes.cache
кэш приложения, но давайте посмотрим на файл манифеста pouchnotes.cache
для нашего демонстрационного приложения .
CACHE MANIFEST # Version 2014.02.10.01 CACHE: index.html css/style.css js/pouchdb-nightly.min.js js/application.js
Мы начали его со строки CACHE MANIFEST
, с которой должны начинаться все манифесты кэша. Вторая строка сообщает нам, какая это версия файла. Браузеры будут обновлять кэш только при изменении манифеста кеша. Изменение номера версии — это самый простой способ вызвать обновление, если мы изменим наши файлы CSS, JavaScript или HTML.
Нам все еще нужно сделать еще одну вещь, хотя. Нам нужно добавить наш манифест в наш HTML-документ. Это требует добавления атрибута manifest
к нашему тегу <html>
, например так:
<html lang="en-us" manifest="pouchnotes.manifest">
Теперь наша база данных и наши файлы будут доступны, даже когда мы не в сети.
Будьте предупреждены: кэш приложений добавляет уровень сложности разработки. Поскольку манифест кэша должен измениться, чтобы браузер мог загружать новые файлы, следует подождать, пока вы будете готовы выпустить версию своего приложения, прежде чем добавлять ее.
Вывод
В PouchDB есть еще кое-что, что мы здесь не рассмотрели. Например, вы можете синхронизировать PouchDB с сервером CouchDB . Синхронизация с сервером базы данных позволяет нам создавать приложения, которые могут легко обмениваться данными и файлами между несколькими браузерами и компьютерами.
Я надеюсь, что эта статья предоставила вам понимание того, что такое PouchDB и как вы можете использовать его для создания программного обеспечения, которое работает даже тогда, когда у нас нет подключения к Интернету.