یکی از بزرگترین مشکلات در نرم افزارهای تحت وب نسبت به نرمافزارهای عادی مشکل قطعی اینترنت است، به طوریکه اگر اتّصال کاربر به هر دلیل از اینترنت قطع شود دیگر نمیتواند با نرم افزار تحت وب کار کند، یا اینکه در مواقعی که سرعت اینترنت کند است کار با نرمافزار تحت وب عذابآور خواهد شد. فناوری Service Worker با هدف رفع این مشکل به پلتفرم وب اضافه شد. با استفاده از Service Worker میتوانید فایلهای مورد نیاز خود را در حافظهی Cache مرورگر ذخیره کنید، سپس تنظیم کنید که در دفعات بعدی از این فایلها استفاده شود، حتّی اگر کاربر کاملاً آفلاین است! (به این استراتژی اصطلاحا Offline first میگوییم، در ادامه با استراتژیهای دیگر Service Worker هم آشنا خواهیم شد). این دقیقاً کاری است که اکثر نرمافزارهای غیر وبی انجام میدهند و یکی از دلایل اصلی ترجیح نرمافزارهای غیر وبی بر نرمافزارهای تحت وب همین است.
پیش از Service Worker از فناوری AppCache برای اجرای وبسایت به صورت آفلاین استفاده میشد که بسیار محدودتر بود. البته کار با AppCache خیلی سادهتر از Service Worker بود. در AppCache در قالب یک فایل تنظیمات، لیست فایلهایی که میخواستیم در حالت آفلاین هم در دسترس باشند را مینوشتیم و آن را به مرورگر معرفی میکردیم، ولی در Service Worker برای مدیریت حالت آفلاین کد جاوااسکریپت مینویسیم و این باعث میشود دست ما خیلی بازتر باشد. در حال حاضر فناوری AppCache از رده خارج شده است و مرورگرهای جدید از آن پشتیبانی نمیکنند.
سرویس ورکر از نگاه فنی چیست؟
در JavaScript زمانی که قرار است کدی به صورت کاملا مجزا از کدهای عادی اجرا شود (یعنی در thread مجزایی اجرا شود)، از چیزی به نام Worker استفاده میشود. Worker فایل JavaScript مستقلی است که به بسیاری از APIهای عادی مرورگر، مثل تعامل با DOM و localStorage دسترسی ندارد و معمولاً برای پردازشهای نسبتاً سنگین در وب مورد استفاده قرار میگیرد، زیرا که در پسزمینه و بدون اینکه بر سرعت صفحه وب تأثیر منفی بگذارد اجرا میشود. Service به برنامهای گفته میشود که در پسزمینه و بدون محیط گرافیکی کارهایی را انجام میدهد، یعنی حتی زمانی که هیچ چیزی را در صفحه نمیبینید Serviceها در حال انجام کارهایی هستند. این مفهوم از قبل در سیستم عاملهای مختلف وجود داشته است، تا اینکه در وب هم با پیدایش برنامههای وبی پیشرو (PWA) سر و کلهاش پیدا شد. Service همواره باید به صورت مستقل از محیط گرافیکیِ برنامه (یا بهتر است بگوییم رابط کاربری) اجرا شود، یعنی باید در یک Worker باشد، از این رو به آن Service Worker گفته میشود.
از آنجایی که Service Worker میتواند درخواستهایی که بین کلاینت و سرور ارسال میشود را بخواند و تغییر دهد، این قابلیت در سایتهایی که https نیستند قابل استفاده نیست؛ چرا که مرورگر به دلایل امنیتی جلوی آن را میگیرد. البته localhost را نیز اکثر مرورگرها امن به حساب میآورند حتی اگر https نباشد.
Service Worker از کلیدی ترین اجزای PWA است و دو کار اصلی انجام میدهد:
- Proxy: سرویسورکر به صورت یک Proxy در سمت کلاینت عمل میکند، هر درخواستی که به سرور ارسال میشود، یا هر پاسخی که از سمت سرور بر میگردد از Service Worker خواهد گذشت. Service Worker میتواند درخواست یا پاسخ را تغییر دهد یا اصلاً به سرور نفرستد. از این قابلیّت برای اجرای برنامهی وبی در حالت Offline استفاده میشود.
- اجرای کارهایی در پسزمینه: Service Worker میتواند حتّی در صورت بسته بودن مرورگر و برنامهی ما، مثلاً اگر اعلانی از سمت سرور بیاید (Push Notification)، کار خاصی را که ما تعیین کردهایم انجام دهد، و مواردی از این دست.
راه اندازی Service Worker
با نوشتن کد جاوااسکریپت زیر Service Worker مستقر خواهد شد، منظور از مستقر شدن این است که Service Worker در پسزمینه اجرا میشود و Eventهایی را که بعداً با آنها آشنا خواهیم شد زیرنظر میگیرد و حتّی در صورت بسته بودن سایت ما اگر زمان اجرای آن Eventها فرا برسد، کدی که نوشتهایم اجرا خواهد شد.
// بررسی میکنیم که مرورگر کاربر از سرویسورکر پشتیبانی بکند
if (navigator.serviceWorker) {
navigator.serviceWorker.register('/sw.js') // آدرس فایل سرویسورکر را میدهیم
.then(registration => {
console.log('congrats. scope is: ', registration.scope);
})
.catch(error => {
console.log('sorry', error);
});
}
حالا داخل فایل سرویسورکر چه کدهایی باید بنویسیم؟ (در مثال بالا این فایل sw.js نام دارد.)
درون فایل سرویس ورکر میتوان Eventهایی خاص را زیر نظر گرفت که به مهمترین آنها اشاره میکنیم.
رویداد install: اگر مرورگر هنگام استقرار Service Worker جدید ببیند که قبلاً Service Worker دیگری ثبت نشده است، یا اینکه ثبت شده است ولی محتوای فایل Service Worker بایت به بایت با فایل جدید یکسان نیست، در این شرایط مرورگر شروع به نصب این Service Worker جدید میکند و رویداد install به وقوع میپیوندد. پس از اینکه نصب پایان یافت Service Worker فعّال میشود، مگر اینکه از قبل Service Worker دیگری مستقر شده باشد که در این صورت Service Worker جدید به حالت «انتظار» میرود و فعال نمیشود. Service Worker فقط در صورت فعال بودن است که میتواند به عنوان Proxy عمل کند و کارهایی را در پسزمینه انجام دهد.
در مثال زیر تنظیم کردهایم در هنگام نصب سرویسورکر تعدادی فایل دانلود شود و در حافظهی کش مرورگر ذخیره شود، تا بعداً بتوانیم تنظیم کنیم در حالت آفلاین از این فایلها استفاده شود.
const CACHE_NAME = 'cache-v1';
const urlsToCache = [
'/',
'/js/main.js',
'/css/style.css',
'/img/bob-ross.jpg',
];
self.addEventListener('install', event => {
caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
});
});
در مثال بالا، در متغیّر urlToCache
آدرس فایلهایی را که قرار است در حافظهی Cache مرورگر ذخیره کنیم نگه داشته ایم.
سپس تنظیم کرده ایم که در زمان نصب سرویسورکر آنها را در حافظهی Cache ذخیره کند. برای این کار از Cache Storage استفاده کرده ایم.
حتّی در این Event میتوانیم نصب Service Worker را به کار دیگری منوط کنیم که معمولاً به منظور آمادهسازی برنامه برای اجرای آفلاین استفاده میشود.
مثلاً میتوانیم لیست فایلهایی را که در صورت عدم اتّصال اینترنت لازم هستند را در این مرحله در حافظهی Cache ذخیره کنیم، ولی نصب سرویسورکر هم منوط به انجام این کار باشد، یعنی اگر به هر دلیلی دانلود این فایلها با شکست روبرو شد، سرویسورکر هم نباید مستقر شود، چرا که در غیر این صورت سرویسورکرِ مستقرشده میخواهد حالت آفلاین برنامه را مدیریت کند، ولی فایلی را در حافظهی Cache به عنوان جایگزین ذخیره نکرده است تا از آن استفاده کند، پس همان بهتر که اصلا مستقر نشود!
برای انجام این کار مشابه مثال زیر از event.waitUntil
استفاده میکنیم، این متد به عنوان ورودی یک Promise میگیرد، و نصب سرویسورکر را منوط به انجام موفق آن میکند، و تا زمانی که این Promise کارش تمام نشده، سرویس ورکر در حالت «installing» خواهد بود.
const CACHE_NAME = 'cache-v1';
const urlsToCache = [
'/',
'/js/main.js',
'/css/style.css',
'/img/bob-ross.jpg',
];
self.addEventListener('install', event => {
const cachePromise = caches.open(CACHE_NAME)
.then(cache => {
return cache.addAll(urlsToCache);
});
event.waitUntil(cachePromise); // نصب سرویسورکر به کش فایلها منوط شود
});
رویداد activate: اگر Service Worker پس از نصب رقیبی نداشته باشد، یعنی Service Worker دیگری از قبل فعّال نباشد، این رویداد به وقوع میپیوندد و بعد از آن Service Worker به حالت «فعّال (active)» میرود. معمولاً در این رویداد فایلهایی که سرویسورکر قبلی در حافظهی Cache ذخیره کرده است و دیگر به آنها نیاز نداریم را حذف میکنند.
اگر از قبل Service Worker فعّالی وجود داشته باشد، Service Worker جدید به حالت «انتظار (waiting)» میرود. یعنی منتظر میماند تا تمام سربرگها و پنجرههای مرورگر که تحت کنترل Service Worker فعّال هستند بسته شود. پس از آن Service Worker قدیمی خاموش میشود و Service Worker جدید به وضعیّت «فعّال» میرود و رویداد activate به وقوع میپیوندد.
رویداد fetch: این رویداد در هر زمان که مرورگر بخواهد درخواستی به سرور ارسال کند به وقوع میپیوندد. در این زمان میتوان درخواست را تغییر داد، یا به هر شکل دیگری پاسخ داد (مثلاً به جای درخواست به سرور میتواند از حافظهی Cache فایلی را بخواند و به عنوان پاسخ بفرستد). حتّی در زمان عدم اتّصال به اینترنت از طریق این رویداد میتوان حالت Offline را مدیریت کرد.
self.addEventListener('fetch', event => {
const { request } = event;
const findResponsePromise = caches.open(CACHE_NAME)
.then(cache => cache.match(request))
.then(response => {
if (response) {
return response;
}
return fetch(request);
});
event.respondWith(findResponsePromise);
});
برای مدیریت حالت آفلاین راهبردهای مختلفی وجود دارد، از جمله:
- Network first: در این راهبرد ابتدا درخواست به سرور ارسال میشود، ولی در صورت برخورد به هر گونه خطایی مثل عدم اتّصال اینترنت یا قطعی سرور در صورت امکان از Cache Storage خوانده میشود و پاسخ داده میشود. ضمناً پس هر پاسخی که سرور میدهد Cache میشود تا اگر بعداً به خطایی برخورد شود چیزی در Cache برای استفاده وجود داشته باشد.
- Cache first: در این راهبرد ابتدا سعی میشود بدون ارسال درخواست به سرور با خواندن از Cache به درخواست پاسخ داده شود، ولی در صورت عدم وجود Cache به سرور درخواست ارسال میکند و پاسخ دریافتی را Cache میکند تا در دفعات بعدی مورد استفاده قرار بگیرد.
- Stale while revalidate: در این راهبرد ابتدا Cache را میخواند و پاسخ درخواست را میدهد، سپس در پسزمینه به سرور درخواست میزند و بعد از گرفتن پاسخ فقط Cache را بروز میکند تا در دفعات بعدی محتوای بروز را ارائه دهد.
- Network only: در این راهبرد مشابه زمانی که Service Worker وجود ندارد عمل میشود و درخواستها همیشه به سرور ارسال میشود.
- Cache only: در این راهبرد درخواستها همیشه از Cache خوانده میشود و در صورت عدم وجود در Cache خطا میدهد و درخواستی به سرور ارسال نمیشود.
رویداد push: زمانی که از سمت سرور push دریافت میشود این رویداد اجرا میشود. فرآیند کلی به این صورت است که ابتدا از کاربر دسترسی ارسال اعلان گرفته میشود. اگر کاربر این مجوّز را داد میتوان با فراخوانی تابعی در JavaScript درخواست دریافت Push را ثبت کرد، این تابع در خروجی خود یک endpoint بر میگرداند که باید در سرور ذخیره شود. از این پس برای ارسال پیام از سمت سرور به کاربر، سرور باید درخواستی به endpoint بزند تا ارائهدهندهی خدمات Push Notification پیام را برای مخاطب ارسال کند.
کارهایی که یک Service Worker میتواند در پسزمینه انجام دهد از این قرار است:
- Push notification: زمانی که از سمت سرور پیام push ارسال میشود Service Worker این پیام را حتّی در صورت بسته بودن برنامه میتواند بگیرد و یک اعلان نمایش دهد، سپس در هنگام کلیک روی اعلان نیز میتواند برنامهی ما را باز کند.
- Background sync: اگر در حین کار با برنامه اینترنت قطع شود یا اینکه سرور دچار اختلال شود و کاری نیمهتمام بماند میتوانیم یک background sync ثبت کنیم تا در زمان اتّصال اینترنت بتوانبم به سرور درخواست بزنیم و کار نیمه تمام را به پایان برسانیم. برای مثال اگر در یک پیامرسان تحت وب پیامی را برای مخاطب خود ارسال کنیم، ولی در همان لحظه شبکه قطع باشد، بعداً که اتّصال شبکه برقرار شود پیام ارسالنشده قابل ارسال است حتّی اگر در آن زمان برنامهی وبی و مرورگر به صورت کامل بسته شده باشند. این کار را Service Worker میتواند در پسزمینه انجام دهد.
- Background fetch: با ایت قابلیت میتوانیم تنظیم کنیم یک یا چند فایل دانلود شوند، آن هم به طوری که حتّی اگر در حین بارگیری مرورگر بسته شود دانلودشان با خطا مواجه نشود و ادامه پیدا کند و اگر دوباره مرورگر باز شود، بتوانیم میزان پیشرفت دانلودشان را در لحظه در رابط کاربری نمایش دهیم، و پس از پایان دانلود از طریق کد جاوااسکریپتمان به آن فایلها دسترسی داشته باشیم.
- Periodic background sync: برنامهی وبی میتواند به صورت دورهای در پسزمینه دادههای خود را با سرور همگام سازد تا در دفعهی بعد که برنامه باز میشود دادههای آن بروز باشد.
- Notification trigger: میتوان تنظیم کرد که بر اساس زمان یا مکان خاص برای کاربر اعلانی فرستاده شود، البته این اعلان باید از قبل تعریف شود و نمیتوان تنظیم کرد در زمان یا مکانی خاص کد جاوااسکریپتی اجرا شود. این قابلیّت هماکنون به صورت Origin trial پیادهسازی شده است که در مورد این مفهوم در بخش «پروژهی قابلیّتها» بحث خواهد شد.
منابع اضافی برای مطالعهی بیشتر:
//blog.88mph.io/2017/07/28/understanding-service-workers
//developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers