Advertisement
  1. Code
  2. Web Development

Работа с базой IndexedDB

Scroll to top
Read Time: 22 min

() translation by (you can also view the original English article)

Одна из наиболее интересный разработок в области веб-стандартов за последнее время – спецификация Indexed Database (или IndexedDB, для краткости). Если хотите весело провести время, то можете ознакомиться со спецификацией самостоятельно. В этом руководстве я буду рассказывать вам о том, как работает эта функциональная возможность, и, надеюсь, слегка вдохновлять вас на то, чтобы вы сами использовали эту мощную функциональную возможность.


Обзор

Как спецификация IndexedDB в настоящее время находится на стадии Candidate Recommendation.

В двух словах, IndexedDB предоставляет вам возможность хранения больших объемов данных в браузерах ваших пользователей. Любому приложению, которому необходимо отправлять большое количество данных по сети, была бы очень полезной возможность хранения данных на стороне клиента, а не сервера. Конечно же, вместилище для данных – только часть уравнения. IndexedDB также предоставляет мощный API для поиска данных, работающий на основе индексов, для получения необходимых вам данных.

Вам, возможно, не совсем понятно, чем IndexedDB отличается от других инструментов для хранения данных?

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

Веб-хранилище (* Локальное хранилище, HTML5-хранилище, DOM-хранилище; встроенная возможность хранения данных на стороне клиента (объем данных значительно больше того, что предоставляется куки-файлами). Здесь и далее примеч. пер.) также хорошо поддерживается браузерами, однако оно ограничено с точки зрения доступного вам объема хранилища. Веб-хранилище не предоставляет вам настоящего «поискового» API, поскольку данные извлекаются только при помощи значений ключей. Веб-хранилище замечательно подходит для хранения "конкретных" вещей, например предпочтений пользователя, тогда как IndexedDB больше подходит для хранения произвольных данных (подобно базе данных).

Перед тем как продолжить, давайте посмотрим, как обстоит ситуация с поддержкой IndexedDB браузерами. Как спецификация IndexedDB в настоящее время находится на стадии Candidate Recommendation (* Возможная рекомендация. Этот документ подтверждает, что предложения W3C (Консорциума World-Wide Web)) тщательно рассмотрены и удовлетворяют требованиям Рабочей Группы. IndexedDB была на этой стадии на момент написания оригинала руководства (2013). В данный момент (2018) находится на стадии Recommendation (R)). На данном этапе разработчики этой спецификации довольны ею, однако ожидают сейчас замечаний и предложений от сообщества разработчиков. Спецификация, вероятно, изменится на финальной стадии, W3C Recommendation, по сравнению с тем, какой она является сейчас. В целом, браузера, которые поддерживают IndexedDB, делают это довольно стабильно, однако разработчики должны быть готовы к использованию префиксов и появлению обновлений в будущем.

Что касается тех браузеров, которые поддерживают IndexedDB, то у нас тут имеется своего рода дилемма (* положение, при котором выбор одной из двух противоположных возможностей одинаково затруднителен). Эту технологию довольно хорошо поддерживают десктопные браузера, однако в мобильных она почти отсутствует. Давайте взглянем на то, что на сообщает по этому поводу великолепный сайт CanIUse.com:

CanIUse Report for IndexedDBCanIUse Report for IndexedDBCanIUse Report for IndexedDB

Chrome для Android действительно поддерживает IndexedDB, однако очень немногие люди сегодня используют этот браузер на устройствах Android. Означает ли отсутствие поддержки для мобильных устройств то, что вам не следует использовать эту технологию? Конечно же, нет! Надеюсь, что все наши читатели знакомы с концепцией прогрессивного улучшения (* предполагает, что веб-интерфейсы должны создаваться поэтапно, циклически, от простого к сложному. На каждом из этапов должен получаться законченный веб-интерфейс, который будет лучше, красивее и удобнее предыдущего; настаивает на важности содержания). Такие возможности как IndexedDB могут быть добавлены в ваше приложение таким образом, что не нарушат его работу в браузерах без поддержки этой технологии. Вы бы могли воспользоваться библиотеками-обертками для переключения на WebSQL при запуске приложения на мобильных устройствах или просто отказаться от локального  хранения данных на стороне клиентов мобильных устройств. Лично я считаю, что возможность кэширования больших блоков данных на стороне клиента достаточно важна, чтобы пользоваться ею сейчас, несмотря на отсутствие поддержки на стороне клиента.


