Как использовать IndexDB для управления состоянием в JavaScript

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

Что мы подразумеваем под «состоянием»?

Содержание статьи:

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

Системы управления состоянием, такие как Redux и Vuex, предоставляют централизованные хранилища данных. Любой компонент JavaScript может читать, обновлять или удалять данные. Некоторые системы позволяют компонентам подписываться на события изменений. Например, когда пользователь переключает светлый/темный режим, все компоненты соответственно обновляют свои стили.

Большинство систем управления состоянием хранят значения в памяти, хотя доступны методы и плагины для передачи данных в localStorage, файлы cookie и т. д.

JavaScript. Быстрый старт

Изучите основы JavaScript на практическом примере по созданию веб-приложения

Подходит ли IndexedDB для хранения состояния?

Как всегда: зависит от обстоятельств. IndexedDB предлагает некоторые преимущества:

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

В отличие от файлов cookie и веб-хранилища (localStorage и sessionStorage), IndexedDB хранит данные нативных объектов JavaScript. Нет необходимости сериализовать в JSON или снова десериализовать.

Доступ к IndexedDB является асинхронным, поэтому он оказывает минимальное влияние на основной поток обработки JavaScript.

Обратите внимание, что веб-хранилище является синхронным: ваш код JavaScript приостанавливает выполнение, пока он обращается к данным. Это может вызвать проблемы с производительностью при сохранении больших наборов данных.

Асинхронный доступ к данным имеет ряд недостатков:

API IndexedDB использует более старые методы обратного вызова и поэтому библиотека-оболочка на основе рromise является практичней.

Конструкторы класса аsync и обработчики геттеров и сеттеров для Proxy невозможны в JavaScript. Это создает некоторые проблемы для систем управления состоянием.

Создание системы управления состоянием на основе IndexedDB

В приведенном ниже примере кода реализована простая система управления состоянием в 35 строках JavaScript. Она предлагает следующие функции:

Вы можете определить состояние с помощью name(строка) и value(примитив, массив, объект и т. д.). Хранилище объектов IndexedDB сохраняет эти значения, используя имя в качестве индекса.

Любой компонент JavaScript может установить или получить значение по имени.

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

Проект списка дел демонстрирует менеджер состояния. Он определяет два веб-компонента, которые обращаются к одному и тому же массиву задач todolist, управляемых объектами State:

todo-list.js: отображает todolistHTML и удаляет элемент, когда пользователь нажимает кнопку «Готово».

todo-add.js: показывает форму «добавить новый элемент», которая добавляет новые задачи в массив todolist.

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

Создание класса-оболочки IndexedDB

В статье «Начало работы» представлена оболочка IndexedDB на основе Promise. Нам нужен аналогичный класс, но он может быть проще, потому что он извлекает отдельные записи name.

Скрипт js/lib/indexeddb.js определяет класс IndexedDB с конструктором. Он принимает имя базы данных, версию и функцию обновления и возвращает созданный объект после успешного подключения к базе данных IndexedDB:

JavaScript // IndexedDB wrapper class export class IndexedDB { // connect to IndexedDB database constructor(dbName, dbVersion, dbUpgrade) { return new Promise((resolve, reject) => { // connection object this.db = null; // no support if (!(‘indexedDB’ in window)) reject(‘not supported’); // open database const dbOpen = indexedDB.open(dbName, dbVersion); if (dbUpgrade) { // database upgrade event dbOpen.onupgradeneeded = e => { dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion); }; } dbOpen.onsuccess = () => { this.db = dbOpen.result; resolve( this ); }; dbOpen.onerror = e => { reject(`IndexedDB error: ${ e.target.errorCode }`); }; }); }

12345678910111213141516171819202122232425262728293031323334353637 // IndexedDB wrapper classexport class IndexedDB {   // connect to IndexedDB database  constructor(dbName, dbVersion, dbUpgrade) {     return new Promise((resolve, reject) => {       // connection object      this.db = null;       // no support      if (!(‘indexedDB’ in window)) reject(‘not supported’);       // open database      const dbOpen = indexedDB.open(dbName, dbVersion);       if (dbUpgrade) {         // database upgrade event        dbOpen.onupgradeneeded = e => {          dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion);        };      }       dbOpen.onsuccess = () => {        this.db = dbOpen.result;        resolve( this );      };       dbOpen.onerror = e => {        reject(`IndexedDB error: ${ e.target.errorCode }`);      };     });   }

Асинхронный метод set сохраняет value с идентификатором name в хранилище объектов storeName. IndexedDB обрабатывает все операции в транзакции, которая запускает события, разрешающие или отклоняющие Promise:

JavaScript // store item set(storeName, name, value) { return new Promise((resolve, reject) => { // new transaction const transaction = this.db.transaction(storeName, ‘readwrite’), store = transaction.objectStore(storeName); // write record store.put(value, name); transaction.oncomplete = () => { resolve(true); // success }; transaction.onerror = () => { reject(transaction.error); // failure }; }); }

123456789101112131415161718192021222324 // store item  set(storeName, name, value) {     return new Promise((resolve, reject) => {       // new transaction      const        transaction = this.db.transaction(storeName, ‘readwrite’),        store = transaction.objectStore(storeName);       // write record      store.put(value, name);       transaction.oncomplete = () => {        resolve(true); // success      };       transaction.onerror = () => {        reject(transaction.error); // failure      };     });   }

Точно так же асинхронный метод get извлекает value с идентификатором name из хранилища объектов storeName:

JavaScript // get named item get(storeName, name) { return new Promise((resolve, reject) => { // new transaction const transaction = this.db.transaction(storeName, ‘readonly’), store = transaction.objectStore(storeName), // read record request = store.get(name); request.onsuccess = () => { resolve(request.result); // success }; request.onerror = () => { reject(request.error); // failure }; }); } }

123456789101112131415161718192021222324252627 // get named item  get(storeName, name) {     return new Promise((resolve, reject) => {       // new transaction      const        transaction = this.db.transaction(storeName, ‘readonly’),        store = transaction.objectStore(storeName),         // read record        request = store.get(name);       request.onsuccess = () => {        resolve(request.result); // success      };       request.onerror = () => {        reject(request.error); // failure      };     });   }  }

Воспроизведение сеанса пользователя

Независимо от того, используете ли вы React, Vue или просто vanillaJS, отладка веб-приложения в рабочей среде может быть сложной и трудоемкой. OpenReplay — это альтернатива с открытым исходным кодом для FullStory, LogRocket и Hotjar. Он позволяет отслеживать и воспроизводить все, что делают ваши пользователи, и показывает, как ваше приложение ведет себя при каждой проблеме. Это похоже на то, как если бы инструмент для веб-разработчика вашего браузера был открыт, когда вы смотрите через плечо пользователя. OpenReplay — единственная доступная альтернатива с открытым исходным кодом.

Создание класса State

Сценарий js/lib/state.js импортирует IndexedDB и определяет класс State. В классе обьявляются пять статических значений свойств для экземпляров:

dbName: имя базы данных IndexedDB, используемой для хранения состояний ( «stateDB»)

dbVersion: номер версии базы данных

storeName: имя хранилища объектов, в котором хранятся все пары имя / значение ( «state»)

DB: ссылка на объект IndexedDB, используемый для доступа к базе данных

target: объект EventTarget (), который может отправлять и получать события для всех объектов State.

JavaScript // simple state handler import { IndexedDB } from ‘./indexeddb.js’; export class State { static dbName = ‘stateDB’; static dbVersion = 1; static storeName = ‘state’; static DB = null; static target = new EventTarget();

12345678910 // simple state handlerimport { IndexedDB } from ‘./indexeddb.js’; export class State {   static dbName = ‘stateDB’;  static dbVersion = 1;  static storeName = ‘state’;  static DB = null;  static target = new EventTarget();

Конструктор принимает два необязательных параметра:

массив имен observed

функцию updateCallback. Эта функция получает name и value всякий раз, когда обновляется состояние.

Обработчик отслеживает события set, вызываемые при изменении состояния.

JavaScript // object constructor constructor(observed, updateCallback) { // state change callback this.updateCallback = updateCallback; // observed properties this.observed = new Set(observed); // subscribe to set events State.target.addEventListener(‘set’, e => { if (this.updateCallback && this.observed.has( e.detail.name )) { this.updateCallback(e.detail.name, e.detail.value); } }); }

12345678910111213141516171819 // object constructor  constructor(observed, updateCallback) {     // state change callback    this.updateCallback = updateCallback;     // observed properties    this.observed = new Set(observed);     // subscribe to set events    State.target.addEventListener(‘set’, e => {       if (this.updateCallback && this.observed.has( e.detail.name )) {        this.updateCallback(e.detail.name, e.detail.value);      }     });   }

Класс не подключается к базе данных IndexedDB, пока это не потребуется. dbConnectМетод устанавливает соединение и использует его для всех объектов State. При первом запуске он создает новое хранилище объектов с именем state (как определено в статическом свойстве storeName):

JavaScript // connect to IndexedDB database async dbConnect() { State.DB = State.DB || await new IndexedDB( State.dbName, State.dbVersion, (db, oldVersion, newVersion) => { // upgrade database switch (oldVersion) { case 0: { db.createObjectStore( State.storeName ); } } }); return State.DB; }

1234567891011121314151617181920 // connect to IndexedDB database  async dbConnect() {     State.DB = State.DB || await new IndexedDB(      State.dbName,      State.dbVersion,      (db, oldVersion, newVersion) => {         // upgrade database        switch (oldVersion) {          case 0: {            db.createObjectStore( State.storeName );          }        }       });     return State.DB;   }

Асинхронный метод set обновляет значение переменной name. Он добавляет name к списку observed, подключается к базе данных IndexedDB, устанавливает новое значение, и объявляет CustomEvent, которое получают все объекты State:

JavaScript. Быстрый старт

Изучите основы JavaScript на практическом примере по созданию веб-приложения

JavaScript // set value in DB async set(name, value) { // add observed property this.observed.add(name); // database update const db = await this.dbConnect(); await db.set( State.storeName, name, value ); // raise event const event = new CustomEvent(‘set’, { detail: { name, value } }); State.target.dispatchEvent(event); }

123456789101112131415 // set value in DB  async set(name, value) {     // add observed property    this.observed.add(name);     // database update    const db = await this.dbConnect();    await db.set( State.storeName, name, value );     // raise event    const event = new CustomEvent(‘set’, { detail: { name, value } });    State.target.dispatchEvent(event);   }

Асинхронный метод get возвращает значение name. Он добавляет name к списку observed, подключается к базе данных IndexedDB и извлекает индексированные данные:

JavaScript // get value from DB async get(name) { // add observed property this.observed.add(name); // database fetch const db = await this.dbConnect(); return await db.get( State.storeName, name ); } }

12345678910111213 // get value from DB  async get(name) {     // add observed property    this.observed.add(name);     // database fetch    const db = await this.dbConnect();    return await db.get( State.storeName, name );   } }

Вы можете получать и обновлять значения состояния, используя новый объект State, например:

JavaScript import { State } from ‘./state.js’; (async () => { // instantiate const state = new State([], stateUpdated); // get latest value and default to zero let myval = await state.get(‘myval’) || 0; // set a new state value await state.set(‘myval’, myval + 1); // callback runs when myval updates function stateUpdated(name, value) { console.log(`${ name } is now ${ value }`) } })()

12345678910111213141516171819 import { State } from ‘./state.js’; (async () => {   // instantiate  const state = new State([], stateUpdated);   // get latest value and default to zero  let myval = await state.get(‘myval’) || 0;   // set a new state value  await state.set(‘myval’, myval + 1);   // callback runs when myval updates  function stateUpdated(name, value) {    console.log(`${ name } is now ${ value }`)  } })()

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

JavaScript new State([‘myval’], (name, value) => { console.log(`I also see ${ name } is now set to ${ value }!`) });

123 new State([‘myval’], (name, value) => {  console.log(`I also see ${ name } is now set to ${ value }!`)});

Создание списка дел

Простое приложение со списком дел демонстрирует систему управления состоянием:

В файле index.html определены два элемента:

<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8″> <title>IndexedDB state management to-do list</title> <meta name=”viewport” content=”width=device-width,initial-scale=1″ /> <link rel=”stylesheet” href=”./css/main.css” /> <script type=”module” src=”./js/main.js”></script> </head> <body> <h1>IndexedDB state management to-do list</h1> <todo-list></todo-list> <todo-add></todo-add> </body> </html>

12345678910111213141516171819 <!DOCTYPE html><html lang=”en”><head><meta charset=”UTF-8″><title>IndexedDB state management to-do list</title><meta name=”viewport” content=”width=device-width,initial-scale=1″ /><link rel=”stylesheet” href=”./css/main.css” /><script type=”module” src=”./js/main.js”></script></head><body>   <h1>IndexedDB state management to-do list</h1>   <todo-list></todo-list>   <todo-add></todo-add> </body></html>

<todo-list>- список задач, управляемый ./js/components/todo-list.js, который обновляет список при добавлении и удалении задач,

<todo-add>- форма для добавления элементов в управляемый список задач ./js/components/todo-list.js.

Cценарий ./js/main.js загружает оба компонентных модуля:

JavaScript // load components import ‘./components/todo-add.js’; import ‘./components/todo-list.js’;

123 // load componentsimport ‘./components/todo-add.js’;import ‘./components/todo-list.js’;

Эти сценарии определяют веб-компоненты, которые получают и устанавливают общее состояние todolist. Веб-компоненты выходят за рамки данной статьи, но основные моменты:

Вы можете определить собственный HTML-элемент (например, todo-list). Имя должно содержать дефис (-), чтобы избежать конфликтов с текущими или будущими элементами HTML.

Класс JavaScript который наследуется от HTMLElement, определяет функционал. Конструктор должен вызвать super().

Браузер вызывает метод connectedCallback(), когда он готов обновить DOM. Метод может добавлять контент, при необходимости используя инкапсулированную теневую DOM, недоступную для других скриптов.

customElements.define регистрирует класс с пользовательским элементом.

Компонент todo-list

Сценарий ./js/components/todo-list.js определяет класс TodoList для компонента todo-list. Он показывает список задач и обрабатывает удаление, когда пользователь нажимает кнопку «Готово». Класс устанавливает статические строки HTML и создает новый объект State. Он отслеживает переменную todolist и запускает метод объекта render() при изменении ее значения:

JavaScript import { State } from ‘../lib/state.js’; class TodoList extends HTMLElement { static style = ` <style> ol { padding: 0; margin: 1em 0; } li { list-style: numeric inside; padding: 0.5em; margin: 0; } li:hover, li:focus-within { background-color: #eee; } button { width: 4em; float: right; } </style> `; static template = `<li>$1 <button type=”button” value=”$2″>done</button></li>`; constructor() { super(); this.state = new State([‘todolist’], this.render.bind(this)); }

123456789101112131415161718 import { State } from ‘../lib/state.js’; class TodoList extends HTMLElement {   static style = `    <style>      ol { padding: 0; margin: 1em 0; }      li { list-style: numeric inside; padding: 0.5em; margin: 0; }      li:hover, li:focus-within { background-color: #eee; }      button { width: 4em; float: right; }    </style>    `;  static template = `<li>$1 <button type=”button” value=”$2″>done</button></li>`;   constructor() {    super();    this.state = new State([‘todolist’], this.render.bind(this));  }

Метод render() получает обновленные name и value. Он сохраняет список как свойство локального объекта, а затем добавляет HTML в Shadow DOM (созданный методом connectedCallback()):

JavaScript // show todo list render(name, value) { // update state this[name] = value; // create new list let list = ”; this.todolist.map((v, i) => { list += TodoList.template.replace(‘$1’, v).replace(‘$2’, i); }); this.shadow.innerHTML = `${ TodoList.style }<ol>${ list }</ol>`; }

123456789101112131415 // show todo list  render(name, value) {     // update state    this[name] = value;     // create new list    let list = ”;    this.todolist.map((v, i) => {      list += TodoList.template.replace(‘$1’, v).replace(‘$2’, i);    });     this.shadow.innerHTML = `${ TodoList.style }<ol>${ list }</ol>`;   }

Метод connectedCallback() сработает, когда DOM готова. Он:

создает новый Shadow DOM и передает последнее состояние todolist методу render(),

присоединяет обработчик события клика, который удаляет элемент из todolist. Метод render() будет выполняться автоматически, так как состояние изменилось.

JavaScript // initialise async connectedCallback() { this.shadow = this.attachShadow({ mode: ‘closed’ }); this.render(‘todolist’, await this.state.get(‘todolist’) || []); // remove item event this.shadow.addEventListener(‘click’, async e => { if (e.target.nodeName !== ‘BUTTON’) return; this.todolist.splice(e.target.value, 1); await this.state.set(‘todolist’, this.todolist); }); }

12345678910111213141516 // initialise  async connectedCallback() {     this.shadow = this.attachShadow({ mode: ‘closed’ });    this.render(‘todolist’, await this.state.get(‘todolist’) || []);     // remove item event    this.shadow.addEventListener(‘click’, async e => {       if (e.target.nodeName !== ‘BUTTON’) return;      this.todolist.splice(e.target.value, 1);      await this.state.set(‘todolist’, this.todolist);     });   }

Затем, класс TodoList регистрируется для компонента todo-list:

JavaScript // register component customElements.define( ‘todo-list’, TodoList );

12 // register componentcustomElements.define( ‘todo-list’, TodoList );

Компонент todo-add

Сценарий ./js/components/todo-add.js определяет класс TodoAdd для компонента. Он показывает форму, которая может добавлять новые задачи в todolist. Он устанавливает статическую строку HTML и создает новый State объект. Также он отслеживает состояние todolist и сохраняет его как свойство локального объекта:

JavaScript class TodoAdd extends HTMLElement { static template = ` <style> form { display: flex; justify-content: space-between; padding: 0.5em; } input { flex: 3 1 10em; font-size: 1em; padding: 6px; } button { width: 4em; } </style> <form method=”post”> <input type=”text” name=”add” placeholder=”add new item” required /> <button>add</button> </form> `; constructor() { super(); this.state = new State([‘todolist’], (name, value) => this[name] = value ); }

123456789101112131415161718 class TodoAdd extends HTMLElement {   static template = `    <style>      form { display: flex; justify-content: space-between; padding: 0.5em; }      input { flex: 3 1 10em; font-size: 1em; padding: 6px; }      button { width: 4em; }    </style>    <form method=”post”>    <input type=”text” name=”add” placeholder=”add new item” required />    <button>add</button>    </form>  `;   constructor() {    super();    this.state = new State([‘todolist’], (name, value) => this[name] = value );  }

Метод connectedCallback() cработает, когда будет готова DOM. Он:

извлекает последнее состояние todolist в локальное свойство, которое по умолчанию является пустым массивом

добавляет HTML-форму в Shadow DOM

присоединяет обработчик события, который добавляет в состояние новый элемент todolist (который, в свою очередь, обновляет todo-list компонент). Затем он очищает поле ввода, чтобы вы могли добавить еще одну задачу.

JavaScript // initialise async connectedCallback() { // get latest todo list this.todolist = await this.state.get(‘todolist’) || []; const shadow = this.attachShadow({ mode: ‘closed’ }); shadow.innerHTML = TodoAdd.template; const add = shadow.querySelector(‘input’); shadow.querySelector(‘form’).addEventListener(‘submit’, async e => { e.preventDefault(); // add item to list await this.state.set(‘todolist’, this.todolist.concat(add.value)); add.value = ”; add.focus(); }); }

123456789101112131415161718192021222324 // initialise  async connectedCallback() {     // get latest todo list    this.todolist = await this.state.get(‘todolist’) || [];     const shadow = this.attachShadow({ mode: ‘closed’ });    shadow.innerHTML = TodoAdd.template;     const add = shadow.querySelector(‘input’);     shadow.querySelector(‘form’).addEventListener(‘submit’, async e => {       e.preventDefault();       // add item to list      await this.state.set(‘todolist’, this.todolist.concat(add.value));       add.value = ”;      add.focus();     });   }

Заключение

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

Автор: Craig Buckler

Источник: webformyself.com

Comments (0)
Add Comment