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

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

ملخّص

تعرَّف على كيفية استخدامنا لمكتبات "خدمة عامل التشغيل" لجعل تطبيق الويب الخاص بمؤتمر Google I/O لعام 2015 سريعًا ويعمل بشكل أساسي بلا إنترنت.

نظرة عامة

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