Давайте приступим

Мы рассмотрели спецификацию и поддержку, теперь давайте перейдем к использованию. Сначала нам нужно проверить, поддерживает ли браузер IndexedDB. Хотя и имеются инструменты, предоставляющие универсальные методы проверки поддержки возможностей браузеров, мы можем значительно упростить задачу, поскольку просто проверяем поддержку одной определенной возможности.

1
document.addEventListener("DOMContentLoaded", function(){
2
3
    if("indexedDB" in window) {
4
        console.log("YES!!! I CAN DO IT!!! WOOT!!!");
5
    } else {
6
        console.log("I has a sad.");
7
    }
8
9
},false);

Во фрагменте кода выше (с которым вы можете ознакомиться в test1.html, если скачали прикрепленный к этому руководству архив) используется событие DOMContentLoaded, чтобы код выполнялся по окончанию загрузки страницы. (Ладно, это вполне очевидно, но я предполагаю, что разработчики, которые пользовались только jQuery, могут не знать этого.) Затем я просто проверяю наличие indexedDB в объекте window, и если это так, то можем продолжать. Это простейший пример, однако обычно нам, вероятно, нужно было бы сохранить результат проверки, чтобы знать позже, можем ли мы использовать indexedDB. Ниже приводится более продвинутый пример (test2.html).

1
var idbSupported = false;
2
3
document.addEventListener("DOMContentLoaded", function(){
4
5
    if("indexedDB" in window) {
6
        idbSupported = true;
7
    }
8
9
},false);

Я всего лишь создал глобальную переменную, idbSupported, которую можно использовать в качестве флажка для проверки поддержки текущим браузером IndexedDB.


Подключение к базе данных

Как вы можете догадаться, в IndexedDB используются базы данных. Проясню, что такая база не является реализацией SQL Server. Она локальна для браузера и доступна только для пользователя. Базы данных IndexedDB работают согласно тем же правилам, что и куки с Веб-хранилищем. База данных привязывается конкретно к домену, с которого была загружена. Поэтому, например, база под названием "Foo", созданная для foo.com не будет конфликтовать с базой данных под тем же названием для goo.com. Она не только не будет конфликтовать, но и не будет доступна для других доменов. Вы можете хранить данные для вашего веб-сайта и быть уверены, что другой веб-сайт не сможет получить к ним доступ.

Подключение к базе данных выполняется при помощи команды open. В простейшем случае вы указываете имя и версию. Очень важно, чтобы вы указали версию, по причинам, которые мы рассмотрим позже. Ниже приводиться простой пример:

1
var openRequest = indexedDB.open("test",1);

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

  • success
  • error
  • upgradeneeded
  • blocked

Вы, вероятно, можете догадаться о значении success и error. Событие upgradeneeded генерируется и тогда, когда пользователь подключается к базе данных впервые, и тогда, когда вы меняете версию. blocked не относится к событиям, которые будут обычно генерироваться, но может быть сгенерировано, если предыдущее соединение никогда не закрывалось.

Обычно при первом обращении к вашему сайту будет сгенерировано событие upgradeneeded. После этого – только success. Давайте рассмотрим простой пример (test3.html):

1
var idbSupported = false;
2
var db;
3
4
document.addEventListener("DOMContentLoaded", function(){
5
6
    if("indexedDB" in window) {
7
        idbSupported = true;
8
    }
9
10
    if(idbSupported) {
11
        var openRequest = indexedDB.open("test",1);
12
13
        openRequest.onupgradeneeded = function(e) {
14
            console.log("Upgrading...");
15
        }
16
17
        openRequest.onsuccess = function(e) {
18
            console.log("Success!");
19
            db = e.target.result;
20
        }
21
22
        openRequest.onerror = function(e) {
23
            console.log("Error");
24
            console.dir(e);
25
        }
26
27
    }
28
29
},false);

