Робота з базою IndexedDB - Частина 3
() translation by (you can also view the original English article)
Вітаю вас в останньому посібнику серії, присвяченій IndexedDB. Коли я почав цю серію посібників, моєю ціллю було пояснити вам, як працює технологія, яку не завжди... легко використовувати. Дійсно, коли я вперше спробував працювати з IndexedDB у минулому році, у мене склалося трохи негативне враження про неї («Трохи негативне» майже настільки, як Всесвіт «трохи старий»). Не легко було її опанувати, але зрештою мені стало доволі легко працювати з IndexedDB, і я віддаю належне тому, які можливості вона пропонує. Це як і раніше технологія, що не підтримується всіма операційними системами (нажаль, вона не підтримується iOS7), проте я щиро вірю, що це та технологія, яку розробники можуть опановувати та використовувати сьогодні.
У цьому останньому посібнику ми продемонструємо деякі додаткові концепції, використовуючи у якості основи «повну» версію, створену нами у попередньому посібнику. Правду кажучи, ви повинні добре пам'ятати, про що йшла мова у попередніх посібниках, інакше вам буде не легко працювати з цим, так що, можливо, ви захочете також ознайомитися з першим.
Підрахунок даних
Давайте почнемо з чогось простого. Уявіть, що вам потрібно розбити ваші дані на сторінки. Як би ви підрахували ваші дані, щоб коректно реалізувати цю можливість? Я вже показав вам, як ви можете отримати всі ваші дані, і без сумніву ви могли би скористатися тим способом для підрахунку даних, проте при цьому вам потрібно було би отримати всі дані. Якщо у вас величезна локальна база даних, то підрахунок міг би відбуватися дуже повільно. На щастя, специфікація IndexedDB надає значно простіший спосіб підрахунку даних.
У результаті виконання методу count() objectStore буде повернено кількість даних. Як і все, що ми раніше виконали, ця операція буде виконуватися асинхронно, проте ви можете спростити код до одного виклику. Для бази даних додатка для ведення записів я написав функцію doCount()
, яка саме це й робить:
1 |
function doCount() { |
2 |
|
3 |
db.transaction(["note"],"readonly").objectStore("note").count().onsuccess = function(event) { |
4 |
$("#sizeSpan").text("("+event.target.result+" Notes Total)"); |
5 |
};
|
6 |
|
7 |
}
|
Пам'ятайте, що якщо з кодом вище важкувато працювати, то ви можете розбити його на блоки. Зверніться до попередніх посібників, де я показав, як це зробити. Обробникові результату передається результат обчислень, що являє собою загальну кількість доступних у сховищі об'єктів. Я змінив інтерфейс користувача нашої демоверсії для додавання пустого елемента span у заголовку.
1 |
<span class="navbar-brand" >Note Database <span id="sizeSpan"></span></span> |



Останнє, що мені потрібно виконати, – додати виклик doCount для тих частин коду, де відбувається запуск додатка, і після виконання будь-яких операцій з додавання або видалення записів. Нижче наведено, як це зробити на прикладі обробника, що запускається у разі вдалого відкриття бази даних.
1 |
openRequest.onsuccess = function(e) { |
2 |
db = e.target.result; |
3 |
|
4 |
db.onerror = function(event) { |
5 |
// Generic error handler for all errors targeted at this database's
|
6 |
// requests!
|
7 |
alert("Database error: " + event.target.errorCode); |
8 |
};
|
9 |
|
10 |
displayNotes(); |
11 |
doCount(); |
12 |
};
|
Ви можете ознайомитися з повним кодом цього прикладу у завантаженому вами архіві, де цей код розташовується у папці fulldemo2
. (До вашого відома, у папці fulldemo1
розташовується код додатка, яким він був наприкінці попереднього посібника)
Фільтрація записів на льоту
Стосовно нашої наступної можливості, ми додамо базовий фільтр для списку з записами. У попередніх посібниках цієї серії я пояснив вам, що в IndexedDB не передбачено підтримку пошуку у вільному форматі (* формат уводу даних, в якому нема жорстко заданих границь полів та/або їх послідовності). Ви не можете (що ж вам буде не просто) шукати контент за ключовим словом. Проте завдяки можливостям, надаваним діапазонами, можна легко, щонайменше, додати підтримку можливості зіставлення символів, що уводяться, з початком рядка.
Якщо ви пам'ятаєте, діапазон дозволяє нам вибрати дані зі сховища, які або починаються з певного значення, або закінчуються певним значенням, або розташовуються між вказаними значеннями. Ми можемо скористатися цим для реалізації базового фільтра для зіставлення символів, що уводяться, з вмістом поля для додавання заголовка наших записів. Для початку нам потрібно додати індекс для цієї властивості. Пам'ятаєте, що це можна тільки зробити в обробнику для події onupgradeneeded.
1 |
if(!thisDb.objectStoreNames.contains("note")) { |
2 |
console.log("I need to make the note objectstore"); |
3 |
objectStore = thisDb.createObjectStore("note", { keyPath: "id", autoIncrement:true }); |
4 |
objectStore.createIndex("title", "title", { unique: false }); |
5 |
}
|
Далі я додав просте поле форми до UI:



Потім я додав обробник "keyup" для цього поля, завдяки чому ми тут же будемо спостерігати оновлення при наборі тексту у ньому.
1 |
$("#filterField").on("keyup", function(e) { |
2 |
var filter = $(this).val(); |
3 |
displayNotes(filter); |
4 |
});
|
Зверніть увагу на те, як я викликаю displayNotes. Це та сама функція, яку я використовував раніше для відображення всіх записів. Я оновлю її таким чином, щоб у ній була підтримка як операції для «добування всіх даних», так і операції для «добування відфільтрованих даних». Давайте розберемо її.
1 |
function displayNotes(filter) { |
2 |
|
3 |
var transaction = db.transaction(["note"], "readonly"); |
4 |
var content="<table class='table table-bordered table-striped'><thead><tr><th>Title</th><th>Updated</th><th>& </td></thead><tbody>"; |
5 |
|
6 |
transaction.oncomplete = function(event) { |
7 |
$("#noteList").html(content); |
8 |
};
|
9 |
|
10 |
var handleResult = function(event) { |
11 |
var cursor = event.target.result; |
12 |
if (cursor) { |
13 |
content += "<tr data-key=\""+cursor.key+"\"><td class=\"notetitle\">"+cursor.value.title+"</td>"; |
14 |
content += "<td>"+dtFormat(cursor.value.updated)+"</td>"; |
15 |
|
16 |
content += "<td><a class=\"btn btn-primary edit\">Edit</a> <a class=\"btn btn-danger delete\">Delete</a></td>"; |
17 |
content +="</tr>"; |
18 |
cursor.continue(); |
19 |
}
|
20 |
else { |
21 |
content += "</tbody></table>"; |
22 |
}
|
23 |
};
|
24 |
|
25 |
var objectStore = transaction.objectStore("note"); |
26 |
|
27 |
if(filter) { |
28 |
//Credit: https://stackoverflow.com/a/8961462/52160
|
29 |
var range = IDBKeyRange.bound(filter, filter + "\uffff"); |
30 |
var index = objectStore.index("title"); |
31 |
index.openCursor(range).onsuccess = handleResult; |
32 |
} else { |
33 |
objectStore.openCursor().onsuccess = handleResult; |
34 |
}
|
35 |
|
36 |
}
|
Насправді ми внесли зміни тільки внизу коду. У результаті відкриття курсору з діапазоном або без нього виходить той самий тип результату, передаваного до обробника події. Це стає нам у пригоді, оскільки завдяки цьому оновити наш код дуже просто. Єдина складність – власне задання діапазону. Зверніть увагу на те, що я тут виконав. Передаваний аргумент, filter, – те, що ввів користувач. Уявіть, що це "The". Нам потрібно вибрати записи із заголовком, що починається на "The" та закінчується будь-яким символом. Це можна реалізувати просто завдяки встановленню в якості значення кінця діапазону кінцевого символу ASCII. Це на моя ідея. Перейдіть за посиланням, вказаним у коді, щоб дізнатися, кому вона належить.
Ви можете ознайомитися з цією демоверсією у папці fulldemo3
. Зверніть увагу на те, що тут ми використовуємо нову базу даних, тому якщо ви виконали код попереднього прикладу, то після першого запуску коду цього прикладу записів не буде.
Хоча цей код працює, ми маємо одну невелику проблему. Уявіть запис із заголовком "Saints Rule." (* Віруючі рулять) (Тому що це й справді так. Це просто так до слова кажучи). Скоріше за все, ви спробуєте знайти запис з цим заголовком, набираючи "saints". У цьому випадку фільтр не спрацює, оскільки він чутливий до регістра (великих або рядкових літер). Як нам вирішити цю проблему?
Один із варіантів – просто зберегти копію заголовка у нижньому регістрі. Це доволі просто зробити. Для початку я змінив індекс для використання нової властивості під назвою titlelc
.
1 |
objectStore.createIndex("titlelc", "titlelc", { unique: false }); |
Потім я змінив код, за допомогою якого зберігаються записи, для створення копії вмісту поля для завдання заголовка:
1 |
$("#saveNoteButton").on("click",function() { |
2 |
|
3 |
var title = $("#title").val(); |
4 |
var body = $("#body").val(); |
5 |
var key = $("#key").val(); |
6 |
var titlelc = title.toLowerCase(); |
7 |
|
8 |
var t = db.transaction(["note"], "readwrite"); |
9 |
|
10 |
if(key === "") { |
11 |
t.objectStore("note") |
12 |
.add({title:title,body:body,updated:new Date(),titlelc:titlelc}); |
13 |
} else { |
14 |
t.objectStore("note") |
15 |
.put({title:title,body:body,updated:new Date(),id:Number(key),titlelc:titlelc}); |
16 |
}
|
Нарешті, я змінив пошуковий запит, щоб просто перевести введені користувачем дані до нижнього регістра. завдяки цьому, якщо ви вводите "Saints", то фільтр спрацює таким же чином, як і при вводі "saints".
1 |
filter = filter.toLowerCase(); |
2 |
var range = IDBKeyRange.bound(filter, filter + "\uffff"); |
3 |
var index = objectStore.index("titlelc"); |
На цьому все. Ви можете ознайомитися з розглядуваним у цьому розділі прикладом у папці fulldemo4
.
Працюємо з властивостями масиву
Стосовно нашого останнього поліпшення, я додам нову можливість для нашого додатка для ведення записів – тегування (* супроводження даних тегами).
Завдяки цьому у вас буде можливість додати будь-яку кількість тегів (вважайте, ключових слів, за допомогою яких описується запис), завдяки чому ви зможете пізніше знайти інші записи з тим же тегом.
Теги будуть зберігатися у вигляді масиву. Це не так вже і важко реалізувати. Я згадав на початку цієї серії, що ви могли би з легкістю зберігати масиви в якості властивостей (* об'єктів, передаваних до бази). Трохи складніше реалізувати пошук за тегами. Давайте поснемо з реалізації можливості додавання тегів до запису.
Для початку я додав до форми для додавання записів нове поле для вводу даних. Завдяки цьому у користувача буде можливість вводити теги, розділені комою:



Я можу зберегти їх, змінивши мій код, за допомогою якого відбувається створення/оновлення запису.
1 |
var tags = []; |
2 |
var tagString = $("#tags").val(); |
3 |
if(tagString.length) tags = tagString.split(","); |
Зверніть увагу на те, що у якості значення за налаштуванням я встановив пустий масив. Я наповнюю його тільки в тому випадку, якщо користувач додав якісь теги. Ми зберігаємо теги просто завдяки додаванню масиву до об'єкта, передаваного до IndexedDB:
1 |
if(key === "") { |
2 |
t.objectStore("note") |
3 |
.add({title:title,body:body,updated:new Date(),titlelc:titlelc,tags:tags}); |
4 |
} else { |
5 |
t.objectStore("note") |
6 |
.put({title:title,body:body,updated:new Date(),id:Number(key),titlelc:titlelc,tags:tags}); |
7 |
}
|
І все. Якщо ви додасте декілька записів та відкриєте вкладку Resources у Chrome, то, власне, побачите збережені дані.



Тепер давайте додамо теги до представлення при відображенні запису. Для нашого додатка я вибрав простий спосіб реалізації цієї можливості. Якщо при відображення запису є теги, то я їх вивожу. Кожний тег буде посиланням. Якщо ви натиснете таке посилання, то в результаті буде відображено список споріднених записів, в яких використовується той самий тег. Давайте спершу поглянемо на логіку цього рішення.
1 |
function displayNote(id) { |
2 |
var transaction = db.transaction(["note"]); |
3 |
var objectStore = transaction.objectStore("note"); |
4 |
var request = objectStore.get(id); |
5 |
|
6 |
request.onsuccess = function(event) { |
7 |
var note = request.result; |
8 |
var content = "<h2>" + note.title + "</h2>"; |
9 |
if(note.tags.length > 0) { |
10 |
content += "<strong>Tags:</strong> "; |
11 |
note.tags.forEach(function(elm,idx,arr) { |
12 |
content += "<a class='tagLookup' title='Click for Related Notes' data-noteid='"+note.id+"'> " + elm + "</a> "; |
13 |
});
|
14 |
content += "<br/><div id='relatedNotesDisplay'></div>"; |
15 |
}
|
16 |
content += "<p>" + note.body + "</p>"; |
17 |
I
|
18 |
$noteDetail.html(content).show(); |
19 |
$noteForm.hide(); |
20 |
};
|
21 |
}
|
У цій функції (новому доповненню до нашого додатка) міститься код для відображення запису, виконуваний при виникненні події click, що генерується при виборі елемента таблиці. Мені потрібен більш абстрактний код (* у порівнянні з displayNotes()), і цей код саме цю роль і виконує. Головним чином код – той самий, проте зверніть увагу на логіку для перевірки довжини масиву, що міститься у властивості tags. Якщо масив не пустий, то контент оновлюється для додавання простого списку тегів. Кожний тег обгортається до елементу для додавання посилання зі спеціальним класом, який я буду використовувати пізніше для пошуку споріднених записів. Також я додав елемент div саме для того, щоб виводити результат того пошуку.



На цьому етапі ми реалізували можливість додавання тегів до запису та їх відображення. Також я планував додати для користувачів можливість вибирати ті теги, завдяки чому вони можуть знайти інші записи, в яких використовується той самий тег. Тепер переходимо до складної частини.
Ви вже бачили, як можете добути контент за допомогою індексів. Але як це працює, коли ми маємо справу з властивостями масиву? Виявляється, що у специфікації є спеціальний прапорець для цих випадків – multiEntry. При створенні індексу на основі масиву ви повинні задати у якості цього значення true. Нижче показано, як це реалізовано у моєму додатку:
1 |
objectStore.createIndex("tags","tags", {unique:false,multiEntry:true}); |
За допомоги вищезазначеного коду вдало вирішується проблема збереження тегів. Тепер давайте розбиратися зі списком. Нижче наводиться обробник події click для елемента а з класом tagLookup:
1 |
$(document).on("click", ".tagLookup", function(e) { |
2 |
var tag = e.target.text; |
3 |
var parentNote = $(this).data("noteid"); |
4 |
var doneOne = false; |
5 |
var content = "<strong>Related Notes:</strong><br/>"; |
6 |
|
7 |
var transaction = db.transaction(["note"], "readonly"); |
8 |
var objectStore = transaction.objectStore("note"); |
9 |
var tagIndex = objectStore.index("tags"); |
10 |
var range = IDBKeyRange.only(tag); |
11 |
|
12 |
transaction.oncomplete = function(event) { |
13 |
if(!doneOne) { |
14 |
content += "No other notes used this tag."; |
15 |
}
|
16 |
content += "<p/>"; |
17 |
$("#relatedNotesDisplay").html(content); |
18 |
};
|
19 |
|
20 |
var handleResult = function(event) { |
21 |
var cursor = event.target.result; |
22 |
if(cursor) { |
23 |
if(cursor.value.id != parentNote) { |
24 |
doneOne = true; |
25 |
content += "<a class='loadNote' data-noteid='"+cursor.value.id+"'>" + cursor.value.title + "</a><br/> "; |
26 |
}
|
27 |
cursor.continue(); |
28 |
}
|
29 |
};
|
30 |
|
31 |
tagIndex.openCursor(range).onsuccess = handleResult; |
32 |
|
33 |
});
|
Тут доволі багато коду, але правду кажучи, він дуже подібний тому, що ми обговорили раніше. Коли ви вибираєте тег, то спочатку за допомогою коду відбувається витягування вмісту посилання. Я створюю об'єкти для транзакції, сховища об'єктів та індексу таким же чином, як і раніше. Діапазон у цей раз задається по-іншому. Замість створення діапазону для якогось проміжку, ми можемо скористатися методом only() API для вказання, що нам потрібен діапазон, який складається тільки з одного значення. І так, подібний код і мені здається дивнуватим. Проте він чудово працює. Ви бачите, що далі ми відкриваємо курсор, і можемо виконати ітерацію над результатами таким же чином, як і раніше. Також є ще деякий додатковий код для тих випадків, коли нема збігу. Також я потурбувався про оригінальний запис, тобто той, що ви переглядаєте у цей момент, так, щоб він також не відображувався. Ось, власне, і все. Є це останній невеликий фрагмент коду, за допомогою якого обробляються події click, що виникають при виборі тих споріднених записів, завдяки чому ви з легкістю можете їх переглянути:
1 |
$(document).on("click", ".loadNote", function(e) { |
2 |
var noteId = $(this).data("noteid"); |
3 |
displayNote(noteId); |
4 |
});
|
Ви можете ознайомитися з цією демоверсією у папці fulldemo5
.
Завершення
Я щиро сподіваюся, що ця серія посібників була для вас корисною. Як я пригадав на початку, IndexedDB спочатку мені не сподобалася. Але чим довше я працював з цією технологією, чим більше намагався розібратися з тим, як вона працює, тим більше я шанував те, наскільки ця технологія може допомогти нам, як веб-розробникам. Весь потенціал цієї технології ще не розкритий, і я точно знаю, що деякі віддають перевагу використанню бібліотек-обгорток, але я вважаю, що цю технологію чекає чудове майбутнє!