ذخیره سازی سمت کلاینت در جاوا اسکریپت — راهنمای جاوا اسکریپت

۱۱۴۳ بازدید
آخرین به‌روزرسانی: ۰۸ شهریور ۱۴۰۲
زمان مطالعه: ۳۲ دقیقه
ذخیره سازی سمت کلاینت در جاوا اسکریپت — راهنمای جاوا اسکریپت

مرورگرهای وب مدرن از چند روش برای ذخیره‌سازی داده‌ها روی رایانه کاربر (با کسب اجازه کاربر) استفاده می‌کنند و در مواقع ضروری آن‌ها را بازیابی می‌کنند. بدین ترتیب می‌توانند داده‌ها را برای مدتی طولانی ذخیره کنند، سایت‌ها یا اسنادی را برای کاربرد آفلاین ذخیره کنند، تنظیمات ویژه کاربر را برای سایت حفظ نمایند و مواردی از این دست را اجرا کنند. در این مقاله به توضیح مبانی مقدماتی طرز کار ذخیره سازی سمت کلاینت می‌پردازیم.

پیش‌نیاز مطالعه این مقاله آشنایی با مبانی جاوا اسکریپت و API-های سمت کلاینت است. هدف از این مقاله نیز آشنایی با شیوه استفاده از API-های ذخیره‌سازی سمت کلاینت برای ذخیره کردن داده‌های اپلیکیشن است. برای مطالعه بخش قبلی این سری مقالات روی لینک زیر کلیک کنید:

ذخیره سازی سمت کلاینت

همان طور که می‌دانید ما به طور کلی دو نوع وب سایت به صورت استاتیک و دینامیک داریم اغلب وب‌سایت‌های مدرن دینامیک هستند و داده‌ها را روی سرور در نوعی پایگاه داده ذخیره می‌کنند که ذخیره‌سازی سمت سرور نام دارد. سپس کد سمت سرور برای بازیابی داده‌های مورد نیاز اجرا می‌شود، آن‌ها را در قالب‌های صفحه وب استاتیک وارد می‌کند و نتیجه را به صورت HTML در اختیار کلاینت قرار می‌دهد تا در مرورگر کاربر نمایش پیدا کنند.

ذخیره‌سازی سمت کلاینت نیز بر اساس مفاهیم مشابهی کار می‌کند، اما کاربرد متفاوتی دارد. این نوع ذخیره‌سازی شامل API-های جاوا اسکریپت است که امکان ذخیره‌سازی داده‌ها روی کلاینت (یعنی دستگاه کاربر) را می‌دهد و سپس در مواقع نیاز آن‌ها را بازیابی می‌کند. این روش کاربردهای زیادی دارد که به شرح زیر هستند:

  • شخصی‌سازی ترجیح‌های کاربر (مانند نمایش انتخاب کاربر از ویجت‌های سفارشی، طرح‌های رنگی و یا اندازه فونت).
  • حفظ فعالیت‌های قبلی در سایت (مانند ذخیره‌سازی محتوای یک سبد خرید در نشست قبلی و به یادآوردن ورود قبلی کاربر در وب سایت).
  • ذخیره‌سازی داده‌ها و فایل‌ها به صوت محلی تا وب سایت سریع‌تر بارگذاری شود یا بدون نیاز به اتصال اینترنتی قابل استفاده باشد.
  • ذخیره‌سازی اسناد تولید شده از سوی اپلیکیشن به صورت محلی برای استفاده آفلاین.

در اغلب موارد ذخیره‌سازی‌های سمت سرور و سمت کلاینت همراه با هم استفاده می‌شوند. برای نمونه می‌توانید یک دسته از فایل‌های موسیقی را که از سوی یک بازی وب یا یک اپلیکیشن پخش موسیقی استفاده می‌شود از پایگاه داده ذخیره کنید و آن‌ها را در پایگاه داده سمت کلاینت ذخیره کنید و در موارد نیاز آن‌ها را پخش کنید. کاربر کافی است فایل‌های موسیقی را یک بار دانلود کند و در بازدیدهای بعدی می‌توانید فایل‌ها را به این سرور از پایگاه داده محلی خود بازیابی کند.

نکته: در مورد حجم داده‌هایی که می‌توان با استفاده از API-های سمت کلاینت روی سیستم کاربر ذخیره کرد، محدودیت‌هایی وجود دارد که هم شامل محدودیت API منفرد و هم تجمیعی می‌شود. محدودیت دقیق بسته به مرورگرهای مختلف متفاوت است و به طور عمده بر مبنای تنظیمات کاربر است.

روش قدیمی استفاده از کوکی

مفهوم ذخیره‌سازی سمت کلاینت مدت‌های مدیدی است که مطرح بوده است. از آغازین روزهای تولد وب، سایت‌ها از کوکی‌ها برای ذخیره‌سازی اطلاعات جهت شخصی‌سازی تجربیات کاربر روی وب‌سایت بهره می‌گیرند. کوکی ابتدایی‌ترین شکل ذخیره‌سازی سمت کلاینت است که در وب کاربرد زیادی دارد.

کوکی‌ها به دلیل قدمتشان چندین مشکل دارند که برخی از آن‌ها فنی و برخی به تجربیات کاربر مرتبط هستند. این مشکلات آن قدر شدید هستند که افراد در زمان نخستین بازدید از یک وب سایت پیام‌های هشداری می‌بینند که از آن‌ها می‌خواهد اجازه بدهند کوکی‌ها را روی سیستمشان ذخیره کنند. دلیل این مسئله قوانین اتحادیه اروپا است:

ذخیره سازی سمت کلاینت

به همین دلایل قصد نداریم روش استفاده از کوکی‌ها را در این مقاله به شما آموزش دهیم، چون کوکی‌ها منسوخ شده‌اند و مشکلات امنیتی زیادی ایجاد می‌کنند و برای ذخیره‌سازی داده‌های پیچیده ناتوان هستند و روش‌های مدرن بسیار بهتری برای ذخیره‌سازی طیف وسیع‌تری از داده‌ها روی رایانه کاربر وجود دارند.

تنها مزیت کوکی این است که از سوی بسیاری از مرورگرهای قدیمی پشتیبانی می‌شود. بنابراین اگر پروژه شما الزام می‌کند که از مرورگرهای منسوخ شده (مانند IE8 یا قدیمی‌تر) پشتیبانی کنید، احتمالاً کوکی‌ها همچنان مفید خواهند بود، اما در اغلب پروژه‌ها دیگر هیچ نیازی به استفاده از کوکی ندارید.

نکته: شاید بپرسید چرا همچنان سایت‌های جدید از کوکی‌ها استفاده می‌کنند؟ دلیل عمده این مسئله عادت‌های توسعه‌دهندگان است. از سوی دیگر افراد از کتابخانه‌های قدیمی استفاده می‌کنند که از کوکی‌ها بهره می‌گیرند و همچنین وجود وب‌سایت‌های زیادی که ارجاع‌ها و آموزش‌های قدیمی برای آموزش ذخیره‌سازی داده‌ها در وب ارائه می‌کند نیز مزید بر علت است.

روش‌های جدید Web Storage و IndexedDB

مرورگرهای مدرن، API-های آسان‌تر و مؤثرتری برای ذخیره‌سازی داده‌های سمت کلاینت نسبت به روش استفاده از کوکی دارند که شامل Web Storage و IndexedDB می‌شود.

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

در ادامه در مورد این API-ها بیشتر صحبت می‌کنیم.

روشی برای آینده به نام Cache API

برخی مرورگرهای مدرن از یک API به نام Cache پشتیبانی می‌کنند. این API برای ذخیره‌سازی پاسخ‌های HTTP به درخواست‌های خاص طراحی شده است و در زمان انجام کارهایی مانند ذخیره‌سازی فایل‌های آفلاین وب‌سایت بسیار مفید است. به این ترتیب سایت می‌تواند در بازدیدهای متعاقب کاربر بدون اتصال اینترنتی نیز بارگذاری شود. Cache به طور معمول همراه با API دیگری به نام Service Worker استفاده می‌شود، اما این کار ضرورتی هم ندارد.

استفاده از Cache و Service Worker یک موضوع پیشرفته است و ما قصد نداریم در این مقاله به بررسی تفصیلی آن‌ها بپردازیم. با این حال یک مثال ساده از روش ذخیره‌سازی فایل‌های آفلاین در بخش بعدی نمایش می‌دهیم.

ذخیره‌سازی داده‌های ساده با Web Storage

API به نام Web Storage کاربرد بسیار ساده‌ای دارد. در این API جفت‌های کلید/مقدار از داده‌ها (محدود به رشته، عدد و غیره) را ذخیره کرده و این مقادیر را در موارد نیاز بازیابی می‌کنیم.

ساختار ابتدایی

روش استفاده از این API به صورت زیر است:

ابتدا به قالب خالی ذخیره‌سازی وب (+) که روی گیت‌هاب قرار داده‌ایم بروید (آن را در یک زبانه جدید مرورگر باز کنید). سپس کنسول جاوا اسکریپت ابزارهای توسعه‌دهنده مرورگر خود را باز کنید.

همه داده‌های ذخیره شده وب درون دو ساختار شبیه به شیء درون مرورگر به نام‌های sessionStorage و localStorage ذخیره شده‌اند. ساختار اول، داده‌هایی برای مدت باز بودن مرورگر ذخیره می‌کند، یعنی با بسته شدن مرورگر این داده‌ها نیز پاک می‌شوند. ساختار دوم داده‌ها را حتی پس از بسته شدن مرورگر نیز حفظ می‌کند. ما از ساختار دوم در این مقاله استفاده می‌کنیم، چون عموماً مفیدتر است.

متد ()Storage.setItem به ما امکان می‌دهد که آیتم‌های داده را در ذخیره کنیم و دو پارامتر به صورت نام آیتم و مقدار آن می‌گیرد. کد زیر را در کنسول جاوا اسکریپت مرورگر خود وارد کنید (در صورت تمایل می‌توانید نام خود را وارد کنید):

1localStorage.setItem('name','Chris');

متد ()Storage.getItem یک پارامتر می‌گیرد که نام آیتم داده‌ای است که می‌خواهیم بازیابی کنیم و مقدار آن را بازگشت می‌دهد. اکنون این خطوط کد را در کنسول جاوا اسکریپت وارد کنید:

1var myName = localStorage.getItem('name');
2myName

به محض وارد کردن خط دوم، باید ببینید که متغیر myName اینک شامل مقدار آیتم داده‌ای name است. متد ()Storage.removeItem یک پارامتر می‌گیرد که آیتم داده‌ای است که می‌خواهید حذف شود و آن آیتم را از ذخیره وب پاک می‌کند. خطوط زیر را در کنسول جاوا اسکریپت وارد کنید:

1localStorage.removeItem('name');
2var myName = localStorage.getItem('name');
3myName

خط سوم اینک باید مقدار null بازگشت دهد، بدین ترتیب آیتم name دیگر در ذخیره وب موجود نیست.

داده‌ها حفظ می‌شوند

یک قابلیت کلیدی web storage این است که داده‌ها در فاصله بین بارگذاری‌های صفحه حفظ می‌شوند. به توضیحات زیر توجه کنید. قالب خالی web storage فوق را یک بار دیگر باز کنید، اما این بار آن را در پنجره مرورگر متفاوتی باز کنید. بدین ترتیب راحت‌تر می‌توانیم با آن کار کنیم. خطوط زیر را در کنسول جاوا اسکریپت مرورگر وارد کنید:

1localStorage.setItem('name','Chris');
2var myName = localStorage.getItem('name');
3myName

اینک باید ببینید که نام آیتم بازگشت می‌یابد. در ادامه مرورگر را ببندید و دوباره آن را باز کنید و خطوط کد زیر را در کنسول وارد نمایید:

1var myName = localStorage.getItem('name');
2myName

اینک می‌بینید که مقدار همچنان موجود است هر چند مرورگر بسته شده و مجدداً باز شده است.

ذخیره‌سازی مجزا برای هر دامنه

برای هر دامنه (هر آدرس وب مجزا که در مرورگر بارگذاری می‌شوند) فضای ذخیره داده مجزایی وجود دارد. اگر دو وب سایت جداگانه را بارگذاری کنید و تلاش کنید به یک آیتم روی یک وب سایت دسترسی پیدا کنید روی وب سایت دیگر قابل دسترسی نخواهد بود. دلیل این مسئله آن است که در صورت دسترسی وب‌سایت‌ها به داده‌های همدیگر مشکلات امنیتی پدید می‌آمد.

یک مثال پیچیده‌تر

در این بخش دانش جدید خود را با نوشتن یک مثال دیگر به کار می‌گیریم تا ایده بهتری از طرز استفاده از web storage پیدا کنیم. مثال ما امکان وارد کردن یک نام را می‌دهد و پس از به‌روزرسانی صفحه، یک خوشامدگویی سفارشی‌شده دریافت می‌کنید. این حالت در زمان بارگذاری مجدد صفحه/مرورگر نیز حفظ می‌شود زیرا نام در web storage ذخیره شده است.

این مثال شامل یک وب سایت ساده با یک هدر، محتوا و فوتر است و فرمی دارد که نام خود را می‌توانید در آن وارد کنید.

ذخیره سازی سمت کلاینت

در ادامه این مثال را گام به گام با هم می‌سازیم تا با طرز کار آن آشنا شوید. ابتدا کد زیر را کپی کرده و در سیستم خود درون یک دایرکتوری در فایلی به نام personal-greeting.html ذخیره کنید.

1<!DOCTYPE html>
2<html>
3  <head>
4    <meta charset="utf-8">
5    <title>Personal greeting</title>
6    <style>
7      html {
8        font-family: sans-serif;
9      }
10      body {
11        margin: 0 auto;
12        max-width: 1024px;
13      }
14      main {
15        padding: 20px;
16      }
17      header, footer {
18        background: cyan;
19        padding: 20px;
20        border: 1px solid black;
21      }
22      header {
23        display: flex;
24        align-items: center;
25        justify-content: space-between;
26      }
27      form {
28        font-size: 0.8rem;
29      }
30      .personal-greeting {
31        font-weight: bold;
32      }
33    </style>
34    <script src="index.js" defer></script>
35  </head>
36  <body>
37    <header>
38      <h1>Our website</h1>
39
40      <form>
41        <div class="remember">
42          <label for="entername">Enter your name:</label>
43          <input id="entername" type="text" required>
44          <input id="submitname" type="submit" value="Say hello">
45        </div>
46        <div class="forget">
47          <label for="forgetname">Want me to forget you?</label>
48          <input id="forgetname" type="reset" value="Forget">
49        </div>
50      </form>
51    </header>
52
53    <main>
54
55      <p class="personal-greeting">Welcome to our website.</p>
56
57      <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam malesuada, metus ut mollis volutpat, felis est rhoncus turpis, in blandit est risus malesuada dui. Phasellus tempus elit at purus vestibulum suscipit. Donec quis est nec dui pretium venenatis sit amet eu nulla. Donec finibus, ipsum non semper dignissim, massa magna sagittis est, vitae vehicula nunc magna vitae diam. Integer ultrices mauris aliquet arcu tempor, at mattis justo sagittis. Nunc ut nulla et erat viverra tincidunt. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer vitae bibendum justo. Vestibulum porta velit sit amet libero accumsan fermentum.</p>
58
59      <p>Ut id mauris urna. In sed porttitor lectus. Suspendisse dignissim dolor id lectus pellentesque, eu bibendum lectus malesuada. Phasellus volutpat sollicitudin suscipit. Donec id libero nisl. Praesent gravida purus vel euismod facilisis. Maecenas sit amet velit non lacus aliquam dictum vitae eu augue. Donec euismod enim elementum elit laoreet sodales. Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
60
61      <p>Integer vulputate, libero sed vulputate eleifend, magna libero malesuada ligula, sit amet tincidunt dui mi vitae mauris. Aliquam aliquam turpis mauris, in sagittis orci rutrum efficitur. Sed vel purus fringilla, pretium sapien sed, accumsan erat. Morbi scelerisque tempor purus in faucibus. Nunc fringilla nulla ut aliquam posuere. Vivamus id lectus eleifend, bibendum urna non, ornare nibh. Fusce bibendum vulputate mollis.</p>
62
63    </main>
64
65    <footer>
66      <p>Copyright nobody. Use the code as you like.</p>
67    </footer>
68  </body>
69</html>

سپس توجه کنید که در HTML ارجاع‌هایی به فایلی به نام index.js داریم (خط 40). ما باید این فایل را ایجاد کرده و کد جاوا اسکریپت خود را درون آن بنویسیم. این فایل index.js را در همان دایرکتوری فایل HTML بسازید.

کار خود را با ایجاد ارجاع‌هایی به همه قابلیت‌های HTML که لازم است در این مثال دستکاری کنیم آغاز می‌کنیم. آن‌ها را به صورت ثابت‌هایی تعریف می‌کنیم زیرا این ارجاع‌ها در چرخه عمر اپلیکیشن تغییر نخواهند یافت. خطوط زیر را به فایل جاوا اسکریپت اضافه کنید:

1// create needed constants
2const rememberDiv = document.querySelector('.remember');
3const forgetDiv = document.querySelector('.forget');
4const form = document.querySelector('form');
5const nameInput = document.querySelector('#entername');
6const submitBtn = document.querySelector('#submitname');
7const forgetBtn = document.querySelector('#forgetname');
8
9const h1 = document.querySelector('h1');
10const personalGreeting = document.querySelector('.personal-greeting');

در ادامه باید یک شنونده رویداد کوچک تعریف کنیم تا وقتی دکمه submit کلیک می‌شود از ارسال فرم در عمل جلوگیری کنیم، چون این رفتاری نیست که می‌خواهیم. به این منظور قطعه کد زیر را در ادامه کد قبلی اضافه کنید:

1// Stop the form from submitting when a button is pressed
2form.addEventListener('submit', function(e) {
3  e.preventDefault();
4});

اکنون باید یک شنونده رویداد به صورت یک تابع دستگیره اضافه کنیم که وقتی دکمه Say hello کلیک می‌شود اجرا شود. کامنت های درون کد کاری که هر بخش انجام می‌دهد را به تفصیل توضیح می‌دهند، اما به صورت خلاصه در این کد نام کاربر را که در کادر ورودی متنی وارد شده است می‌گیریم و آن را با استفاده از ()setItem در web storage ذخیره می‌کنیم. سپس یک تابع به نام ()nameDisplayCheck اجرا می‌کنیم که کار به‌روزرسانی متن وب‌سایت را در عمل انجام می‌دهد. کد زیر را به انتهای کدهای قبلی اضافه کنید:

1// run function when the 'Say hello' button is clicked
2submitBtn.addEventListener('click', function() {
3  // store the entered name in web storage
4  localStorage.setItem('name', nameInput.value);
5  // run nameDisplayCheck() to sort out displaying the
6  // personalized greetings and updating the form display
7  nameDisplayCheck();
8});

در این مرحله باید یک دستگیره رویداد نیز داشته باشیم که تابعی را در زمان کلیک شدن دکمه Forget اجرا کند. این دکمه صرفاً پس از آن که دکمه Say Hello کلیک شد، نمایش پیدا می‌کند. در این تابع name را با استفاده از ()removeItem از web storage حذف می‌کنیم. سپس بار دیگر ()nameDisplayCheck اجرا می‌کنیم تا موارد نمایش یافته به‌روزرسانی شوند. کد زیر را به انتهای کدهای موجود اضافه کنید:

1// run function when the 'Forget' button is clicked
2forgetBtn.addEventListener('click', function() {
3  // Remove the stored name from web storage
4  localStorage.removeItem('name');
5  // run nameDisplayCheck() to sort out displaying the
6  // generic greeting again and updating the form display
7  nameDisplayCheck();
8});

اکنون زمان آن رسیده است که خود تابع ()nameDisplayCheck را تعریف کنیم. در این تابع با استفاده از localStorage.getItem('name') به عنوان یک آزمون شرطی بررسی می‌کنیم که نام آیتم در web storage ذخیره شده است یا نه. اگر ذخیره شده باشد، این فراخوانی به صورت true ارزیابی می‌شود و در غیر این صورت false خواهد بود. اگر true باشد، یک خوشامدگویی سفارشی نمایش می‌یابد. در ادامه بخش forget فرم نمایان می‌شود و بخش Say Hello پنهان می‌شود. اگر false باشد یک خوشامدگویی کلی نمایش می‌دهیم و خلاف حالت فوق عمل می‌کنیم. در این مرحله کد زیر را در انتهای فایل اضافه کنید:

1// define the nameDisplayCheck() function
2function nameDisplayCheck() {
3  // check whether the 'name' data item is stored in web Storage
4  if(localStorage.getItem('name')) {
5    // If it is, display personalized greeting
6    let name = localStorage.getItem('name');
7    h1.textContent = 'Welcome, ' + name;
8    personalGreeting.textContent = 'Welcome to our website, ' + name + '! We hope you have fun while you are here.';
9    // hide the 'remember' part of the form and show the 'forget' part
10    forgetDiv.style.display = 'block';
11    rememberDiv.style.display = 'none';
12  } else {
13    // if not, display generic greeting
14    h1.textContent = 'Welcome to our website ';
15    personalGreeting.textContent = 'Welcome to our website. We hope you have fun while you are here.';
16    // hide the 'forget' part of the form and show the 'remember' part
17    forgetDiv.style.display = 'none';
18    rememberDiv.style.display = 'block';
19  }
20}

در انتها باید اشاره کنیم که تابع ()nameDisplayCheck هر بار که صفحه مجدداً بارگذاری می‌شود مجدداً اجرا خواهد شد. اگر این کار انجام نیابد، خوشامدگویی سفارشی در فاصله بین بارگذاری مجدد صفحه حفظ نمی‌شود. کد زیر را به انتهای کد موجود اضافه کنید:

1document.body.onload = nameDisplayCheck;

اینک کار ساخت این مثال پایان یافته است. تنها کاری که باقی مانده است، ذخیره کد و تست صفحه HTML در یک مرورگر است.

ذخیره‌سازی داده‌های پیچیده با استفاده از IndexedDB

API به نام IndexedDB (برخی اوقات به اختصار IDB نامیده می‌شود) یک سیستم کامل پایگاه داده است که در مرورگر ارائه می‌شود و با استفاده از آن می‌توانید داده‌های با ارتباط‌های پیچیده را ذخیره کنید. انواع این داده‌ها دیگر محدود به مقادیر ساده‌ای مانند رشته یا اعداد نیست. بدین ترتیب می‌توانید ویدئو، تصاویر و تقریباً هر چیز دیگری را در وهله‌ای از IndexedDB ذخیره بکنید.

با این حال این امکان هزینه‌هایی دارد. کاربرد IndexedDB نسبت به API قبلی یعنی Web Storage بسیار پیچیده‌تر است. در این بخش، تنها به بررسی بخش کوچکی از ظرفیت‌های این API می‌پردازیم، اما اطلاعاتی که ارائه می‌شود برای آغاز به کار با این API کافی است.

بررسی مثال ذخیره‌سازی یادداشت

در این بخش مثالی را بررسی می‌کنیم که امکان ذخیره‌سازی یادداشت‌هایی را در مرورگر می‌دهد. همچنین می‌توانید یادداشت‌های خود را مشاهده یا حذف کنید. شما می‌توانید این مثال را همراه با توضیحات ما برای خودتان بسازید و در مورد بخش‌های مهم‌تر IDB نکاتی برای خود یادداشت کنید. ظاهر این اپلیکیشن چیزی مانند تصویر زیر خواهد بود:

ذخیره سازی سمت کلاینت

هر یادداشت دارای یک عنوان و بخش بدنه متن است که هر یک به صورت مجزا قابل ویرایش است. کد جاوا اسکریپت آن که در ادامه ارائه می‌کنیم دارای کامنت‌هایی است که به درک بهتر اتفاقاتی که رخ می‌دهند کمک می‌کند.

شروع

قبل از هر چیز یک کپی از کدهای زیر در فایل‌های مربوطه روی یک دایرکتوری سیستم خود ایجاد کنید:

فایل index.html

1<!DOCTYPE html>
2<html>
3  <head>
4    <meta charset="utf-8">
5    <title>IndexedDB demo</title>
6    <link href="style.css" rel="stylesheet">
7    <script src="index.js" defer></script>
8  </head>
9  <body>
10    <header>
11      <h1>IndexedDB notes demo</h1>
12    </header>
13
14    <main>
15      <section class="note-display">
16        <h2>Notes</h2>
17        <ul>
18
19        </ul>
20      </section>
21      <section class="new-note">
22        <h2>Enter a new note</h2>
23        <form>
24          <div>
25            <label for="title">Note title</label>
26            <input id="title" type="text" required>
27          </div>
28          <div>
29            <label for="body">Note text</label>
30            <input id="body" type="text" required>
31          </div>
32          <div>
33            <button>Create new note</button>
34          </div>
35        </form>
36      </section>
37    </main>
38
39    <footer>
40      <p>Copyright nobody. Use the code as you like.</p>
41    </footer>
42  </body>
43</html>

فایل style.css

1html {
2  font-family: sans-serif;
3}
4
5body {
6  margin: 0 auto;
7  max-width: 800px;
8}
9
10header, footer {
11  background-color: green;
12  color: white;
13  line-height: 100px;
14  padding: 0 20px;
15}
16
17.new-note, .note-display {
18  padding: 20px;
19}
20
21.new-note {
22  background: #ddd;
23}
24
25h1 {
26  margin: 0;
27}
28
29ul {
30  list-style-type: none;
31}
32
33div {
34  margin-bottom: 10px;
35}

فایل index-start.js

1// Create needed constants
2const list = document.querySelector('ul');
3const titleInput = document.querySelector('#title');
4const bodyInput = document.querySelector('#body');
5const form = document.querySelector('form');
6const submitBtn = document.querySelector('form button');

اگر نگاهی به این فایل‌ها بیندازید می‌بینید که HTML بسیار ساده است و یک وب‌سایت با هدر و فوتر و همچنین ناحیه محتوای اصلی است که شامل مکان‌هایی برای نمایش یادداشت‌ها و یک فرم برای وارد کردن یادداشت‌های جدید در پایگاه داده ارائه شده است. CSS نوعی استایل‌بندی ساده ارائه می‌کند که موجب روشن‌تر فرایند کار می‌شود. فایل جاوا اسکریپت نیز شامل اعلان پنج ثابت است که شامل ارجاع‌هایی به عنصر <ul> است که یادداشت‌ها در آن نمایش می‌یابد، عناصر <input> عنوان و بدنه یادداشت و خود <form> به همراه <button > است.

نام فایل جاوا اسکریپت را به index.js تغییر دهید. اینک آماده اضافه کردن کد هستیم.

راه‌اندازی اولیه پایگاه داده

در این مرحله ابتدا نگاهی به آن چه در آغاز باید انجام می‌دهیم خواهیم داشت تا پایگاه داده خود را راه‌اندازی کنیم. اعلان‌های ثابت زیر را به فایل اضافه کنید:

1// Create an instance of a db object for us to store the open database in
2let db;

در این کد یک متغیر به نام db اعلان می‌کنیم که در ادامه برای ذخیره‌سازی یک شیء برای نمایندگی پایگاه داده استفاده می‌کنیم. از این شیء در چند جا استفاده خواهیم کرد و از این رو آن را به صورت سراسری اعلان می‌کنیم تا کارها آسان‌تر شوند.

سپس کد زیر را به انتهای کد موجود اضافه می‌کنیم:

1window.onload = function() {
2
3};

ما همه کدهای بعدی را درون تابع دستگیره رویداد window.onload خواهیم نوشت که در زمان ارسال رویداد load پنجره فراخوانی می‌شود تا مطمئن شویم که از کارکرد IndexedDB پیش از بارگذاری کامل اپلیکیشن استفاده نمی‌شود.

درون دستگیره window.onload کد زیر را اضافه می‌کنیم:

1// Open our database; it is created if it doesn't already exist
2// (see onupgradeneeded below)
3let request = window.indexedDB.open('notes_db', 1);

خط فوق یک request برای باز کردن نسخه 1 پایگاه داده به نام notes_db ایجاد می‌کند. اگر این پایگاه داده موجود نباشد، از سوی کد بعدی برای ما ایجاد می‌شود. این الگوی درخواست را در سراسر زمان استفاده از IndexedDB مشاهده خواهید کرد. عملیات پایگاه داده زمان‌بر است. ما نمی‌خواهیم مرورگر زمانی که منتظر نتیجه است متوقف شود و از این رو عملیات پایگاه داده به صورت «ناهمگام» (asynchronous) اجرا می‌شود، یعنی به جای اجرای بی‌درنگ، زمانی در آینده اجرا خواهد شد و زمانی که این اتفاق بیفتد به شما اعلام می‌شود.

برای مدیریت این وضعیت در IndexedDB یک شیء درخواست (که می‌توان هر چیزی نامید و ما آن را می‌نامیم) ایجاد می‌کنیم. سپس از دستگیره‌های رویداد برای اجرای د در زمان کامل شدن درخواست، ناموفق بودن و موارد دیگر استفاده می‌کنیم. در ادامه در این خصوص بیشتر توضیح داده‌ایم.

نکته: شماره نسخه بسیار مهم است. اگر می‌خواهید پایگاه داده خود را ارتقا دهید (برای نمونه با تغییر ساختار جدول) باید کد خود را با افزایش شماره نسخه اجرا کنید، همچنین طرح‌بندی متفاوتی درون دستگیره onupgradeneeded تعیین می‌شود. در این راهنمای مقدماتی به مبحث ارتقای پایگاه داده نخواهیم پرداخت.

اکنون دستگیره‌های رویداد را درست زیر کد قبلی و بار دیگر درون دستگیره window.onload اضافه کنید:

1// onerror handler signifies that the database didn't open successfully
2request.onerror = function() {
3  console.log('Database failed to open');
4};
5
6// onsuccess handler signifies that the database opened successfully
7request.onsuccess = function() {
8  console.log('Database opened successfully');
9
10  // Store the opened database object in the db variable. This is used a lot below
11  db = request.result;
12
13  // Run the displayData() function to display the notes already in the IDB
14  displayData();
15};

دستگیره در صورتی اجرا می‌شود که سیستم اعلام کند نتیجه درخواست ناموفق بوده است. بدین ترتیب می‌توانیم به این مشکل پاسخ دهیم. در مثال ساده ما صرفاً یک پیام در کنسول جاوا اسکریپت نمایش پیدا می‌کند.

در سوی دیگر دستگیره request.onsuccess در صورتی اجرا می‌شود که نتیجه درخواست موفق باشد یعنی پایگاه داده با موفقیت باز شده باشد. در این حالت شیء نماینده پایگاه داده باز شده در مشخصه request.result در اختیار ما قرار دارد و به ما امکان دستکاری پایگاه داده را می‌دهد این شیء را در متغیر db که قبلاً برای کاربرد بعدی ایجاد کردیم ذخیره می‌کنیم. ضمناً یک تابع سفارشی به نام ()displayData اجرا می‌کنیم که داده‌های پایگاه داده را درون یک <ul> نمایش می‌دهد آن را به این جهت اینک اجرای کنیم که یادداشت‌های قبلی موجود در پایگاه داده به محض بارگذاری صفحه نمایش پیدا می‌کنند. تعریف این وضعیت را در ادامه خواهید دید.

در نهایت احتمالاً مهم‌ترین دستگیره رویداد را برای راه‌اندازی پایگاه داده به نام request.onupgradeneeded اضافه می‌کنیم. این دستگیره در صورتی اجرا می‌شود که پایگاه داده از قبل راه‌اندازی نشده باشد یا اگر پایگاه داده با یک شماره نسخه بالاتر (در زمان ارتقا) از نسخه موجود باز شده باشد اجرا خواهید شد. کد زیر را در ادامه کدهای قبلی دستگیره اضافه کنید:

1// Setup the database tables if this has not already been done
2request.onupgradeneeded = function(e) {
3  // Grab a reference to the opened database
4  let db = e.target.result;
5
6  // Create an objectStore to store our notes in (basically like a single table)
7  // including a auto-incrementing key
8  let objectStore = db.createObjectStore('notes_os', { keyPath: 'id', autoIncrement:true });
9
10  // Define what data items the objectStore will contain
11  objectStore.createIndex('title', 'title', { unique: false });
12  objectStore.createIndex('body', 'body', { unique: false });
13
14  console.log('Database setup complete');
15};

این همان جایی است که شِمای (ساختار) پایگاه داده را تعریف می‌کنیم. منظور از شِما مجموعه ستون‌ها (یا فیلدها)-یی است که در پایگاه داده وجود دارند. در این بخش یک ارجاع به پایگاه داده موجود از به دست می‌آوریم که در شیء request است. این کار معادل خط کد زیر است:

1db = request.result;

این خط کد درون دستگیره onsuccess قرار دارد که ما باید آن را جدا کنیم، چون دستگیره onupgradeneeded پیش از دستگیره onsuccess اجرا می‌شود و این بدان معنی است که اگر این کار را نکنیم مقدار db در موجود نخواهد بود.

سپس از ()IDBDatabase.createObjectStore برای ایجاد یک شیء جدید درون پایگاه داده باز شده به نام notes_os استفاده می‌کنیم. این کار معادل یک جدول منفرد در یک سیستم پایگاه داده متعارف است. نام آن را notes می‌گذاریم و یک فیلد کلید به صورت autoIncrement به نام id نیز برای آن تعیین می‌کنیم. بدین ترتیب هر فیلد جدیدی که درج شود، مقدار این id به صورت خودکار یک واحد افزایش می‌یابد و دیگر لازم نیست توسعه‌دهنده این کار را خود انجام دهد. فیلد id به عنوان کلید برای شناسایی یکتای فیلد رکوردها استفاده می‌شود که در زمان حذف یا نمایش یک رکورد کاربرد دارد.

همچنین دو اندیس (فیلد) دیگر با استفاده از متد به نام‌های title و body ایجاد می‌کنیم. title شامل یک عنوان برای یادداشت و body شامل متن یادداشت مورد نظر ما است.

بدین ترتیب شِمای پایگاه داده این مثال ساده است و می‌توانیم شروع به افزودن رکوردها به پایگاه داده بکنیم که هر رکورد نماینده یک شیء از طریق کدهای زیر است:

1{
2  title: "Buy milk",
3  body: "Need both cows milk and soya.",
4  id: 8
5}

افزودن داده به پایگاه داده

در این بخش به بررسی روش اضافه کردن رکوردها به پایگاه داده می‌پردازیم. این کار با استفاده از یک فرم در صفحه ما انجام می‌یابد.

در ادامه دستگیره رویداد قبلی (اما همچنان درون دستگیره window.onload)، خط زیر را اضافه می‌کنیم که یک دستگیره onsubmit تنظیم می‌کند و یک تابع به نام ()addData در زمان تحویل فرم اجرا می‌کند:

1// Create an onsubmit handler so that when the form is submitted the addData() function is run
2form.onsubmit = addData;

اکنون تابع ()addData را تعریف می‌کنیم. خطوط زیر را در ادامه کدهای قبلی اضافه کنید:

1// Define the addData() function
2function addData(e) {
3  // prevent default - we don't want the form to submit in the conventional way
4  e.preventDefault();
5
6  // grab the values entered into the form fields and store them in an object ready for being inserted into the DB
7  let newItem = { title: titleInput.value, body: bodyInput.value };
8
9  // open a read/write db transaction, ready for adding the data
10  let transaction = db.transaction(['notes_os'], 'readwrite');
11
12  // call an object store that's already been added to the database
13  let objectStore = transaction.objectStore('notes_os');
14
15  // Make a request to add our newItem object to the object store
16  var request = objectStore.add(newItem);
17  request.onsuccess = function() {
18    // Clear the form, ready for adding the next entry
19    titleInput.value = '';
20    bodyInput.value = '';
21  };
22
23  // Report on the success of the transaction completing, when everything is done
24  transaction.oncomplete = function() {
25    console.log('Transaction completed: database modification finished.');
26
27    // update the display of data to show the newly added item, by running displayData() again.
28    displayData();
29  };
30
31  transaction.onerror = function() {
32    console.log('Transaction not opened due to error');
33  };
34}

این کد کاملاً پیچیده است، بنابراین در ادامه آن را جزء به جزء تشریح می‌کنیم.

()Event.preventDefault را روی شیء رویداد اجرا می‌کنیم تا از تحویل واقعی فرم به روش متعارف جلوگیری کنیم، چون این کار موجب رفرش شدن صفحه می‌شود و تجربه کاربری را به هم میزند.

یک شیء ایجاد می‌کنیم که نماینده یک رکورد است که قرار است وارد پایگاه داده شود و مقدار آن را بر اساس ورودی فرم تعین می‌کنیم. توجه کنید که مقدار id را صریح نگنجانده‌ایم چون چنان که قبلاً توضیح دادیم به صورت خودکار در پایگاه داده افزایش می‌یابد.

یک تراکنش readwrite روی شیء notes_os باز می‌شود تا با استفاده از متد ()IDBDatabase.transaction ذخیره شود. این شیء تراکنش به ما امکان می‌دهد که به شیء ذخیره شده دسترسی پیدا کنیم و می‌توانیم کاری روی آن انجام دهیم، مثلاً یک رکورد جدید اضافه کنیم.

با استفاده از متد ()IDBTransaction.objectStore به شیء ذخیره شده دسترسی می‌یابیم و نتیجه را در متغیر objectStore ذخیره می‌کنیم.

با استفاده از ()IDBObjectStore.add رکورد جدیدی به پایگاه داده اضافه می‌کنیم. این متد یک شیء درخواست جدید به همان روش که قبلاً دیدیم، می‌سازد.

یک دسته از شونده‌های رویداد به request و transaction اضافه می‌کنیم تا کدهای مختلفی را در نقاط حساس چرخه عمر رویداد اجرا کنند. زمانی که درخواست موفق باشد، ورودی‌های فرم را پاک می‌کنیم تا آماده وارد کردن یادداشت بعدی باشد. زمانی که تراکنش کامل شود، تابع ()displayData را بار دیگر اجرا می‌کنیم تا نمایش یادداشت‌های روی صفحه به‌روزرسانی شوند.

نمایش داده‌ها

ما تاکنون ()displayData را دو بار در کد، مورد ارجاع قرار داده‌ایم و از این رو احتمالاً بهتر است آن را تعریف کنیم. کد زیر را به کدهای قبلی اضافه کنید:

1// Define the displayData() function
2function displayData() {
3  // Here we empty the contents of the list element each time the display is updated
4  // If you didn't do this, you'd get duplicates listed each time a new note is added
5  while (list.firstChild) {
6    list.removeChild(list.firstChild);
7  }
8
9  // Open our object store and then get a cursor - which iterates through all the
10  // different data items in the store
11  let objectStore = db.transaction('notes_os').objectStore('notes_os');
12  objectStore.openCursor().onsuccess = function(e) {
13    // Get a reference to the cursor
14    let cursor = e.target.result;
15
16    // If there is still another data item to iterate through, keep running this code
17    if(cursor) {
18      // Create a list item, h3, and p to put each data item inside when displaying it
19      // structure the HTML fragment, and append it inside the list
20      let listItem = document.createElement('li');
21      let h3 = document.createElement('h3');
22      let para = document.createElement('p');
23
24      listItem.appendChild(h3);
25      listItem.appendChild(para);
26      list.appendChild(listItem);
27
28      // Put the data from the cursor inside the h3 and para
29      h3.textContent = cursor.value.title;
30      para.textContent = cursor.value.body;
31
32      // Store the ID of the data item inside an attribute on the listItem, so we know
33      // which item it corresponds to. This will be useful later when we want to delete items
34      listItem.setAttribute('data-note-id', cursor.value.id);
35
36      // Create a button and place it inside each listItem
37      let deleteBtn = document.createElement('button');
38      listItem.appendChild(deleteBtn);
39      deleteBtn.textContent = 'Delete';
40
41      // Set an event handler so that when the button is clicked, the deleteItem()
42      // function is run
43      deleteBtn.onclick = deleteItem;
44
45      // Iterate to the next item in the cursor
46      cursor.continue();
47    } else {
48      // Again, if list item is empty, display a 'No notes stored' message
49      if(!list.firstChild) {
50        let listItem = document.createElement('li');
51        listItem.textContent = 'No notes stored.';
52        list.appendChild(listItem);
53      }
54      // if there are no more cursor items to iterate through, say so
55      console.log('Notes all displayed');
56    }
57  };
58}

در ادامه کد فوق را تشریح می‌کنیم.

ابتدا محتوای عنصر <ul> را خالی کرده و سپس آن را با محتوای به‌روزرسانی شده پر می‌کنیم. اگر این کار را انجام ندهید، در نهایت لیست عظیمی از محتواهای تکراری که به طور مکرر در هر به‌روزرسانی به لیست اضافه شده‌اند خواهید داشت.

سپس با استفاده از ()IDBDatabase.transaction و ()IDBTransaction.objectStore همان طور که در مورد ()addData عمل کردیم، یک ارجاع به شیء notes_os به دست می‌آوریم. تنها تفاوت این است که آن‌ها را در یک خط به هم زنجیر کرده‌ایم.

گام بعدی استفاده از متد ()IDBObjectStore.openCursor برای باز کردن یک درخواست برای کرسر است. این سازه‌ای است که می‌توان برای تعریف حلقه تکرار روی رکوردها در یک شیء ذخیره‌سازی استفاده کرد. دستگیره onsuccess را به انتهای این خط اتصال می‌دهیم تا کد منسجم‌تر شود و زمانی که کرسر با موفقیت بازگشت یافت، دستگیره اجرا شود.

با استفاده از cursor = e.target.result یک ارجاع به خود کرسر (یک شیء IDBCursor) به دست می‌آوریم.

سپس بررسی می‌کنیم که آیا کرسر شامل رکوردی از داده‌گاه است یا نه. اگر چنین باشد یک فرگمان DOM ایجاد کرده و آن را با داده‌های رکورد مقداردهی می‌کنیم و در صفحه درج می‌نماییم. همچنین یک دکمه delete نیز تعریف می‌کنیم که وقتی کلیک شود، یادداشت را با اجرای تابع حذف می‌کند. این فرایند را در بخش بعدی توضیح می‌دهیم.

در انتهای بلوک if از متد ()IDBCursor.continue برای بردن کرسر به رکورد بعدی داده‌گاه استفاده می‌کنیم و محتوای بلوک if را یک بار دیگر اجرا می‌کنیم. اگر رکورد دیگری وجود داشته باشد که بخواهیم روی آن تکرار کنیم، این امر موجب می‌شود در صفحه درج شود و در ادامه ()continue اجرا می‌شود و همین طور تا آخر می‌رود.

زمانی که هیچ رکورد دیگری برای تکرار وجود نداشته باشد، cursor مقدار بازگشت می‌دهد و از این رو بلوک else به جای بلوک if اجرا می‌شود. این بلوک بررسی می‌کند که آیا یادداشتی در <ul> درج شده یا نه و اگر چنین نباشد یک پیام در آن وارد می‌کند که هیچ یادداشتی ذخیره نشده است.

حذف یک یادداشت

همان طور که پیش‌تر اشاره کردیم زمانی که دکمه حذف یادداشت فشرده شود، یادداشت حذف می‌شود. این کار از طریق تابع ()deleteItem اجرا می‌شود که به صورت زیر است:

1// Define the deleteItem() function
2function deleteItem(e) {
3  // retrieve the name of the task we want to delete. We need
4  // to convert it to a number before trying it use it with IDB; IDB key
5  // values are type-sensitive.
6  let noteId = Number(e.target.parentNode.getAttribute('data-note-id'));
7
8  // open a database transaction and delete the task, finding it using the id we retrieved above
9  let transaction = db.transaction(['notes_os'], 'readwrite');
10  let objectStore = transaction.objectStore('notes_os');
11  let request = objectStore.delete(noteId);
12
13  // report that the data item has been deleted
14  transaction.oncomplete = function() {
15    // delete the parent of the button
16    // which is the list item, so it is no longer displayed
17    e.target.parentNode.parentNode.removeChild(e.target.parentNode);
18    console.log('Note ' + noteId + ' deleted.');
19
20    // Again, if list item is empty, display a 'No notes stored' message
21    if(!list.firstChild) {
22      let listItem = document.createElement('li');
23      listItem.textContent = 'No notes stored.';
24      list.appendChild(listItem);
25    }
26  };
27}

در بخش نخست کد فوق، ID رکوردی که باید حذف شود با استفاده از کد زیر بازیابی می‌شود:

1Number(e.target.parentNode.getAttribute('data-note-id'))

به خاطر دارید که ID رکورد در زمان نمایش یافتن آن در خصوصیت data-note-id روی <ul> ذخیره می‌شود. با این حال ما باید این خصوصیت را از طریق شیء سراسری درونی ()Number ارسال کنیم چون دارای نوع داده رشته است و از این رو از سوی پایگاه داده که منتظر یک عدد است شناسایی نخواهد شد.

سپس ارجاعی به داده‌گاه با استفاده از همان الگویی که قبلاً دیدیم به دست می‌آوریم و با ارسال ID از متد بری حذف رکورد از پایگاه داده استفاده می‌کنیم.

زمانی که تراکنش پایگاه داده کامل شود، یادداشت را از <li> در DOM نیز حذف می‌کنیم و بار دیگر بررسی می‌کنیم آیا <ul> خالی است یا نه، تا در صورت خالی بودن پیام مناسبی در آن درج می‌کنیم.

بدین ترتیب مثال ما اینک کامل شده است و باید به درستی کار کند. اگر کد شما به هر دلیلی کار نمی‌کند، می‌توانید آن را با کد کامل زیر بررسی کنید تا متوجه شوید کجا مشکلی وجود دارد:

1// Create needed constants
2const list = document.querySelector('ul');
3const titleInput = document.querySelector('#title');
4const bodyInput = document.querySelector('#body');
5const form = document.querySelector('form');
6const submitBtn = document.querySelector('form button');
7
8// Create an instance of a db object for us to store the open database in
9let db;
10
11window.onload = function() {
12  // Open our database; it is created if it doesn't already exist
13  // (see onupgradeneeded below)
14  let request = window.indexedDB.open('notes_db', 1);
15
16  // onerror handler signifies that the database didn't open successfully
17  request.onerror = function() {
18    console.log('Database failed to open');
19  };
20
21  // onsuccess handler signifies that the database opened successfully
22  request.onsuccess = function() {
23    console.log('Database opened succesfully');
24
25    // Store the opened database object in the db variable. This is used a lot below
26    db = request.result;
27
28    // Run the displayData() function to display the notes already in the IDB
29    displayData();
30  };
31
32  // Setup the database tables if this has not already been done
33  request.onupgradeneeded = function(e) {
34
35    // Grab a reference to the opened database
36    let db = e.target.result;
37
38    // Create an objectStore to store our notes in (basically like a single table)
39    // including a auto-incrementing key
40    let objectStore = db.createObjectStore('notes_os', { keyPath: 'id', autoIncrement:true });
41
42    // Define what data items the objectStore will contain
43    objectStore.createIndex('title', 'title', { unique: false });
44    objectStore.createIndex('body', 'body', { unique: false });
45
46    console.log('Database setup complete');
47  };
48
49  // Create an onsubmit handler so that when the form is submitted the addData() function is run
50  form.onsubmit = addData;
51
52  // Define the addData() function
53  function addData(e) {
54    // prevent default - we don't want the form to submit in the conventional way
55    e.preventDefault();
56
57    // grab the values entered into the form fields and store them in an object ready for being inserted into the DB
58    let newItem = { title: titleInput.value, body: bodyInput.value };
59
60    // open a read/write db transaction, ready for adding the data
61    let transaction = db.transaction(['notes_os'], 'readwrite');
62
63    // call an object store that's already been added to the database
64    let objectStore = transaction.objectStore('notes_os');
65
66    // Make a request to add our newItem object to the object store
67    var request = objectStore.add(newItem);
68    request.onsuccess = function() {
69      // Clear the form, ready for adding the next entry
70      titleInput.value = '';
71      bodyInput.value = '';
72    };
73
74    // Report on the success of the transaction completing, when everything is done
75    transaction.oncomplete = function() {
76      console.log('Transaction completed: database modification finished.');
77
78      // update the display of data to show the newly added item, by running displayData() again.
79      displayData();
80    };
81
82    transaction.onerror = function() {
83      console.log('Transaction not opened due to error');
84    };
85  }
86
87  // Define the displayData() function
88  function displayData() {
89    // Here we empty the contents of the list element each time the display is updated
90    // If you ddn't do this, you'd get duplicates listed each time a new note is added
91    while (list.firstChild) {
92      list.removeChild(list.firstChild);
93    }
94
95    // Open our object store and then get a cursor - which iterates through all the
96    // different data items in the store
97    let objectStore = db.transaction('notes_os').objectStore('notes_os');
98    objectStore.openCursor().onsuccess = function(e) {
99      // Get a reference to the cursor
100      let cursor = e.target.result;
101
102      // If there is still another data item to iterate through, keep running this code
103      if(cursor) {
104        // Create a list item, h3, and p to put each data item inside when displaying it
105        // structure the HTML fragment, and append it inside the list
106        let listItem = document.createElement('li');
107        let h3 = document.createElement('h3');
108        let para = document.createElement('p');
109
110        listItem.appendChild(h3);
111        listItem.appendChild(para);
112        list.appendChild(listItem);
113
114        // Put the data from the cursor inside the h3 and para
115        h3.textContent = cursor.value.title;
116        para.textContent = cursor.value.body;
117
118        // Store the ID of the data item inside an attribute on the listItem, so we know
119        // which item it corresponds to. This will be useful later when we want to delete items
120        listItem.setAttribute('data-note-id', cursor.value.id);
121
122        // Create a button and place it inside each listItem
123        let deleteBtn = document.createElement('button');
124        listItem.appendChild(deleteBtn);
125        deleteBtn.textContent = 'Delete';
126
127        // Set an event handler so that when the button is clicked, the deleteItem()
128        // function is run
129        deleteBtn.onclick = deleteItem;
130
131        // Iterate to the next item in the cursor
132        cursor.continue();
133      } else {
134        // Again, if list item is empty, display a 'No notes stored' message
135        if(!list.firstChild) {
136          let listItem = document.createElement('li');
137          listItem.textContent = 'No notes stored.'
138          list.appendChild(listItem);
139        }
140        // if there are no more cursor items to iterate through, say so
141        console.log('Notes all displayed');
142      }
143    };
144  }
145
146  // Define the deleteItem() function
147  function deleteItem(e) {
148    // retrieve the name of the task we want to delete. We need
149    // to convert it to a number before trying it use it with IDB; IDB key
150    // values are type-sensitive.
151    let noteId = Number(e.target.parentNode.getAttribute('data-note-id'));
152
153    // open a database transaction and delete the task, finding it using the id we retrieved above
154    let transaction = db.transaction(['notes_os'], 'readwrite');
155    let objectStore = transaction.objectStore('notes_os');
156    let request = objectStore.delete(noteId);
157
158    // report that the data item has been deleted
159    transaction.oncomplete = function() {
160      // delete the parent of the button
161      // which is the list item, so it is no longer displayed
162      e.target.parentNode.parentNode.removeChild(e.target.parentNode);
163      console.log('Note ' + noteId + ' deleted.');
164
165      // Again, if list item is empty, display a 'No notes stored' message
166      if(!list.firstChild) {
167        let listItem = document.createElement('li');
168        listItem.textContent = 'No notes stored.';
169        list.appendChild(listItem);
170      }
171    };
172  }
173
174};

ذخیره‌سازی داده‌های پیچیده از طریق IndexedDB

همچنان که پیش‌تر اشاره کردیم، می‌توان از IndexedDB برای ذخیره چیزی بیش از رشته‌های متنی استفاده کرد. با استفاده از آن می‌توان تقریباً هر چیزی را ذخیره کرد که شامل اشیای پیچیده مانند blob-های صوتی و ویدئویی نیز می‌شود. این کار هیچ دشواری خاصی نسبت به انواع داده دیگر ندارد.