Опять-таки, мы проверяем, поддерживается ли собственно IndexedDB, и если да, то подключаемся к базе данных. Мы добавили в этом примере обработчики для трех событий: upgradeneeded, success и error. Сейчас давайте рассмотрим событие success. Оно передается в обработчик при помощи target.result. Мы скопировали его в глобальную переменную под названием db. Именно ею мы воспользуемся позже для, собственно, добавления данных. Если вы выполните этот код в вашем браузере (в одном из тех, что поддерживает IndexedDB, конечно же!), то при первом запуске скрипта должны будете увидеть в вашей консоли сообщения, выводимые при возникновении событий upgrade и success. При последующих запусках скрипта вы должны будете увидеть лишь сообщения, выводимые при возникновении события success.


Хранилища объектов

Пока что мы проверили, поддерживает ли браузер IndexedDB, убедились, что это так, и подключились к базе данных. Теперь нам необходимо место, где будем хранить данные. В IndexedDB имеется концепция под названием «хранилище объектов". Вы можете считать его типичной таблицей базы данных. (Приблизительно, но пока не заморачивайтесь.) В хранилище объектов содержатся данные (не удивительно), а также ключевое поле и необязательный набор индексов (* файл в СУБД, хранящий список ключей, каждый из которых определяет уникальную запись в БД и содержит информацию о её физическом расположении. Служит для ускорения поиска и сортировки данных. Другими словами - таблица указателей для быстрого поиска записей в БД). Ключевые поля являются по сути уникальными идентификаторами для ваших данных и могут быть указаны в нескольких разных форматах. Мы рассмотрим индексы позднее, когда пойдет речь о извлечении данных.

Теперь кое-что важное. Помните упомянутое выше событие upgradeneeded? Вы можете создать хранилища объектов только при возникновении события upgradeneeded. Знайте, что по умолчанию оно будет сгенерировано автоматически при первом обращении пользователя к сайту. Вы можете этим воспользоваться для создания ваших хранилищ объектов. Очень важно запомнить то, что если вам когда-либо нужно будет изменить ваши объекты хранилищ, вы должны будете обновить версию (в упомянутом ранее методе open) и внести необходимые изменения в код. Давайте посмотрим, как это реализуется на практике:

1
var idbSupported = false;
2
var db;
3
4
document.addEventListener("DOMContentLoaded", function(){
5
6
    if("indexedDB" in window) {
7
        idbSupported = true;
8
    }
9
10
    if(idbSupported) {
11
        var openRequest = indexedDB.open("test_v2",1);
12
13
        openRequest.onupgradeneeded = function(e) {
14
            console.log("running onupgradeneeded");
15
            var thisDB = e.target.result;
16
17
            if(!thisDB.objectStoreNames.contains("firstOS")) {
18
                thisDB.createObjectStore("firstOS");
19
            }
20
21
        }
22
23
        openRequest.onsuccess = function(e) {
24
            console.log("Success!");
25
            db = e.target.result;
26
        }
27
28
        openRequest.onerror = function(e) {
29
            console.log("Error");
30
            console.dir(e);
31
        }
32
33
    }
34
35
},false);

Этот пример (test4.html) основан на предыдущих, поэтому мы просто сосредоточимся на том, что было добавлено. Я получил значение переменной базы данных, переданной в  событие upgradeneeded (thisDB). Одним из свойств этой переменной является список имеющихся хранилищ объектов под названием objectStoreNames. Для тех, кому интересно, это не простой массив, а "DOMStringList." Не спрашивайте меня, но это так. Мы можем воспользоваться методом contains, чтобы проверить, существует ли наше хранилище объектов, и если нет, то создать его. Это одна из немногих синхронных функций в IndexedDB, так что нам не нужно использовать слушатель события.

