localForage: улучшенное офлайн-хранилище

У веб-приложений есть возможности для автономной работы, например, возможность хранить большие наборы данных и бинарных файлов на протяжении некоторого времени. Можно даже кешировать MP3. Браузерные технологии умеют хранить данные в офлайн-режиме, причем в большом количестве. Однако проблема в том, что выбор технологий для этого фрагментирован.

localStorage предоставляет возможность хранения данных, однако работает медленно и плохо подходит для хранения бинарных файлов. IndexedDB и WebSQL асинхронные, быстрые и поддерживают большие наборы данных, однако имеют довольно запутанные API. Также IndexedDB, ни WebSQL поддерживаются не во всех современных браузерах и не похоже на то, что в ближайшем будущем будут.

Примечание редактора: на сегодняшний день разработка WebSQL прекращена, a поддержка IndexDB реализована во всех современных браузерах.

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

Примечание редактора: последний коммит на момент публикации 22 мая 2016 года, так что она более чем актуальна.

Я хорошенько разобрался с хранением данных оффлайн, пока писал HTML5 клиент для Foursquare под названием around. Мы, естественно, разберем как пользоваться localForage, но если вы из тех, кто предпочитает учиться на чужом коде — можете использовать его в качестве примера.

localForage — это библиотека JavaScript, использующая очень простой API localStorage. По сути, вы получаете возможности localStorage, такие как get (извлечение данных), set (сохранение), remove (удаление элемента объекта), clear (удаление всех элементов) и length (определение количества элементов), а также:

Добавление поддержки IndexedDB и WebSQL позволяет хранить большее количество данных для веб-приложения, чем позволяет только localStorage. Неблокирующая природа их API позволяет приложению работать быстрее, так как основной поток не зависает при вызовах извлечения/сохранения. Поддержка промисов превращает написание JavaScript в приятное занятие и позволяет избежать «эффекта бумеранга». Конечно же, если вы фанат функций обратного вызова, localForage их также поддерживает.

Достаточно разговоров; покажите мне как это работает!

Традиционный localStorage API во многих отношениях очень хорош: он прост в использовании, не навязывает сложных структур данных и не предусматривает использование шаблонного кода. Если у вас есть информация о конфигурации приложения, которую нужно сохранить, вам нужно написать следующее:

// Значения конфигурации, которые нужно сохранить в автономном режиме.
var config = {
    fullName: document.getElementById('name').getAttribute('value'),
    userId: document.getElementById('id').getAttribute('value')
};
 
// Сохраним их для следующей загрузки приложения.
localStorage.setItem('config', JSON.stringify(config));
 
// При следующей загрузке приложения можно сделать следующее:
var config = JSON.parse(localStorage.getItem('config'));

Обратите внимание, что значения в localStorage нужно сохранить в виде строк, чтобы иметь возможность конвертировать их в/из формата JSON при взаимодействии с ним.

Всё кажется удивительно простым, но вы сразу же заметите некоторые проблемные моменты localStorage:

  1. Он синхронный. Нам приходится ждать пока данные будут прочитаны с диска и обработаны, независимо от их размера. Это негативно влияет на отзывчивость приложения. Особенно плохо это работает на мобильных устройствах; главный поток остановлен пока не будут извлечены данные, приложение кажется медленным и даже может перестать реагировать на действия пользователя.
  2. Поддерживаются только строки. Вы заметили, что нам пришлось использовать JSON.parse и JSON.stringify? Это потому, что localStorage поддерживает значения только в виде строк JavaScript. Никаких чисел и булевых значений, блобов, и т.д. Хранение чисел или массивов становится очень неудобным, а блобов — фактически невозможным (или же с ними придётся помучиться и это займёт много времени).

Лучшее решение с помощью localForage

localForage позволяет решить две этих проблемы посредством использования асинхронных API, но вместе с localStorage API. Сравните использование IndexedDB в localForage для той же части данных:

Код IndexedDB

// IndexedDB.
var db;
var dbName = "dataspace";
var users = [ {id: 1, fullName: 'Matt'}, {id: 2, fullName: 'Bob'} ];
var request = indexedDB.open(dbName, 2);
 
