TypeScript ve IndexedDB

TypeScript ve IndexedDB

Bu makale TypeScript ve IndexedDB hakkında açıklamalar içerir.

TypeScript ve IndexedDB'yi pratik örneklerle açıklayacağız.

YouTube Video

TypeScript ve IndexedDB

IndexedDB, yapılandırılmış verilerin tarayıcıda kalıcı olarak saklanmasına olanak tanıyan düşük seviyeli bir NoSQL depolamadır. TypeScript ile şemaları tür güvenli bir şekilde ifade edebilir, hataları azaltabilir ve bakımı kolaylaştırabilirsiniz.

Temel Terimler ve İş Akışı

IndexedDB, tarayıcı içinde küçük bir veritabanıdır. Verileri, isim ve versiyonlara sahip veritabanları, nesne depoları, işlemler, indeksler ve imleçler gibi mekanizmalarla yönetir. Veritabanlarının sürümleri vardır ve sürüm yükseltildiğinde şema güncellemesi (örneğin tablo oluşturma veya değiştirme) için onupgradeneeded çağrılır.

IndexedDB'yi Açmak (temel desen)

Öncelikle IndexedDB ile bir veritabanı açma ve gerekirse onupgradeneeded sırasında bir nesne deposu oluşturma örneği gösteriyoruz.

 1// Open an IndexedDB database and create an object store if needed.
 2// This code shows the classic callback-based IndexedDB API wrapped into a Promise.
 3function openDatabase(dbName: string, version: number): Promise<IDBDatabase> {
 4  return new Promise((resolve, reject) => {
 5    const request = indexedDB.open(dbName, version);
 6
 7    request.onerror = () => {
 8      reject(request.error);
 9    };
10
11    request.onupgradeneeded = (event) => {
12      const db = request.result;
13      if (!db.objectStoreNames.contains('todos')) {
14        // Create object store with keyPath 'id'
15        db.createObjectStore('todos', { keyPath: 'id' });
16      }
17    };
18
19    request.onsuccess = () => {
20      resolve(request.result);
21    };
22  });
23}
24
25// Usage example:
26openDatabase('my-db', 1)
27  .then(db => {
28    console.log('DB opened', db.name, db.version);
29    db.close();
30  })
31  .catch(err => console.error('Failed to open DB', err));
  • Bu kod, veritabanını açar ve başarılı olup olmadığını kaydeder.
  • Gerekirse, todos deposu onupgradeneeded içinde oluşturulur.

TypeScript'te Türlerin Tanımlanması (Modeller)

Bir sonraki adımda TypeScript kullanarak veri türlerini tanımlıyoruz. Bu, sonraki CRUD işlemlerinde tür güvenliğini sağlar.

1// Define the TypeScript interface for a Todo item.
2// This helps with type safety in the database operations.
3interface Todo {
4  id: string;          // primary key
5  title: string;
6  completed: boolean;
7  createdAt: number;
8}
  • Burada, Todo türünü tanımlıyoruz.

Basit CRUD fonksiyonu uygulama örnekleri

Bir sonraki adımda, nesne deposundan veri ekleme, getirme, güncelleme ve silme gibi temel CRUD işlemlerini gösteriyoruz. Her fonksiyon bir IDBDatabase alır ve bir Promise döndürür.

 1// CRUD utilities for the 'todos' object store.
 2// Each operation uses a transaction and returns a Promise for easier async/await usage.
 3
 4function addTodo(db: IDBDatabase, todo: Todo): Promise<void> {
 5  return new Promise((resolve, reject) => {
 6    const tx = db.transaction('todos', 'readwrite');
 7    const store = tx.objectStore('todos');
 8    const req = store.add(todo);
 9
10    req.onsuccess = () => {
11      console.log('Todo added', todo.id);
12    };
13    req.onerror = () => reject(req.error);
14
15    tx.oncomplete = () => resolve();
16    tx.onerror = () => reject(tx.error);
17  });
18}
  • Bu fonksiyon, IndexedDB'deki todos deposuna yeni bir Todo ekler. Asenkron işlem için bir Promise döndürür ve işlem tamamlandığında çözülür.
 1function getTodo(db: IDBDatabase, id: string): Promise<Todo | undefined> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readonly');
 4    const store = tx.objectStore('todos');
 5    const req = store.get(id);
 6
 7    req.onsuccess = () => resolve(req.result as Todo | undefined);
 8    req.onerror = () => reject(req.error);
 9  });
