يوم جيد ، أيها الأصدقاء!
أقدم انتباهكم إلى ترجمة لمقالة Jake Archibald الممتازة "Offline Cookbook" المخصصة لحالات الاستخدام المختلفة لواجهة برمجة تطبيقات ServiceWorker وواجهة برمجة تطبيقات ذاكرة التخزين المؤقت.
من المفترض أنك على دراية بأساسيات هذه التقنيات ، لأنه سيكون هناك الكثير من التعليمات البرمجية وكلمات قليلة.
إذا لم يكن مألوفًا ، فابدأ بـ MDN ثم عد. إليك مقال آخر جيد عن العاملين في الخدمة خصيصًا للعناصر المرئية.
بدون مقدمة أخرى.
متى تحفظ الموارد؟
يسمح لك العامل بمعالجة الطلبات بشكل مستقل عن ذاكرة التخزين المؤقت ، لذلك سننظر فيها بشكل منفصل.
السؤال الأول هو متى يجب تخزين الموارد مؤقتًا؟
عندما يتم تثبيتها كبعية
أحد الأحداث التي تحدث أثناء تشغيل العامل هو حدث التثبيت. يمكن استخدام هذا الحدث للتحضير للتعامل مع الأحداث الأخرى. عندما يتم تثبيت عامل جديد ، يستمر العامل القديم في خدمة الصفحة ، لذا لا ينبغي أن يؤدي التعامل مع حدث التثبيت إلى كسرها.
مناسب لأنماط التخزين المؤقت والصور والنصوص والقوالب ... بشكل عام ، لأي ملفات ثابتة مستخدمة على الصفحة.
نحن نتحدث عن تلك الملفات التي بدونها لا يمكن للتطبيق أن يعمل مثل الملفات المضمنة في التنزيل الأولي للتطبيقات الأصلية.
self.addEventListener('install', event => {
event.waitUntil(
caches.open('mysite-static-v3')
.then(cache => cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js'
// ..
]))
)
})
يقبل event.waitUntil وعدًا بتحديد مدة التثبيت ونتيجته. إذا تم رفض الوعد ، فلن يتم تثبيت العامل. caches.open and cache.addAll عودة الوعود. إذا لم يتوفر أحد الموارد ،
فسيتم رفض استدعاء cache.addAll.
عندما لا يتم تركيبه كتبعية
هذا مشابه للمثال السابق ، لكن في هذه الحالة لا ننتظر حتى يكتمل التثبيت ، لذلك لن يتم إلغاء التثبيت.
مناسب للموارد الكبيرة غير المطلوبة في الوقت الحالي ، مثل الموارد للمستويات اللاحقة من اللعبة.
self.addEventListener('install', event => {
event.waitUntil(
caches.open('mygame-core-v1')
.then(cache => {
cache.addAll(
// 11-20
)
return cache.addAll(
// 1-10
)
})
)
})
نحن لا نمرر ذاكرة التخزين المؤقت cache.addAll إلى event.wait حتى المستويات 11-20 ، لذلك إذا تم رفضها ، ستستمر اللعبة في العمل دون اتصال. بالطبع ، يجب أن تهتم بالمشكلات المحتملة في التخزين المؤقت للمستويات الأولى ، على سبيل المثال ، حاول التخزين المؤقت مرة أخرى في حالة الفشل.
يمكن إيقاف العامل بعد معالجة الأحداث قبل تخزين المستويات 11-20 مؤقتًا. هذا يعني أن هذه المستويات لن يتم حفظها. في المستقبل ، من المخطط إضافة واجهة تحميل خلفية للعامل لحل هذه المشكلة ، وكذلك لتنزيل ملفات كبيرة مثل الأفلام.
تقريبا. Per.: تم تنفيذ هذه الواجهة في نهاية عام 2018 وأطلق عليها اسم جلب الخلفية ، ولكنها تعمل حتى الآن فقط في Chrome و Opera (68٪ وفقًا لـ CanIUse ).
على تفعيل
مناسب لحذف ذاكرة التخزين المؤقت وعمليات الترحيل القديمة.
بعد تثبيت عامل جديد وإيقاف العامل القديم ، يتم تنشيط العامل الجديد ونتلقى حدث تنشيط. هذه فرصة رائعة لاستبدال الموارد وحذف ذاكرة التخزين المؤقت القديمة.
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(cacheNames => Promise.all(
cacheNames.filter(cacheName => {
// true, , ,
// ,
}).map(cacheName => caches.delete(cacheName))
))
)
})
أثناء التنشيط ، يتم وضع أحداث أخرى مثل الجلب في قائمة الانتظار ، لذلك قد يؤدي التنشيط الطويل إلى حظر الصفحة نظريًا. لذا استخدم هذه المرحلة فقط للأشياء التي لا يمكنك فعلها مع العامل القديم.
عندما يقع حدث مخصص
مناسب عندما لا يمكن نقل الموقع بالكامل إلى وضع عدم الاتصال. في هذه الحالة ، نمنح المستخدم القدرة على تحديد ذاكرة التخزين المؤقت. على سبيل المثال ، فيديو Youtube أو صفحة Wikipedia أو معرض صور على Flickr.
امنح المستخدم زر "قراءة لاحقًا" أو "حفظ". عند النقر على الزر ، احصل على المورد واكتبه في ذاكرة التخزين المؤقت.
document.querySelector('.cache-article').addEventListener('click', event => {
event.preventDefault()
const id = event.target.dataset.id
caches.open(`mysite-article ${id}`)
.then(cache => fetch(`/get-article-urls?id=${id}`)
.then(response => {
// get-article-urls JSON
// URL
return response.json()
}).then(urls => cache.addAll(urls)))
})
تتوفر واجهة التخزين المؤقت على الصفحة ، تمامًا مثل العامل نفسه ، لذلك لا نحتاج إلى استدعاء الأخير لحفظ الموارد.
أثناء تلقي الرد
مناسب للموارد التي يتم تحديثها بشكل متكرر مثل صندوق بريد المستخدم أو محتوى المقالة. مناسب أيضًا للمحتوى الثانوي مثل الصور الرمزية ، ولكن كن حذرًا في هذه الحالة.
إذا لم يكن المورد المطلوب في ذاكرة التخزين المؤقت ، فإننا نحصل عليه من الشبكة ، ونرسله إلى العميل ونكتبه في ذاكرة التخزين المؤقت.
إذا كنت تطلب عناوين URL متعددة ، مثل مسارات الصورة الرمزية ، فتأكد من أنها لا تتجاوز مخزن الأصل (الأصل - البروتوكول والمضيف والمنفذ) - إذا احتاج المستخدم إلى تحرير مساحة على القرص ، فلا يجب أن تكون الأول. احرص على إزالة الموارد غير الضرورية.
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('mysite-dynamic')
.then(cache => cache.match(event.request)
.then(response => response || fetch(event.request)
.then(response => {
cache.put(event.request, response.clone())
return response
})))
)
})
لاستخدام الذاكرة بكفاءة ، نقرأ جسم الاستجابة مرة واحدة فقط. يستخدم المثال أعلاه طريقة النسخ لإنشاء نسخة من الاستجابة. يتم ذلك من أجل إرسال رد في نفس الوقت إلى العميل وكتابته في ذاكرة التخزين المؤقت.
أثناء التحقق من الجدة
مناسب لتحديث الموارد التي لا تتطلب أحدث الإصدارات. يمكن أن ينطبق هذا أيضًا على الصور الرمزية.
إذا كان المورد موجودًا في ذاكرة التخزين المؤقت ، فإننا نستخدمه ، ولكننا نحصل على تحديث في الطلب التالي.
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('mysite-dynamic')
.then(cache => cache.match(event.request)
.then(response => {
const fetchPromise = fetch(event.request)
.then(networkResponse => {
cache.put(event.request, networkResponse.clone())
return networkResponse
})
return response || fetchPromise
}))
)
})
عندما تتلقى إشعار الدفع
واجهة برمجة تطبيقات Push هي فكرة مجردة عن العامل. يسمح للعامل بالتشغيل استجابةً لرسالة من نظام التشغيل. علاوة على ذلك ، يحدث هذا بغض النظر عن المستخدم (عند إغلاق علامة تبويب المتصفح). ترسل الصفحة عادةً طلبًا إلى المستخدم للحصول على إذن لتنفيذ إجراءات معينة.
مناسب للمحتوى الذي يعتمد على الإشعارات ، مثل رسائل الدردشة وموجز الأخبار ورسائل البريد الإلكتروني. تُستخدم أيضًا لمزامنة المحتوى مثل المهام الموجودة في قائمة أو العلامات الموجودة في التقويم.
والنتيجة هي إشعار يفتح ، عند النقر فوقه ، الصفحة المقابلة. ومع ذلك ، من المهم جدًا الحفاظ على الموارد قبل إرسال الإشعار. يكون المستخدم متصلاً بالإنترنت عند تلقي الإشعار ، لكنه قد يكون غير متصل بالإنترنت عند النقر فوقه ، لذلك من المهم أن يكون المحتوى متاحًا في وضع عدم الاتصال في تلك اللحظة. يقوم تطبيق Twitter للجوال بهذا الخطأ قليلاً.
بدون اتصال بالشبكة ، لا يوفر تويتر محتوى متعلقًا بالإشعارات. ومع ذلك ، يؤدي النقر فوق الإخطار إلى حذفه. لا تفعل ذلك!
يقوم الكود التالي بتحديث ذاكرة التخزين المؤقت قبل إرسال الإشعار:
self.addEventListener('push', event => {
if (event.data.text() === 'new-email') {
event.waitUntil(
caches.open('mysite-dynamic')
.then(cache => fetch('/inbox.json')
.then(response => {
cache.put('/inbox.json', response.clone())
return response.json()
})).then(emails => {
registration.showNotification('New email', {
body: `From ${emails[0].from.name}`,
tag: 'new-email'
})
})
)
}
})
self.addEventListener('notificationclick', event => {
if (event.notification.tag === 'new-email') {
// , , /inbox/ ,
// ,
new WindowClient('/inbox/')
}
})
مع مزامنة الخلفية
مزامنة الخلفية هي فكرة تجريدية أخرى على العامل. يسمح لك بطلب مزامنة بيانات الخلفية لمرة واحدة أو بشكل دوري. وهي أيضًا مستقلة عن المستخدم. ومع ذلك ، يتم إرسال طلب الإذن إليه أيضًا.
مناسب لتحديث الموارد غير المهمة ، حيث سيكون الإرسال المنتظم للإخطارات حوله متكررًا جدًا وبالتالي مزعجًا للمستخدم ، على سبيل المثال ، الأحداث الجديدة في شبكة اجتماعية أو مقالات جديدة في موجز الأخبار.
self.addEventListener('sync', event => {
if (event.id === 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic')
.then(cache => cache.add('/leaderboard.json'))
)
}
})
حفظ ذاكرة التخزين المؤقت
يوفر مصدرك قدرًا معينًا من المساحة الحرة. هذه المساحة مشتركة بين جميع المخازن: المحلية والجلسات ، قاعدة البيانات المفهرسة ، نظام الملفات ، وبالطبع ذاكرة التخزين المؤقت.
أحجام التخزين غير ثابتة وتختلف حسب الجهاز وظروف التخزين. يمكنك التحقق من ذلك على النحو التالي:
navigator.storageQuota.queryInfo('temporary').then(info => {
console.log(info.quota)
// : < >
console.log(info.usage)
// < >
})
عندما يصل حجم هذا التخزين أو ذاك إلى الحد الأقصى ، يتم مسح هذا التخزين وفقًا لقواعد معينة لا يمكن تغييرها في الوقت الحالي.
لحل هذه المشكلة ، تم اقتراح واجهة إرسال طلب إذن (requestPeristent):
navigator.storage.requestPersistent().then(granted => {
if (granted) {
// ,
}
})
بالطبع ، يجب على المستخدم منح الإذن لذلك. يجب أن يكون المستخدم جزءًا من هذه العملية. إذا كانت الذاكرة على جهاز المستخدم ممتلئة ولم يؤد حذف البيانات الثانوية إلى حل المشكلة ، فيجب على المستخدم تحديد البيانات التي سيحتفظ بها وأيها سيحذفها.
لكي يعمل هذا ، يجب أن يتعامل نظام التشغيل مع متاجر المستعرض على أنها عناصر منفصلة.
الرد على الطلبات
لا يهم عدد الموارد التي تخزنها مؤقتًا ، فلن يستخدمها العامل حتى تخبره بموعد وماذا يستخدم. فيما يلي بعض القوالب للتعامل مع الطلبات.
نقد فقط
مناسب لأي موارد ثابتة للإصدار الحالي من الصفحة. يجب تخزين هذه الموارد مؤقتًا أثناء مرحلة إعداد العامل حتى تتمكن من إرسالها استجابة للطلبات.
self.addEventListener('fetch', event => {
// ,
//
event.respondWith(caches.match(event.request))
})
الشبكة فقط
مناسب للموارد التي لا يمكن تخزينها مؤقتًا ، مثل بيانات التحليلات أو الطلبات غير التابعة لـ GET.
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request))
// event.respondWith
//
})
أولاً ، ذاكرة التخزين المؤقت ، ثم عند الفشل ، الشبكة
مناسب للتعامل مع معظم الطلبات في التطبيقات غير المتصلة بالإنترنت.
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})
يتم إرجاع الموارد المحفوظة من ذاكرة التخزين المؤقت والموارد غير المحفوظة من الشبكة.
من لديه الوقت ، أكل
مناسب للموارد الصغيرة سعياً وراء أداء أفضل للأجهزة ذات الذاكرة المنخفضة.
يمكن أن يؤدي الجمع بين القرص الصلب القديم ومكافحة الفيروسات والاتصال السريع بالإنترنت إلى جعل جلب البيانات من الشبكة أسرع من جلب البيانات من ذاكرة التخزين المؤقت. ومع ذلك ، فإن استرداد البيانات من الشبكة أثناء تخزين البيانات على جهاز المستخدم يعد إهدارًا للموارد.
// Promise.race ,
// .
//
const promiseAny = promises => new Promise((resolve, reject) => {
// promises
promises = promises.map(p => Promise.resolve(p))
// ,
promises.forEach(p => p.then(resolve))
// ,
promises.reduce((a, b) => a.catch(() => b))
.catch(() => reject(Error(' ')))
})
self.addEventListener('fetch', event => {
event.respondWith(
promiseAny([
caches.match(event.request),
fetch(event.request)
])
)
})
تقريبا. لين: يمكنك الآن استخدام Promise.allSettled لهذا الغرض ، لكن دعم المتصفح الخاص به هو 80٪: من المحتمل أن يكون -20٪ من المستخدمين أكثر من اللازم.
الشبكة أولاً ، ثم ذاكرة التخزين المؤقت عند الفشل
مناسب للموارد التي يتم تحديثها بشكل متكرر ولا تؤثر على الإصدار الحالي من الموقع ، على سبيل المثال ، المقالات ، الصور الرمزية ، موجز الأخبار على الشبكات الاجتماعية ، تقييمات اللاعبين ، إلخ.
هذا يعني أنك تقدم محتوى جديدًا للمستخدمين عبر الإنترنت ومحتوى قديمًا للمستخدمين غير المتصلين. إذا نجح طلب مورد من الشبكة ، فمن المحتمل أن يتم تحديث ذاكرة التخزين المؤقت.
هذا النهج له عيب واحد. إذا كان المستخدم يعاني من مشاكل في الاتصال أو كان بطيئًا ، فعليه الانتظار حتى يكتمل الطلب أو يفشل بدلاً من جلب المحتوى على الفور من ذاكرة التخزين المؤقت. قد يكون هذا الانتظار طويلًا جدًا ، مما يؤدي إلى تجربة مستخدم مروعة.
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
)
})
أولا ذاكرة التخزين المؤقت ، ثم الشبكة
مناسبة للموارد التي يتم تحديثها بشكل متكرر.
يتطلب هذا من الصفحة إرسال طلبين ، أحدهما لذاكرة التخزين المؤقت والآخر للشبكة. الفكرة هي إرجاع البيانات من ذاكرة التخزين المؤقت ثم تحديثها عند تلقي البيانات من الشبكة.
في بعض الأحيان يمكنك استبدال البيانات الحالية عندما تتلقى بيانات جديدة (على سبيل المثال ، تصنيف اللاعبين) ، ولكن هذا يمثل مشكلة بالنسبة لأجزاء كبيرة من المحتوى. قد يؤدي ذلك إلى اختفاء ما يقرأه المستخدم حاليًا أو يتفاعل معه.
يضيف Twitter محتوى جديدًا فوق المحتوى الحالي مع الحفاظ على التمرير: يرى المستخدم إشعارًا بتغريدات جديدة في أعلى الشاشة. هذا ممكن بفضل الترتيب الخطي للمحتوى. لقد قمت بنسخ هذا القالب لعرض المحتوى من ذاكرة التخزين المؤقت في أسرع وقت ممكن وإضافة محتوى جديد كما يحصل من الويب.
الرمز في الصفحة:
const networkDataReceived = false
startSpinner()
//
const networkUpdate = fetch('/data.json')
.then(response => response.json())
.then(data => {
networkDataReceived = true
updatePage(data)
})
//
caches.match('/data.json')
.then(response => {
if (!response) throw Error(' ')
return response.json()
}).then(data => {
//
if (!networkDataReceived) {
updatePage(data)
}
}).catch(() => {
// , -
return networkUpdate
}).catch(showErrorMessage).then(stopSpinner)
رمز العامل:
نقوم بالوصول إلى الشبكة وتحديث ذاكرة التخزين المؤقت.
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('mysite-dynamic')
.then(cache => fetch(event.request)
.then(response => {
cache.put(event.request, response.clone())
return response
}))
)
})
شبكة الأمان
إذا فشلت محاولات الحصول على المورد من ذاكرة التخزين المؤقت والشبكة ، فيجب أن يكون هناك رجوع.
مناسب للعناصر النائبة (استبدال الصور بالدمية) ، طلبات POST الفاشلة ، صفحات "غير متاح عند عدم الاتصال".
self.addEventListener('fetch', event => {
event.respondWith(
//
// ,
caches.match(event.request)
.then(response => response || fetch(event.request))
.catch(() => {
// ,
return caches.match('/offline.html')
//
// URL
})
)
})
إذا أرسلت صفحتك بريدًا إلكترونيًا ، فيمكن للعامل حفظه في قاعدة بيانات مفهرسة قبل الإرسال وإخطار الصفحة بفشل الإرسال ، ولكن تم حفظ البريد الإلكتروني.
إنشاء العلامات من جانب العامل
مناسبة للصفحات التي يتم عرضها على جانب الخادم ولا يمكن تخزينها مؤقتًا.
يعد عرض الصفحات من جانب الخادم عملية سريعة جدًا ، ولكنها تجعل تخزين المحتوى الديناميكي في ذاكرة التخزين المؤقت بلا فائدة ، حيث يمكن أن يكون مختلفًا لكل عرض. إذا كان أحد العاملين يتحكم في صفحتك ، فيمكنك طلب الموارد وعرض الصفحة هناك.
import './templating-engine.js'
self.addEventListener('fetch', event => {
const requestURL = new URL(event.request.url)
event.respondWith(
Promise.all([
caches.match('/article-template.html')
.then(response => response.text()),
caches.match(`${requestURL.path}.json`)
.then(response => response.json())
]).then(responses => {
const template = responses[0]
const data = responses[1]
return new Response(renderTemplate(template, data), {
headers: {
'Content-Type': 'text/html'
}
})
})
)
})
سويا
ليس عليك أن تكون مقيدًا بقالب واحد. سيكون عليك على الأرجح دمجها بناءً على الطلب. على سبيل المثال ، يستخدم برنامج التدريب على الإثارة ما يلي:
- التخزين المؤقت لإعداد العامل لعناصر واجهة المستخدم الثابتة
- التخزين المؤقت عند استجابة الخادم لصور وبيانات Flickr
- استرداد البيانات من ذاكرة التخزين المؤقت ، وعند الفشل من الشبكة لمعظم الطلبات
- استرداد الموارد من ذاكرة التخزين المؤقت ثم من الويب لنتائج بحث Flick
ما عليك سوى إلقاء نظرة على الطلب وتحديد ما يجب فعله به:
self.addEventListener('fetch', event => {
// URL
const requestURL = new URL(event.request.url)
//
if (requestURL.hostname === 'api.example.com') {
event.respondWith(/* */)
return
}
//
if (requestURL.origin === location.origin) {
//
if (/^\/article\//.test(requestURL.pathname)) {
event.respondWith(/* */)
return
}
if (/\.webp$/.test(requestURL.pathname)) {
event.respondWith(/* */)
return
}
if (request.method == 'POST') {
event.respondWith(/* */)
return
}
if (/cheese/.test(requestURL.pathname)) {
event.respondWith(
// . .: - ?
new Response('Flagrant cheese error', {
//
status: 512
})
)
return
}
}
//
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})
آمل أن تكون هذه المقالة مفيدة لك. شكرآ لك على أهتمامك.