Работа с локальным хранилищем, как с объектом — Продолжение

  • Tutorial
Этим постом я бы хотел исправиться и предложить адекватный способ работы с хранилищем, как с объектом. Код из поста «Работа с локальным хранилищем, как с объектом? Легко!» использовать крайне не рекомендую, там всё плохо (минусы изложены внизу). Цель данного — объяснить всем тем, кто добавил статью по ссылке в избранное или поставил плюсик, никогда не использовать этот код.

Сейчас же я хочу продемонстрировать простейшую идею, предложенную Scalar в комментарии.

При загрузке страницы (даже до события готовности DOM дерева) обращаемся к хранилищу (в данном случае, это localStorage и sessionStorage), получить JSON, десериализовать его и положить в какую-нибудь переменную.
localObject = JSON.parse( localStorage.getItem( '_myStorage' ) ); // "{'a':1, 'b':2}" → {a:1, b:2}

Затем, каждые N миллисекунд производить обратный процесс:
localStorage.setItem( '_myStorage', JSON.stringify( localObject ) );

При событии onbeforeunload делать то же самое.

Реализация идеи проста (уровень сложности задачи низок и доступен даже новичку). Но, не все (и я в том числе) до этого додумались.
Код конструктора ObjectStorage
var ObjectStorage = function ObjectStorage( name, duration ) {
	var self,
		name = name || '_objectStorage',
		defaultDuration = 5000;
		
	// дабы не плодить кучу экземпляров, использующих один и тот же ключ хранилища, 
	// просто возвращаем единственный с заданным именем,
	// меняя только duration (если имеется)
	if ( ObjectStorage.instances[ name ] ) {
		self = ObjectStorage.instances[ name ];
		self.duration = duration || self.duration;
	} else {
		self = this;
		self._name = name;
		self.duration = duration || defaultDuration;
		self._init();
		ObjectStorage.instances[ name ] = self;
	}
	
	return self;
};
ObjectStorage.instances = {};
ObjectStorage.prototype = {
	// type == local || session
	_save: function ( type ) {
		var stringified = JSON.stringify( this[ type ] ),
			storage = window[ type + 'Storage' ];
		if ( storage.getItem( this._name ) !== stringified ) {
			storage.setItem( this._name, stringified );
		}
	},

	_get: function ( type ) {
		this[ type ] = JSON.parse( window[ type + 'Storage' ].getItem( this._name ) ) || {};
	},

	_init: function () {
		var self = this;
		self._get( 'local' );
		self._get( 'session' );

		( function callee() {
			self.timeoutId = setTimeout( function () {
				self._save( 'local' );
				callee();
			}, self._duration );
		})();

		window.addEventListener( 'beforeunload', function () {
			self._save( 'local' );
			self._save( 'session' );
		});
	},
	// на случай, если нужно удалить таймаут (clearTimeout( storage.timeoutId ))
	timeoutId: null,
	local: {},
	session: {}
};



Использование:
var storage = new ObjectStorage;
storage.local = {a:4, b: {c:5}};
storage.session = {a:7, b: {c:8}};
b = storage.local.b;
b.c = {d:6};


Перезагружаете страницу,
var storage = new ObjectStorage;
console.log( storage );
/* 
{
	_name: ..., duration: ...,
	local: {a:4, b: {c: {d: 6}}},
	session: {a:7, b: {c :8}}
}
*/


Как вариант, при вызове конструктора ObjectStorage, можно передать два аргумента: name — имя ключа в хранилище и duration — интервал сохранения данных в localStorage.
new ObjectStorage( '_myStorage', 1000 );

Можно оставить имя стандартным:
new ObjectStorage( null, 1000 );

А можно и после инициализации изменить duration:
var storage = new ObjectStorage;
storage.duration = 2000;

(можно и storage._name изменить, но это не рекомендуется, я даже палочку поставил, типа приватное свойство :))

Плюсы, по сравнению с решением по ссылке в начале этой статьи:
  • Производительность: нет никаких геттеров, не нужно каждый раз при вызове ключа дёргать хранилище.
  • Компактность.
  • Можно работать как с обычным объектом:
    storage.local = {a:{}}
    storage.local.a.b = 5; // сработает
    a = storage.local.a;
    a.b = 5; // и так сработает
    
  • Встроенное решение для sessionStorage.


Общие минусы:
  • Если браузер закроется некорректно (БСОД, отключение электричества и пр.) можно потерять (о боги!) работу за последне M секунд (по умолчанию, пять). Сколько я ни старался придумать сценария, где этот момент критичен, не смог.


Добра.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 42

    +1
    setTimeout конечно напрягает немного, а так все здорово конечно.
      0
      setInterval не лучше :)
        0
        интервал нужно использовать с умом, ибо буут коллизии, если первая порция не успеет сохраниться а вторая начнет.
        А вообще оба варианта напрягают, ибо за 5 секунд может очень многое случиться, а опера еще и не отлавливает анлоад.
      0
      Не забываем, что каждая запись в localStorage — физическое обращение к диску)
        +3
        Так, жесткий диск Василия Пупкина был убит в износ неизвестным хакером из России…
          –4
          )))))))))))))))
            –1
            Жесткий диск русского убит русским… Может Пупкин сам его и убил?
            +2
            RubtsovAV подсказал, что можно сравнивать текущее значение хранилища и сериализованного объекта, а затем уже сохранять.
              +2
              Устроим ад SSDшникам :)
              +2
              Можно добавить проверку в _save перед сохранением на предмет: а были собсно изменения с последнего сохранения? В остальном оч интересное решение, нужно брать на вооружение. Спасибо!

                +2
                Хоть экономия и копеешная, но всё же… :)
                  0
                  Хм, да, можно.
                0
                В таймере забыли сохранить sessionStorage
                  0
                  Нет нужды, кроме как при beforeunload. Если браузер закроется некорректно, данные уничтожатся.
                    0
                    Вообще да, логично! Чет я об этом не подумал…
                  0
                  Таймер не остановится если в коде потеряется ссылка на экземпляр ObjectStorage. Может вместо плоского типа сделать что-то вроде менеджера?
                    0
                    Подробнее, что вы имеете в виду?
                      +1
                      function someTest() {
                          // ...
                      
                          // Здесь в конструкторе ObjectStorage создался таймер -->
                          var storage = new ObjectStorage("someStorage");
                          
                          
                          // ...
                      
                          // Функция закончила своё действие, ссылка на storage 
                          // потеряна, а таймер живёт, да и сборщик мусора объект
                          // не убьет
                      }
                      
                      function someTest2() {
                          // ...
                      
                          // То же самое, что и в someTest
                          var storage = new ObjectStorage("someStorage");
                      
                          // ...
                      }
                      
                      someTest();
                      someTest2();
                      


                      В итоге после вызова someTest и someTest2 будут крутится два потерянных таймера. И так каждый раз при создании экземпляра ObjectStorage.
                        0
                        Первое предложение я понял, а вот второе нет («Может вместо плоского типа сделать что-то вроде менеджера?»).

                        Какое решение этой проблемы вы предложите? Что-то типа синглтона?
                          0
                          Нет, обычный плоский JS-объект :)

                          Набросал код на скорую руку:
                          pastebin.com/yAkqVK5Q
                            0
                            Честно говоря, не вижу никакого смысла. В моём варианте человек может создать два экземпляра конструктора с разными именами и интервалами. Во-первых это позволит сохранять реже или чаще те или иные данные, в зависимости от «важности», во вторых не придется каждый раз сериализовывать всё, когда можно это делать по кускам.

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

                            А как быть с «убийством» интервала, когда ссылка не объект утеряна? Я вижу только один вариант: сделать какой-нибудь метод, который убивает интервал.
                              0
                              Чуть подредактировал, учитывая возможность очистки таймаута и создания лишь одного экземпляра с одним и тем же именем.
                                0
                                а this в функции callee ссылается на экземпляр класса или на саму функцию? может надежнее self заюзать?
                                  +1
                                  Пардон, это опечатка.
                                0
                                Вот теперь хорошо. Можно как вариант у ObjectStorage сделать метод destroy, который уничтожает таймаут и убирает экземпляр из инстансов.
                              0
                              Про unload торжественно забыл :)

                              Исправленная паста: pastebin.com/Rye1T0Jr
                        –1
                        можно класть по unload, надежнее
                          0
                          Используйте геттеры и сеттеры вместо таймаута
                            0
                            На осле не будет работать, а такой подход обладает массой преимуществ.
                              0
                              И недостатков тоже

                              На старом ослике вообще нету localStorage :). А на новом — вроде поддерживается. Если нет — определяйте и для него юзайте таймаут
                            0
                            И спойлер обязательно впихнули).
                              0
                              Ну они действительно смотрятся неплохо :)
                              0
                              А в Opera Mobile setTimeout работает?

                              И кто-то «каждые несколько миллисекунд» — тестировал на большом объёме данных? Когда локал сторадж забит на все свои 5 Мб?
                                0
                                Насчет Timeout не скажу, а вот про «милисекунды» — сложно представить случай, когда нужно так часто сохранять данные. По мне интервал в 1-5 секунд самый оптимальный для таких задач.
                                  0
                                  При больших объемах можно отключать таймаут и делать сохранение, только в нужных местах. Можно добавить условие, что если duration === false, то таймер не запускать.
                                  p.s. даже лучше вот так пожалуй if(duration > 0) setTimeout(....)
                                    0
                                    Без таймаута идея не особо-то и нужна — можно и «ручками» в local- и sessionStorage объект загнать
                                      0
                                      Да можно всё ручками :). С оберткой, то всё равно удобнее: есть задел на масштабируемость и разные фичи.
                                  0
                                  В принципе, гораздо проще так:
                                  localStorage.setup = function(obj){
                                   for(var i in obj){
                                    if(Object.prototype.hasOwnProperty.call(obj, i))
                                     localStorage.setItem(i, obj[i]);
                                   }
                                  }
                                  

                                  А потом так:
                                  localStorage.setup({ a:10, b:20 });
                                  

                                    0
                                    А если так:
                                    localStorage.setup({ a:10, b:{c:[1,2,3,4],d:'error?'} });
                                      0
                                      Ок, тогда проще:
                                      localStorage.setup = function(obj){
                                       localStorage.setItem('setupped', JSON.stringify(obj));
                                      }
                                      localStorage.get = function(obj){
                                       return JSON.parse(localStorage.setupped);
                                      }
                                      
                                    0
                                    Цель данного — объяснить всем тем, кто добавил статью по ссылке в избранное или поставил плюсик, никогда не использовать этот код.

                                    И где же собственно разоблачение?
                                    Возможно ваш код лучше, но чем тот код аж так плох?
                                      0
                                      И тот и этот код — мой. Все плюсы по сравнению с тем подходом смотрите внизу.

                                    Only users with full accounts can post comments. Log in, please.