10}
  • Bu fonksiyon, belirtilen ID'ye sahip Todoyu alır ve bulunursa nesneyi döndürür. Eşleşen veri bulunmazsa undefined döndürür.
 1function updateTodo(db: IDBDatabase, todo: Todo): Promise<void> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readwrite');
 4    const store = tx.objectStore('todos');
 5    const req = store.put(todo);
 6
 7    req.onsuccess = () => {
 8      console.log('Todo updated', todo.id);
 9    };
10    req.onerror = () => reject(req.error);
11
12    tx.oncomplete = () => resolve();
13    tx.onerror = () => reject(tx.error);
14  });
15}
  • Bu fonksiyon, mevcut Todo verilerini günceller. Başarılı olduğunda, güncellenen Todonun kimliği kaydedilir.
 1function deleteTodo(db: IDBDatabase, id: string): Promise<void> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readwrite');
 4    const store = tx.objectStore('todos');
 5    const req = store.delete(id);
 6
 7    req.onsuccess = () => {
 8      console.log('Todo deleted', id);
 9    };
10    req.onerror = () => reject(req.error);
11
12    tx.oncomplete = () => resolve();
13    tx.onerror = () => reject(tx.error);
14  });
15}
  • Bu fonksiyon, belirtilen ID'ye sahip Todoyu siler. İşlem başarılıysa, silinen kimlik kaydedilir.

  • Bu fonksiyonlar, işlemin tamamlanmasına veya hataya göre bir Promise'i çözümler veya reddeder. console.log ile çıktı almak, yürütme sırasında neler olduğunu takip etmeyi kolaylaştırır.

İndeksler ve bileşik sorgular

IndexedDB'de indeksler kullanarak belirli alanlarda verimli arama yapabilirsiniz. Burada, createdAt alanı için bir indeks oluşturup, aralık sorgusu örneği sunuyoruz.

 1// When opening DB, create an index for createdAt.
 2// Then demonstrate a range query using the index.
 3
 4function openDatabaseWithIndex(dbName: string, version: number): Promise<IDBDatabase> {
 5  return new Promise((resolve, reject) => {
 6    const request = indexedDB.open(dbName, version);
 7
 8    request.onupgradeneeded = () => {
 9      const db = request.result;
10      if (!db.objectStoreNames.contains('todos')) {
11        const store = db.createObjectStore('todos', { keyPath: 'id' });
12        // Create an index on createdAt for sorting/filtering
13        store.createIndex('by-createdAt', 'createdAt', { unique: false });
14      } else {
15        const store = request.transaction!.objectStore('todos');
16        if (!store.indexNames.contains('by-createdAt')) {
17          store.createIndex('by-createdAt', 'createdAt', { unique: false });
18        }
19      }
20    };
21
22    request.onerror = () => reject(request.error);
23    request.onsuccess = () => resolve(request.result);
24  });
25}
  • Bu fonksiyon veritabanını açar ve createdAt alanında by-createdAt indeksini oluşturur veya kontrol eder. Bu, oluşturulma tarihine göre verimli bir şekilde arama ve sıralama yapılmasını sağlar.
 1async function getTodosCreatedAfter(db: IDBDatabase, timestamp: number): Promise<Todo[]> {
 2  return new Promise((resolve, reject) => {
 3    const tx = db.transaction('todos', 'readonly');
 4    const store = tx.objectStore('todos');
 5    const index = store.index('by-createdAt');
 6    const range = IDBKeyRange.lowerBound(timestamp, true); // exclusive
 7    const req = index.openCursor(range);
 8
 9    const results: Todo[] = [];
10    req.onsuccess = (event) => {
11      const cursor = (event.target as IDBRequest).result as IDBCursorWithValue | null;
12      if (cursor) {
13        results.push(cursor.value as Todo);
14        cursor.continue();
15      } else {
16        resolve(results);
17      }
18    };
19    req.onerror = () => reject(req.error);
20  });
21}
  • Bu fonksiyon, yalnızca belirtilen zaman damgasından sonra oluşturulan Todoları alır. İndeks kullanarak veriler oluşturulma tarihlerine göre verimli şekilde taranır.

  • Bu örnekte, veritabanı yükseltmesi sırasında by-createdAt adında bir indeks oluşturulur ve belirtilen zamandan sonra oluşturulan Todolar bir imleç ile listelenir.

