سرعت یکی از مهم ترین فاکتورها در دنیای وب امروز است و یکی از روش های بالاتر بردن سرعت سایت منتقل کردن اطلاعات کمتر است. بسیاری از داده هایی که امروزه بین مرورگر و سرور رد و بدل می شوند اضافی هستند و می توانند در مرورگر خود کاربر ذخیره شوند چرا که اطلاعاتی حساس نیستند. مثلا انتخاب تم روشن یا تیره توسط کاربر بهتر است در همان مرورگر کاربر ذخیره شود نه اینکه از سمت سرور ارسال شود.
به طور مثال Performance API را در نظر بگیرید. این یک API در جاوا اسکریپت است که به شما اجازه می دهد زمان را با دقت بالا اندازه گیری کنید و معمولا از آن برای سنجش سرعت یک سایت استفاده می شود. فرض کنید شما می خواهید زمان لازم برای اجرای یک رویداد خاص در مرورگر را اندازه گیری کنید. فرض کنید شما چنین کاری را انجام داده اید و به محض آماده شدن داده ها آن ها را به سمت سرور خود ارسال می کنید. با انجام چنین کاری سرعت سایت خود را بسیار کاهش می دهید. بهتر است داده های آماده شده را در مرورگر کاربر ذخیره کنید و سپس با استفاده از Web Worker ها نتایج را بعدا به سمت سرور ارسال نمایید.
برای ذخیره داده ها در مرورگر کاربر دو API توسط جاوا اسکریپت ارائه شده است:
IndexedDB در سال ۲۰۱۱ معرفی و در سال ۲۰۱۵ به یک استاندارد وب (W3C) تبدیل شد بنابراین از نظر پشتیبانی مرورگر ها هیچ مشکلی نخواهید داشت.
سطح مقاله: این مقاله برای افرادی در نظر گرفته شده است که با زبان جاوا اسکریپت و کار با آن در مرورگر آشنا هستند.
ما می خواهیم در این مقاله سیستمی برای اندازه گیری زمان بارگذاری صفحات و عناصر مختلف سایت بسازیم بنابراین باید قبل از آن با برخی از اصطلاحات IndexedDB آشنا شویم:
ما در این پروژه یک پایگاه داده به نام performance را طراحی می کنیم که دو object store دارد. یادتان باشد که object store معادل جدول در MySQL است بنابراین من برای ساده تر شدن درک شما در این مقاله با نام جدول به آن اشاره می کنم.
در طول این پروژه می توانید از سربرگ Application در مرورگر کروم یا سربرگ Storage در مرورگر فایرفاکس اطلاعات ذخیره شده در مرورگرتان را مشاهده کنید.
من در ابتدا پوشه ای به نام IndexedDB-demo را می سازم و سپس درون آن دو پوشه css و js را ایجاد می کنم. درون پوشه js یک فایل به نام indexeddb.js را تعریف می کنیم. این فایل باید حاوی کلاسی برای کار با پایگاه داده باشد:
export class IndexedDB { constructor(dbName, dbVersion, dbUpgrade) { return new Promise((resolve, reject) => { // شیء اتصال this.db = null; // بررسی سازگاری و پشتیبانی if (!("indexedDB" in window)) reject("not supported"); // باز کرده پایگاه داده const dbOpen = indexedDB.open(dbName, dbVersion); if (dbUpgrade) { // به روز رسانی پایگاه داده dbOpen.onupgradeneeded = e => { dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion); }; } dbOpen.onsuccess = () => { this.db = dbOpen.result; resolve(this); }; dbOpen.onerror = e => { reject(`IndexedDB error: ${e.target.errorCode}`); }; }); } }
در ابتدا constructor این کلاس را داریم که سه پارامتر می گیرد: dbName (نام پایگاه داده)، dbVersion (نسخه پایگاه داده)، dbUpgrade (تابعی برای به روز رسانی پایگاه داده). در قدم اول یک promise را داریم که برگردانده می شود. چرا؟ به دلیل اینکه indexedDB به صورت پیش فرض از callback ها استفاده می کند که قدیمی شده اند. من می خواهم کد هایمان را درون یک promise قرار بدهم تا بتوانیم از دستورات جدید تر مانند async/await نیز استفاده کنیم.
ابتدا یک خصوصیت به نام db را برای این کلاس تعریف می کنیم که مقدارش فعلا روی null است. در مرحله بعدی بررسی می کنیم که آیا indexedDB در شیء window وجود دارد یا خیر. اگر وجود نداشت یعنی مرورگر کاربر از indexedDB پشتیبانی نمی کند بنابراین promise را کاملا reject می کنیم و کار دیگری انجام نمی دهیم. در غیر این صورت متد open را روی شیء indexedDB صدا می زنیم که باعث متصل شدن ما به پایگاه داده می شود. این متد نام پایگاه داده و نسخه آن را دریافت می کند و ما هم آن ها را پاس داده ایم. متغیر dbOpen همان اتصال شما به پایگاه داده است و مقدارش یکی از حالت های زیر است:
ما در هنگام فراخوانی متد open نسخه پایگاه داده را نیز به آن پاس داده ایم که باید یک عدد صحیح باشد. این عدد یک عدد دلخواه است که توسط خودمان تعیین می شود بنابراین اگر نسخه پایگاه داده از نسخه پاس داده شده توسط ما کمتر باشد، یک رویداد به نام upgradeneeded اجرا می شود. در حال حاضر پایگاه داده ما هنوز تعریف نشده است بنابراین نسخه آن به صورت پیش فرض صفر است.
در مرحله بعدی گفته ام اگر نیاز به آپدیت کردن نسخه پایگاه داده داریم از متد onupgradeneeded کمک می گیریم که رویداد به روز رسانی (e) را به صورت خودکار به تابع ما پاس می دهد. همچنین رویداد onsuccess را تعریف کرده ایم تا اگر به موفقیت به پایگاه داده متصل شدیم خصوصیت db را برابر با dbOpen.result قرار داده و سپس this (نمونه کلاس فعلی) را به عنوان نتیجه promise برمی گردانیم. در نهایت رویداد onerror را داریم که در صورت موفقیت آمیز نبودن اتصال به پایگاه داده اجرا می شود. در این حالت با یک پیام خطای ساده promise را رد کرده ایم.
حالا در پوشه js یک فایل دیگر به نام performance.js را تعریف می کنیم که از کلاس بالا استفاده می کند:
// performance.js import { IndexedDB } from "./indexeddb.js"; window.addEventListener("load", async () => { // اتصال به پایگاه داده const perfDB = await new IndexedDB( "performance", 1, (db, oldVersion, newVersion) => { console.log(`upgrading database from ${oldVersion} to ${newVersion}`); switch (oldVersion) { case 0: { const navigation = db.createObjectStore("navigation", { keyPath: "date", }), resource = db.createObjectStore("resource", { keyPath: "id", autoIncrement: true, }); resource.createIndex("dateIdx", "date", { unique: false }); resource.createIndex("nameIdx", "name", { unique: false }); } } } ); // در این بخش کد های بیشتری اضافه خواهیم کرد });
همانطور که می بینید ابتدا منتظر بارگذاری صفحه می مانیم (addEventListener) و پس از آنکه صفحه بارگذاری شد یک نمونه جدید از کلاس IndexedDB را ایجاد می کنیم. اگر یادتان باشد باید ابتدا نام پایگاه داده و سپس نسخه آن و نهایتا متد به روز رسانی آن را به این کلاس پاس بدهیم. من نام پایگاه داده را performance گذاشته و نسخه آن را نیز ۱ تعیین کرده ام اما برای متد به روز رسانی چطور؟
متد به روز رسانی ابتدا اتصال پایگاه داده و سپس نسخه قبلی پایگاه داده و سپس نسخه جدید آن را دریافت می کند. ما این سه را خودمان در فایل indexeddb.js پاس داده بودیم. حالا از یک دستور switch استفاده می کنیم تا ببنیم اگر نسخه پایگاه داده صفر باشد (یعنی پایگاه داده هنوز ساخته نشده باشد) آن را بسازیم. متد createObjectStore یک object store یا مخزن شیئی می سازد که معادل جدول در MySQL است بنابراین من دو جدول را ساخته ام (در ابتدای مقاله در اینباره توضیح داده بودم). این متد دو پارامتر می گیرد:
در مرحله بعدی با استفاده از متدهای createIndex باید ایندکسی را برای پایگاه داده خود بسازیم. چرا؟ به دلیل اینکه در حالت ساده فقط می توانیم با استفاده از کلید ها در داده ها جست و جو کنیم اما با ایندکس کردن فیلد ها می توانیم با استفاده از مقادیر دیگر نیز جست و جوی خودمان را انجام بدهیم. متد createIndex می تواند سه پارامتر دریافت کند:
name: نام دلخواه شما برای ایندکس. keyPath: فیلدی از object store که باید ایندکس شود. ما با استفاده از همین فیلد جست و جو خواهیم کرد. option: یک شیء غیر اجباری که می تواند دو خصوصیت را مشخص کند. ابتدا unique که به معنای یکتا و غیر تکراری بودن ایندکس است. اگر این مقدار را روی true بگذارید و بعدا بخواهید ستونی تکراری در آن ایندکس قرار بدهید یک خطا دریافت می کنید. multiEntry نیز خصوصیت بعدی است. اگر keyPath شما یک آرایه باشد کل آن آرایه به صورت پیش فرض ایندکس خواهد بود اما اگر خصوصیت multiEntry را روی true قرار بدهید تک تک اعضای آن آرایه ایندکس خواهند بود.
حالا هر برنامه ای که صفحه را باز کند از یک نسخه یکسان از پایگاه داده استفاده می کند مگر آنکه کاربر برنامه را در دو سربرگ (tab) جداگانه باز کرده باشد. حالا در پوشه اصلی پروژه یک فایل به نام index.html ایجاد می کنیم و فایل performance.js را به آن اضافه می کنیم:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>IndexedDB performance store</title> <meta name="viewport" content="width=device-width,initial-scale=1" /> <link rel="stylesheet" href="./css/main.css" /> <script type="module" src="./js/performance.js"></script> </head> <body> <h1>IndexedDB performance test</h1> </body> </html>
حالا این صفحه را در مرورگر خود باز کنید. زمانی که این کار را انجام دادید باید پایگاه داده برایتان ساخته شده باشد. من برای تست این موضوع به قسمت developer tools و سپس سربرگ Application در کروم می روم.
در پوشه css یک فایل جدید به نام main.css را تعریف کنید که محتوای ساده زیر را داشته باشد:
body { font-family: sans-serif; }
من می خواهم متوسط زمان لازم برای دانلود این فایل را اندازه گیری کنم بنابراین اصلا مهم نیست که واقعا آن را پر از استایل های CSS کنیم. فعلا برای تست فقط همین کد ساده را درون آن می نویسیم.
حالا به کلاس IndexedDB برمی گردیم تا متدهایی را در آن بنویسیم. این متدها بعدا در فایل performance.js استفاده می شوند و کار ما را ساده تر می کنند. اولین متد یک getter ساده است که اتصال شما به پایگاه داده (indexedDB) را برمی گرداند:
get connection() { return this.db; }
متد بعدی ما برای ثبت موارد جدید در پایگاه داده است. از آنجایی که ثبت و به روز رسانی در IndexedDB یکی است من نام این متد را update گذاشته ام:
// ثبت آیتم های جدید update(storeName, value, overwrite = false) { return new Promise((resolve, reject) => { // تراکنش جدید const transaction = this.db.transaction(storeName, 'readwrite'), store = transaction.objectStore(storeName); // اطمینان از اینکه مقادیر درون یک آرایه هستند value = Array.isArray(value) ? value : [ value ]; // ثبت تمام مقادیر در پایگاه داده value.forEach(v => { if (overwrite) store.put(v); else store.add(v); }); transaction.oncomplete = () => { resolve(true); // عملیات موفق بوده است }; transaction.onerror = () => { reject(transaction.error); // عملیات با شکست مواجه شده است }; }); }
متد بالا سه پارامتر را می گیرد: storeName که نام یا همان کلیدی مقدار شما است. value که خود مقدار است و overwrite که به صورت پیش فرض روی false بوده و وظیفه اش این است که مشخص کند آیا اگر کلید یکسانی از قبل وجود داشت آن را جایگزین کنیم (آن کلید با یک مقدار جدید به روز رسانی شود یا خیر).
نکته: تمام عملیات های IndexedDB باید در قالب تراکنش انجام شوند.
در مرحله بعدی یک تراکنش یا transaction را داریم. این متد مسئول ساخت یک شیء تراکنش است و یک یا چند object store را به همراه سطح دسترسی به آن ها تعریف می کند. متد transaction دو پارامتر می گیرد. اولین پارامتر نام store ای است که تراکنش باید به آن متصل شود و پارامتر دوم نوع دسترسی به آن store است. ما در تراکنش ها دو حالت دسترسی داریم: حالت readonly که فقط برای خواندن داده ها است و حالت readwrite که برای خواندن و نوشتن داده ها است.
در مرحله بعدی با استفاده از متد objectStore یک object store را برای کار در تراکنش انتخاب می کنیم. طبیعتا همان پارامتر اول متد خودمان (storeName) را به آن پاس داده ایم. با این کار اعلام کرده ایم که تراکنش ما قرار است روی این object store اجرا شود. در مرحله بعدی مقداری که باید ثبت شود (value - پارامتر دوم) را بررسی کرده ایم تا آرایه باشد. این مسئله انتخاب شخصی من است و شما ملزم به ثبت آرایه ها نیستید بلکه می توانید از اشیاء نیز استفاده کنید. من آرایه ها را انتخاب کرده ام چرا که ممکن است بخواهم چند مقدار را یکجا ثبت کنم و گردش روی آرایه ها آسان تر از گردش روی اشیاء است.
حالا با استفاده از یک متد forEach روی value گردش می کنیم و تک تک مقادیر درون آن را در پایگاه داده ثبت می کنیم. برای این کار دو متد وجود دارد: متد put بررسی می کند که قبلا چنین کلیدی وجود نداشته باشد. اگر قبلا چنین کلیدی وجود داشت، به جای ثبت مقدار جدید همان مقدار قبلی را با مقدار جدید جایگزین می کند که یک update یا به روز رسانی است اما اگر کلید وجود نداشت یک مقدار جدید را در پایگاه داده ثبت می کند. از طرفی متد add هیچ گاه به روز رسانی انجام نمی دهد و بدون توجه به دیگر ردیف ها فقط یک مقدار جدید را در پایگاه داده ثبت خواهد کرد. اینجاست که پارامتر سوم (overwrite) تعیین کننده خواهد بود.
توجه داشته باشید که تمام این عملیات ها درون یک promise انجام شده است. چرا؟ همانطور که گفتم من می خواهم از ساختار های جدیدتر async/await استفاده کنم بنابراین کد ها را درون یک promise گذاشته ام. در نهایت دو رویداد را خواهیم داشت. رویداد oncomplete که در صورت موفقیت آمیز بودن تراکنش اجرا می شود و رویداد onerror که در صورت شکست تراکنش اجرا خواهد شد. در حالت اول promise را resolve کرده ایم و در حالت دوم آن را به همراه متن خطا reject کرده ایم.
متد بعدی ما یک تراکنش برای خواندن داده ها از پایگاه داده است. این متد از object store یا از یک ایندکس خاص شروع به خواندن داده ها می کند:
index(storeName, indexName) { const transaction = this.db.transaction(storeName), store = transaction.objectStore(storeName); return indexName ? store.index(indexName) : store; }
در ابتدا با استفاده از متد transaction یک تراکنش را شروع کرده و نام store مورد نظرمان را به آن پاس داده ایم. در مرحله بعدی object store خودمان برای آن تراکنش را نیز مشخص کرده ایم. حالا اگر نام ایندکس (indexName - پارامتر دوم) پاس داده شده باشد متد index را روی store صدا می زنیم و آن را برمی گردانیم، در غیر این صورت خود store را برمی گردانیم. این متد یک متد کمکی است بنابراین کاربرد آن را در ادامه خواهید دید.
bound(lowerBound, upperBound) { let bound; if (lowerBound && upperBound) bound = IDBKeyRange.bound(lowerBound, upperBound); else if (lowerBound) bound = IDBKeyRange.lowerBound(lowerBound); else if (upperBound) bound = IDBKeyRange.upperBound(upperBound); return bound; }
در جاوا اسکریپت یک شیء سراسری به نام IDBKeyRange وجود دارد که متدی به نام bound را روی خود دارد. این متد یک key range را تولید کرده و به ما می دهد. key range ها در پایگاه داده IndexedDB به «کلید های بازه» مشهور هستند چرا که یک بازه را مشخص می کنند. برخی از متدهای IndexedDB می توانند یک بازه خاص را به عنوان فیلتر قبول کنند و نتایج را فقط در آن بازه خاص نمایش بدهند.
متد بعدی ما شمردن داده های خاص در پایگاه داده است. من نام این متد را count گذاشته ام:
// شمارش آیتم ها count(storeName, indexName, lowerBound = null, upperBound = null) { return new Promise((resolve, reject) => { const request = this.index(storeName, indexName) .count( this.bound(lowerBound, upperBound) ); request.onsuccess = () => { resolve(request.result); // برگرداندن تعداد شمرده شده }; request.onerror = () => { reject(request.error); }; }); }
برای اجرای این متد نیاز به چند پارامتر متفاوت داریم. اولا نام store ای را می خواهیم که باید در آن شمارش را انجام بدهیم. من نام storeName را برایش انتخاب کرده ام که پارامتر اول ما است. در مرحله بعدی باید نام ایندکس را داشته باشیم که indexName و پارامتر دوم ما است. در ضمن ما می توانیم بازه شمارش را محدود کنیم. به همین دلیل پارامتر های سوم (lowerBound) و چهارم (upperBound) را داریم که به ترتیب پایین ترین و بالاترین حد شمارش را مشخص می کنند.
ما در این متد از متد index استفاده کرده ایم تا آن ایندکس خاص را از store مورد نظرمان دریافت کنیم. توجه کنید که ایندکس های ما یکتا نیستند (در فایل performance.js خصوصیت unique را روی false گذاشتیم). در مرحله بعدی متد count را روی آن صدا زده ایم تا آن ایندکس شروع به شمارش کند. ما می توانیم متد bound را نیز به متد count پاس بدهیم تا بازه شمارش نیز تعیین شود. مثل همیشه دو رویداد onsuccess و onerror را نیز داریم.
متد بعدی ما برای دریافت یک آیتم با استفاده از cursor است. اگر با پایگاه های داده ای مانند mongodb کار کرده باشید حتما با مفهوم cursor آشنایی دارید. تصور کنید که به دنبال حجم نسبتا بزرگی از داده هستید. به جای اینکه تمام این داده ها را از پایگاه داده به صورت یکجا دریافت کنید می توانید فقط بخش مهمی از آن را بگیرید، آن بخش را فیلتر کرده و نهایتا داده های مورد نظرتان را دریافت کنید. یا مثلا اگر می خواهید ۱۰۰ داده مختلف از پایگاه داده را بررسی کنید چطور؟ آیا عاقلانه است که ۱۰۰ آیتم را یکجا در مموری برنامه بگیریم؟ قطعا خیر چرا که دریافت این حجم از داده به صورت یکجا پایگاه داده را شدیدا درگیر می کند. بنابراین یک cursor به ما داده می شود که به ما اجازه می دهد به صورت تک به تک روی این ۱۰۰ آیتم حرکت کنیم. بدین صورت علاوه بر درگیر نکردن پایگاه داده، مموری سیستم را نیز اشغال نمی کنیم.
fetch(storeName, indexName, lowerBound = null, upperBound = null, callback) { const request = this.index(storeName, indexName) .openCursor( this.bound(lowerBound, upperBound) ); // اجرای کالبک با مقدار فعلی request.onsuccess = () => { if (callback) callback(request.result); }; request.onerror = () => { return(request.error); // عملیات با شکست مواجه شده است }; }
همانطور که از ساختار متد بالا متوجه شده اید، cursor ها نیز می توانند حد بالا و پایین داشته باشند (lowerBound و upperBound). ما می توانیم با استفاده از متد index به ایندکس های مورد نظرمان دسترسی داشته باشیم و سپس با متد openCursor یک cursor را روی آن ها باز کنیم. حالا آیتم فعلی را به callback پاس داده شده (پارامتر پنجم) ارسال می کنیم.
با این حساب تمام کد های فایل indexeddb.js باید بدین شکل باشد:
// IndexedDB wrapper class export class IndexedDB { // connect to IndexedDB database constructor(dbName, dbVersion, dbUpgrade) { return new Promise((resolve, reject) => { // connection object this.db = null; // no support if (!("indexedDB" in window)) reject("not supported"); // open database const dbOpen = indexedDB.open(dbName, dbVersion); if (dbUpgrade) { // database upgrade event dbOpen.onupgradeneeded = e => { dbUpgrade(dbOpen.result, e.oldVersion, e.newVersion); }; } dbOpen.onsuccess = () => { this.db = dbOpen.result; resolve(this); }; dbOpen.onerror = e => { reject(`IndexedDB error: ${e.target.errorCode}`); }; }); } // return database connection get connection() { return this.db; } // store item update(storeName, value, overwrite = false) { return new Promise((resolve, reject) => { // new transaction const transaction = this.db.transaction(storeName, "readwrite"), store = transaction.objectStore(storeName); // ensure values are in array value = Array.isArray(value) ? value : [value]; // write all values value.forEach(v => { if (overwrite) store.put(v); else store.add(v); }); transaction.oncomplete = () => { resolve(true); // success }; transaction.onerror = () => { reject(transaction.error); // failure }; }); } // count items count(storeName, indexName, lowerBound = null, upperBound = null) { return new Promise((resolve, reject) => { const request = this.index(storeName, indexName).count( this.bound(lowerBound, upperBound) ); request.onsuccess = () => { resolve(request.result); // return count }; request.onerror = () => { reject(request.error); }; }); } // get items using cursor fetch(storeName, indexName, lowerBound = null, upperBound = null, callback) { const request = this.index(storeName, indexName).openCursor( this.bound(lowerBound, upperBound) ); // run callback with current value request.onsuccess = () => { if (callback) callback(request.result); }; request.onerror = () => { return request.error; // failure }; } // start a new read transaction on object store or index index(storeName, indexName) { const transaction = this.db.transaction(storeName), store = transaction.objectStore(storeName); return indexName ? store.index(indexName) : store; } // get bounding object bound(lowerBound, upperBound) { let bound; if (lowerBound && upperBound) bound = IDBKeyRange.bound(lowerBound, upperBound); else if (lowerBound) bound = IDBKeyRange.lowerBound(lowerBound); else if (upperBound) bound = IDBKeyRange.upperBound(upperBound); return bound; } }
کلاس indexeddb کلاسی کمکی برای استفاده از indexedDB بود تا هنگام صدا زدن آن راحت تر باشیم. کار اصلی اندازه گیری ما درون فایل performance.js اتفاق می افتد بنابراین به این فایل رفته و کد هایش را کامل می کنیم:
import { IndexedDB } from './indexeddb.js'; window.addEventListener('load', async () => { const perfDB = await new IndexedDB('performance', 1, (db, oldVersion, newVersion) => { console.log(`upgrading database from ${ oldVersion } to ${ newVersion }`); switch (oldVersion) { case 0: { const navigation = db.createObjectStore('navigation', { keyPath: 'date' }), resource = db.createObjectStore('resource', { keyPath: 'id', autoIncrement: true }); resource.createIndex('dateIdx', 'date', { unique: false }); resource.createIndex('nameIdx', 'name', { unique: false }); } } }); if (!('performance' in window) || !perfDB) return; // بقیه کد ها
همانطور که قبلا هم توضیح داده بودم ما در ابتدا با یک event listener منتظر بارگذاری اولیه صفحه می مانیم. زمانی که صفحه بارگذاری شد ابتدا نسخه صفر از پایگاه داده خود را می سازیم که همان راه اندازی اولیه است (ساخت object store و ایندکس ها و الی آخر). در مرحله بعدی با استفاده از یک شرط if چک می کنم که performance در شیء سراسری window باشد. چرا؟ به دلیل اینکه می خواهیم از performance (یکی از API های جاوا اسکریپت) برای اندازه گیری زمان استفاده کنیم بنابراین مرورگر کاربر باید از آن پشتیبانی کند. اگر اینطور نبود یا مرورگر کاربر از IndexedDB پشتیبانی نمی کرد (شیء perfDB خالی یا دارای خطا بود) فقط return می کنیم تا از event listener خارج شویم.
در مرحله بعدی مطمئن می شویم که همه چیز توسط مرورگر کاربر پشتیبانی می شود بنابراین شروع به ثبت اطلاعات مرورگر می کنیم:
// ثبت اطلاعات صفحه در پایگاه داده const date = new Date(), nav = Object.assign( { date }, performance.getEntriesByType('navigation')[0].toJSON() ); await perfDB.update('navigation', nav);
در ابتدا تاریخ روز را در شیء date ذخیره کرده ایم، سپس این تاریخ را به همراه اطلاعات جا به جایی در صفحات در شیء nav ذخیره می کند و نهایتا آن را با استفاه از متد update در پایگاه داده ثبت می کنیم. در ضمن متد getEntriesByType یک شیء PerformanceEntry را به شما برمی گرداند. این شیء مجموعه ای از جوانب مختلف برای اندازه گیری را در خود دارد که ما از بین آن navigation (یعنی جا به جایی بین صفحات) را انتخاب کرده ایم تا اطلاعات مربوط به گردش در صفحات مختلف برایمان ثبت شود. این داده ها در اولین object store به نام navigation ذخیره می شوند.
در مرحله بعدی باید داده های مربوط به بارگذاری عناصر صفحه را در object store دیگرمان به نام resources ثبت کنیم:
const res = performance.getEntriesByType('resource').map( r => Object.assign({ date }, r.toJSON()) ); await perfDB.update('resource', res);
این بار اطلاعات مربوط به resource را دریافت کرده ایم و با تابع map روی تک تک آن ها گردش کرده ایم. در هر گردش تاریخ را به همراه آن داده ها در شیء res ذخیره کرده ایم. در نهایت این شیء را به متد update داده ایم تا در پایگاه داده ثبت شود. حالا باید سه دستور log را داشته باشیم تا تعداد منابع برای هر کدام را چاپ کنیم:
console.log('page navigation records: ', await perfDB.count('navigation')); console.log('resource records: ', await perfDB.count('resource')); console.log('page load times during 2021:');
من این اکر را به صورت سلیقه ای انجام داده ام، طبیعتا شما می توانید این کار را انجام ندهید.
حالا اشیاء مربوط به جا به جایی در صفحات را دریافت کرده کرده و نمایش می دهیم:
perfDB.fetch( 'navigation', null, // بدون ایندکس new Date(2021,0,1,10,40,0,0), // حد پایین new Date(2021,11,1,10,40,0,0), // حد بالا cursor => { // کالبک انتخابی ما if (cursor) { console.log(`${ cursor.value.date }: ${ cursor.value.domContentLoadedEventEnd }`); cursor.continue(); } } );
اگر یادتان باشد متد fetch یک cursor را به ما می داد. با صدا زدن متد continue روی این cursor می توانید به نتیجه بعدی بروید. ما نیز در این بخش همین کار را کرده ایم و اطلاعات ثبت شده در مرورگر کاربر در سال ۲۰۲۱ را دریافت کرده ایم (ابتدا و انتهای سال ۲۰۲۱ را به عنوان حد پایین و بالای بازه مشخص کرده ایم). در نهایت اگر در این بازه مقداری وجود داشته باشد (cursor داشته باشیم) مقادیر آن را log کرده ایم و سپس با صدا زدن continue به داده بعدی می رویم.
در نهایت اگر بخواهیم متوسط زمان دانلود فایل main.css را محاسبه کنیم باید باز هم از fetch استفاده کرده و زمان مورد نظر را log می کنیم. به کد زیر توجه نمایید:
let filename = 'http://localhost:8888/css/main.css', count = 0, total = 0; perfDB.fetch( 'resource', // object store 'nameIdx', // index filename, // matching file filename, cursor => { // callback if (cursor) { count++; total += cursor.value.duration; cursor.continue(); } else { // all records processed if (count) { const avgDuration = total / count; console.log(`average duration for ${ filename }: ${ avgDuration } ms`); } } });
ابتدا متغیر filename را داریم که آدرس کامل فایل من است. از آنجایی که من این فایل را روی سیستم خودم و در پورت ۸۸۸۸ اجرا می کنم چنین آدرسی را به آن داده ام اما شما باید آدرس دقیق خودتان را بدهید. سپس متغیر های count و total را داریم که به ترتیب تعداد داده های پایگاه داده و زمان کل برای دانلود آن را در نظر می گیرد.
من با استفاده از fetch دوباره شروع به گردش بین نتایج کرده ام و تا زمانی که cursor را داشته باشیم یک واحد به count اضافه کرده و سپس زمان دانلود آن را به متغیر total اضافه می کنم. این کار را تا زمانی ادامه می دهیم که دیگر cursor وجود نداشته باشد (داده ای در پایگاه داده باقی نمانده باشد). آنگاه زمان کل را بر تعداد دانلود تقسیم کرده و آن را به عنوان زمان متوسط در نظر می گیریم. نهایتا آن را log می کنیم.
من این کد ها را به انتهای فایل performance.js متصل کرده و سپس تمام کد های این فایل را برایتان قرار می دهم. محتویات کامل این فایل باید بدین شکل باشد:
import { IndexedDB } from './indexeddb.js'; window.addEventListener('load', async () => { // IndexedDB connection const perfDB = await new IndexedDB('performance', 1, (db, oldVersion, newVersion) => { console.log(`upgrading database from ${ oldVersion } to ${ newVersion }`); switch (oldVersion) { case 0: { const navigation = db.createObjectStore('navigation', { keyPath: 'date' }), resource = db.createObjectStore('resource', { keyPath: 'id', autoIncrement: true }); resource.createIndex('dateIdx', 'date', { unique: false }); resource.createIndex('nameIdx', 'name', { unique: false }); } } }); if (!('performance' in window) || !perfDB) return; // record page navigation information const date = new Date(), nav = Object.assign( { date }, performance.getEntriesByType('navigation')[0].toJSON() ); await perfDB.update('navigation', nav); // record resource const res = performance.getEntriesByType('resource').map( r => Object.assign({ date }, r.toJSON()) ); await perfDB.update('resource', res); // counr all records console.log('page navigation records: ', await perfDB.count('navigation')); console.log('resource records: ', await perfDB.count('resource')); console.log('page load times during 2021:'); // fetch page navigation objects in 2021 perfDB.fetch( 'navigation', null, // not an index new Date(2021,0,1,10,40,0,0), // lower new Date(2021,11,1,10,40,0,0), // upper cursor => { // callback function if (cursor) { console.log(`${ cursor.value.date }: ${ cursor.value.domContentLoadedEventEnd }`); cursor.continue(); } } ); // calculate average download time using index let filename = 'http://localhost:8888/css/main.css', count = 0, total = 0; perfDB.fetch( 'resource', // object store 'nameIdx', // index filename, // matching file filename, cursor => { // callback if (cursor) { count++; total += cursor.value.duration; cursor.continue(); } else { // all records processed if (count) { const avgDuration = total / count; console.log(`average duration for ${ filename }: ${ avgDuration } ms`); } } }); });
در حال حاضر می توانید به مرورگر خود رفته و از سربرگ Application داده های موجود در پایگاه داده خود را مشاهده کنید. همچنین زمان ها در بخش console برایتان چاپ می شوند.
منبع: وب سایت openreplay
در این قسمت، به پرسشهای تخصصی شما دربارهی محتوای مقاله پاسخ داده نمیشود. سوالات خود را اینجا بپرسید.