برنامه نویسی 12 بازدید

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

پیش‌نیاز مطالعه این مقاله آشنایی با مبانی جاوا اسکریپت و 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 به ما امکان می‌دهد که آیتم‌های داده را در ذخیره کنیم و دو پارامتر به صورت نام آیتم و مقدار آن می‌گیرد. کد زیر را در کنسول جاوا اسکریپت مرورگر خود وارد کنید (در صورت تمایل می‌توانید نام خود را وارد کنید):

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

شروع

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

فایل index.html

فایل style.css

فایل index-start.js

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ابتدا محتوای عنصر <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 اجرا می‌شود که به صورت زیر است:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

در مثال فوق تقریباً همه جوانب ذخیره‌سازی فایل‌های بزرگ در یک پایگاه داده 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 ثبت شده است.

نکته: مسیر مفروض برای فایل 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 برای واکشی یک سری از فایل‌ها و افزودن پاسخ آن‌ها به کش استفاده می‌کنیم.

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

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

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

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

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

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

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

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

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

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

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

سخن پایانی

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

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

==

به عنوان حامی، استارتاپ، محصول و خدمات خود را در انتهای مطالب مرتبط مجله فرادرس معرفی کنید.

telegram
twitter

میثم لطفی

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

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

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