Promise tabanlı hafif sarmalayıcı

Düşük seviyeli IndexedDB API'sini yazmak karmaşıktır ve benzer işlemlerin tekrarı, fazlalık ve hatalara yol açabilir. Bu nedenle, işlemleri soyutlayan genel bir TypeScript sarmalayıcı sınıfı hazırlayarak kodun sadeliğini ve sürdürülebilirliğini artırıyoruz. Aşağıda temel işlevselliğe odaklanan bir uygulama var.

1// A minimal TypeScript wrapper around IndexedDB to simplify common operations.
2// This class is generic over the store's value type and assumes 'keyPath' is 'id'.
3
4class IDBWrapper<T extends { id: string }> {
5  private dbPromise: Promise<IDBDatabase>;
6
7  constructor(private dbName: string, private version: number, private storeName: string) {
8    this.dbPromise = this.open();
9  }
  • Bu sınıf, IndexedDB işlemlerini sarmalar ve tür güvenli CRUD yöntemleri sağlar. id anahtarı olan bir nesne deposu varsayılır.
 1  private open(): Promise<IDBDatabase> {
 2    return new Promise((resolve, reject) => {
 3      const req = indexedDB.open(this.dbName, this.version);
 4      req.onerror = () => reject(req.error);
 5      req.onupgradeneeded = () => {
 6        const db = req.result;
 7        if (!db.objectStoreNames.contains(this.storeName)) {
 8          db.createObjectStore(this.storeName, { keyPath: 'id' });
 9        }
10      };
11      req.onsuccess = () => resolve(req.result);
12    });
13  }
  • Veritabanını açar ve gerekirse yeni bir nesne deposu oluşturur. Depo başlatma işlemi upgrade olayı ile yapılır.
 1  async add(item: T): Promise<void> {
 2    const db = await this.dbPromise;
 3    await new Promise<void>((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readwrite');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.add(item);
 7      req.onsuccess = () => {
 8        console.log('added', item.id);
 9      };
10      req.onerror = () => reject(req.error);
11      tx.oncomplete = () => resolve();
12      tx.onerror = () => reject(tx.error);
13    });
14  }
  • IndexedDB deposuna veri ekler. Ekledikten sonra kimlik konsola kaydedilir.
 1  async get(id: string): Promise<T | undefined> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readonly');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.get(id);
 7      req.onsuccess = () => resolve(req.result as T | undefined);
 8      req.onerror = () => reject(req.error);
 9    });
10  }
  • Belirtilen ID'ye karşılık gelen veriyi getirir. Eğer veri yoksa undefined döndürülür.
 1  async put(item: T): Promise<void> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readwrite');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.put(item);
 7      req.onsuccess = () => {
 8        console.log('put', item.id);
 9      };
10      req.onerror = () => reject(req.error);
11      tx.oncomplete = () => resolve();
12      tx.onerror = () => reject(tx.error);
13    });
14  }
  • Mevcut veriyi günceller veya yeni veri ekler. İşlemden sonra güncellenen kimlik kaydedilir.
 1  async delete(id: string): Promise<void> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readwrite');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.delete(id);
 7      req.onsuccess = () => {
 8        console.log('deleted', id);
 9      };
10      req.onerror = () => reject(req.error);
11      tx.oncomplete = () => resolve();
12      tx.onerror = () => reject(tx.error);
13    });
14  }
  • Belirtilen ID'ye sahip veriyi siler. Başarılıysa silinen kimlik kaydedilir.
 1  async getAll(): Promise<T[]> {
 2    const db = await this.dbPromise;
 3    return new Promise((resolve, reject) => {
 4      const tx = db.transaction(this.storeName, 'readonly');
 5      const store = tx.objectStore(this.storeName);
 6      const req = store.getAll();
 7      req.onsuccess = () => resolve(req.result as T[]);
 8      req.onerror = () => reject(req.error);
 9    });
10  }
11}
  • Depodaki tüm verileri getirir. Dönüş değeri T türünde bir dizi olur.
 1// Example usage with Todo type:
 2interface Todo {
 3  id: string;
 4  title: string;
 5  completed: boolean;
 6  createdAt: number;
 7}
 8
 9const todoStore = new IDBWrapper<Todo>('my-db', 1, 'todos');
