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

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

Но представьте себе ситуацию, когда приложением уже пользуются реальные люди и Вам необходимо как-то изменить структуру данных.

Вот тут-то и начинаются проблемы. В лучшем случае приложение будет работать с ошибками, в худшем же, перестанет работать вовсе.

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

Раскрываем проблему глубже (и совсем чуть-чуть бомбежки).

Вообще тема хранения данных на стороне клиента хорошо расписана в Cookbook на официальном сайте Vue.js.

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

Единственное, что я нашел:

As a quick aside, these dev tools also offer you a way to remove storage values. This can be very useful when testing.

Или в русской локализации (гнусавым голосом):

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

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

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

Там в конце статьи еще указаны несколько сторонних решений, которые делают локальное хранилище умнее.

Из них хочу выделить vue2-storage, в котором реализован механизм установки времени жизни записи (как в куках). Это уже неплохо, но не совсем то, что нужно.

Итак, начинаем.

Способ первый. Самый простой и не самый оптимальный.

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

localStorage.removeItem('data-name'); // Удалили старый
localStorage.setItem('new-data-name', data); // Создали новый

Вроде решение хорошее. Но это только на первый взгляд.

А что если данные придётся изменять часто. Настолько часто, что Ваш хук mounted превратится вот в такое:

localStorage.removeItem('data-name');
localStorage.removeItem('new-data-name');
localStorage.removeItem('new-data-name1');
localStorage.removeItem('new-data-name2');
localStorage.removeItem('new-data-name3');
localStorage.removeItem('new-data-name4');
localStorage.removeItem('new-data-name5');
localStorage.removeItem('new-data-name6');
localStorage.removeItem('new-data-name7');
localStorage.removeItem('new-data-name8');
localStorage.removeItem('new-data-name9');

localStorage.setItem('new-data-name10', data); // Надеюсь это последняя правка, я сдам проект и уеду жить на необитаемый остров, где никто меня не достанет

Я понимаю, что если Вы разрабатываете свой проект, то такое случится с очень маленькой вероятностью.

Но если делаете проект на заказ, то нужно быть готовым к доработкам "на горячую". Это когда нужно максимально быстро дописать функционал, которого изначально в приложении не планировалось и сразу же отправить новую версию в продакшен.

Примеси? Что это?

Писать своё решение мы будем в виде примеси (mixins - https://vuejs.org/v2/api/#mixins и https://vuejs.org/v2/guide/mixins.html).

Почему примеси? А почему бы и нет?

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

Ну за одно мы узнаем и попробуем что-то новое.

Простой пример:

// simple-mixin.js
export default {
  methods: {
    log(s) {
      window.console.log(s);
    }
  }
}
// component.vue
import simpleMixin from 'simple-mixin.js';

export default {
  mixins: [simpleMixin],

  created() {
    log('Hello from simple-mixin');
  }
}

Обязательно запомните, что миксин имеет те же самые поля, что и компонент. Если Вы напишете метод log не внутри объекта methods, то работать не будет.

Способ второй. Самописный.

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

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

Звучит зловще, но на самом деле всё очень просто.

Для начала распишу алгоритм:

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

У нашего решения будет одна зависимость - библиотека для конвертирования строки в MD5 (https://github.com/blueimp/JavaScript-MD5/)

Если кто-то считает килобайты и 3.68 KB для него критично - можно переписать код и избавиться от зависимости. Но тогда в локальном хранилище будет много лишней информации.

Вынесем нашу примесь в отдельный модуль local-storage-mixin.js:

import md5 from 'blueimp-md5';

export default {
  methods: { // Помним про methods
    // Принимаем название ключа и данные
    saveStateItem(itemName, data) {
      // Записываем измененное значение
      localStorage.setItem(itemName, JSON.stringify(this[data]));
    },
    // Принимаем название ключа и данные
    initStateItem(itemName, data) {
      // Превращаем начальный стейт в строку и конвертируем в MD5
      const jsonData = JSON.stringify(this[data]);
      const md5Data = md5(jsonData);

      // Имя начального стейта будет накое же как и пользовательского, но с постфиксом
      const initItemName = `${itemName}--init`;

      // Получаем измененные пользователем данные
      const item = localStorage.getItem(itemName);

      if (item) {
        try {
            // Получаем начальный стейт из локального хранилища
            const initItem = localStorage.getItem(initItemName);

            if (initItem) {
              try {
                if (md5Data === initItem) {
                  // Если начальные стейты идентичны, то переопределяем данные приложения на измененные пользователем данные
                  this[data] = JSON.parse(item);
                } else {
                  // В противном случае обновляем все данные в локальном хранилище
                  localStorage.setItem(itemName, jsonData);
                  localStorage.setItem(initItemName, md5Data);
                }
              } catch (e) {
                localStorage.removeItem(initItemName);
                localStorage.removeItem(itemName);
              }
            } else {
              localStorage.setItem(initItemName, md5Data);
            }
        } catch (e) {
          localStorage.removeItem(itemName);
          localStorage.removeItem(initItemName);
        }
      } else {
        localStorage.setItem(itemName, jsonData);
        localStorage.setItem(initItemName, md5Data);
      }
    }
  }
};

Теперь посмотрим на пример использования в простом компоненте simple-form.vue:

<template>
  <div class="simple-form">
    <button @click="profileOpen = true">Редактировать профиль</button>

    <div v-if="profileOpen">
      <h1>{{header}}</h1>

      <form>
        <div>
          <span>Ваше имя:</span> <input v-model="personal.name">
        </div>
        
        <div>
          <span>Ваш возраст:</span> <input v-model="personal.age">
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import localStorageMixin from 'local-storage-mixin';

const name = 'simple-form';

export default {
  name,
  mixins: [localStorageMixin],
  data() {
    return {
      header: 'Введите Ваши данные:',
      personal: {
        name: '',
        age: ''
      },
      profileOpen: false
    };
  },
  created() { // Переопределяем начальный стейт
    this.initStateItem(`${name}:personal`, 'personal');
    this.initStateItem(`${name}:profileOpen:`, 'profileOpen');

    console.log('Данные из локального хранилища заменили начальные данные компонента');
  },
  updated() { // Сработает, когда стейт компонента будет измененён
    this.saveStateItem(`${name}:personal`, 'personal');
    this.saveStateItem(`${name}:profileOpen:`, 'profileOpen');
    
    console.log('Данные в локальном хранилище обновились');
  }
}
</script>

Как видите, я вынес данные формы в отдельный объект. Лично я считаю это хорошей практикой. Это упорядочивает стейт нашего компонента.

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

Я сомневаюсь, что хранение поля header является хорошей идеей.

Из примера так же видно, что наш миксин умеет работать как с простыми типами данных, так и со сложными.

Онлайн демонстрацию данного компонента Вы можете посмотреть здесь - https://codepen.io/login2030/pen/bOpPjE

Так же есть расширенная демонстрация: https://codepen.io/login2030/pen/MZwWvz.

Надеюсь статья была Вам полезна. Всем удачи, здоровья и хорошего настроения (с).

Регистрация

Для регистрации введите актуальный адрес электронной почты. На него вам придет письмо для активации аккаунта.

Вход

или

Сброс пароля

Для сброса пароля введите адрес электронной почты указанный при регистрации. На этот адрес придет письмо с инструкцией.

Регистрация

Для регистрации введите актуальный адрес электронной почты. На него вам придет письмо для активации аккаунта.

Уровень

Сумма оплаты составляет 500 руб. Доступ будет действовать до 25.07.2021.