آفلاین ساختن وب اپلیکیشن – به زبان ساده


امروزه دنیای ما بیش از هر زمان دیگر به دنیایی به هم پیوسته تبدیل شده است. تعداد افرادی که به اینترنت دسترسی دارند به بیش از 4.5 میلیارد نفر رسیده است. اما بسیاری از این جمعیت افرادی هستند که اتصالهای اینترنتی کُند یا همراه با مشکل دارند. حتی در ایالات متحده 4.9 میلیون خانه نمیتوانند دسترسی با سیم یا سرعت بالاتر از 3 مگابیت بر ثانیه به اینترنت داشته باشند. در چنین شرایطی بهتر متوجه اهمیت آفلاین ساختن وب اپلیکیشن میشویم.
بقیه دنیا یعنی افرادی که به اینترنت پایدار دسترسی دارند نیز همچنان در معرض از دست دادن اتصال خود هستند. برخی از عواملی که میتواند روی کیفیت اتصال شبکه تأثیر گذارد به شرح زیر هستند:
- پوشش ضعیف ارائهدهنده
- شرایط آب و هوایی بد
- قطع برق
- جابجایی کاربران به مناطق کور مانند ساختمانهایی که اتصال شبکه را مسدود کردهاند
- جابجایی در قطار یا رفتن به داخل تونل
- اتصالهایی که به وسیله شخص ثالث و برحسب زمان مدیریت میشوند
- رویههای فرهنگی که در زمانهای خاصی از روز اینترنت محدود یا کلاً قطع اینترنت را موجب میشوند
با توجه به این عوامل روشن است که باید تجربههای آفلاین را در زمان طراحی و ساخت اپلیکیشنها در نظر داشته باشیم.
در این مقاله با روش افزودن قابلیت استفاده آفلاین از اپلیکیشن با کمک گرفتن از service workers ،cache storage و IndexedDB بررسی میکنیم. کار فنی مورد نیاز برای آفلاین ساختن اپلیکیشن را میتوان در چهار وظیفه مشخص دستهبندی کرد که آنها را در ادامه مورد بررسی قرار میدهیم.
Service Worker
اپلیکیشنهایی که برای کارکرد آفلاین طراحی میشوند، نباید به شبکه وابستگی داشته باشند. در نتیجه باید fallback-های معقولی در زمان قطع اتصال طراحی شده باشند.
یک fallback معقول در زمان شکست بارگذاری یک وب اپلیکیشن همچنان باید فرم فایلهای مرورگر (HTML/CSS/JavaScript) را بگیرد. اگر نتوانیم درخواست شبکه را ارسال کنیم، این فایلها از کجا باید به دست ما برسند؟ شاید بتوانیم از cache استفاده کنیم. بسیاری از افراد موافق هستند که ارائه یک تجربه کاربری تاریخگذشته بهتر از نمایش یک صفحه خالی به کاربر است.
مرورگر به طور مرتب درخواستهایی برای دادهها ارسال میکند. ارائه دادههای کَششده به صورت یک fallback ما را ملزم میسازد که این درخواستهای مرورگر را تفسیر کرده و قواعد کش کردن را بنویسیم. این همان جایی است که service worker به کار میآید. در واقع آنها را میتوان مانند یک میانجی تصور کرد.
سرویس ورکر یک فایل جاوا اسکریپت است که درون آن در رویدادها مشترک میشویم و قواعدی برای کش کردن و مدیریت درخواستهای شکستخورده مینویسیم. با ما همراه باشید تا این کار را عملیاتی کنیم.
اپلیکیشن دمو
ما قصد دارید کارکرد آفلاین را در این راهنما به اپلیکیشن دمو اضافه کنیم. این اپلیکیشن دمو یک صفحه ساده تحویل/بازگشت کتاب در یک کتابخانه است. پیشرفت کار به شکل یک سری از Gif-ها نمایش مییابد که از شبیهسازی آفلاین Chrome DevTools بهره میگیرد. حالت ابتدایی به صورت زیر است:
گام 1: کش کردن فایلهای استاتیک
فایلهای استاتیک به فایلهایی گفته میشود که به ندرت تغییر مییابند. برای نمونه فایلهای HTML ،CSS، جاوا اسکریپت و تصاویر را میتوان در این دسته جای داد. مرورگر تلاش میکند فایلهای استاتیک را از طریق درخواستهای شبکه بارگذاری کند. این درخواستها میتوانند از سوی یک سرویس ورکر تفسیر شوند. کار خود را با ثبت یک سرویس ورکر آغاز میکنیم:
1if ('serviceWorker' in navigator) {
2 window.addEventListener('load', function() {
3 navigator.serviceWorker.register('/sw.js');
4 });
5}
سرویس ورکرها در واقع وبورکر (Web Worker) هستند و باید از فایل جاوا اسکریپت مجزایی بارگذاری شوند. ثبت کردن یک سرویس ورکر به سادگی فراخوانی متد register پس از بارگذاری یک سایت است. اکنون که سرویس ورکر ما بارگذاری شده است، فایلهای استاتیک خود را کش میکنیم:
1var CACHE_NAME = 'my-offline-cache';
2var urlsToCache = [
3 '/',
4 '/static/css/main.c9699bb9.css',
5 '/static/js/main.99348925.js'
6];
7
8self.addEventListener('install', function(event) {
9 event.waitUntil(
10 caches.open(CACHE_NAME)
11 .then(function(cache) {
12 return cache.addAll(urlsToCache);
13 })
14 );
15});
از آنجا که URL-های فایلهای استاتیک را در کنترل خود داریم، میتوانیم آنها را بیدرنگ پس از نصب سرویس ورکر با استفاده از Cache Storage کش کنیم.
اکنون که کش با جدیدترین فایلهای استاتیک درخواستی پر شده است، میتوانیم در صورت شکست در اتصال، این فایلها را از کش ارائه کنیم.
1self.addEventListener('fetch', function(event) {
2 event.respondWith(
3 fetch(event.request).catch(function() {
4 caches.match(event.request).then(function(response) {
5 return response;
6 }
7 );
8 );
9});
هر بار که مرورگر یک درخواست برای دادهها ارسال میکند، رویداد fetch سرویس ورکر ایجاد میشود. دستگیره رویداد fetch جدید اکنون منطقی اضافی برای بازگشت پاسخهای کش شده در صورت شکست درخواست شبکه دارد.
دموی شماره 1
اپلیکیشن دموی ما اینک میتواند فایلهای استاتیک را در زمان آفلاین بودن اپلیکیشن عرضه کند، اما دادههای ما کجا هستند؟
گام 2: کش کردن فایلهای دینامیک
اپلیکیشنهای تکصفحهای (SPA) عموماً دادهها را به صورت افزایشی پس از بارگذاری ابتدایی صفحه بارگذاری میکنند و اپلیکیشن دموی ما نیز در این زمینه استثنا محسوب نمیشود. لیست کتابها تا کمی بعدتر واکشی نمیشود. این دادهها به صورت معمول از درخواستهای XHR ناشی میشوند و پاسخهایی که غالباً برای بهروزرسانی حالت اپلیکیشن میآیند نیز کاملاً دشوار هستند، بنابراین به جای این که لیست از پیش تعریفشدهای برای فایلهای استاتیک داشته باشیم، آنها را به محض رسیدن کش میکنیم. دستگیره رویداد fetch را به خاطر بیاورید:
1self.addEventListener('fetch', function(event) {
2 event.respondWith(
3 fetch(event.request).catch(function() {
4 caches.match(event.request).then(function(response) {
5 return response;
6 }
7 );
8 );
9});
میتوانیم این پیادهسازی را با افزودن کمی کد که درخواستهای fetch موفق و پاسخها کش میکند دستکاری کنیم. بدین ترتیب مطمئن میشویم که به طور ثابتی درخواستهای جدید را به کش خود اضافه میکنیم و دادههای کش شده را بهروز نگه میداریم.
1self.addEventListener('fetch', function(event) {
2 event.respondWith(
3 fetch(event.request)
4 .then(function(response) {
5 caches.open(CACHE_NAME).then(function(cache) {
6 cache.put(event.request, response);
7 });
8 })
9 .catch(function() {
10 caches.match(event.request).then(function(response) {
11 return response;
12 }
13 );
14 );
15});
Cache Storage اکنون مدخلهای جدیدی دارد:
دموی شماره 2
اکنون دموی ما صرفنظر از وضعیت آنلاین بودن، دقیقاً همانند بارگذاری اولیه به نظر میرسد. در ادامه تلاش میکنیم از اپلیکیشن خود استفاده کنیم:
همچنان که میبینید تعداد پیامهای خطا بالا است. به نظر میرسد که همه تراکنشهای کاربر از دست رفتهاند و دیگر نمیتوانیم هیچ کتابی را تحویل گرفته یا بدهیم.
گام 3: ساخت UI خوشبینانه
مشکل اپلیکیشن در این لحظه آن است که منطق واکشی دادهها همچنان به طرز زیادی به پاسخهای شبکه وابسته است. یک عمل تحویل گرفتن یا دادن کتاب، درخواستی به سرور ارسال میکند و منتظر یک پاسخ موفق میماند. این وضعیت برای انسجام دادهها مفید است، اما برای تجربه آفلاین خوب نیست.
برای این که این تعاملها به طور آفلاین نیز کار کنند، باید کاری کنیم که دموی ما به صورت «خوشبینانه» (Optimistic) عمل کند. تعاملهای خوشبینانه نیازمند دریافت پاسخ از سوی سرور نیستند و یک نمای بهروز شده از دادهها را رندر میکنند. یک عملیات خوشبینانه رایج در اغلب وب اپلیکیشنها عملیات delete است. در صورتی که همه اطلاعات مورد نیاز کاربر را از قبل در اختیار داشته باشیم، چرا نباید به او بازخوردی آنی بدهیم؟
جدا کردن اپلیکیشن از شبکه با استفاده از یک رویکرد خوشبینانه کاری نسبتاً سرراست محسوب میشود.
1case CHECK_OUT_SUCCESS:
2case CHECK_OUT_FAILURE:
3 list = [...state.list];
4 list.push(action.payload);
5 return {
6 ...state,
7 list,
8 };
9case CHECK_IN_SUCCESS:
10case CHECK_IN_FAILURE;
11 list = [...state.list];
12 for (let i = 0; i < list.length; i++) {
13 if (list[i].id === action.payload.id) {
14 list.splice(i, 1, action.payload);
15 }
16 }
17 return {
18 ...state,
19 list,
20 };
نکته کلیدی این است که اقدام کاربر را صرفنظر از این که درخواست شبکه موفق بوده یا شکستخورده به روشی یکسان مدیریت کنیم. قطعه کد فوق مربوط به یک ریداسر ریداکس در اپلیکیشن است. SUCCESS و FAILURE بر اساس دسترسی به شبکه تحریک میشوند. صرفنظر از این که درخواست شبکه موفق باشد یا نه، لیست کتابها را بهروزرسانی خواهیم کرد.
دموی شماره 3
تعاملهای کاربر اکنون آنلاین هستند. هر دو دکمه تحویل گرفتن و تحویل دادن بنا به مورد اینترفیس را بهروزرسانی میکنند، هر چند از روی پیامهای قرمز رنگ کنسول مشخص است که درخواستها موفق نیستند.
یک نکته دیگر نیز وجود دارد که در زمان رندر کردن خوشبینانه در حالت آفلاین باید در نظر داشته باشیم و آن زمانی است که تغییرات از دست میروند.
گام 4: صفبندی اقدامهای کاربر برای همگامسازی مجدد
باید رد اقدامهای کاربر را در زمانی که آفلاین است حفظ کنیم تا زمانی که دوباره آنلاین شد، با سرور همگامسازی مجدد کنیم. مرورگر چند سازوکار ذخیرهسازی دارد که به صورت صف اقدام استفاده شود. ما قصد داریم از IndexedDB استفاده کنیم. IndexedDB چند امکان به شرح زیر ارائه میکند که LocalStorage ندارد:
- عملیات ناهمگام و غیر مسدودکننده
- محدودیت ذخیرهسازی به مراتب بالاتر
- مدیریت تراکنشها
کد ریداسر خوشبینانه قبلی را به خاطر بیاورید:
1case CHECK_OUT_SUCCESS:
2case CHECK_OUT_FAILURE:
3 list = [...state.list];
4 list.push(action.payload);
5 return {
6 ...state,
7 list,
8 };
9case CHECK_IN_SUCCESS:
10case CHECK_IN_FAILURE;
11 list = [...state.list];
12 for (let i = 0; i < list.length; i++) {
13 if (list[i].id === action.payload.id) {
14 list.splice(i, 1, action.payload);
15 }
16 }
17 return {
18 ...state,
19 list,
20 };
این کد را طوری ویرایش میکنیم که رویدادهای تحویل گرفتن و تحویل دادن را در IndexedDB برای اقدامهای FAILURE ذخیره کند.
1case CHECK_OUT_FAILURE:
2 list = [...state.list];
3 list.push(action.payload);
4 addToDB(action); // QUEUE IT UP
5 return {
6 ...state,
7 list,
8 };
9case CHECK_IN_FAILURE;
10 list = [...state.list];
11 for (let i = 0; i < list.length; i++) {
12 if (list[i].id === action.payload.id) {
13 list.splice(i, 1, action.payload);
14 addToDB(action); // QUEUE IT UP
15 }
16 }
17 return {
18 ...state,
19 list,
20 };
کد پیادهسازی برای ایجاد یک IndexedDB همراه با تابع کمکی addToDB به صورت زیر است:
1let db = indexedDB.open('actions', 1);
2db.onupgradeneeded = function(event) {
3 let db = event.target.result;
4 db.createObjectStore('requests', { autoIncrement: true });
5};
6
7const addToDB = action => {
8 var db = indexedDB.open('actions', 1);
9 db.onsuccess = function(event) {
10 var db = event.target.result;
11 var objStore = db
12 .transaction(['requests'], 'readwrite')
13 .objectStore('requests');
14 objStore.add(action);
15 };
16};
اکنون که همه اقدامهای آفلاین کاربر در حافظه مرورگر ذخیره میشوند، میتوانیم از شنونده رویداد online مرورگر برای پاسخ دادن به این رویدادها در زمان اتصال به شبکه بهره بگیریم:
1window.addEventListener('online', () => {
2 const db = indexedDB.open('actions', 1);
3 db.onsuccess = function(event) {
4 let db = event.target.result;
5 let objStore = db
6 .transaction(['requests'], 'readwrite')
7 .objectStore('requests');
8 objStore.getAll().onsuccess = function(event) {
9 let requests = event.target.result;
10 for (let request of requests) {
11 send(request); // sync with the server
12 }
13 };
14 };
15});
در این مرحله میتوانیم صف همه درخواستها را که با موفقیت به سرور ارسال شدهاند پاک کنیم.
دموی شماره 4
دموی نهایی ما کمی پیچیدهتر است. از سوی دیگر یک پنجره ترمینال سیاه همه فعالیتهای API را برای سرور بکاند لاگ میکند. این دمو شامل حالت آفلاین است و به بررسی چند کتاب میپردازد و سپس آنلاین میشود.
به روشنی میتوان دید که درخواستهای آفلاین صفبندی میشوند و زمانی که کاربر دوباره آنلاین شود به طور همزمان به سرور ارسال میشوند.
این رویکرد replay تا حدودی خاماندیشانه است. برای نمونه احتمالاً در صورتی که یک کتاب یکسان تحویل گرفته و تحویل میشود، لازم نباشد دو درخواست ارسال کنیم. همچنین در صورتی که چند نفر از اپلیکیشن واحدی استفاده کنند، عمل نخواهد کرد. البته این موارد بسیار فراتر از یک اپلیکیشن دمو هستند.
سخن پایانی
بدین ترتیب در این راهنما با روش عملیاتی ساختن کارکرد آفلاین در اپلیکیشن آشنا شدیم. در این مطلب تنها معدودی از مواردی که میتوان با استفاده از ظرفیتهای آفلاین اپلیکیشنها انجام داد، توضیح داده شدند و قطعاً راهنمای جامعی محسوب نمیشود.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- معرفی جاوا اسکریپت ناهمگام — به زبان ساده
==