خلاصه
بیاموزید که چگونه از کتابخانههای سرویسدهنده استفاده کردیم تا برنامه وب Google I/O 2015 را سریع و آفلاین کنیم.
نمای کلی
برنامه وب امسال Google I/O 2015 توسط تیم روابط توسعهدهنده Google بر اساس طرحهای دوستان ما در Instrument نوشته شده است که آزمایش صوتی/بصری بسیار خوبی را نوشتهاند. ماموریت تیم ما این بود که اطمینان حاصل کنیم که برنامه وب I/O (که با نام رمز آن، IOWA به آن اشاره می کنم) هر کاری را که وب مدرن می تواند انجام دهد را به نمایش بگذارد. یک تجربه کامل آفلاین اول در بالای لیست ما از ویژگی های ضروری بود.
اگر اخیراً هر یک از مقالات دیگر این سایت را خوانده اید، بدون شک با کارکنان خدماتی مواجه شده اید و از شنیدن اینکه پشتیبانی آفلاین IOWA به شدت به آنها وابسته است، تعجب نخواهید کرد. با انگیزه نیازهای دنیای واقعی IOWA، ما دو کتابخانه را برای رسیدگی به دو مورد مختلف استفاده آفلاین توسعه دادیم: sw-precache
برای خودکار کردن پیش کش منابع استاتیک و sw-toolbox
برای مدیریت استراتژی های ذخیره سازی زمان اجرا و بازگشت.
کتابخانهها به خوبی یکدیگر را تکمیل میکنند و به ما اجازه میدهند تا یک استراتژی عملکردی را پیادهسازی کنیم که در آن «پوسته» محتوای ایستا IOWA همیشه مستقیماً از حافظه پنهان ارائه میشود و منابع پویا یا راه دور از شبکه ارائه میشوند، با بازگشت به پاسخهای ذخیرهشده یا ایستا در زمانی که مورد نیاز است.
پیش کش با sw-precache
منابع استاتیک IOWA - HTML، جاوا اسکریپت، CSS و تصاویر آن - پوسته اصلی برنامه وب را فراهم می کند. دو الزام خاص وجود داشت که هنگام فکر کردن به ذخیرهسازی این منابع مهم بود: ما میخواستیم مطمئن شویم که اکثر منابع استاتیک در حافظه پنهان هستند و بهروز نگه داشته میشوند. sw-precache
با در نظر گرفتن این الزامات ساخته شده است.
ادغام زمان ساخت
sw-precache
با فرآیند ساخت مبتنی بر gulp
IOWA، و ما به یک سری الگوهای glob تکیه میکنیم تا اطمینان حاصل کنیم که فهرست کاملی از همه منابع استاتیک مورد استفاده IOWA تولید میکنیم.
staticFileGlobs: [
rootDir + '/bower_components/**/*.{html,js,css}',
rootDir + '/elements/**',
rootDir + '/fonts/**',
rootDir + '/images/**',
rootDir + '/scripts/**',
rootDir + '/styles/**/*.css',
rootDir + '/data-worker-scripts.js'
]
روشهای جایگزین، مانند کدگذاری سخت فهرستی از نام فایلها در یک آرایه، و به خاطر سپردن شماره نسخه حافظه پنهان هر بار که هر یک از آن فایلها تغییر میکنند، بسیار مستعد خطا هستند، بهویژه با توجه به اینکه چندین عضو تیم داریم که کد را بررسی میکنند. هیچ کس نمی خواهد پشتیبانی آفلاین را با کنار گذاشتن یک فایل جدید در آرایه ای که به صورت دستی نگهداری می شود، قطع کند! ادغام زمان ساخت به این معنی است که میتوانیم بدون داشتن این نگرانیها، تغییراتی در فایلهای موجود ایجاد کنیم و فایلهای جدید اضافه کنیم.
به روز رسانی منابع ذخیره شده
sw-precache
یک اسکریپت پایه سرویس کارگر تولید می کند که شامل یک هش MD5 منحصر به فرد برای هر منبعی است که از پیش ذخیره می شود. هر بار که یک منبع موجود تغییر می کند یا یک منبع جدید اضافه می شود، اسکریپت Service Worker دوباره تولید می شود. این به طور خودکار جریان بهروزرسانی کارگر سرویس را راهاندازی میکند که در آن منابع جدید در حافظه پنهان ذخیره میشوند و منابع قدیمی پاک میشوند. هر منبع موجودی که دارای هش MD5 یکسان باشد همانطور که هست باقی میماند. این بدان معناست که کاربرانی که قبلاً از سایت بازدید کردهاند، تنها مجموعه حداقل منابع تغییر یافته را دانلود میکنند، که منجر به تجربه بسیار کارآمدتری نسبت به منقضی شدن کل حافظه پنهان میشود.
هر فایلی که با یکی از الگوهای glob مطابقت دارد، اولین باری که کاربر از IOWA بازدید می کند، دانلود و در حافظه پنهان ذخیره می شود. ما تلاش کردیم تا اطمینان حاصل کنیم که فقط منابع حیاتی مورد نیاز برای ارائه صفحه از پیش ذخیره می شوند. محتوای ثانویه، مانند رسانه مورد استفاده در آزمایش صوتی/بصری ، یا تصاویر نمایه بلندگوهای جلسات، عمداً از پیش ذخیره نشده بودند، و در عوض از کتابخانه sw-toolbox
برای رسیدگی به درخواستهای آفلاین برای آن منابع استفاده کردیم.
sw-toolbox
، برای همه نیازهای پویا ما
همانطور که گفته شد، پیش کش کردن هر منبعی که یک سایت برای کار آفلاین به آن نیاز دارد، امکان پذیر نیست. برخی از منابع بسیار بزرگ هستند یا به ندرت استفاده می شوند تا آن را ارزشمند کنند، و منابع دیگر پویا هستند، مانند پاسخ های یک API یا سرویس راه دور. اما فقط به این دلیل که یک درخواست از پیش ذخیره نشده است به این معنی نیست که باید منجر به خطای NetworkError
شود. sw-toolbox
به ما این انعطافپذیری را داد تا کنترلکنندههای درخواست را پیادهسازی کنیم که ذخیرهسازی زمان اجرا را برای برخی منابع و بازگشتهای سفارشی برای برخی دیگر را مدیریت میکنند. ما همچنین از آن برای به روز رسانی منابع ذخیره شده قبلی خود در پاسخ به اعلان های فشار استفاده کردیم.
در اینجا چند نمونه از کنترلکنندههای درخواست سفارشی وجود دارد که ما در بالای جعبه ابزار sw ساختهایم. ادغام آنها با اسکریپت اصلی سرویس کارگر از طریق importScripts parameter
sw-precache
، که فایلهای جاوا اسکریپت مستقل را به محدوده سرویسکار میکشد، آسان بود.
آزمایش صوتی / تصویری
برای آزمایش صوتی/بصری ، از استراتژی کش networkFirst
sw-toolbox
استفاده کردیم. تمام درخواستهای HTTP که با الگوی URL آزمایش مطابقت دارند، ابتدا در برابر شبکه انجام میشوند، و اگر پاسخ موفقیتآمیز برگردانده شود، آن پاسخ با استفاده از API حافظه پنهان پنهان میشود. اگر درخواست بعدی در زمانی که شبکه در دسترس نبود ارسال می شد، از پاسخ ذخیره شده قبلی استفاده می شود.
از آنجایی که حافظه پنهان هر بار که یک پاسخ شبکه موفق برمی گشت به طور خودکار به روز می شد، ما مجبور نبودیم منابع را به طور خاص نسخه کنیم یا ورودی ها را منقضی کنیم.
toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);
تصاویر نمایه بلندگو
برای تصاویر نمایه بلندگو، هدف ما این بود که یک نسخه ذخیره شده قبلی از تصویر یک بلندگو را در صورت موجود بودن، نمایش دهیم و در صورت نبودن تصویر، دوباره به شبکه برگردیم تا تصویر را بازیابی کنیم. اگر آن درخواست شبکه ناموفق بود، به عنوان آخرین بازگشت، از یک تصویر مکاننمای عمومی استفاده میکردیم که از قبل ذخیره شده بود (و بنابراین همیشه در دسترس خواهد بود). این یک استراتژی متداول برای استفاده در هنگام برخورد با تصاویری است که میتوان آنها را با یک مکاننمای عمومی جایگزین کرد، و اجرای آن با زنجیرهای کردن کنترلکنندههای cacheFirst
و cacheOnly
sw-toolbox
آسان بود.
var DEFAULT_PROFILE_IMAGE = 'images/touch/homescreen96.png';
function profileImageRequest(request) {
return toolbox.cacheFirst(request).catch(function() {
return toolbox.cacheOnly(new Request(DEFAULT_PROFILE_IMAGE));
});
}
toolbox.precache([DEFAULT_PROFILE_IMAGE]);
toolbox.router.get('/(.+)/images/speakers/(.*)',
profileImageRequest,
{origin: /.*\.googleapis\.com/});
به روز رسانی برنامه های کاربران
یکی از ویژگیهای کلیدی IOWA این بود که به کاربرانی که وارد سیستم شده بودند اجازه میداد برنامهای از جلساتی را که برای شرکت در آن برنامهریزی کرده بودند ایجاد و حفظ کنند. همانطور که انتظار دارید، بهروزرسانیهای جلسه از طریق درخواستهای HTTP POST
به یک سرور پشتیبان انجام شد و ما مدتی را صرف یافتن بهترین راه برای رسیدگی به درخواستهای تغییر وضعیت در زمانی که کاربر آفلاین است، کردیم. ما با ترکیبی از درخواستهای ناموفق در IndexedDB همراه با منطق در صفحه اصلی وب که IndexedDB را برای درخواستهای در صف بررسی میکرد و هر کدام را که پیدا کرد دوباره امتحان میکردیم.
var DB_NAME = 'shed-offline-session-updates';
function queueFailedSessionUpdateRequest(request) {
simpleDB.open(DB_NAME).then(function(db) {
db.set(request.url, request.method);
});
}
function handleSessionUpdateRequest(request) {
return global.fetch(request).then(function(response) {
if (response.status >= 500) {
return Response.error();
}
return response;
}).catch(function() {
queueFailedSessionUpdateRequest(request);
});
}
toolbox.router.put('/(.+)api/v1/user/schedule/(.+)',
handleSessionUpdateRequest);
toolbox.router.delete('/(.+)api/v1/user/schedule/(.+)',
handleSessionUpdateRequest);
از آنجایی که تلاشهای مجدد از متن صفحه اصلی انجام شدهاند، میتوانیم مطمئن باشیم که مجموعه جدیدی از اعتبار کاربر را شامل میشود. پس از موفقیت آمیز بودن تلاش های مجدد، پیامی به کاربر نشان دادیم که به روز رسانی های قبلی در صف اعمال شده است.
simpleDB.open(QUEUED_SESSION_UPDATES_DB_NAME).then(function(db) {
var replayPromises = [];
return db.forEach(function(url, method) {
var promise = IOWA.Request.xhrPromise(method, url, true).then(function() {
return db.delete(url).then(function() {
return true;
});
});
replayPromises.push(promise);
}).then(function() {
if (replayPromises.length) {
return Promise.all(replayPromises).then(function() {
IOWA.Elements.Toast.showMessage(
'My Schedule was updated with offline changes.');
});
}
});
}).catch(function() {
IOWA.Elements.Toast.showMessage(
'Offline changes could not be applied to My Schedule.');
});
گوگل آنالیتیکس آفلاین
در روشی مشابه، ما یک کنترلکننده برای قرار دادن درخواستهای شکستخورده Google Analytics و تلاش برای پخش مجدد آنها بعداً، زمانی که امیدواریم شبکه در دسترس بود، پیادهسازی کنیم. با این رویکرد، آفلاین بودن به معنای قربانی کردن بینش هایی که Google Analytics ارائه می دهد نیست. ما پارامتر qt
را به هر درخواست در صف اضافه کردیم، تنظیم شده به مدت زمانی که از اولین بار درخواست گذشته گذشته است، تا مطمئن شویم که زمان انتساب رویداد مناسب به باطن Google Analytics رسیده است. Google Analytics به طور رسمی از مقادیر qt
تنها تا 4 ساعت پشتیبانی می کند ، بنابراین ما بهترین تلاش را برای پخش مجدد آن درخواست ها در اسرع وقت، هر بار که سرویس دهنده شروع به کار کرد، انجام دادیم.
var DB_NAME = 'offline-analytics';
var EXPIRATION_TIME_DELTA = 86400000;
var ORIGIN = /https?:\/\/((www|ssl)\.)?google-analytics\.com/;
function replayQueuedAnalyticsRequests() {
simpleDB.open(DB_NAME).then(function(db) {
db.forEach(function(url, originalTimestamp) {
var timeDelta = Date.now() - originalTimestamp;
var replayUrl = url + '&qt=' + timeDelta;
fetch(replayUrl).then(function(response) {
if (response.status >= 500) {
return Response.error();
}
db.delete(url);
}).catch(function(error) {
if (timeDelta > EXPIRATION_TIME_DELTA) {
db.delete(url);
}
});
});
});
}
function queueFailedAnalyticsRequest(request) {
simpleDB.open(DB_NAME).then(function(db) {
db.set(request.url, Date.now());
});
}
function handleAnalyticsCollectionRequest(request) {
return global.fetch(request).then(function(response) {
if (response.status >= 500) {
return Response.error();
}
return response;
}).catch(function() {
queueFailedAnalyticsRequest(request);
});
}
toolbox.router.get('/collect',
handleAnalyticsCollectionRequest,
{origin: ORIGIN});
toolbox.router.get('/analytics.js',
toolbox.networkFirst,
{origin: ORIGIN});
replayQueuedAnalyticsRequests();
صفحات فرود اعلان فشار
کارکنان خدمات فقط عملکرد آفلاین IOWA را مدیریت نمیکردند، بلکه اعلانهای فشاری را که برای اطلاع رسانی به کاربران در مورد بهروزرسانیهای جلسات نشانکگذاریشدهشان استفاده میکردیم، نیرو میدادند. صفحه فرود مرتبط با آن اعلان ها جزئیات جلسه به روز شده را نمایش می دهد. آن صفحات فرود قبلاً به عنوان بخشی از سایت کلی ذخیره می شدند، بنابراین قبلاً به صورت آفلاین کار می کردند، اما ما باید مطمئن می شدیم که جزئیات جلسه در آن صفحه به روز باشد، حتی زمانی که به صورت آفلاین مشاهده می شد. برای انجام این کار، ابردادههای جلسه ذخیرهشده قبلی را با بهروزرسانیهایی که اعلان فشار را راهاندازی میکردند، اصلاح کردیم و نتیجه را در حافظه پنهان ذخیره کردیم. دفعه بعد که صفحه جزئیات جلسه باز شود، چه به صورت آنلاین یا آفلاین، از این اطلاعات به روز استفاده خواهد شد.
caches.open(toolbox.options.cacheName).then(function(cache) {
cache.match('api/v1/schedule').then(function(response) {
if (response) {
parseResponseJSON(response).then(function(schedule) {
sessions.forEach(function(session) {
schedule.sessions[session.id] = session;
});
cache.put('api/v1/schedule',
new Response(JSON.stringify(schedule)));
});
} else {
toolbox.cache('api/v1/schedule');
}
});
});
Gotchas و ملاحظات
البته، هیچ کس روی پروژهای در مقیاس IOWA کار نمیکند بدون اینکه با چند گوچا مواجه شود. در اینجا برخی از مواردی که با آنها برخورد کردیم و نحوه کار در اطراف آنها آورده شده است.
محتوای قدیمی
هر زمان که در حال برنامهریزی یک استراتژی ذخیرهسازی هستید، خواه از طریق سرویسکارها یا با حافظه پنهان مرورگر استاندارد اجرا شود، بین تحویل منابع در سریعترین زمان ممکن و ارائه جدیدترین منابع، یک موازنه وجود دارد. از طریق sw-precache
، ما یک استراتژی تهاجمی-اول cache را برای پوسته برنامه خود اجرا کردیم، به این معنی که کارمند سرویس ما قبل از بازگرداندن HTML، جاوا اسکریپت و CSS در صفحه، شبکه را برای به روز رسانی بررسی نمی کند.
خوشبختانه، ما توانستیم از رویدادهای چرخه عمر کارگر خدمات استفاده کنیم تا بفهمیم چه زمانی محتوای جدید پس از بارگیری صفحه در دسترس است. هنگامی که یک سرویسکار بهروز شناسایی میشود، یک پیام نان تست به کاربر نمایش میدهیم که به او اطلاع میدهد که باید صفحه خود را مجدداً بارگذاری کند تا جدیدترین محتوا را ببیند.
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.onstatechange = function(e) {
if (e.target.state === 'redundant') {
var tapHandler = function() {
window.location.reload();
};
IOWA.Elements.Toast.showMessage(
'Tap here or refresh the page for the latest content.',
tapHandler);
}
};
}
مطمئن شوید که محتوای استاتیک ثابت است!
sw-precache
از هش MD5 از محتویات فایل های محلی استفاده می کند و فقط منابعی را که هش آنها تغییر کرده است واکشی می کند. این بدان معنی است که منابع تقریباً بلافاصله در صفحه در دسترس هستند، اما همچنین به این معنی است که هنگامی که چیزی در حافظه پنهان ذخیره می شود، تا زمانی که یک هش جدید در اسکریپت سرویس کارگر به روز شده به آن اختصاص داده شود، در حافظه پنهان باقی می ماند.
به دلیل نیاز به آپدیت پویا شناسههای ویدیوی پخش زنده YouTube برای هر روز کنفرانس، در طول I/O با این رفتار با مشکل مواجه شدیم . از آنجایی که فایل الگوی اصلی ثابت بود و تغییری نکرد، جریان بهروزرسانی کارگر سرویس ما راهاندازی نشد و آنچه قرار بود پاسخی پویا از سرور با بهروزرسانی ویدیوهای YouTube باشد، در نهایت به پاسخ ذخیرهشده تعدادی از کاربران تبدیل شد. .
شما می توانید با اطمینان از اینکه برنامه وب شما به گونه ای ساختار یافته است که پوسته همیشه ثابت است و می تواند به طور ایمن پیش کش شود، از این نوع مشکل جلوگیری کنید، در حالی که هر منبع پویا که پوسته را تغییر می دهد به طور مستقل بارگذاری می شود.
Cache-bust درخواست های Precaching شما
هنگامی که sw-precache
درخواست هایی برای منابع برای پیش کش می دهد، تا زمانی که فکر می کند که هش MD5 برای فایل تغییر نکرده است، از این پاسخ ها به طور نامحدود استفاده می کند. این بدان معنی است که بسیار مهم است که مطمئن شوید که پاسخ به درخواست پیش کش یک پاسخ جدید است و از حافظه پنهان HTTP مرورگر بازگردانده نمی شود. (بله، درخواستهای fetch()
ساختهشده در یک سرویسکار میتوانند با دادههای حافظه پنهان HTTP مرورگر پاسخ دهند.)
برای اطمینان از اینکه پاسخهایی که پیش کش میکنیم مستقیماً از شبکه هستند و نه حافظه پنهان HTTP مرورگر، sw-precache
بهطور خودکار یک پارامتر پرس و جوی پنهانکننده حافظه پنهان را به هر URL که درخواست میکند اضافه میکند . اگر از sw-precache
استفاده نمیکنید و از استراتژی cache-first پاسخ استفاده میکنید، مطمئن شوید که مشابه آن را در کد خود انجام میدهید !
یک راه حل تمیزتر برای از بین بردن حافظه پنهان، تنظیم حالت حافظه پنهان هر Request
مورد استفاده برای پیش کش برای reload
است، که اطمینان حاصل می کند که پاسخ از شبکه می آید. با این حال، از زمان نوشتن این مقاله، گزینه حالت کش در کروم پشتیبانی نمی شود .
پشتیبانی از ورود و خروج
IOWA به کاربران اجازه میداد با استفاده از حسابهای Google خود وارد سیستم شوند و برنامههای رویداد سفارشیشده خود را بهروزرسانی کنند، اما این بدان معنا بود که کاربران ممکن است بعداً از سیستم خارج شوند. ذخیره دادههای پاسخ شخصیشده در حافظه پنهان آشکارا موضوعی پیچیده است و همیشه یک رویکرد درست وجود ندارد.
از آنجایی که مشاهده برنامه شخصی شما، حتی در حالت آفلاین، هسته اصلی تجربه IOWA بود، ما تصمیم گرفتیم که استفاده از داده های ذخیره شده در حافظه پنهان مناسب باشد. هنگامی که یک کاربر از سیستم خارج میشود، مطمئن میشویم که دادههای جلسه ذخیرهشده قبلی را پاک میکنیم.
self.addEventListener('message', function(event) {
if (event.data === 'clear-cached-user-data') {
caches.open(toolbox.options.cacheName).then(function(cache) {
cache.keys().then(function(requests) {
return requests.filter(function(request) {
return request.url.indexOf('api/v1/user/') !== -1;
});
}).then(function(userDataRequests) {
userDataRequests.forEach(function(userDataRequest) {
cache.delete(userDataRequest);
});
});
});
}
});
مراقب پارامترهای درخواست اضافی باشید!
هنگامی که یک سرویس دهنده پاسخ ذخیره شده را بررسی می کند، از URL درخواست به عنوان کلید استفاده می کند. بهطور پیشفرض، URL درخواست باید دقیقاً با URL مورد استفاده برای ذخیره پاسخ ذخیره شده، از جمله هر پارامتر پرس و جو در بخش جستجوی URL، مطابقت داشته باشد.
این در نهایت باعث ایجاد مشکلی برای ما در طول توسعه شد، زمانی که ما شروع به استفاده از پارامترهای URL برای ردیابی ترافیک ما از کجا کردیم. به عنوان مثال، ما پارامتر utm_source=notification
را به URL هایی که با کلیک بر روی یکی از اعلان های ما باز می شدند اضافه کردیم و utm_source=web_app_manifest
در start_url
برای مانیفست برنامه وب خود استفاده کردیم. نشانیهای اینترنتی که قبلاً با پاسخهای حافظه پنهان مطابقت داشتند، هنگام الحاق آن پارامترها بهعنوان گمشده ظاهر میشدند.
این تا حدی توسط گزینه ignoreSearch
که می تواند هنگام فراخوانی Cache.match()
مورد استفاده قرار گیرد، برطرف می شود. متأسفانه، Chrome هنوز از ignoreSearch
پشتیبانی نمیکند ، و حتی اگر پشتیبانی میکرد، این یک رفتار همه یا هیچ است. آنچه ما نیاز داشتیم راهی برای نادیده گرفتن برخی از پارامترهای جستجوی URL و در نظر گرفتن سایر پارامترهای معنی دار بود.
ما در نهایت sw-precache
را گسترش دادیم تا برخی از پارامترهای پرس و جو را قبل از بررسی تطابق حافظه پنهان حذف کنیم، و به توسعه دهندگان اجازه دهیم تا از طریق گزینه ignoreUrlParametersMatching
، پارامترهایی را که نادیده گرفته می شوند، سفارشی کنند. در اینجا پیاده سازی اساسی است:
function stripIgnoredUrlParameters(originalUrl, ignoredRegexes) {
var url = new URL(originalUrl);
url.search = url.search.slice(1)
.split('&')
.map(function(kv) {
return kv.split('=');
})
.filter(function(kv) {
return ignoredRegexes.every(function(ignoredRegex) {
return !ignoredRegex.test(kv[0]);
});
})
.map(function(kv) {
return kv.join('=');
})
.join('&');
return url.toString();
}
این چه معنایی برای شما دارد
ادغام کارکنان سرویس در برنامه وب Google I/O احتمالاً پیچیدهترین و واقعیترین کاربرد دنیای واقعی است که تا این مرحله به کار گرفته شده است. ما مشتاقانه منتظر جامعه توسعه دهندگان وب هستیم که از ابزارهایی که sw-precache
و sw-toolbox
ایجاد کردیم و همچنین تکنیک هایی را که توضیح می دهیم برای تقویت برنامه های کاربردی وب شما استفاده کنند. کارکنان خدمات یک پیشرفت تدریجی هستند که می توانید از امروز شروع به استفاده از آن کنید، و هنگامی که به عنوان بخشی از یک برنامه وب با ساختار مناسب استفاده می شود، سرعت و مزایای آفلاین برای کاربران شما قابل توجه است.