Pull to refresh

Работа с локальным хранилищем, как с объектом? Легко!

Reading time3 min
Views15K
Когда появился localStorage я был рад, как слон, но, немногим позже, после подробного изучения вопроса, я несколько разочаровался: там могут храниться только строки, а об объектах можно было забыть. Приходилось превращать их в строки для хранения, приходилось обратно конвертировать строку в объект для работы с ним, затем опять конвертировать в строку, чтоб сохранить. Не спорю, есть замечательные библиотеки, позволяющие манипулировать хранилищем максимально просто, но вызывать функции как-то не очень хотелось.

localStorage = { a: { b: 1 }, c: { d: 2 } }
localStorage.a.b = 3;

Круто было бы, не так ли?

Относительно давно, где-то около полугода назад я задался вопросом: как же, блин, сделать так, чтоб можно было работать с localStorage вообще без функций, чисто как с объектом. Challenge accepted!

Как говорит небезызвестный Геша: “Всё, я здзелал”.

Дальше читать не обязательно. Более разумное решение находится здесь: habrahabr.ru/post/144998


Главной подзадачей было изобретение способа повесить сеттер не только на сам объект, но и на подобъекты, причем ключи заранее не известны. Это решается крайне просто: ставим геттер на объект:


Object.defineProperty( window, 'objectLocalStorage', {
		// возвращается какой-то объект
		get: function() { return {}; }
});

Теперь, когда мы присваиваем что-либо какому-либо из ключей,

window.objectLocalStorage.a = 1;

Вызывается геттер.

Итак, всё до безобразия просто.

(function() {
    // объект, который будет хранить данные, пока окно браузера не перезагрузят
    // берем данные из хранилища в виде json и парсим их
    var _objectLocalStorage = JSON.parse( localStorage.getItem( 'objectStorage' ) ) || {},
		timer = null;
    // определяем объект с именем objectLocalStorage в window и добавляем ему геттер и сеттер
    // во избежание недоразумений, мы не трогаем localStorage, он каким был, тактим остаётся
    Object.defineProperty( window, 'objectLocalStorage', {
        get: function() {
            
			// timer нужен для того, чтоб не вызывать стрингификацию при каждом запросе объекта
			// и для того, чтоб старые данные localStorage не переписали новые данные,
			// что было следствием асинхронности
			
			// setTimeout для сохранения объекта >после< присваивания, а не до
			if( timer === null ) {
				timer = setTimeout( function(){
					var stringified = JSON.stringify( _objectLocalStorage );
					// некое подобие оптимизации: если данные в объекте не изменились,
					// значит присваивания никакого не было, сработал обычный гет
					if( stringified !== localStorage.getItem( 'objectStorage' ) ) {
						// сохраняем 
						localStorage.setItem( 'objectStorage', stringified );
					}
					timer = null;
				}, 0);
			}
            
            return _objectLocalStorage;
        },
        // на случай, если objectLocalStorage присвоили целый объект
        set: function( v ) {
            _objectLocalStorage = v;
            localStorage.setItem( 'objectStorage', JSON.stringify( _objectLocalStorage ) );
        }
    } );
})();

(Прошу обратить внимание, что таймер вызывается только для сохранения результатов в localStorage. В остальном, результат присваивания предсказуем, как и в обычном объекте, так как _objectLocalStorage возвращается сразу. Навеяно этим комментарием.)

Как пользоваться?

Вставляем код выше в свой js файл, и используем:

objectLocalStorage = { a: 4, b: {c: 2} };
objectLocalStorage.b.c = {d: 5}

Перезагружаем страницу,

console.log( objectLocalStorage ); // { a: 4, b: {c: {d: 5}} }


Как это работает?

При гете objectLocalStorage возвращается локальный объект _objectLocalStorage. Значит, когда мы присваиваем что-нибудь одному из ключей подобъекта в objectLocalStorage, возвращается _objectLocalStorage и присваивание идет в него. То есть
objectLocalStorage.a.b = 5;
аналогично
_objectLocalStorage.a.b = 5;

Но, в первом случае, после присваивания, вызывается сохранение объекта в localStorage. Если не поместить эту часть в таймаут, то сохранение в хранилище будет до присваивания. Согласен, такой способ чреват багами, но другого способа сохранять после, я не нашел.

Вот, собственно, и всё. В идеале, конечно, хорошо бы иметь:
  • Поддержку IE < 9. Как известно, Object.defineProperty не кроссбраузерен
  • Навешивание геттера на все дочерние объекты, то есть, сейчас:
    objectLocalStorage.a.b = 5; // сработает
    a = objectLocalStorage.a;
    a.b = 5; // не сработает
    

  • Сделать то же самое, но для sessionStorage

Но это потом, я просто хотел поделиться радостью :)

Лучей бобра вам.

UPD
Хотел бы отдельно выделить альтернативное, простое и гениальное решение, высказанное Scalar в комментариях.

Мне кажется проще читать и писать в обычный объект, который сериализуется в LS (по таймауту и при onbeforeunload), и десериализуется при старте приложения.
Tags:
Hubs:
Total votes 60: ↑49 and ↓11+38
Comments35

Articles