
ملخّص
تعرَّف على كيفية استخدامنا لمكتبات "خدمة عامل التشغيل" لجعل تطبيق الويب الخاص بمؤتمر Google I/O لعام 2015 سريعًا ويعمل بشكل أساسي بلا إنترنت.
نظرة عامة
تم تطوير تطبيق الويب الخاص بمؤتمر Google I/O لعام 2015 من قِبل فريق "العلاقات مع المطوّرين" في Google، استنادًا إلى تصاميم أعدّها أصدقاؤنا في Instrument، وهم من كتبوا التجربة الصوتية/المرئية الرائعة. كانت مهمة فريقنا هي التأكّد من أنّ تطبيق الويب I/O (الذي سأشير إليه باسمه الرمزي IOWA) يعرض كل ما يمكن أن يفعله الويب الحديث. وكانت تجربة العمل بلا إنترنت في المقام الأول في قائمة الميزات التي يجب أن تتوفّر.
إذا قرأت أيًا من المقالات الأخرى على هذا الموقع مؤخرًا، لا شك أنّك عثرت على خدمة workers،
ولن تتفاجأ لمعرفة أنّ ميزة الدعم بلا إنترنت في IOWA تعتمد بشكل كبير
على هذه الخدمة. استجابةً للاحتياجات الفعلية لخدمة IOWA، طوّرنا مكتبتَين
لمعالجة حالتَي استخدام مختلفتَين بلا إنترنت:
sw-precache
لأتمتة
التخزين المُسبَق للموارد الثابتة، و
sw-toolbox
لمعالجة
التخزين المؤقت أثناء التشغيل واستراتيجيات الحلول الاحتياطية.
تتكامل المكتبات مع بعضها بشكل جيد، ما سمح لنا بتنفيذ استراتيجية تحقّق أداءً جيدًا، حيث يتم عرض "قشرة" المحتوى الثابت في IOWA دائمًا مباشرةً من ذاكرة التخزين المؤقت، ويتم عرض الموارد الديناميكية أو البعيدة من الشبكة، مع استخدام العناصر الاحتياطية للردود المخزّنة مؤقتًا أو الثابتة عند الحاجة.
التخزين المؤقت المُسبَق باستخدام sw-precache
توفّر الموارد الثابتة في IOWA، مثل HTML وJavaScript و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 فريدة لكل موارد يتم تخزينها مؤقتًا مسبقًا. في كل مرة يتغيّر فيها مورد حالي،
أو تتم إضافة مورد جديد، تتم إعادة إنشاء نص عامل الخدمة. يؤدي ذلك
تلقائيًا إلى بدء عملية تحديث الخدمة العاملة،
التي يتم فيها تخزين المراجع الجديدة مؤقتًا وحذف المراجع القديمة.
لا يتم إجراء أي تغييرات على أي موارد حالية تتضمّن تجزئات MD5 متطابقة. ويعني ذلك أنّه لن يتم تنزيل سوى مجموعة أساسية من الموارد التي تم تغييرها للمستخدمين الذين سبق لهم زيارة الموقع الإلكتروني، ما يؤدي إلى تجربة أكثر فعالية مقارنةً بما لو تم إيقاف ذاكرة التخزين المؤقت بالكامل بشكل جماعي.
يتم تنزيل كل ملف يتطابق مع أحد أنماط النطاقات الشاملة وتخزينه مؤقتًا في المرة الأولى التي يزور فيها المستخدِم IOWA. لقد بذلنا جهدًا لضمان تخزين موارد
الضرورية فقط لعرض الصفحة مسبقًا. لم يتم عمدًا
تخزين المحتوى الثانوي مسبقًا، مثل
الوسائط المستخدَمة في التجربة الصوتية/المرئية،
أو صور الملفات الشخصية للمتحدثين في الجلسات، ولقد استخدمنا بدلاً من ذلك مكتبة sw-toolbox
لمعالجة الطلبات بلا إنترنت لهذه الموارد.
sw-toolbox
، لجميع احتياجاتنا الديناميكية
كما ذكرنا، ليس من المُجدي تخزين كلّ مورد يحتاجه الموقع الإلكتروني للعمل بلا إنترنت مسبقًا. بعض الموارد كبيرة جدًا أو يتم استخدامها بشكل غير متكرّر لكي يكون
استخدامها مجديًا، وتكون الموارد الأخرى ديناميكية، مثل الردود الواردة من واجهة برمجة التطبيقات أو الخدمة البعيدة. ومع ذلك، لا يعني عدم الاحتفاظ بالطلبات مؤقتًا في ذاكرة التخزين أنّه
يجب أن يؤدي ذلك إلى ظهور NetworkError
.
من خلال sw-toolbox
، اكتسبنا
مرونة تنفيذ معالجات الطلبات
التي تتعامل مع التخزين المؤقت أثناء التشغيل لبعض الموارد والعناصر الاحتياطية المخصّصة
للموارد الأخرى. واستخدمناها أيضًا لتعديل الموارد المخزّنة مؤقتًا في السابق استجابةً
للإشعارات الفورية.
في ما يلي بعض الأمثلة على معالجات الطلبات المخصّصة التي أنشأناها استنادًا إلى مكتبة
sw-toolbox. كان من السهل دمجها مع نص الخدمة الأساسي
من خلال importScripts parameter
في sw-precache
،
الذي يسحب ملفات JavaScript المستقلة إلى نطاق الخدمة.
تجربة صوتية/مرئية
بالنسبة إلى التجربة الصوتية/المرئية،
استخدمنا استراتيجية ذاكرة التخزين المؤقت networkFirst
في sw-toolbox
. سيتم أولاً إرسال جميع طلبات HTTP التي تتطابق مع نمط عنوان URL للتجربة
إلى الشبكة، وإذا تم عرض استجابة ناجحة، سيتم تخزين هذه الاستجابة باستخدام واجهة برمجة التطبيقات
Cache Storage API.
إذا تم تقديم طلب لاحق عندما كانت الشبكة غير متاحة، سيتم استخدام
الاستجابة المخزّنة مؤقتًا سابقًا.
وبما أنّه كان يتم تعديل ذاكرة التخزين المؤقت تلقائيًا في كل مرة يتم فيها تلقّي ردّ إيجابي من الشبكة، لم يكن علينا تحديد إصدارات للموارد أو تحديد مهلة انتهاء صلاحية للادخالات.
toolbox.router.get('/experiment/(.+)', toolbox.networkFirst);
صور الملف الشخصي للمتحدثين
بالنسبة إلى صور الملفات الشخصية للمتحدثين، كان هدفنا عرض نسخة تم تخزينها مؤقتًا من
صورة متحدث معيّن إذا كانت متاحة، والرجوع إلى الشبكة لاسترداد
الصورة إذا لم تكن متاحة. إذا تعذّر طلب الشبكة هذا، استخدمنا
صورة نائبة عامة تم تخزينها مؤقتًا مسبقًا (وبالتالي ستكون متوفرة
دائمًا) كحل بديل نهائي. هذه استراتيجية شائعة يتم استخدامها عند التعامل مع الصور التي
يمكن استبدالها بعنصر نائب عام، وكان من السهل تنفيذها من خلال
ربط معالِجَي sw-toolbox
cacheFirst
و
cacheOnly
.
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" بلا إنترنت
وفي سياق مشابه، نفّذنا معالِجًا لوضع أي طلبات "إحصاءات Google" التي تعذّر إكمالها في "قائمة الانتظار" ومحاولة إعادة تشغيلها لاحقًا، عندما تكون الشبكة متاحة. باستخدام هذا النهج، لا يعني عدم الاتصال بالإنترنت التضحية
بالإحصاءات التي تقدّمها "إحصاءات Google". أضفنا المَعلمة qt
إلى كل طلب في "قائمة الانتظار"، وتم ضبطها على المدة التي مرت
منذ محاولة إرسال الطلب لأول مرة، لضمان وصول وقت تحديد مصدر مناسب
للحدث إلى الخلفية في "إحصاءات Google". تتيح "إحصاءات Google"
رسميًا
قيمًا لـ 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');
}
});
});
الأخطاء والنقاط التي يجب مراعاتها
بالطبع، لا يعمل أحد على مشروع بحجم IOWA بدون مواجهة بعض الصعوبات. في ما يلي بعض المشاكل التي واجهناها وكيفية حلّها.
المحتوى القديم
عند التخطيط لاستراتيجيات التخزين المؤقت، سواء تم تنفيذها من خلال عمال الخدمة
أو باستخدام ذاكرة التخزين المؤقت العادية للمتصفّح، هناك مفاضلة بين
تقديم الموارد في أسرع وقت ممكن مقابل تقديم أحدث
الموارد. من خلال sw-precache
، نفّذنا استراتيجية قوية تعتمد على التخزين المؤقت أولاً
لشَكل تطبيقنا، ما يعني أنّ عامل الخدمة لن يتحقّق من
الشبكة بحثًا عن آخر الأخبار قبل عرض صفحات HTML وJavaScript و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 لمحتوى الملفات المحلية، ولا يجلب سوى
الموارد التي تغيّرت تجزئتها. وهذا يعني أنّ الموارد متاحة في الصفحة
على الفور تقريبًا، ولكنّه يعني أيضًا أنّه بعد تخزين محتوى في ذاكرة التخزين المؤقت، سيظلّ
مخزّنًا في ذاكرة التخزين المؤقت إلى أن يتمّ تخصيص قيمة تجزئة جديدة له في نصّ عامل الخدمة المعدَّل.
واجهنا مشكلة في هذا السلوك أثناء مؤتمر I/O بسبب الحاجة إلى تعديل قاعدة البيانات ديناميكيًا لأرقام تعريف فيديوهات البث المباشر على YouTube لكل يوم من أيام المؤتمر. ولأنّ ملف النموذج الأساسي كان ثابتًا ولم يتغيّر، لم يتم بدء عملية تعديل الخدمة العاملة، وأصبح الردّ المخزّن مؤقتًا لعدد من المستخدمين بدلاً من الردّ الديناميكي الذي كان من المفترض أن يصلهم من الخادم مع تعديل فيديوهات YouTube.
يمكنك تجنُّب هذا النوع من المشاكل من خلال التأكّد من أنّ تطبيق الويب مُنظَّم بحيث تكون القشرة ثابتة دائمًا ويمكن تخزينها مؤقتًا بأمان، بينما يتم تحميل أي موارد ديناميكية تعدّل القشرة بشكل مستقل.
إزالة ذاكرة التخزين المؤقت لطلبات التخزين المؤقت
عندما يُرسل sw-precache
طلبات للحصول على موارد لتخزينها مؤقتًا، يستخدم هذه
الاستجابات إلى أجل غير مسمى ما دام يعتقد أنّ تجزئة MD5 للملف لم
تتغيّر. وهذا يعني أنّه من المهم بشكل خاص التأكّد من أنّ الاستجابة لطلب التجميع المُسبَق هي استجابة جديدة، وأنّها لم يتم عرضها من ذاكرة التخزين المؤقت لبروتوكول HTTP في المتصفّح. (نعم، يمكن أن تستجيب طلبات fetch()
التي يتم إجراؤها في Worker الخدمة باستخدام
البيانات من ذاكرة التخزين المؤقت لبروتوكول HTTP في المتصفّح).
لضمان أن تكون الاستجابات التي نخزّنها مسبقًا مباشرةً من الشبكة وليس من ملف التخزين المؤقت لبروتوكول HTTP في المتصفّح، sw-precache
تُلحق تلقائيًا مَعلمة طلب بحث لإزالة ذاكرة التخزين المؤقت
بكل عنوان URL تطلبه. إذا كنت لا تستخدم sw-precache
وكنت
تستخدم استراتيجية الاستجابة من خلال ذاكرة التخزين المؤقت أولاً، احرص على
تنفيذ إجراء مشابه
في الرمز البرمجي الخاص بك.
لحلّ أنظف لإزالة ذاكرة التخزين المؤقت، يمكنك ضبط وضع ذاكرة التخزين المؤقت لكل Request
المستخدَم في التخزين المؤقت المُسبَق على reload
، ما سيضمن أنّ Request
يأتي من الشبكة. ومع ذلك، اعتبارًا من وقت كتابة هذه المقالة، لا يتوفّر خيار وضع التخزين المؤقت في Chrome.
إتاحة تسجيل الدخول والخروج
سمحت 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 المستخدَم لتخزين الردّ المخزّن مؤقتًا، بما في ذلك أي مَعلمات طلب بحث في جزء search من عنوان URL.
وقد تسبّب ذلك في حدوث مشكلة لنا أثناء التطوير، عندما بدأنا باستخدام
مَعلمات عناوين URL لتتبُّع مصدر
الزيارات. على سبيل المثال، أضفنا
المَعلمة utm_source=notification
إلى عناوين URL التي تم فتحها عند النقر على أحد
إشعاراتنا، واستخدمنا utm_source=web_app_manifest
في start_url
لبيان تطبيق الويب.
كانت عناوين 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
من قِبل منتدى مطوّري الويب، بالإضافة إلى
التقنيات التي نوضّحها لتعزيز تطبيقات الويب الخاصة بك.
مشغّلو الخدمات هم تحسين تدريجي
يمكنك البدء في استخدامه اليوم، وعند استخدامه كجزء من تطبيق ويب
منظَّم بشكل صحيح، تكون سرعة التطبيق ومزايا استخدامه بلا إنترنت مُهمّة للمستخدمين.