Библиотека storage-facade, о которой пойдет речь в этой статье, предоставляет единый синхронный / асинхронный API хранилища, являющийся абстракцией над реальной реализацией хранилища. Для конечного пользователя она упрощает использование любых хранилищ, для которых абстрактный класс из storage-facade будет реализован. Как автор этой библиотеки, расскажу о её использовании.

Есть реализации для IndexedDB, localStorage, sessionStorage, обёртка для Map.

Рассмотрим самый простой вариант, storage-facade-localstoragethin.

Установка

npm install storage-facade@4 storage-facade-localstoragethin@1

Использование

Вот такой код:

import { createStorage } from 'storage-facade';
import { LocalStorageThin } from 'storage-facade-localstoragethin';

const storage = createStorage({
  use: new LocalStorageThin(),
  useCache: true, // поддержка кеширования (мемоизации)
});

try {
  storage.Pen = { data: [40, 42] };
  storage.pineApple = 10;
  storage.apple = [1, 2, 3];
  storage.pen = 'Uh!';
} catch (e) {
  console.error((e as Error).message);
  // Если вы не используете TypeScript то замените на
  // console.error(e.message);
}

Приведёт к созданию следующих ключей в localStorage:

Эта магия реализована при помощи Proxy (MDN): мы перехватываем обращение к ключам объекта хранящегося в переменной storage, а так же операцию удаления ключей, например delete storage.pen;.