Если обобщить, то вот что происходило бы при посещении пользователем вашего сайта. При первом посещении генерируется событие upgradeneeded. При помощи кода проверяется, существует ли хранилище объектов "firstOS". Его не будет. Поэтому оно создается. Затем запускается обработчик события success. При последующих посещениях сайта номер версии будет тем же, поэтому событие upgradeneeded не возникает.

Теперь давайте представим, что вам нужно добавить второе хранилище объектов. Для этого вам необходимо увеличить номер версии и по сути продублировать приведенный выше блок кода, где использовались методы contains/createObjectStore. Классно то, что код, выполняемый при возникновении события upgradeneeded, будет корректно работать и для совершенно новых посетителей сайта, и для тех, у кого уже имеется первое хранилище объектов. Ниже приводится пример кода для этого случая (test5.html):

1
var openRequest = indexedDB.open("test_v2",2);
2
3
openRequest.onupgradeneeded = function(e) {
4
    console.log("running onupgradeneeded");
5
    var thisDB = e.target.result;
6
7
    if(!thisDB.objectStoreNames.contains("firstOS")) {
8
        thisDB.createObjectStore("firstOS");
9
    }
10
11
    if(!thisDB.objectStoreNames.contains("secondOS")) {
12
        thisDB.createObjectStore("secondOS");
13
    }
14
15
}

Добавление данных

После подготовки хранилищ объектов можете приступить к добавлению данных. Это, вероятно, один из наиболее крутых аспектов IndexedDB. В отличие от традиционных баз данных, работающих на основе таблиц, IndexedDB позволяет вам сохранять объект, как есть. Это означает, что вы можете взять обычный объект JavaScript и просто его сохранить. Готово. Конечно же, не все так просто, но по большей части это так.

Для работы с данными вам нужно использовать транзакцию. Транзакции принимают два аргумента. Первый – массив таблиц, с которыми вы будете работать. В большинстве случаев это будет одна таблица. Второй аргумент – тип транзакции. Имеется два типа транзакций: readonly и readwrite. Добавление данных будет относиться к операции типа readwrite. Давайте начнем с создания транзакции:

1
//Assume db is a database variable opened earlier

2
var transaction = db.transaction(["people"],"readwrite");

Обратите внимание на хранилище объектов "people", которое создали в примере выше. Мы будем его использовать в нашей следующей демоверсии. После получения транзакции вы запрашиваете указанное вами хранилище объектов:

1
var store = transaction.objectStore("people");

Теперь, когда вы получили хранилище, можете добавить данные. Это выполняется при помощи – дождитесь-ка его – метода add.

1
//Define a person

2
var person = {
3
    name:name,
4
    email:email,
5
    created:new Date()
6
}
7
8
//Perform the add

9
var request = store.add(person,1);

Помните, как я ранее упомянул, что вы можете хранить какие-угодно данные (в целом). Так что мой объект person выше создан полностью произвольно. Я бы мог использовать firstName и lastName вместо просто name. Я бы мог использовать свойство gender. Вы уловили суть. Второй аргумент – ключ для однозначной идентификации данных. В нашем примере мы установили в качестве его значения 1, из-за чего у нас скоро возникнет проблема. Это нормально, скоро мы узнаем, как это поправить.

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

1
request.onerror = function(e) {
2
    console.log("Error",e.target.error.name);
3
    //some type of error handler

4
}
5
6
request.onsuccess = function(e) {
7
    console.log("Woot! Did it");
8
}

У нас имеется обработчик onerror на случай возникновения ошибок, и onsuccess для обработки результата успешно выполненной операции. Вполне очевидно, но давайте взглянем на весь пример. Вы можете ознакомиться с ним в папке test6.html.

 >