10
11(async () => {
12  const newTodo: Todo = { id: '1', title: 'Learn IndexedDB', completed: false, createdAt: Date.now() };
13  await todoStore.add(newTodo);
14  const fetched = await todoStore.get('1');
15  console.log('fetched', fetched);
16  newTodo.completed = true;
17  await todoStore.put(newTodo);
18  const all = await todoStore.getAll();
19  console.log('all todos', all);
20  await todoStore.delete('1');
21})();
  • Bu kod, IDBWrapper sınıfının gerçek bir kullanım örneğidir. Bu, Todo verisini ekleme, alma, güncelleme, listeleme ve silme akışını gösterir.

  • Bu sarmalayıcı, temel CRUD işlemlerinin kolayca yapılmasını sağlar. Gerçek bir ortamda, ayrıca hata yönetimi ve şema yönetimi (indeksler) de ele alınmalıdır.

Şema geçişi (sürüm yükseltme)

Veritabanı şemasını değiştirmek için indexedDB.open fonksiyonunun ikinci argümanını (sürüm) artırın ve bunu onupgradeneeded fonksiyonunda güncelleyin. Mevcut işlemlerin tamamlandığından ve yıkıcı değişikliklerin önlendiğinden emin olacak şekilde tasarlamanız gerekir.

 1// Example of handling upgrade to version 2: add an index and perhaps migrate data.
 2// onupgradeneeded receives an event where oldVersion and newVersion are accessible.
 3
 4function upgradeToV2(dbName: string): Promise<IDBDatabase> {
 5  return new Promise((resolve, reject) => {
 6    const req = indexedDB.open(dbName, 2);
 7    req.onupgradeneeded = (ev) => {
 8      const db = req.result;
 9      const oldVersion = (ev as IDBVersionChangeEvent).oldVersion;
10      console.log('Upgrading DB from', oldVersion, 'to', db.version);
11      let store: IDBObjectStore;
12      if (!db.objectStoreNames.contains('todos')) {
13        store = db.createObjectStore('todos', { keyPath: 'id' });
14      } else {
15        store = req.transaction!.objectStore('todos');
16      }
17      // Add index if not present
18      if (!store.indexNames.contains('by-completed')) {
19        store.createIndex('by-completed', 'completed', { unique: false });
20      }
21
22      // Optional: data migration logic if necessary can go here,
23      // but heavy migrations often should be done lazily on read.
24    };
25    req.onsuccess = () => resolve(req.result);
26    req.onerror = () => reject(req.error);
27  });
28}
  • onupgradeneeded içinde ağır işlemler UI'yı engelleyebilir, bu yüzden minimumda tutun ve mümkünse gecikmeli geçişi (uygulama başlangıcında işlem yapmayı) düşünün.

İşlemler hakkında uyarılar (yaşam döngüsü ve hatalar)

İşlemler, oluşturan kodun çalışması bitmeden otomatik olarak onaylanır. Bir işlem içinde await kullanıldığında işlem beklenmeyen bir şekilde onaylanabilir; aynı işlemde birden fazla asenkron işlem yapılacaksa dikkatli olunmalıdır.

 1// Bad pattern: awaiting outside transaction callbacks can cause tx to auto-commit.
 2// Good pattern is to chain requests and resolve on tx.oncomplete as shown earlier.
 3
 4// Example: Do multiple operations inside single tx, avoid awaiting inside.
 5function multiOperation(db: IDBDatabase, items: Todo[]): Promise<void> {
 6  return new Promise((resolve, reject) => {
 7    const tx = db.transaction('todos', 'readwrite');
 8    const store = tx.objectStore('todos');
 9
10    for (const item of items) {
11      const req = store.put(item);
12      req.onerror = () => console.error('put error', req.error);
13      // Do NOT await here; just schedule requests synchronously.
14    }
15
16    tx.oncomplete = () => {
17      console.log('All operations in transaction completed');
18      resolve();
19    };
20    tx.onerror = () => reject(tx.error);
21  });
22}
  • İşlem yaşam süresine dikkat edin; gerekiyorsa ayrı işlemler kullanın ya da tüm işlemleri aynı anda (senkron şekilde) işlem içinde gerçekleştirin.

İmleç uygulamaları ve sayfalama

