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

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

ملخّص

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

نظرة عامة

صمّم فريق علاقات المطوّرين في Google تطبيق الويب مؤتمر Google I/O 2015 هذا العام، استنادًا إلى تصميمات صمّمها أصدقاؤنا في الآلة، الذين كتبوا التجربة الصوتية/المرئية الرائعة. كان هدف فريقنا هو التأكّد من أنّ تطبيق الويب في مؤتمر I/O (الذي سأشير إليه باسمه الرمزي IOWA) يعرض كل ما يمكن أن تفعله شبكة الويب الحديثة. وقد كانت التجربة الكاملة التي يتم توفيرها بلا اتصال بالإنترنت على رأس قائمة الميزات الضرورية.

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

يتم تنزيل كل ملف يتطابق مع أحد أنماط glob وتخزينه مؤقتًا في المرة الأولى التي يزور فيها المستخدم IOWA. لقد بذلنا جهدًا للتأكد من أن الموارد المهمة المهمة فقط اللازمة لعرض الصفحة تم تخزينها مؤقتًا بشكل مسبق. تم عن قصد عدم تخزين المحتوى الثانوي، مثل الوسائط المستخدمة في التجربة الصوتية/المرئية، أو صور الملفات الشخصية للمتحدثين في الجلسات، وبدلاً من ذلك استخدمنا مكتبة sw-toolbox للتعامل مع الطلبات بلا اتصال بالإنترنت لتلك الموارد.

"sw-toolbox" لتلبية كل احتياجاتنا الديناميكية

كما ذكرنا سابقًا، لا يمكن إجراء تخزين مؤقت لكل مورد يحتاجه الموقع الإلكتروني للعمل بلا اتصال بالإنترنت. بعض الموارد كبيرة جدًا أو نادرًا ما يتم استخدامها لجعلها جديرة بالاهتمام، وهناك موارد أخرى ديناميكية، مثل الاستجابات الواردة من واجهة برمجة تطبيقات أو خدمة عن بُعد. لا يعني عدم تخزين الطلب مؤقتًا بشكل مسبق أنّه يجب أن يؤدي إلى إنشاء NetworkError. منحتنا sw-toolbox المرونة في تنفيذ معالِجات الطلبات التي تتعامل مع التخزين المؤقت في وقت التشغيل لبعض الموارد والعناصر الاحتياطية المخصّصة للموارد الأخرى. كما استخدمنا هذا الزر لتحديث الموارد التي تم تخزينها مؤقتًا استجابةً للإشعارات الفورية.

إليك بعض الأمثلة على معالجات الطلبات المخصصة التي أنشأناها فوق مجموعة أدوات sw. كان من السهل دمجها مع النص البرمجي لمشغِّل الخدمات الأساسي من خلال importScripts parameter من sw-precache، الذي يسحب ملفات JavaScript المستقلة إلى نطاق مشغّل الخدمة.

تجربة صوتية ومرئية

من أجل التجربة الصوتية/المرئية، استخدمنا استراتيجية ذاكرة التخزين المؤقت networkFirst من sw-toolbox. يجب أولاً إجراء جميع طلبات HTTP التي تتطابق مع نمط عنوان URL للتجربة على الشبكة، وفي حال عرض استجابة ناجحة، سيتم تخزين هذه الاستجابة بعد ذلك باستخدام واجهة برمجة التطبيقات Cache Storage 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" بلا اتصال بالإنترنت

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