1
<!doctype html>
2
<html>
3
<head>
4
</head>
5
6
<body>
7
8
<script>
9
var db;
10
11
function indexedDBOk() {
12
    return "indexedDB" in window;
13
}
14
15
document.addEventListener("DOMContentLoaded", function() {
16
17
    //No support? Go in the corner and pout.

18
    if(!indexedDBOk) return;
19
20
    var openRequest = indexedDB.open("idarticle_people",1);
21
22
    openRequest.onupgradeneeded = function(e) {
23
        var thisDB = e.target.result;
24
25
        if(!thisDB.objectStoreNames.contains("people")) {
26
            thisDB.createObjectStore("people");
27
        }
28
    }
29
30
    openRequest.onsuccess = function(e) {
31
        console.log("running onsuccess");
32
33
        db = e.target.result;
34
35
        //Listen for add clicks

36
        document.querySelector("#addButton").addEventListener("click", addPerson, false);
37
    }
38
39
    openRequest.onerror = function(e) {
40
        //Do something for the error

41
    }
42
43
},false);
44
45
function addPerson(e) {
46
    var name = document.querySelector("#name").value;
47
    var email = document.querySelector("#email").value;
48
49
    console.log("About to add "+name+"/"+email);
50
51
    var transaction = db.transaction(["people"],"readwrite");
52
    var store = transaction.objectStore("people");
53
54
    //Define a person

55
    var person = {
56
        name:name,
57
        email:email,
58
        created:new Date()
59
    }
60
61
    //Perform the add

62
    var request = store.add(person,1);
63
64
    request.onerror = function(e) {
65
        console.log("Error",e.target.error.name);
66
        //some type of error handler

67
    }
68
69
    request.onsuccess = function(e) {
70
        console.log("Woot! Did it");
71
    }
72
}
73
</script>
74
75
<input type="text" id="name" placeholder="Name"><br/>
76
<input type="email" id="email" placeholder="Email"><br/>
77
<button id="addButton">Add Data</button>
78
79
</body>
80
</html>

В коде выше мы создаем небольшую форму с кнопкой, чтобы сгенерировать событие для сохранения данных в IndexedDB. Запустите его в вашем браузере, введите какой-то текст в поля формы и нажмите кнопку для добавления данных. Если у вас открыты инструменты разработчика, то вы должны будете увидеть нечто подобное:

Data Entry FormData Entry FormData Entry Form

Сейчас уместно упомянуть, что в Chrome имеется замечательный инструмент для просмотра данных IndexedDB. Если вы откроете вкладу Resources, развернете раздел IndexedDB, то увидите созданную в нашем примере базу данных и только что добавленный объект.

Chrome Dev Tools and IndexedDBChrome Dev Tools and IndexedDBChrome Dev Tools and IndexedDB

Давайте, ради интереса, опять нажмите ту кнопку Add Data. Вы должны будете увидеть в консоли сообщение об ошибке.

Error adding data againError adding data againError adding data again

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


Ключи

Ключи – версия IndexedDB первичных ключей. В традиционных базах данных могут иметься таблицы без ключей, однако каждое хранилище объектов должно иметь ключ. В IndexedDB предусмотрено несколько различных типов ключей.

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

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

Ваш третий вариант, и на мой взгляд самый простой, – использование генератора ключей. Он подобен первичному ключу, работающему в автоинкрементном режиме (* с увеличением содержимого на единицу), и является наиболее простым методом задания ключей.

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

1
thisDb.createObjectStore("test", { keyPath: "email" });  
2
thisDb.createObjectStore("test2", { autoIncrement: true });

Мы можем изменить предыдущую демоверсию, создав хранилище объектов с ключом, работающим в автоинкрементном режиме:

1
thisDB.createObjectStore("people", {autoIncrement:true});

Наконец, мы можем воспользоваться ранее упомянутым вызовом Add и удалить заданный нами ключ:

1
var request = store.add(person);

Вот и все! Теперь вы можете добавлять данные весь день. Вы можете ознакомить с кодом этого примера в файле test7.html.


Считывание данных

Теперь давайте перейдем к считыванию отдельных фрагментов данных (как считывать более крупные наборы данных, мы рассмотрим позже). Опять-таки, это асинхронная операция, которая будет выполнена в транзакции. Ниже приводится простой пример:

1
var transaction = db.transaction(["test"], "readonly");
2
var objectStore = transaction.objectStore("test");
3
4
//x is some value

5
var ob = objectStore.get(x);
6
7
ob.onsuccess = function(e) {
8
9
}

Обратите внимание, что указанная транзакция принадлежит к типу «только для чтения». В качестве вызова API используется простой вызов метода get, которому передан ключ. Вам на заметку: если вы считаете, что для использования IndexedDB требуется довольно много слов, то заметьте, что вы также можете сцепить многие из тех вызовов. Ниже приводиться тот же самый пример, написанный намного компактнее:

1
db.transaction(["test"], "readonly").objectStore("test").get(X).onsuccess = function(e) {}

Лично я по-прежнему считаю IndexedDB довольно сложной технологией, так что я предпочитаю «развернутый» способ записи, что помогает мне уследить, что происходит в коде.

Обработчику onsuccess передается сохраненный вами ранее объект. Как только вы получили тот объект, можете делать, что хотите. В нашем следующим примере (test8.html) мы добавили просто поле формы, чтобы вы могли ввести ключ и вывести результат. Ниже показан пример:

Fetching dataFetching dataFetching data

Код обработчика для кнопки Get Data приводится ниже:

1
function getPerson(e) {
2
    var key = document.querySelector("#key").value;
3
    if(key === "" || isNaN(key)) return;
4
5
    var transaction = db.transaction(["people"],"readonly");
6
    var store = transaction.objectStore("people");
7
8
    var request = store.get(Number(key));
9
10
    request.onsuccess = function(e) {
11
12
        var result = e.target.result;
13
        console.dir(result);
14
        if(result) {
15
            var s = "&lt;h2>Key "+key+"&lt;/h2>&lt;p>";
16
            for(var field in result) {
17
                s+= field+"="+result[field]+"&lt;br/>";
18
            }
19
            document.querySelector("#status").innerHTML = s;
20
        } else {
21
            document.querySelector("#status").innerHTML = "&lt;h2>No match&lt;/h2>";
22
        }   
23
    }   
24
}

Значительная часть кода должна говорить сама за себя. Получаем значение, переданное в поле, и вызываем метод get хранилища объектов, полученного из транзакции. Обратите внимание, что при помощи кода для отображения данных мы просто получаем значения всех полей и выводим их. В приложении из реального мира разработки вы должны были бы (будем надеяться) знать, что содержится в ваших данных, и работать с определенными полями.


Считывание более крупных наборов данных

Что ж, мы разобрались с тем, как получить один фрагмент данных. Как насчет получения крупных наборов данных? В IndexedDB имеется поддержка так называемого курсора. Курсор позволяет вам обходить данные. Вы можете создать курсоры, передавая необязательные диапазон (базовый фильтр) и направление.

В качестве примера взгляните на следующий блок кода, при  помощи которого открывается курсор для извлечение всех данных из хранилища объектов. Как и все рассмотренное нами ранее, это асинхронная операция, выполняемая в транзакции.

1
var transaction = db.transaction(["test"], "readonly");
2
var objectStore = transaction.objectStore("test");
3
4
var cursor = objectStore.openCursor();
5
6
cursor.onsuccess = function(e) {
7
    var res = e.target.result;
8
    if(res) {
9
        console.log("Key", res.key);
10
        console.dir("Data", res.value);
11
        res.continue();
12
    }
13
}

В обработчик, запускаемый в случае успешного выполнения операции, передается объект с результатом (который мы сохраняем в переменной res). В нем содержится ключ, объект с данными (в ключе value выше) и метод continue, используемый для перехода к следующему фрагменту данных.

В следующей функции мы воспользовались курсором для обхода всех данных, содержащихся в хранилище объектов. Поскольку мы имеем дело с данными о «личностях», то назвали ее getPeople:

1
function getPeople(e) {
2
3
    var s = "";
4
5
    db.transaction(["people"], "readonly").objectStore("people").openCursor().onsuccess = function(e) {
6
        var cursor = e.target.result;
7
        if(cursor) {
8
            s += "&lt;h2>Key "+cursor.key+"&lt;/h2>&lt;p>";
9
            for(var field in cursor.value) {
10
                s+= field+"="+cursor.value[field]+"&lt;br/>";
11
            }
12
            s+="&lt;/p>";
13
            cursor.continue();
14
        }
15
        document.querySelector("#status2").innerHTML = s;
16
    }
17
}

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

List of DataList of DataList of Data

Что ж, теперь вы знаете, как получить один фрагмент данных и как получить все данные. Сейчас давайте перейдем к нашей последней теме – работе с индексами.


Эта технология называется IndexedDB, верно?

Мы говорили об IndexedDB все это время, но не работали еще собственно с какими-либо, что ж, индексами. Индексы – ключевой момент хранилищ объектов IndexedDB. За счет них вы можете извлечь данные на основании их значения и указать, должно ли быть значение уникальным в пределах хранилища. Позже мы продемонстрируем, как использовать индексы для получения какого-то диапазона данных.

Для начала рассмотрим, как создавать индексы. Как и все конструктивное, они должны создаваться в обработчике для события upgrade, в принципе тогда же, когда вы создаете ваш хранилище объектов. Ниже показан пример:

1
var objectStore = thisDb.createObjectStore("people", 
2
                { autoIncrement:true });
3
//first arg is name of index, second is the path (col);

4
objectStore.createIndex("name","name", {unique:false});
5
objectStore.createIndex("email","email", {unique:true});

В первой строке мы создаем хранилище. Мы берем результат выполнения этой операции (объект objectStore) и вызываем метод createIndex. В качестве первого аргумента он принимает имя индекса, а в качестве второго – свойство, которое будет проиндексировано. Думаю, что в большинстве случаев вы будете использовать одинаковое название для обоих. Последний аргумент – набор опций. Пока что мы используем только одну – unique. Первый индекс для name – не уникальный. А второй для email – уникальный. При сохранении нами данных IndexedDB проверит эти индексы и удостоверится, что свойство email имеет уникальное значение. Также она обработает данные определенным образом скрыто от нас, чтобы гарантировать то, что мы можем извлечь данные при помощи этих индексов.

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

1
var transaction = db.transaction(["people"],"readonly");
2
var store = transaction.objectStore("people");
3
var index = store.index("name");
4
5
//name is some value

6
var request = index.get(name);

Для начала мы получаем транзакцию, затем хранилище и индекс. Как мы видели ранее, вы могли бы сцепить те три первые строки, чтобы код выглядел более компактным.

После получения объекта индекса вы можете вызвать его метод get для получения данных на основании имени. Мы могли бы выполнить нечто подобное и для email. Тот вызов является асинхронной операцией, к результату выполнения которой вы можете привязать обработчик onsuccess. Ниже приводится пример такого обработчика, код которого располагается в test10.html:

1
request.onsuccess = function(e) {
2
3
    var result = e.target.result;
4
    if(result) {
5
        var s = "&lt;h2>Name "+name+"&lt;/h2>&lt;p>";
6
        for(var field in result) {
7
            s+= field+"="+result[field]+"&lt;br/>";
8
        }
9
        document.querySelector("#status").innerHTML = s;
10
    } else {
11
        document.querySelector("#status").innerHTML = "&lt;h2>No match&lt;/h2>";
12
    }   
13
}

Обратите внимание, что метод объекта индекса get может возвращать множество объектов. Поскольку наше имя – не уникальное, нам, вероятно, следует изменить код, однако это необязательно.