Bir imleç kullanarak büyük ölçekli verileri sıralı olarak işleyebilir veya offset kullanmadan basit bir sayfalandırma gerçekleştirebilirsiniz.

 1// Example: fetch first N items using a cursor (ascending by key).
 2function fetchFirstN(db: IDBDatabase, n: number): Promise<Todo[]> {
 3  return new Promise((resolve, reject) => {
 4    const tx = db.transaction('todos', 'readonly');
 5    const store = tx.objectStore('todos');
 6    const req = store.openCursor();
 7    const out: Todo[] = [];
 8    req.onsuccess = (ev) => {
 9      const cursor = (ev.target as IDBRequest).result as IDBCursorWithValue | null;
10      if (cursor && out.length < n) {
11        out.push(cursor.value as Todo);
12        cursor.continue();
13      } else {
14        resolve(out);
15      }
16    };
17    req.onerror = () => reject(req.error);
18  });
19}
  • İmleç ile verileri teker teker getirerek bellek kullanımı azaltılabilir. Sayfalama uygularken, genellikle son okunan anahtarı saklamak yaygındır.

Hata yönetimi ve yedekleme

1// Feature detection
2if (!('indexedDB' in window)) {
3  console.warn('IndexedDB is not supported. Falling back to localStorage.');
4  // implement fallback logic...
5}
  • Tarayıcılar arasındaki uygulama farklılıkları veya kullanıcıların gizlilik ayarları (örneğin gizli gezinme modu) nedeniyle IndexedDB kullanılamayabilir. Bu nedenle, indexedDB'nin mevcut olup olmadığını kontrol edin ve mevcut değilse, localStorage gibi bir geri dönüş yöntemi sağlayın.

Performans ve en iyi yöntemler

IndexedDB hızlı ve güçlüdür, ancak performansı tasarımına ve verilerin nasıl işlendiğine göre büyük ölçüde değişebilir. Kullanım durumuna bağlı olarak optimizasyon aşağıdaki şekillerde yapılabilir:.

  • Nesne deposunu gerçek kullanıma göre tasarlayın. Örneğin, çok okuma varsa indeksler ekleyin; çok yazma varsa anahtar tasarımını basit tutun.
  • Resim ve ses gibi büyük ikili veriler Blob olarak saklanmalı veya gerekirse File API ya da servis çalışanları ile yönetilmelidir. Gerekirse sıkıştırma da düşünülebilir.
  • İşlemleri olabildiğince kısa tutun ve kilit süresini en aza indirmek için ağır işlemleri işlemin dışında gerçekleştirin.
  • İndeksler aramaları hızlandırabilir ancak ekleme ve güncellemeleri yavaşlatır, bu nedenle gerçekten gerekli olanları oluşturun.
  • Birçok küçük veriyle çalışırken, hepsini bir kerede getAll() ile almak belleği tüketebilir. İşlemi imleçlerle bölerek bellek kullanımını azaltabilirsiniz.

Güvenlik ve gizlilik

IndexedDB verileri, aynı köken ilkesi gereği, alan adı ve protokole göre yalıtılmıştır. Kullanıcıların tarayıcı verilerini silmesi veya gizli mod kullanması durumunda verilerin kaybolabileceği varsayımıyla tasarlayın.

Özet ve önerilen tasarım desenleri

IndexedDB'yi TypeScript ile etkili bir şekilde kullanmak için, türleri ve asenkron süreçleri hazırlamak, sürüm yönetimi ve işlem tasarımının farkında olmak ve bakım kolaylığını artırmak için ortak işlemleri sarmak önemlidir.

  • TypeScript'te tür tanımlamak ve IndexedDB işlemlerini Promise/async/await ile sarmak güvenliği ve kod okunabilirliğini artırır.
  • Şema değişiklikleri için onupgradeneeded ile sürüm yönetimi kullanılmalı ve mümkünse ağır işlemler ertelenmelidir.
  • İşlemleri kısa tasarlayın ve aynı işlem içindeki ağır asenkron işlemlerden kaçının.
  • Sarıcı sınıflar oluşturarak hata yönetimi, kayıt tutma ve tür tanımlamaları gibi tekrarlayan ortak işlemleri azaltabilirsiniz.

Yukarıdaki makaleyi, YouTube kanalımızda Visual Studio Code'u kullanarak takip edebilirsiniz. Lütfen YouTube kanalını da kontrol edin.

YouTube Video