request.onerror = function(event) {
    // Обработка ошибок.
};
 
request.onupgradeneeded = function(event) {
    db = event.target.result;
      
    var objectStore = db.createObjectStore("users", { keyPath: "id" });
 
    objectStore.createIndex("fullName", "fullName", { unique: false });
 
    objectStore.transaction.oncomplete = function(event) {
        var userObjectStore = db.transaction("users", "readwrite").objectStore("users");
    }
};
 
// Когда база данных создана, добавим в неё пользователя…
var transaction = db.transaction(["users"], "readwrite");
 
// Выполнение действия после добавления всех данных в базу.
transaction.oncomplete = function(event) {
    console.log("All done!");
};
 
transaction.onerror = function(event) {
    // Не забудьте разобраться с ошибками!
};
 
var objectStore = transaction.objectStore("users");
 
for (var i in users) {
    var request = objectStore.add(users[i]);
    request.onsuccess = function(event) {
        // Содержит нашу информацию о пользователе.
        console.log(event.target.result);
    };
}

WebSQL был бы менее многословным, однако всё же потребовал бы определённой доли шаблонного кода. Для localForage, вам нужно прописать следующее:

Код localForage

// Сохранение пользователей.
var users = [ {id: 1, fullName: 'Matt'}, {id: 2, fullName: 'Bob'} ];
localForage.setItem('users', users, function(result) {
    console.log(result);
});

Так потребуется немного меньше работы.

Данные, отличные от строк

Представим, что вам нужно загрузить аватар пользователя для приложения и кешировать его для работы без подключения к сети. С localForage бинарные данные сохранять просто:

// Загружаем фото пользователя с помощью AJAX.
var request = new XMLHttpRequest();
 
// Извлекаем фото первого пользователя.
request.open('GET', "/users/1/profile_picture.jpg", true);
request.responseType = 'arraybuffer';
 
// Когда состояние AJAX изменяется, сохраняем фото локально.
request.addEventListener('readystatechange', function() {
    if (request.readyState === 4) { // readyState DONE
        // Сохраняем бинарные данные в исходном виде; с localStorage это бы не сработало.
        localForage.setItem('user_1_photo', request.response, function() {
            // Фото сохранено, продолжаем работу!
        });
    }
});
 
request.send()

В следующий раз мы можем извлечь фото из localForage с помощью всего лишь трёх строчек кода:

localForage.getItem('user_1_photo', function(photo) {
    // Создание data URI или ему подобного для помещения фото в тег img или ему подобный.
    console.log(photo);
});

Функции обратного вызова и промисы

Если вы не любите использовать функции обратного вызова в коде, можете использовать ES6 Promises вместо аргумента функции обратного вызова в localForage. Давайте извлечём фото как в предыдущем примере, только с помощью промисов вместо функции обратного вызова:

localForage.getItem('user_1_photo').then(function(photo) {
    // Создание data URI или ему подобного для помещения фото в тег img или ему подобный.
    console.log(photo);
});

Следует признать, что этот пример немного надуман, однако в репозитории around представлен реальный код если вам интересно посмотреть как библиотека работает на практике.

Кроссбраузерная поддержка

localForage поддерживается всеми современными браузерами. IndexedDB доступна во всех современных браузерах кроме Safari (IE 10+, IE Mobile 10+, Firefox 10+, Firefox для Android 25+, Chrome 23+, Chrome для Android 32+ и Opera 15+). При этом дефолтный Android Browser (2.1+) и Safari используют WebSQL.

В худшем случае в качестве резервного варианта вместо localForage будет использоваться localStorage, так что вы получите возможность сохранять хотя бы базовые данные в автономном режиме (это не касается блобов и выполняться будет намного медленнее). Он хотя бы позаботится об автоматической конвертации данных из/в строки JSON, которые нужны localStorage для хранения данных.

Узнайте больше о localForage на GitHub и создавайте issue если хотели бы чтобы у библиотеки появились новые возможности!