برای نمایش شیوه انجام این کار یک مثال دیگر به نام IndexedDB video store نوشته‌ایم که می‌تواند در این آدرس (+) آن را مشاهده کنید. زمانی که نخستین بار این مثال را اجرا کنید، همه ویدئوها را از شبکه دانلود می‌کند و آن‌ها را در پایگاه داده IndexedDB ذخیره می‌کند و سپس ویدئوها را در UI درون عناصر <video> نمایش می‌دهد. دومین بار که آن را اجرا کنید ویدئوها را در پایگاه داده می‌یابید و به جای دانلود از شبکه آن‌ها را از رایانه محلی نمایش می‌دهد. بدین ترتیب بارگذاری‌های متعاقب بسیار سریع‌تر صوت می‌گیرد و به پهنای باند زیادی نیاز ندارد.

در ادامه جالب‌ترین بخش‌های این مثال را بررسی می‌کنیم. البته آن را به طور کامل بررسی نمی‌کنیم، چون بخش عمده آن با مثال قبلی مشابه است و کد نیز به خوبی مستندسازی شده است.

در این مثال ساده نام‌های ویدئوها ذخیره شده است تا در آرایه‌ای از اشیا واکشی شود:

1const videos = [
2  { 'name' : 'crystal' },
3  { 'name' : 'elf' },
4  { 'name' : 'frog' },
5  { 'name' : 'monster' },
6  { 'name' : 'pig' },
7  { 'name' : 'rabbit' }
8];

در آغاز کار زمانی که پایگاه داده با موفقیت باز شود، تابع ()init را اجرا می‌کنیم. این تابع روی نام‌های مختلف ویدئو، حلقه تکرار تعریف می‌کند و تلاش می‌کند که هر ویدئو را بر اساس نام از پایگاه داده videos بارگذاری کند.

اگر هر ویدئو در پایگاه داده پیدا شود، فایل‌های ویدئویی مربوطه (که به صورت blob ذخیره شده است) و نام آن‌ها مستقیماً به تابع ()displayVideo ارسال می‌شوند تا درون رابط کاربری قرار گیرند. اگر چنین نباشد نام ویدئو به تابع ()fetchVideoFromNetwork ارسال می‌شود تا ویدئو از شبکه دانلود شود.

1function init() {
2  // Loop through the video names one by one
3  for(let i = 0; i < videos.length; i++) {
4    // Open transaction, get object store, and get() each video by name
5    let objectStore = db.transaction('videos_os').objectStore('videos_os');
6    let request = objectStore.get(videos[i].name);
7    request.onsuccess = function() {
8      // If the result exists in the database (is not undefined)
9      if(request.result) {
10        // Grab the videos from IDB and display them using displayVideo()
11        console.log('taking videos from IDB');
12        displayVideo(request.result.mp4, request.result.webm, request.result.name);
13      } else {
14        // Fetch the videos from the network
15        fetchVideoFromNetwork(videos[i]);
16      }
17    };
18  }
19}

قطعه کد زیر از تابع ()fetchVideoFromNetwork گرفته شده است. در این کد نسخه‌های MP4 و WebM از ویدئو با استفاده از دو درخواست مجزای ()WindowOrWorkerGlobalScope.fetch واکشی می‌شوند. سپس از متد ()Body.blob برای استخراج بدنه هر درخواست به صورت یک blob استفاده می‌کنیم تا یک شیء بازنمایی از ویدئوها داشته باشیم و بتوانیم آن‌ها را در ادامه ذخیره کرده و نمایش دهیم.

با آن حال در اینجا مشکلی وجود دارد، این دو درخواست هر دو ناهمگام هستند اما زمانی که هر دو درخواست اجرا شوند، تنها می‌خواهیم ویدئو را نمایش داده یا ذخیره کنیم و نه هر دو. خوشبختانه یک متد داخلی وجود دارد که چنین مشکلاتی را حل می‌کند و آن ()Promise.all است. این متد یک آرگومان می‌گیرد که ارجاعی به همه Promise-هایی است که می‌خواهم در آرایه قرار دهیم و خود مبتنی بر Promise است.

زمانی که همه Promise-ها برآورده شدند، ()Promise.all با آرایه‌ای شامل همه مقادیر منفرد موفق بازگشت می‌یابد. در ادامه درون بلوک ()all می‌توانیم تابع ()displayVideo را مانند کاری که قبل برای نمایش ویدئوها در رابط کاربری انجام دادیم، فراخوانی کنیم و سپس تابع ()storeVideo را برای ذخیره‌سازی این مقادیر درون پایگاه داده فرابخوانیم.

1let mp4Blob = fetch('videos/' + video.name + '.mp4').then(response =>
2  response.blob()
3);
4let webmBlob = fetch('videos/' + video.name + '.webm').then(response =>
5  response.blob()
6);
7
8// Only run the next code when both promises have fulfilled
9Promise.all([mp4Blob, webmBlob]).then(function(values) {
10  // display the video fetched from the network with displayVideo()
11  displayVideo(values[0], values[1], video.name);
12  // store it in the IDB using storeVideo()
13  storeVideo(values[0], values[1], video.name);
14});

ابتدا به بررسی ()storeVideo می‌پردازیم. این تابع شباهت زیادی به الگویی دارد که در مثال قبلی برای افزودن داده‌ها به پایگاه داده دیدیم، ما یک تراکنش readwrite باز کرده و ارجاعی به شیء videos_os به دست می‌آوریم و یک شیء نماینده رکورد برای افزودن به پایگاه داده ایجاد می‌کنیم. سپس آن را با استفاده از ()IDBObjectStore.add اضافه می‌کنیم.

1function storeVideo(mp4Blob, webmBlob, name) {
2  // Open transaction, get object store; make it a readwrite so we can write to the IDB
3  let objectStore = db.transaction(['videos_os'], 'readwrite').objectStore('videos_os');
4  // Create a record to add to the IDB
5  let record = {
6    mp4 : mp4Blob,
7    webm : webmBlob,
8    name : name
9  }
10
11  // Add the record to the IDB using add()
12  let request = objectStore.add(record);
13
14  ...
15
16};

در نهایت تابع ()displayVideo را داریم که عناصر DOM مورد نیاز برای درج ویدئو در رابط کاربری را ایجاد کرده و سپس آن‌ها را به صفحه الحاق می‌کند. جالب‌ترین بخش‌های این کار در ادامه نمایش یافته‌اند. برای نمایش واقعی ویدئو در یک عنصر <video> باید URL-های شیء را با استفاده از متد ()URL.createObjectURL بسازیم. زمانی که این کار انجام یافت، می‌توانیم URL-های شیء را روی مقدار خصوصیت src عنصر <source> تنظیم کنیم تا به درستی کار کند.

1function displayVideo(mp4Blob, webmBlob, title) {
2  // Create object URLs out of the blobs
3  let mp4URL = URL.createObjectURL(mp4Blob);
4  let webmURL = URL.createObjectURL(webmBlob);
5
6  ...
7
8  let video = document.createElement('video');
9  video.controls = true;
10  let source1 = document.createElement('source');
11  source1.src = mp4URL;
12  source1.type = 'video/mp4';
13  let source2 = document.createElement('source');
14  source2.src = webmURL;
15  source2.type = 'video/webm';
16
17  ...
18}

ذخیره‌سازی آفلاین فایل‌ها

در مثال فوق تقریباً همه جوانب ذخیره‌سازی فایل‌های بزرگ در یک پایگاه داده IndexedDB را برای اجتناب از نیاز به دانلود آن‌ها بیش از یک بار، مورد بررسی قرار دادیم. این خود یک بهبود عظیم در تجربه کاربری محسوب می‌شود، اما هنوز یک بهینه‌سازی عمده را نداریم و آن فایل‌های HTML، CSS و JavaScript است که باید هر بار که از سایت بازدید می‌کنیم دوباره دانلود شوند.

این بدان معنی است که در صورت نبود اتصال اینترنتی به سایت یا اپلیکیشن مربوطه دسترسی نخواهیم داشت.

ذخیره سازی سمت کلاینت

این همان جایی است که Service worker و همراه همیشگی آن Cache API وارد عمل می‌شوند.

به بیان ساده Service worker یک فایل جاوا اسکریپت است که هنگام دسترسی مرورگر روی یک منشأ خاص (وب‌سایت، یا بخشی از وب‌سایت در دامنه خاص) ثبت می‌شود. Service worker پس از ثبت شدن می‌تواند صفحه‌های موجود روی آن منشأ را کنترل کند. این کار از طریق قرار گرفتن بین یک صفحه بارگذاری شده و شبکه انجام می‌یابد و درخواست‌های شبکه با هدف آن دامنه را تفسیر می‌کند.

زمانی که یک درخواست از سوی Service worker تفسیر شود می‌تواند هر کاری که دوست دارد با آن بکند، اما مثال کلاسیک آن ذخیره‌سازی پاسخ‌های شبکه به صورت آفلاین است و سپس این پاسخ‌ها را به جای پاسخ‌های شبکه در زمان قطع شدن ارائه می‌کند. در واقع Service worker همواره باعث می‌شود که یک وب‌سایت بتواند به صورت کاملاً آفلاین کار کند.

