Использование IndexedDB с Vue.js

Spread the love

После статьи описывающие базовые принципы использования API — IndexedDB самое время написать статью о использования IndexedDB в приложение на Vue.js. Главным образом потому, что, как ни крути, в последнее время не так уж много статей об этом.

Спонсор поста Ремонт ноутбуков Алматы
Сервис центр Disketa disketa-service.kz
Оперативный ремонт и обслуживание ноутбуков:
— Цены от 2200 тг.
— Бесплатная консультация и диагностика.
— Опытные мастера со стажем более 7 лет.
— Оплата только после ремонта.
— Предоставляем гарантию от 1 года!

И так начнем с создания нового Vue приложения через CLI.

vue create indexdb

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

Я выбрал следующиее опции.

Vue CLI v4.1.1
┌─────────────────────────────────────────┐
│                                         │
│   New version available 4.1.1 → 4.1.2   │
│    Run npm i -g @vue/cli to update!     │
│                                         │
└─────────────────────────────────────────┘

? Please pick a preset: Manually select features
? Check the features needed for your project: 
 ◉ Babel
 ◯ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◯ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, PWA, Vuex, Linter
? Pick a linter / formatter config: 
  ESLint with error prevention only 
  ESLint + Airbnb config 
❯ ESLint + Standard config 
  ESLint + Prettier
$ cd indexdb

После создания нового приложения удалим все лишние из него. Удалим все лишнее из App.vue

<template>
  <div id="app">
  </div>
</template>

<script>

export default {
  name: 'app'
}
</script>

И удалим компонент HelloWorld.vue (если он был создан CLI)

В нашем тестовом приложение мы будем использовать простейшую базу книг в которой будем хранить название книги и ее цену. У нас будет одна кнопка для создания новой записи и возле каждой записи кнопка для ее удаления. Книги будут отображаться в виде списка следующим образом.

Далее создадим новый компонент Books для отображения книг. Для этого создадим новый файл components/books.vue со следующим содержимым:

 <template>
  <div class="books">
  </div>
</template>

<script>
export default {
  name: 'Books',
  data () {
    return {
    }
  },
  computed: {
  },
  created () {
  },
  methods: {
    
  }

}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

Подключим этот новый компонент в файле App.vue. Для этого внесем в App.vue следующие изменения:

<template>
  <div id="app">
    <Books />
  </div>
</template>

<script>
import Books from './components/Books.vue'

export default {
  name: 'app',
  components: {
    Books
  }
}
</script>

Далее поправим наше хранилище. В каталоге проекта должен быть создан каталог store, в нем должен находится файл index.js со следующим содержимым:

import Vue from 'vue'
import Vuex from 'vuex'
import state from '@/store/state'
import mutations from '@/store/mutations'
import actions from '@/store/actions'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions
})

Так же в этом каталоге нужно создать 3 файла state.js, actions.js, mutations.js Структура каталога store должна быть следующей:

...
store/
     index.js
     state.js
     actions.js
     mutations.js
....

В файле state.js будет храниться наши переменные состояния хранилища. Мы будет там хранить только список книг, поэтому содержимое state.js будет следующим:

export default {
  books: []
}

В файле mutations.js будут храниться базовые операции над этим списком книг, поэтому его содержимое будет следующее:

export default {
  SET_BOOKS (state, books) {
    state.books = books
  }
}

Файл actions.js, пока пусть будет пустым, мы вернемся к нему чуть позже.

Далее создадим самое важное для этого тестового приложения. Создадим файл с API для работы с IndexedDB. Для создадим в папке src новым каталог api и в нем создадим новый файл idb.js, со следующим содержимым:

const DB_NAME = 'bookdb'
const STORAGE_NAME = 'books'
const DB_VERSION = 1
let DB

