مقدمه

با وجودی که Web Storage برای ذخیره‌سازی مقادیر ساده و کم‌حجم عالی است، اما زمانی که با حجم زیادی از داده‌های ساختاریافته سروکار داریم یا به قابلیت‌های جستجوی پیشرفته نیاز داریم، به یک راه‌حل قدرتمندتر نیازمندیم. IndexedDB یک پایگاه داده کامل و شیءگرا در سمت کلاینت است که توسط مرورگرها ارائه می‌شود. این API به ما اجازه می‌دهد تا حجم بسیار زیادی از داده (حتی فایل‌ها و Blobها) را ذخیره کرده، برای جستجوی سریع روی آنها ایندکس بسازیم، و تمام عملیات را در قالب تراکنش‌ها (transactions) برای تضمین یکپارچگی داده‌ها انجام دهیم.

مفاهیم اصلی IndexedDB

برای کار با IndexedDB، ابتدا باید با معماری و مفاهیم اصلی آن آشنا شویم:

  • پایگاه‌داده (Database): بالاترین سطح که شامل تمام داده‌های اپلیکیشن ماست. هر پایگاه‌داده با یک نام و یک شماره نسخه شناسایی می‌شود.
  • آبجکت استور (Object Store): می‌توان آن را معادل یک «جدول» در پایگاه‌داده‌های رابطه‌ای در نظر گرفت. هر آبجکت استور مجموعه‌ای از اشیاء جاوااسکریپت را نگهداری می‌کند.
  • ایندکس (Index): یک نوع خاص از آبجکت استور است که برای سازماندهی داده‌ها در یک آبجکت استور دیگر بر اساس یک پراپرتی مشخص استفاده می‌شود. ایندکس‌ها به ما اجازه می‌دهند تا داده‌ها را به سرعت و بر اساس آن پراپرتی جستجو و بازیابی کنیم.
  • تراکنش (Transaction): تمام عملیات خواندن و نوشتن در IndexedDB باید در قالب یک تراکنش انجام شود. تراکنش یک پوشش امنیتی است که تضمین می‌کند مجموعه‌ای از عملیات به صورت یک واحد کامل انجام شوند (یا همگی موفق شوند یا هیچ‌کدام اعمال نشوند).
  • کرسر (Cursor): مکانیزمی برای پیمایش روی چندین رکورد در یک آبجکت استور یا ایندکس.

گردش کار با IndexedDB

API خام IndexedDB کمی پرجزئیات و مبتنی بر رویداد است. بیایید با یک مثال کامل، مراحل اصلی باز کردن پایگاه‌داده، ایجاد schema، و افزودن و خواندن داده را بررسی کنیم.

۱. باز کردن اتصال به پایگاه‌داده

اولین قدم، باز کردن یک اتصال به پایگاه‌داده با indexedDB.open(dbName, version) است. این عملیات ناهمزمان است و یک شیء request برمی‌گرداند که باید به رویدادهای success، error و upgradeneeded آن گوش دهیم.

Copy Icon JAVASCRIPT
let db;
// 1. Open a connection to the database. Provide a name and a version number.
const request = indexedDB.open('MyNotesDatabase', 1);

// 2. This event only fires when the database is first created, or the version number changes.
// This is the ONLY place to define the database schema (object stores and indexes).
request.onupgradeneeded = (event) => {
    db = event.target.result;
    // Create an object store called 'notes'
    const objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
    // Create an index to search notes by title.
    objectStore.createIndex('title', 'title', { unique: false });
};

// 3. Fires when the connection is successful.
request.onsuccess = (event) => {
    db = event.target.result;
    console.log('Database opened successfully.');
};

// 4. Fires when there is an error.
request.onerror = (event) => {
    console.error('Database error:', event.target.error);
};

مهم‌ترین بخش این کد، رویداد onupgradeneeded است. این رویداد تنها جایی است که می‌توانیم ساختار پایگاه‌داده خود را تغییر دهیم (مثلاً آبجکت استورها و ایندکس‌های جدیدی بسازیم). این رویداد زمانی اجرا می‌شود که پایگاه‌داده برای اولین بار ایجاد می‌شود یا زمانی که شماره نسخه‌ای که در open پاس می‌دهیم، از نسخه فعلی پایگاه‌داده بیشتر باشد.

۲. افزودن و خواندن داده (تراکنش‌ها)

پس از برقراری اتصال، تمام عملیات داده باید داخل یک تراکنش انجام شود. برای شروع یک تراکنش، از متد db.transaction() استفاده می‌کنیم که نام آبجکت استور(های) مورد نظر و نوع تراکنش ('readonly' یا 'readwrite') را به عنوان آرگومان می‌گیرد.

Copy Icon JAVASCRIPT
function addNote(noteData) {
    // Start a read-write transaction
    const transaction = db.transaction(['notes'], 'readwrite');
    const objectStore = transaction.objectStore('notes');
    const request = objectStore.add(noteData);
    
    request.onsuccess = () => console.log('Note added successfully!');
    request.onerror = (e) => console.error('Error adding note:', e.target.error);
}

function getNote(noteId) {
    const transaction = db.transaction(['notes'], 'readonly');
    const objectStore = transaction.objectStore('notes');
    const request = objectStore.get(noteId);

    request.onsuccess = () => console.log('Found note:', request.result);
}

// Example usage (must be called after the 'onsuccess' of the DB opening)
// addNote({ title: 'My First Note', text: 'Hello IndexedDB!' });
// getNote(1);

این توابع کمکی، گردش کار یک تراکنش را نشان می‌دهند: ۱. شروع تراکنش، ۲. گرفتن دسترسی به آبجکت استور، ۳. اجرای عملیات (مانند add یا get). هر یک از این عملیات نیز خود یک شیء request برمی‌گرداند که باید به رویدادهای success و error آن گوش دهیم.

چالش ناهمزمانی و کتابخانه‌های کمکی

همانطور که مشاهده کردید، API خام IndexedDB بسیار پرجزئیات و مبتنی بر شنوندگان رویداد است که می‌تواند منجر به کدهای تودرتو و پیچیده (معروف به "Callback Hell") شود. به همین دلیل، بسیاری از توسعه‌دهندگان ترجیح می‌دهند از کتابخانه‌های کمکی (wrapper libraries) کوچک استفاده کنند.

این کتابخانه‌ها، API مبتنی بر رویداد IndexedDB را به یک API مبتنی بر Promise تبدیل می‌کنند که کار با آن، به خصوص با استفاده از async/await، بسیار ساده‌تر و خواناتر می‌شود. یکی از معروف‌ترین این کتابخانه‌ها، idb نوشته Jake Archibald است.

در این درس با IndexedDB به عنوان قدرتمندترین راه‌حل ذخیره‌سازی سمت کلاینت آشنا شدیم. دیدیم که با وجود API پیچیده و ناهمزمان، این تکنولوژی امکانات یک پایگاه داده واقعی را برای ساخت وب اپلیکیشن‌های آفلاین و غنی از داده فراهم می‌کند. با این درس، فصل «ذخیره‌سازی داده‌ها در سمت کاربر» به پایان می‌رسد. ما از کوکی‌های ساده شروع کردیم، با Web Storage ادامه دادیم و با پایگاه‌داده قدرتمند IndexedDB کار را به اتمام رساندیم. در فصل بعدی، به سراغ «کار با ماژول‌ها» خواهیم رفت و یاد می‌گیریم که چگونه کدهای جاوااسکریپت خود را برای پروژه‌های بزرگ، به صورت ماژولار و قابل مدیریت سازماندهی کنیم.