Cache API یک ساز و کار دیگر برای ذخیره‌سازی سمت کلاینت است که تفاوت اندکی دارد. این ساز و کار برای ذخیره پاسخ‌های HTTP طراحی شده و به همین جهت با Service worker به خوبی کار می‌کند.

نکته: Service worker-ها و Cache API در اغلب مرورگرهای مدرن پشتیبانی می‌شوند. در زمان نگارش این مقاله Safari مشغول پیاده‌سازی آن است، اما به زودی در این مرورگر نیز ارائه می‌شود.

مثالی از یک Service worker

در این بخش به بررسی یک مثال می‌پردازیم تا ایده‌ای کلی از یک Service worker به دست بدهیم. ما نسخه دیگری از مثال ذخیره ویدئویی که در بخش قبلی دیدیم می‌سازیم که همان کارکرد را دارد، اما این بار فایل‌های HTML ،CSS و JavaScript را در Cache API از طریق service worker ذخیره می‌کند و بدین ترتیب مثال ما می‌تواند به صورت آفلاین کار کند. برای مشاهده کد این مثال به این ریپوی گیت‌هاب (+) مراجعه کنید. در این صفحه (+) نیز نسخه اجرایی را ملاحظه کنید.

ثبت Service worker

نخستین چیزی که باید توجه کنیم این است که در فایل جاوا اسکریپت (به نام index.js) کد بیشتری قرار گرفته است. ابتدا تست تشخیص قابلیت را اجرا می‌کنیم تا ببینیم آیا عضو serviceWorker در شیء Navigator موجود است یا نه. اگر پاسخ مثبت باشد، می‌دانیم که دست کم امکان پشتیبانی مقدماتی از Service worker وجود دارد. درون این کد از متد ()ServiceWorkerContainer.register برای ثبت یک Service worker درون فایل sw.js در برابر منشائی که در آن قرار دارد استفاده می‌کنیم، به این ترتیب می‌توانیم صفحاتی را در همان دایرکتوری و زیردایرکتوری‌های آن کنترل کنیم. زمانی که promise برآورده می‌شود، Service worker ثبت شده است.

1// Register service worker to control making site work offline
2
3  if('serviceWorker' in navigator) {
4    navigator.serviceWorker
5             .register('/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js')
6             .then(function() { console.log('Service Worker Registered'); });
7  }

نکته: مسیر مفروض برای فایل sw.js به صورت نسبی با توجه به منشأ سایت تعریف شده است و نه فایل جاوا اسکریپت که شامل کد است. service worker در آدرس زیر قرار دارد:

 https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js

منشأ به صورت https://mdn.github.io است و از این رو مسیر مفروض باید به صورت زیر باشد:

 /learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js

اگر می‌خواهید این مثال را روی سرور خود میزبانی کنید، باید این مورد را بر همین اساس تغییر دهید. این مسئله کمی سردرگم‌کننده است، اما به دلایل امنیتی چنین طراحی شده است.

نصب Service worker

دفعه بعد که به هر صفحه‌ای زیر دامنه تحت کنترل service worker دسترسی پیدا کنید، service worker روی آن صفحه نصب می‌شود، یعنی شروع به کنترل کردن آن می‌کند. زمانی که این اتفاق بیفتد، یک رویداد install در برابر service worker ارائه می‌شود و می‌توانید کدی درون خود service worker بنویسید که به نصب پاسخ می‌دهد.

در ادامه به بررسی مثالی در فایل sw.js می‌پردازیم. چنان که مشاهده خواهید کرد، شنونده install در برابر self ثبت می‌شود. این کلیدواژه Self روشی برای اشاره به دامنه سراسری service worker از درون فایل service worker است.

درون دستگیره install از متد ()ExtendableEvent.waitUntil استفاده می‌کنیم که روی شیء رویداد موجود است تا اعلام کنیم که مرورگر نباید تا زمانی که promise درون آن با موفقیت برآورده نشده است، نصب شود.

در ادامه Cache API را در عمل مشاهده می‌کنیم. ما از متد ()CacheStorage.open برای باز کردن شیء کش جدید استفاده می‌کنیم که می‌توانیم پاسخ‌ها را در آن ذخیره کنیم. این promise با یک شیء Cache برآورده می‌شود که نماینده کش video-store است. سپس از متد ()Cache.addAll برای واکشی یک سری از فایل‌ها و افزودن پاسخ آن‌ها به کش استفاده می‌کنیم.

1self.addEventListener('install', function(e) {
2 e.waitUntil(
3   caches.open('video-store').then(function(cache) {
4     return cache.addAll([
5       '/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/',
6       '/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html',
7       '/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js',
8       '/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css'
9     ]);
10   })
11 );
12});

بدین ترتیب فرایند نصب به پایان می‌رسد.

پاسخ‌دهی به درخواست‌های بیشتر

اینک که service worker ثبت و روی صفحه HTML نصب شد و فایل‌های مرتبط همگی به کش اضافه شدن تقریباً آماده استفاده از این امکان هستیم. تنها یک کار دیگر مانده که انجام دهیم و آن نوشتن کدی است که به درخواست‌های آتی شبکه پاسخ دهد.

این دومین بخش کد در مستندات sw.js است. ما شنونده دیگری به دامنه سراسری service worker اضافه می‌کنیم که تابع دستگیره را هنگامی که رویداد fetch رخ می‌دهد اجرا می‌کند. این رویداد هر بار که مرورگر یک درخواست به فایل موجود در آن دایرکتوری که service worker روی آن ثبت شده ارسال می‌کند اتفاق خواهد افتد.

درون دستگیره از استفاده می‌کنیم تا بررسی کنیم که آیا درخواست مطابقی می‌توان در کش یافت یا نه. در صورت پیدا شدن یک مورد مطابق، این promise با پاسخ مطابق برآورده می‌شود و در غیر این صورت مقدار undefined بازگشت می‌یابد.

اگر مورد مطابقی پیدا شود، کافی است آن را به عنوان یک پاسخ سفارشی بازگشت دهیم. در غیر این صورت با ()fetch پاسخ را از شبکه واکشی کرده و آن را بازگشت می‌دهیم.

1self.addEventListener('fetch', function(e) {
2  console.log(e.request.url);
3  e.respondWith(
4    caches.match(e.request).then(function(response) {
5      return response || fetch(e.request);
6    })
7  );
8});

بدین ترتیب service worker ساده ما به پایان می‌رسد. البته کارهای زیاد دیگری با آن می‌توان انجام داد که توصیه می‌کنیم برای کسب اطلاعات بیشتر راهنماهای مختلفی که در این زمینه وجود دارد را مطالعه کنید.

تست کردن مثال آفلاین

برای تست کردن مثال service worker باید آن را چند بار بارگذاری کنید تا مطمئن شوید که نصب شده است. زمانی که این اتفاق افتاد می‌توانید:

  • ارتباط اینترنتی خود را قطع کنید.
  • اگر از فایرفاکس استفاده می‌کنید به منوی File > Work Offline بروید.
  • در صورتی که از کروم استفاده می‌کنید، به devtools بروید و سپس به بخش Application > Service Workers بروید و کادر تیک Offline را بررسی کنید.

در این زمان اگر صفحه مثال را رفرش کنید، می‌بینید که به درستی بارگذاری می‌شود. همه چیز به صورت آفلاین ذخیره شده است و فایل‌های صفحه در کش مرورگر قرار دارد و ویدئوها نیز در پایگاه داده IndexedDB ذخیره شده‌اند.

سخن پایانی

بدین ترتیب به پایان این راهنمای نسبتاً طولانی رسیدیم، در این راهنما با مفاهیم ذخیره‌سازی سمت کلاینت در جاوا اسکریپت آشنا شدیم و فناوری‌های مفید این حوزه را مورد بررسی تفصیلی قرار دادیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۸ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
developer.mozilla
۳ دیدگاه برای «ذخیره سازی سمت کلاینت در جاوا اسکریپت — راهنمای جاوا اسکریپت»

نزدیک به 16 روز جستجو ، به مقاله شما رسیدم. خدا قوت خیر ببینید ، دمتون گرم.

تشکر از شما بابت مطالب ارزندتون خیلی زحمت کشیدین.ان شاءالله موفق باشید.

باسلام

“آب در کوزه و ما تشنه لبان گرد جهان میگردیم”

واقعا از شما سپاسگزارم
بدنبال این مطالب ساعتها وقت گذاشتم و فیلم آموزشی دیدم ولی موفق به یافتن سوالاتم نمیشدم ولی در این مقاله آموزشی شما، من بطور کامل به جوابهای خودم رسیدم
دستمریزاد سنگ تمام گذاشتید

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *