Статьи

Добавление функции IndexedDB в приложение


В этой статье, основанной на
главе 5 документа HTML5 в действии Роберта Кроутера, Джо Леннона и Эша Блю , показано, как вы используете API IndexedDB для создания, подключения и использования базы данных, которая хранится на стороне клиента в веб-браузер пользователя.

 

На первый взгляд API IndexedDB может быть очень сложным, особенно если у вас нет опыта написания кода JavaScript, который работает асинхронно. Лучший способ узнать, как его использовать, — это на примере, и в этой статье вы сделаете именно это, добавив функциональность базы данных в пример приложения My Tasks.

IndexedDB был добавлен в HTML5 довольно поздно в процессе спецификации. В результате поддержка браузера для него была намного медленнее, чем с другими частями спецификации. До IndexedDB HTML5 включал спецификацию базы данных на стороне клиента, известную как Web SQL, которая определяла API для полноценной реляционной базы данных, которая будет жить в браузере. Несмотря на то, что Web SQL теперь не работает и больше не является частью HTML5, многие поставщики браузеров уже предоставили ему достойную поддержку, особенно мобильные браузеры. В результате мы будем использовать Web SQL в качестве запасного варианта в нашем примере приложения, обеспечивая его работу в браузерах, совместимых с Web SQL, даже если они еще не поддерживают IndexedDB. По мере того как поддержка IndexedDB становится все более распространенной, наше приложение автоматически решит использовать ее через веб-SQL, где это возможно.

В этой статье вы будете использовать API IndexedDB для создания, подключения и использования базы данных, которая хранится на стороне клиента в веб-браузере пользователя. Вы будете реализовывать представление формы Add Task в приложении, сохраняя новые задачи в базе данных. Затем вы обновите существующие задачи, позволяя пользователям отмечать задачи как выполненные, установив соответствующий флажок. Вы также сможете разрешить пользователям удалять задачи по одному, нажимая кнопку «Удалить» на одной задаче, или все сразу, используя функцию «Очистить задачи». Наконец, вы загрузите существующие задачи из IndexedDB и отобразите их для пользователя вместе с функциями пользовательского интерфейса, необходимыми для того, чтобы они могли обновлять и удалять задачи, если это необходимо.

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

Чтобы добавить функции IndexedDB и Web SQL в приложение «Мои задачи», вам потребуется добавить код из предстоящих списков во внешний файл JavaScript app.js. Большинство добавленных вами функций будут добавлены в новый объект tasksDB, который будет располагаться рядом с основным объектом приложения. Давайте начнем с того, что сделаем некоторое сопоставление имен для префиксных реализаций в Firefox и Chrome и определим нашу базовую оболочку объектовDB.

Перечисление 1 оболочка для наших функций, связанных с данными

if('mozIndexedDB' in window || 'webkitIndexedDB' in window) {
    window.indexedDB = window.mozIndexedDB || window.webkitIndexedDB;
    window.IDBTransaction = window.IDBTransaction || 
      window.webkitIDBTransaction;
    window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
}

var tasksDB = {
    db: null
};

Давайте сразу же начнем добавлять функции к объекту tasksDB. Первая функция, которую вам нужно определить, это open, которая будет отвечать за открытие соединения с базой данных IndexedDB или Web SQL, создание хранилища объектов или таблицы, если это необходимо, и запуск функции загрузки для извлечения любых существующих задач. Добавьте код из следующего листинга внутри объекта tasksDB.

Перечисление 2 Открытая функция

open: function() {
    if('indexedDB' in window) {						
        var req = window.indexedDB.open("tasks");				#1
        req.onsuccess = function(event) {
            tasksDB.db = event.target.result;				#2
            if(tasksDB.db.version != "1") {
                req = tasksDB.db.setVersion("1");				
                req.onsuccess = function(e) {
                    var objStore = tasksDB.db.createObjectStore(		#3
                        "tasks", { keyPath: "id" });			#3
                    tasksDB.load();
                }
            } else {
                tasksDB.load();
            }
        }
    } else if('openDatabase' in window) {
        tasksDB.db = openDatabase('tasks', '1.0', 'Tasks database', 	
            (5*1024*1024));							
        tasksDB.db.transaction(function(tx) {				#4
            tx.executeSql('CREATE TABLE IF NOT EXISTS tasks ('
                + 'id INTEGER PRIMARY KEY ASC, desc TEXT, due DATETIME,'     
                + 'complete BOOLEAN)', [],
                tasksDB.onsuccess, tasksDB.onerror);
        });
        tasksDB.load();
    }
}