Теперь давайте усложним задачу. Вы видели, как использовать метод get API объекта индекса для получения значения проиндексированного свойства. Что если вам необходимо получить более обширный набор данных? Последний термин, который мы рассмотрим, – диапазоны. Диапазоны – способ выбора подмножества индекса. Например, если проиндексировано свойство name, то мы можем воспользоваться диапазоном для получения набора, который включает имена, начиная с тех, что начинаются на А, по те, что начинаются на С. Имеется несколько различных вариантов диапазонов. Это может быть диапазон, включающий «все значения ниже определенной метки», «все значения выше определенной метки» и «значения между нижней и верхней метками». Также, это просто вам на заметку, диапазоны могут быть включающими (* с … по … включительно) или исключающими. По сути это означает, что для диапазона A-C мы можем указать, хотим ли мы включить в него имена, начинающиеся на A и C, или же только имена, начинающиеся с букв, расположенных между ними. Наконец, вы можете также указать, нужно ли проводить итерацию при отборе значений по возрастанию или по убыванию.

Диапазоны создаются при помощи объекта высшего уровня под названием IDBKeyRangeНа. Нам интересны три его метода: lowerBound, upperBound и bound. lowerBound используется для создания диапазона, который начинается с нижнего значения и при помощи которого возвращаются все значения «выше» него. upperBound работает наоборот. И, наконец, метод bound используется для указания диапазона, содержащего верхнюю и нижнюю границы. Давайте взглянем на некоторые примеры:

1
//Values over 39

2
var oldRange = IDBKeyRange.lowerBound(39);
3
4
//Values 40a dn over

5
var oldRange2 = IDBKeyRange.lowerBound(40,true);
6
7
//39 and smaller...

8
var youngRange = IDBKeyRange.upperBound(40);
9
10
//39 and smaller...

11
var youngRange2 = IDBKeyRange.upperBound(39,true);
12
13
//not young or old... you can also specify inclusive/exclusive

14
var okRange = IDBKeyRange.bound(20,40)

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

1
Starting with: <input type="text" id="nameSearch" placeholder="Name"><br/>
2
Ending with: <input type="text" id="nameSearchEnd" placeholder="Name"><br/>
3
<button id="getButton">Get By Name Range</button>

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

1
function getPeople(e) {
2
    var name = document.querySelector("#nameSearch").value;
3
4
    var endname = document.querySelector("#nameSearchEnd").value;
5
6
    if(name == "" &amp;&amp; endname == "") return;
7
8
    var transaction = db.transaction(["people"],"readonly");
9
    var store = transaction.objectStore("people");
10
    var index = store.index("name");
11
12
    //Make the range depending on what type we are doing

13
    var range;
14
    if(name != "" &amp;&amp; endname != "") {
15
        range = IDBKeyRange.bound(name, endname);
16
    } else if(name == "") {
17
        range = IDBKeyRange.upperBound(endname);
18
    } else {
19
        range = IDBKeyRange.lowerBound(name);
20
    }
21
22
    var s = "";
23
24
    index.openCursor(range).onsuccess = function(e) {
25
        var cursor = e.target.result;
26
        if(cursor) {
27
            s += "&lt;h2>Key "+cursor.key+"&lt;/h2>&lt;p>";
28
            for(var field in cursor.value) {
29
                s+= field+"="+cursor.value[field]+"&lt;br/>";
30
            }
31
            s+="&lt;/p>";
32
            cursor.continue();
33
        }
34
        document.querySelector("#status").innerHTML = s;
35
    }
36
37
}

Рассмотрим код сверху вниз. Мы начинаем с получения двух полей формы. Далее мы создаем транзакцию и получаем из нее хранилище и индекс. Теперь переходим к более сложной части. Поскольку нам необходимо поддерживать три типа диапазонов, мы должны добавить немного условной логики, чтобы выяснить, какой диапазон использовать. Мы выбираем тип на основании заполненных вами полей. Замечательно то, что после получения диапазона мы просто передаем его в индекс и открываем курсор. Вот и все! Вы можете ознакомиться с полным примером в test11.html. Убедитесь, что вначале ввели какие-то значения, чтобы у вас было что искать.


Что дальше?

Хотите – верьте, хотите – нет, но мы только начали обсуждать IndexedDB. В следующем руководстве мы рассмотрим дополнительные вопросы, включая обновление и удаление данных, работу со свойствами, являющимися массивами, и некоторые общие советы по работе с IndexedDB.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.