Close

7 августа, 2021

Как использовать 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 — единственная доступная альтернатива с открытым исходным кодом.

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

Создание класса 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 }!`)});

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

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

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

В файле 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

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Яндекс.Метрика
Открыть чат
Если есть вопросы пишите нам
Здравствуйте.
Чем вам помочь?