عاملو الخدمات في مرحلة الإنتاج

لقطة شاشة بالوضع العمودي

ملخّص

تعرَّف على كيفية استخدامنا لمكتبات مهام الخدمة لجعل تطبيق الويب الخاص بمؤتمر 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/});
صور الملفات الشخصية من صفحة جلسة
صور الملف الشخصي من صفحة جلسة

تعديلات على جداول المستخدمين الزمنية

كانت إحدى الميزات الرئيسية في تطبيقات الويب المعزولة هي السماح للمستخدمين الذين سجّلوا الدخول بإنشاء جدول زمني للجلسات التي خططوا للمشاركة فيها و الحفاظ عليه. كما هو متوقّع، تم إجراء تعديلات الجلسة من خلال طلبات 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() التي يتم إجراؤها في عامل خدمة باستخدام بيانات من ذاكرة التخزين المؤقت لبروتوكول 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 من قِبل مجتمع مطوّري تطبيقات الويب، بالإضافة إلى التقنيات التي نوضّحها لتعزيز تطبيقات الويب الخاصة بك. مشغّلو الخدمات هم تحسين تدريجي يمكنك البدء في استخدامه اليوم، وعند استخدامه كجزء من تطبيق ويب منظَّم بشكل صحيح، تكون سرعة التطبيق وفوائده بلا إنترنت مُهمّة للمستخدمين.