export default {
  async getDb () {
    return new Promise((resolve, reject) => {
      if (DB) {
        return resolve(DB)
      }
      const request = window.indexedDB.open(DB_NAME, DB_VERSION)

      request.onerror = e => {
        console.log('Error opening db', e)
        // eslint-disable-next-line prefer-promise-reject-errors
        reject('Error')
      }

      request.onsuccess = e => {
        DB = e.target.result
        resolve(DB)
      }

      request.onupgradeneeded = e => {
        let db = e.target.result
        db.createObjectStore(STORAGE_NAME, { autoIncrement: true, keyPath: 'id' })
      }
    })
  },
  async deleteBook (book) {
    const db = await this.getDb()

    return new Promise(resolve => {
      const trans = db.transaction([STORAGE_NAME], 'readwrite')
      trans.oncomplete = () => {
        resolve()
      }

      const store = trans.objectStore(STORAGE_NAME)
      store.delete(book.id)
    })
  },
  async getBooks () {
    let db = await this.getDb()

    return new Promise(resolve => {
      let trans = db.transaction([STORAGE_NAME], 'readonly')
      trans.oncomplete = () => {
        resolve(books)
      }

      const store = trans.objectStore(STORAGE_NAME)
      const books = []

      store.openCursor().onsuccess = e => {
        const cursor = e.target.result
        if (cursor) {
          books.push(cursor.value)
          cursor.continue()
        }
      }
    })
  },
  async saveBook (book) {
    let db = await this.getDb()

    return new Promise(resolve => {
      let trans = db.transaction([STORAGE_NAME], 'readwrite')
      trans.oncomplete = () => {
        resolve()
      }

      let store = trans.objectStore(STORAGE_NAME)
      store.put(book)
    })
  }
}

В этом файле у нас реализованы один внутренний метод getDb который используется только внутри API и три внешних метода: getBooks для получения списка книг из БД, saveBook для создания новой книги и deleteBook для удаления выбранной книги. Более подробно как работают эти методы вы можете почитать в моей предыдущей статье.

Далее задействуем эти методы в файле хранилища store/actions.js:

import api from '@/api/idb'

export default {
  addBookToDb ({ commit }, { book }) {
    return api.saveBook(book)
  },

  async getBooks ({ commit }) {
    let books = await api.getBooks()
    commit('SET_BOOKS', books)
  },

  deleteBookFromDb ({ commit }, { book }) {
    return api.deleteBook(book)
  }
}

Тут мы описали три действия (action): получения списка книг, добавления новой книги и удаление выбранной книги.

Теперь все готово что бы описать весь нужный функционал в компоненте Books.vue

<template>
  <div class="books">
    <button @click="addBook" :disabled="addDisabled">Add Book</button>

    <ul>
      <li v-for="book in books" :key="book.id">
        {{book.name}} with price: {{book.price}}. <button @click="deleteBook(book)">Delete</button>
      </li>
    </ul>
  </div>
</template>

<script>
import { mapActions } from 'vuex'

export default {
  name: 'Books',
  data () {
    return {
      addDisabled: false
    }
  },
  computed: {
    books () {
      return this.$store.state.books
    }
  },
  created () {
    this.getBooks()
  },
  methods: {
    ...mapActions(['getBooks', 'addBookToDb']),
    async addBook () {
      this.addDisabled = true
      // random book for now
      const book = {
        name: 'Book_' + Math.floor(Math.random() * 100),
        price: Math.floor(Math.random() * 10) + 1
      }
      console.log('about to add ' + JSON.stringify(book))
      await this.addBookToDb({ book })
      this.getBooks()
      this.addDisabled = false
    },

    async deleteBook (book) {
      await this.$store.dispatch('deleteBookFromDb', { book })
      this.getBooks()
    }

  }

}
</script>

Тут все очень просто. В шаблоне мы добавили кнопку создания новой книги и вывели список книг. В каждой строке списка вы выводим название книги ее цену и кнопку удаления данной книги. Список книг books мы формируем прямо из хранилища через computed свойство. В момент создания компонента в методе created мы погружаем список книг в хранилище через action this.getBooks. И у нас еще два метода addBook для создания новой книги и deleteBook для удаления выбранной книги.

На этом все наше тестовое приложение готов. Теперь достаточно запустить команду CLI

npm run serve

И все должно работать.

Заключение

В целом мы рассмотрели очень простой и понятный пример использования IndexedDB во Vue.js. Надеюсь, если у вас возникла необходимость использования встроенной в браузере БД этот пример облегчит вам понимание того как можно использовать IndexedDB в Vue.js. Если вы хотите узнать больше, обязательно посмотрите MDN документацию по этому вопросу. Как всегда, оставляйте комментарий ниже, если у вас есть вопрос или предложение!

Была ли вам полезна эта статья?
[12 / 4.7]

Spread the love
Подписаться
Уведомление о
guest
3 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Ярослав
Ярослав
4 лет назад

Мы менеджим в проекте данные между вкладками браузера с помощью IndexedDB. Данные развесистые и большие localstorage не хватает.
Используем в проекте Dexie (https://dexie.org). Пакет сам по себе мелкий, но удобно.

edteam
Администратор
4 лет назад
Reply to  Ярослав

Спасибо за наводку

Анонимно
Анонимно
4 лет назад

А как насчет обновления данных о книге? Dexie вроде предоставляет эту возможность