Объект хранилища предоставляет следующие методы:

  • .clear() - очищает хранилище

  • .entries() - возвращает массив пар ключ-значение

  • .deleteStorage() - удаляет хранилище (зависит от конкретной реализации, обычно сначала выполняется .clear(), а затем объект хранилища блокируется для чтения, записи и использования методов, выбрасывая ошибку при попытке доступа.

  • .size() - возвращает количество пар ключ-значение

  • .key(index: number) - возвращает имя ключа по его индексу

Кроме того, есть методы для работы с "дефолтными значениями". Дефолтные значения хранятся не в хранилище (в данном случае не в localStorage), а в экземпляре. Дефолтные значения используются, если хранилище при запросе ключа возвращает undefined.

Это удобно, мы можем задавать в коде дефолтное значение, например, для темы (тёмная или светлая), после чего по клику пользователя на кнопку, просто менять значение на противоположное. Если пользователь ещё не менял тему, то будет использовано дефолтное значение, если же он уже ранее менял тему, то будет использовано сохранённое в localStorage значение. Нам не нужно беспокоиться об этой логике.

  • .addDefault(obj) - добавляет ключи и значения переданного объекта к уже хранящимся в экземпляре

  • .setDefault(obj) - заменяет объект содержащий ключи и значения в экземпляре переданным пользователем

  • .getDefault() - возвращает объект, содержащий дефолтные ключи и значения

  • .clearDefault() - заменяет объект с дефолтными ключами и значениями пустым объектом

Вот пример, который должен прояснить использование дефолтных значений на практике:

import { createStorage } from 'storage-facade';
import { LocalStorageThin } from 'storage-facade-localstoragethin';

const storage = createStorage({
  use: new LocalStorageThin(),
  useCache: true,
});

try {
  // Такого ключа нет
  console.log(storage.value) // undefined

  // Добавим дефолтные значения
  storage.addDefault({ value: 9, other: 3 });
  // `1` перезапишет `9` в `value`
  storage.addDefault({ value: 1, value2: 2 });

  // Так как `storage.value = undefined`
  // то будет использовано дефолтное значение
  console.log(storage.value);  // 1
  // аналогично
  console.log(storage.value2); // 2
  console.log(storage.other);  // 3

  // Теперь установим значение
  storage.value = 42;
  // Когда мы установили значение отличное от `undefined`,
  // дефолтное значение больше не используется
  console.log(storage.value); // 42

  // Снова изменим на `undefined`
  storage.value = undefined;
  // используется дефолтное значение
  console.log(storage.value); // 1

  // `null` не приводит к использованию дефолтных значений
  storage.value = null;
  console.log(storage.value); // null

  // Удалим ключ из хранилища
  delete storage.value;
  // Теперь снова используется дефолтное значение
  console.log(storage.value); // 1

  // getDefault
  console.log(storage.getDefault()); // { value: 1, value2: 2, other: 3 }

  // Замена 'default'
  storage.setDefault({ value: 30 });

  // Тут выводится дефолтное значение `30` заданное строкой выше
  console.log(storage.value); // 30
  console.log(storage.value2); // undefined

  // clearDefault
  storage.clearDefault();

  // Так как дефолтные значения очищены,
  // мы больше не видим `30`
  console.log(storage.value); // undefined
  console.log(storage.value2); // undefined
} catch (e) {
  console.error((e as Error).message);
}

Ограничения

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

  // Read
  // С чтением проблем нет
  console.log((storage.value as Record<string, unknown>).data); // Ok

  // Write
  // Не делайте так
  storage.value.data = 42; // Никакого эффекта

Вместо этого используйте следующий подход:

  // Read
  console.log((storage.value as Record<string, unknown>).data); // Ok

  // Write
  // Получаем объект
  const updatedValue = storage.value as Record<string, unknown>;
  // Вносим изменения
  updatedValue.data = 42;
  // Обновляем хранилище
  storage.value = updatedValue; // Ок 

Другие возможности

Есть расширенная версия этой библиотеки для localStoragestorage-facade-localstorage. Она позволяет создавать "виртуальные" хранилища, которые можно очищать не затрагивая данные в других виртуальных хранилищах и другие ключи (возможно от других библиотек), хранящихся в localStorage. Кроме того, можно обходить каждое отдельное хранилище при помощи метода .entries(). Цена за это – префиксы у ключей и хранение дополнительного ключа содержащего массив имен ключей для каждого виртуального хранилища.

Более подробная документация и ссылки на все реализованные на данный момент интерфейсы на странице библиотеки storage-facade.

Спасибо за внимание, хорошего дня!

Комментарии (5)


  1. Alexandroppolus
    06.09.2023 09:04

    Из того что вызывает вопросы - в интерфейсе (абстрактном классе) StorageInterface есть два комплекта методов, синхронные и асинхронные. Реализуется всегда только один из них. Подозреваю, что тут нарушение принципов L и I из SOLID


    1. vadglinka Автор
      06.09.2023 09:04

      в интерфейсе (абстрактном классе) StorageInterface есть два комплекта методов, синхронные и асинхронные. Реализуется всегда только один из них.

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


  1. iliazeus
    06.09.2023 09:04

    Я правильно понимаю, что разработчик этой библиотеки - вы? Судя по репозиторию, это так, поэтому вопросы про интерфейс и реализацию буду задавать вам.

    Самый главный вопрос: для чего такая абстракция? У localStorage и sessionStorage и так общий интерфейс. IndexedDB же обычно используется для других целей, чем Storage. Ограничения на значения и особенности их хранения у него, опять же, другие.

    Мне кажется, можно было бы не изобретать свой интерфейс, а взять тот же самый Storage, и дописать для него нужные вам реализации (для Map, например).

    Использование Proxy тоже выглядит не слишком оправданным. Например, из-за такого интерфейса, получается, в вашем хранилище нельзя сохнанить данные по ключу entries, раз он занят методом? Использование ключей, начинающихся с __ (вместо, например, приватных полей) тоже вызывает вопросы к тому, что возможны коллизии.

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


    1. vadglinka Автор
      06.09.2023 09:04
      -1

      Я правильно понимаю, что разработчик этой библиотеки - вы?

      Так и есть.

      Самый главный вопрос: для чего такая абстракция?

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

      получается, в вашем хранилище нельзя сохнанить данные по ключу entries, раз он занят методом?

      Да, так и есть, это описано в документации в разделе Limitations. К сожалению, с этим ничего не поделаешь.

      Использование ключей, начинающихся с __ (вместо, например, приватных полей)

      Насколько я понял, вы увидели __ на скриншоте. Это ключи в localStorage, а не в экземпляре. Если не включено кеширование, ключи вобще не хранятся в экземпляре, а сразу пишутся в localStorage, следовательно использование приватных полей невозможно. Если говорить про включенное кеширование, то кешированные ключи хранятся в Map (MDN). При попытке записи ключа, имя которого совпадает с именем ключа в котором хранится массив ключей для "виртуального" хранилища будет выброшена ошибка. Но это достаточно маловероятная ситуация, чтобы её получить надо написать что-то вроде storage[__storageTwo-keys-array] = .... В любом случае, такая проблема может возникнуть только в расширенной версии с "виртуальными" хранилищами, в SessionStorageThin ключей с __ в localStorage нет.

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

      Возможность работать с хранилищем как с простым объектом это и есть основная фишка этой библиотеки. Но да, вы правы, это может быть неожиданно.


      1. vadglinka Автор
        06.09.2023 09:04

        Опечатка, вместо SessionStorageThin должно быть LocalStorageThin.