# 1 Открывает базу данных
# 2 Подключается к базе данных
# 3 Создает хранилище объектов
# 4 Запускает транзакцию SQL

Это может показаться большим количеством кода для открытия соединения с базой данных, но он также позаботится о создании хранилища объектов при необходимости и предоставляет запасной вариант Web SQL для браузеров, которые не поддерживают IndexedDB. Функция проверяет, может ли поддержка пользовательского агента использовать IndexedDB; если это возможно, он немедленно попытается открыть соединение с базой данных с именем «tasks» (# 1). Поскольку API IndexedDB являются асинхронными, определите функцию обратного вызова, которая позволит вам получить дескриптор объекта базы данных (# 2), который вы назначаете свойству tasksDB.db . Вы можете использовать это свойство в других функциях для удобства. После проверки номера версии базы данных, вызовите setVersionФункция, если вам нужно создать хранилище объектов, что должно быть сделано в рамках транзакции setVersion. Затем вы фактически создаете само хранилище объектов (# 3), присваивая ему имя «tasks» и устанавливая свойство ключа как «id» .

Как упоминалось ранее, в случае, если IndexedDB не поддерживается (как это не существует в текущих версиях многих настольных и мобильных веб-браузеров), вы можете предоставить запасной вариант Web SQL, который позволит приложению работать в этих браузерах. Имейте в виду, что есть много старых браузеров (и даже не очень старых версий Internet Explorer), которые не поддерживают ни одну из этих новых функций HTML5, поэтому обязательно используйте сравнительно новый браузер. API openDatabase позволяет вам получить соединение с базой данных Web SQL. В этом случае вы даете ему имя задачи , версию 1.0, описание и максимальный размер 5 мегабайт. Затем используйте транзакцию, чтобы выполнить команду executeSql (# 4), которая создаст таблицу с именем tasks, если она еще не существует.

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

Далее, давайте добавим код, который позволит пользователям добавлять новые задачи. Снова в объекте tasksDB добавьте две новые функции, addTask и addSuccess , как показано в следующем листинге.

Перечисление 3, Добавляющее задачи

addTask: function() {
    var task = {
        id: new Date().getTime(),
        desc: document.getElementById("txt_desc").value,
        due: document.getElementById("txt_due").value,
        complete: false
    }    
    if('indexedDB' in window) {
        var tx = tasksDB.db.transaction(["tasks"], 			#1
            IDBTransaction.READ_WRITE, 0);
        var objStore = tx.objectStore("tasks");
        var req = objStore.add(task);					#2
        req.onsuccess = tasksDB.addSuccess;				
        req.onerror = tasksDB.onerror;
    } else if('openDatabase' in window) {
        tasksDB.db.transaction(function(tx) {
            tx.executeSql('INSERT INTO tasks(desc, due, complete) VALUES('
                '?, ?, ?)',
                [task.desc, task.due, task.complete],
                tasksDB.addSuccess, tasksDB.onerror);
        });
    }

    return false;
},
addSuccess: function() {
    tasksDB.load();								#3
    alert("Your task was successfully added", "Task added");
    document.getElementById("txt_desc").value = "";
    document.getElementById("txt_due").value = "";
    location.hash = "#list";						#4
}

# 1 Инициирует транзакцию
# 2 Добавляет задачу
# 3 Перезагрузка задач
# 4 Перенаправляет в список задач

В функции addTask вы сначала создаете объект задачи, а затем инициируете транзакцию базы данных (# 1). Затем вы вызываете API добавления в хранилище объектов для сохранения объекта задачи (# 2), определяя функцию обратного вызова tasksDB.addSuccess , которая будет запущена после успешного добавления объекта в хранилище объектов. Снова выполните аналогичную резервную операцию Web SQL, если IndexedDB недоступен. Функция addSuccess перезагружает задачи из базы данных (# 3) и отображает сообщение, сбрасывая поля формы, а затем перенаправляет пользователя в главное представление списка задач (# 4), чтобы они могли видеть свою новую задачу среди других своих задач. Последний фрагмент головоломки, касающийся функции «Добавить задачу», — это подключение новогоtasksDB.addTask функционирует до соответствующей формы. Вам также необходимо открыть соединение с базой данных при запуске приложения. Вфункцию app.launch добавьте следующий код в конец функции:

document.forms.add.onsubmit = tasksDB.addTask;
tasksDB.open();

Изменение существующих задач

Обновление, удаление и очистка задач работает аналогичным образом. Вы создадите функции updateTask и deleteTask с комбинацией обратного вызова drop и dropSuccess, используемой для функциональности очистки задач. Код для всех этих функций выглядит следующим образом.

Перечисление 4 Обновление, удаление и очистка задач

updateTask: function(task) {
    if('indexedDB' in window) {
        var tx = tasksDB.db.transaction(["tasks"], 
            IDBTransaction.READ_WRITE, 0);
        var objStore = tx.objectStore("tasks");
        var req = objStore.put(task);					#1
        req.onerror = tasksDB.onerror;
    } else if('openDatabase' in window) {
        var complete = 0;
        if(task.complete) complete = 1; 
        tasksDB.db.transaction(function(tx) {
           tx.executeSql('UPDATE tasks SET complete = ? WHERE id = ?',
                [complete, task.id],
                tasksDB.load, tasksDB.onerror); 
        });
    }
},
deleteTask: function(id) {
    if('indexedDB' in window) {
        var tx = tasksDB.db.transaction(["tasks"], 
            IDBTransaction.READ_WRITE, 0);
        var objStore = tx.objectStore("tasks");
        var req = objStore['delete'](id);					#2
        req.onsuccess = function(event) {
            tasksDB.load();
        }
        req.onerror = tasksDB.onerror;
    } else if('openDatabase' in window) {
        tasksDB.db.transaction(function(tx) {
            tx.executeSql('DELETE FROM tasks WHERE id = ?', [id], 
                tasksDB.load, tasksDB.onerror);
        });
    }
},
drop: function() {
    if('indexedDB' in window) {
        var tx = tasksDB.db.transaction(["tasks"], 
            IDBTransaction.READ_WRITE, 0);
        var objStore = tx.objectStore("tasks");
        var req = objStore.clear();					#3
        req.onsuccess = tasksDB.dropSuccess,				
        req.onerror = tasksDB.onerror;
    } else if('openDatabase' in window) {
        tasksDB.db.transaction(function(tx) {
           tx.executeSql('DELETE FROM tasks', [], tasksDB.dropSuccess, 
               tasksDB.onerror); 
        });
    }
},
dropSuccess: function() {
    tasksDB.load();
    app.loadSettings();
    alert("Settings and tasks reset successfully", "Reset complete");
    location.hash = "#list";
}

# 1 Обновляет задачу
# 2 Удаляет задачу
# 3 Очищает хранилище объектов

Большая часть кода в этих функциях похожа на функцию addTask — вам обычно нужно создать объект транзакции , получить ссылку на хранилище объектов, а затем вызвать соответствующий API для этой ссылки. В функции updateTask вы используете API put (# 1) с объектом задачи в качестве аргумента для обновления задачи в базе данных. Крайне важно, чтобы у объекта задачи было правильное значение ключа, иначе он может создать новый объект в хранилище, а не обновить существующий. Для удаления задач используйте функцию удаления API. Некоторые браузеры не любят это как удалитьявляется зарезервированным словом в JavaScript, поэтому, чтобы быть в безопасности, вы должны использовать квадратную скобку для вызова API (# 2). Очистить хранилище объектов так же просто, как вызвать clear API (# 3). В этом случае прикрепите функцию обратного вызова к событию успеха в этом API, которая перезагрузит задачи из базы данных (теперь должна быть пустой), перезагрузит настройки из localStorage и перенаправит пользователя в представление списка задач.

Вернувшись в объект приложения, вы должны добавить следующую строку в функцию resetData , чуть ниже строки, которая очищает данные настроек localStorage :

tasksDB.drop();

Загрузка задач из базы данных

Последний элемент функциональности, который необходимо добавить в приложение, — это загрузка самих задач в представление «Список задач», создание прослушивателей событий, которые позволяют пользователю обновлять задачу, установив флажок, чтобы указать, что она выполнена, и удаление задачи с помощью нажав на красную кнопку, которая появляется на правой стороне каждой задачи. Этот код является сердцем вашего приложения, потому что он определяет, как должны отображаться задачи, и связывает соответствующие события по мере необходимости. Эти функции должны быть добавлены в объект tasksDB, как и большая часть остального кода, обсуждаемого в этой статье.

Перечисление 5, Загружающее и отображающее задачи

load: function() {
    var task_list = document.getElementById("task_list");
    task_list.innerHTML = "";
    
    if('indexedDB' in window) {
    var tx = tasksDB.db.transaction(["tasks"], IDBTransaction.READ_WRITE, 
      0);
        var objStore = tx.objectStore("tasks");
        var cursor = objStore.openCursor();				#1
        var i = 0;
        cursor.onsuccess = function(event) {
            var result = event.target.result;				
            if(result == null) return;
            i++;
            tasksDB.showTask(result.value, task_list);			#2
            result['continue']();						
        }
        tx.oncomplete = function(event) {
            if(i === 0) {							
               var emptyItem = document.createElement("li");
               emptyItem.innerHTML = '<div class="item_title">'
+'No tasks to display. <a href="#add">Add one</a>?';
               document.getElementById("task_list").appendChild(emptyItem);
            }
        }
    } else if('openDatabase' in window) {
        tasksDB.db.transaction(function(tx) {
           tx.executeSql('SELECT * FROM tasks', [], function(tx, results) {
                var i = 0, len = results.rows.length,			
                    list = document.getElementById("task_list");
                for(;i<len;i++) {
                    tasksDB.showTask(results.rows.item(i), list);	#A
                }
                if(len === 0) {
                    var emptyItem = document.createElement("li");
                    emptyItem.innerHTML = '<div class="item_title">'
                      +'No tasks to display. <a href="#add">Add one</a>?';                    
                    document.getElementById("task_list")
                            .appendChild(emptyItem);
                }
           }, tasksDB.onerror);
        });
    }
},
showTask: function(task, list) {
    var newItem = document.createElement("li"), checked = '';
    var checked = (task.complete == 1) ? ' checked="checked"' : '';	
    newItem.innerHTML = '<div class="item_complete">'			#3
        +'<input type="checkbox" name="item_complete" id="chk_' 		#3
        +task.id+'"'+checked+'></div>'					#3
        + '<div class="item_delete">'					#3
        + '<a class="lnk_delete" id="del_'+task.id+'">Delete</a></div>'	#3
        + '<div class="item_title">'+task.desc+'</div>'			#3
        + '<div class="item_due">'+task.due+'</div>';			#3
    list.appendChild(newItem);
    document.getElementById('del_'+task.id).onclick = function(e) {	
        if(confirm("Are you sure wish to delete this task?", 
          "Confirm delete")) {
            tasksDB.deleteTask(task.id);
        }
    }
    
    document.getElementById('chk_'+task.id).onchange = function(e) {	
        var updatedTask = {
            id: task.id,
            desc: task.desc,
            due: task.due,
            complete: e.target.checked
        };
        tasksDB.updateTask(updatedTask);
    }
}

# 1 Использует курсор для получения всех задач
# 2 Передает объект в showTask
#A Передает объект в showTask
# 3 Присоединяет событие к новому элементу

Хотя здесь довольно много кода, это относительно просто, если учесть работу, которую он выполняет. Функция загрузки отвечает за извлечение задач из базы данных IndexedDB или Web SQL, итерацию по набору результатов и передачу каждой задачи в функцию showTask , которая, в свою очередь, отобразит задачу в представлении «Список задач» в вашем приложении. Функция загрузки сначала использует API openCursor (# 1) для извлечения всех задач из базы данных. Затем он перебирает каждую запись, передавая объект задачи в функцию showTask (# 2) для рендеринга. Он использует API продолжения курсора для перехода к следующей записи и завершает работу, когда не осталось записей для рендеринга.

The showTask function accepts the task object and a handle to the task list element on the HTML page as arguments, constructs the list item, and adds it to the list accordingly. If the task is marked as complete, this function will set the checkbox to be checked, and it then constructs the various parts of each list item, including the description, due date, checkbox and delete link. Next, the code gets a handle to the new Delete link and attaches the deleteTask function to the click event for the link. Finally, the function gets a handle to the checkbox for the task in question and attaches the updateFunction to the checkbox’s change event (#3).

At this stage, the sample application should be fully functional. Try it out on your iOS or Android device, or indeed on any other desktop or mobile browser that has support for localStorage and either IndexedDB and/or Web SQL. If both IndexedDB and Web SQL are available, the application will favor the former by default.

Summary

In this article, you saw how to open a database connection and create an object store, adding, updating, and deleting records as required. You also learned how to iterate over the task objects in the store using a cursor. In addition to using IndexedDB, the code presented in this article provided a fallback for the HTML5 browsers that have implemented the Web SQL specification but have yet to provide support for IndexedDB.

 

You might also be interested in:

Quick & Easy HTML5 and CSS3M
Rob Crowther

Sass and Compass in Action
Wynn Netherland, Nathan Weizenbaum, and Chris Eppstein

Secrets of the JavaScript Ninja
John Resig and Bear Bibeault