ตำราอาหารแบบออฟไลน์

เมื่อใช้ Service Worker เราเลิกพยายามแก้ปัญหาแบบออฟไลน์แล้ว และมอบชิ้นส่วนที่เคลื่อนไหวให้กับนักพัฒนาแอปเพื่อแก้ปัญหาด้วยตนเอง ซึ่งจะช่วยให้คุณควบคุมการแคชและวิธีจัดการคําขอได้ ซึ่งหมายความว่าคุณจะสร้างลายของคุณเองได้ มาดูรูปแบบที่เป็นไปได้ 2-3 รูปแบบกัน แต่ในทางปฏิบัติ คุณอาจใช้รูปแบบหลายรูปแบบร่วมกัน ทั้งนี้ขึ้นอยู่กับ URL และบริบท

ดูการสาธิตการใช้งานรูปแบบเหล่านี้บางส่วนได้ที่Trained-to-thrill และวิดีโอนี้ที่แสดงผลกระทบต่อประสิทธิภาพ

เครื่องแคช - กรณีที่ควรจัดเก็บทรัพยากร

Service Worker ช่วยให้คุณจัดการคำขอได้โดยไม่เกี่ยวข้องกับการแคช ดังนั้นเราจะสาธิตแยกกัน อันดับแรกคือแคช ควรทำเมื่อใด

ขณะติดตั้ง - ใช้เป็นทรัพยากร Dependency

เมื่อติดตั้ง - ใช้เป็นข้อกําหนด
เมื่อติดตั้ง - ใช้เป็น Dependency

Service Worker จะส่งเหตุการณ์ install ให้คุณ คุณสามารถใช้คำสั่งนี้เพื่อเตรียมสิ่งต่างๆ ซึ่งต้องพร้อมก่อนจัดการเหตุการณ์อื่นๆ ในระหว่างนี้ เวอร์ชันก่อนหน้าของ Service Worker จะยังคงทำงานและแสดงหน้าเว็บอยู่ ดังนั้นสิ่งที่คุณทําที่นี่ต้องไม่รบกวนการทำงานดังกล่าว

เหมาะสำหรับ: CSS, รูปภาพ, แบบอักษร, JS, เทมเพลต… โดยทั่วไปคือทุกอย่างที่คุณถือว่าคงที่สำหรับ"เวอร์ชัน" ของเว็บไซต์นั้น

สิ่งเหล่านี้จะทำให้เว็บไซต์ของคุณใช้งานไม่ได้เลยหากดึงข้อมูลไม่สำเร็จ ซึ่งแอปเฉพาะแพลตฟอร์มที่เทียบเท่าจะเป็นส่วนหนึ่งของการดาวน์โหลดครั้งแรก

self.addEventListener('install', function (event) {
  event
.waitUntil(
    caches
.open('mysite-static-v3').then(function (cache) {
     
return cache.addAll([
       
'/css/whatever-v3.css',
       
'/css/imgs/sprites-v6.png',
       
'/css/fonts/whatever-v8.woff',
       
'/js/all-min-v4.js',
       
// etc.
     
]);
   
}),
 
);
});

event.waitUntil ดำเนินการตามสัญญาเพื่อกำหนดระยะเวลาและความสำเร็จของการติดตั้ง หาก Promise ปฏิเสธ ระบบจะถือว่าการติดตั้งล้มเหลวและ Service Worker นี้จะหยุดทำงาน (หากมีเวอร์ชันเก่าที่ทำงานอยู่ ระบบจะไม่แตะต้องเวอร์ชันเก่า) caches.open() และ cache.addAll() สัญญาคืนสินค้า หากดึงข้อมูลทรัพยากรไม่สำเร็จ การเรียก cache.addAll() จะปฏิเสธ

ใน trained-to-thrill เราใช้ตัวเลือกนี้เพื่อแคชชิ้นงานแบบคงที่

ขณะติดตั้ง ไม่ใช่เป็นการอ้างอิง

เมื่อติดตั้ง - ไม่ใช่เป็นการอ้างอิง
ในการติดตั้ง - ไม่ใช่เป็นข้อกำหนดเบื้องต้น

ซึ่งคล้ายกับด้านบน แต่จะไม่ได้ทำให้การติดตั้งล่าช้าและไม่ทำให้การติดตั้งล้มเหลวหากแคชไม่สำเร็จ

เหมาะสำหรับ: ทรัพยากรขนาดใหญ่ที่ไม่จําเป็นต้องใช้ในทันที เช่น ชิ้นงานสําหรับด่านในเกมระดับหลังๆ

self.addEventListener('install', function (event) {
  event
.waitUntil(
    caches
.open('mygame-core-v1').then(function (cache) {
      cache
       
.addAll
       
// levels 11–20
       
();
     
return cache
       
.addAll
       
// core assets and levels 1–10
       
();
   
}),
 
);
});

ตัวอย่างข้างต้นไม่เป็นไปตามสัญญาของ cache.addAll สำหรับระดับ 11-20 ที่ส่งกลับไปให้ event.waitUntil ดังนั้นแม้ว่าจะดำเนินการไม่สำเร็จ เกมจะยังคงเล่นแบบออฟไลน์ได้ แน่นอนว่าคุณจะต้องเตรียมพร้อมในกรณีที่ไม่มีระดับเหล่านั้น และพยายามแคชอีกครั้งหากไม่มี

ระบบอาจหยุด Service Worker ขณะดาวน์โหลดระดับ 11-20 เนื่องจากจัดการเหตุการณ์เสร็จแล้ว ซึ่งหมายความว่าระบบจะไม่แคชระดับดังกล่าว ในอนาคต Web Periodic Background Synchronization API จะจัดการกับกรณีเช่นนี้และการดาวน์โหลดขนาดใหญ่ เช่น ภาพยนตร์ ปัจจุบัน API ดังกล่าวรองรับเฉพาะใน Chromium Fork เท่านั้น

เปิดใช้งาน

เปิดใช้งาน
เปิดใช้งาน

เหมาะสําหรับ: การจัดระเบียบและการย้ายข้อมูล

เมื่อติดตั้ง Service Worker ใหม่และไม่ได้ใช้งานเวอร์ชันก่อนหน้า Service Worker เวอร์ชันใหม่จะเปิดใช้งานและคุณจะได้รับเหตุการณ์ activate เมื่อเวอร์ชันเก่าถูกนำออกไปแล้ว ก็ถึงเวลาจัดการการย้ายข้อมูลสคีมาใน IndexedDB และลบแคชที่ไม่ได้ใช้

self.addEventListener('activate', function (event) {
  event
.waitUntil(
    caches
.keys().then(function (cacheNames) {
     
return Promise.all(
        cacheNames
         
.filter(function (cacheName) {
           
// Return true if you want to remove this cache,
           
// but remember that caches are shared across
           
// the whole origin
         
})
         
.map(function (cacheName) {
           
return caches.delete(cacheName);
         
}),
     
);
   
}),
 
);
});

ในระหว่างการเปิดใช้งาน ระบบจะจัดคิวเหตุการณ์อื่นๆ เช่น fetch ดังนั้นการเปิดใช้งานที่ใช้เวลานานอาจบล็อกการโหลดหน้าเว็บได้ พยายามทำให้การเปิดใช้งานมีประสิทธิภาพมากที่สุด และโปรดใช้การเปิดใช้งานกับสิ่งที่คุณทําไม่ได้ขณะที่เวอร์ชันเก่ายังทำงานอยู่

ใน trained-to-thrill เราใช้คำสั่งนี้เพื่อนำแคชเก่าออก

เกี่ยวกับการโต้ตอบของผู้ใช้

เกี่ยวกับการโต้ตอบของผู้ใช้
เกี่ยวกับการโต้ตอบของผู้ใช้

เหมาะสำหรับกรณีที่ทั้งเว็บไซต์ไม่สามารถออฟไลน์ได้ และคุณเลือกอนุญาตให้ผู้ใช้เลือกเนื้อหาที่ต้องการให้พร้อมใช้งานแบบออฟไลน์ เช่น วิดีโอบน YouTube, บทความใน Wikipedia, แกลเลอรีที่เฉพาะเจาะจงใน Flickr

ให้ปุ่ม "อ่านภายหลัง" หรือ "บันทึกไว้ดูแบบออฟไลน์" แก่ผู้ใช้ เมื่อมีการคลิก จะมีการดึงข้อมูลที่ต้องการจากเครือข่ายและใส่ไว้ในแคช

document.querySelector('.cache-article').addEventListener('click', function (event) {
  event
.preventDefault();

 
var id = this.dataset.articleId;
  caches
.open('mysite-article-' + id).then(function (cache) {
    fetch
('/get-article-urls?id=' + id)
     
.then(function (response) {
       
// /get-article-urls returns a JSON-encoded array of
       
// resource URLs that a given article depends on
       
return response.json();
     
})
     
.then(function (urls) {
        cache
.addAll(urls);
     
});
 
});
});

Caches API พร้อมใช้งานจากหน้าเว็บและ Service Worker ซึ่งหมายความว่าคุณสามารถเพิ่มลงในแคชได้โดยตรงจากหน้าเว็บ

เกี่ยวกับการตอบสนองของเครือข่าย

กำลังรอการตอบกลับจากเครือข่าย
ในการตอบกลับของเครือข่าย

เหมาะสําหรับ: ทรัพยากรที่อัปเดตบ่อย เช่น กล่องจดหมายของผู้ใช้หรือเนื้อหาบทความ นอกจากนี้ยังมีประโยชน์สำหรับเนื้อหาที่ไม่จำเป็น เช่น รูปโปรไฟล์ แต่ต้องใช้ความระมัดระวัง

หากคําขอไม่ตรงกับข้อมูลใดๆ ในแคช ระบบจะดึงข้อมูลจากเครือข่าย ส่งไปยังหน้าเว็บ และเพิ่มลงในแคชพร้อมกัน

หากทําเช่นนี้กับ URL หลายรายการ เช่น รูปโปรไฟล์ คุณจะต้องระมัดระวังไม่ให้พื้นที่เก็บข้อมูลของต้นทางมีขนาดใหญ่ขึ้น หากผู้ใช้ต้องการเรียกคืนพื้นที่ว่างในดิสก์ คุณไม่ต้องการให้ตัวเองเป็นตัวเลือกแรก อย่าลืมนำรายการในแคชที่ไม่จําเป็นต้องใช้แล้วออก

self.addEventListener('fetch', function (event) {
  event
.respondWith(
    caches
.open('mysite-dynamic').then(function (cache) {
     
return cache.match(event.request).then(function (response) {
       
return (
          response
||
          fetch
(event.request).then(function (response) {
            cache
.put(event.request, response.clone());
           
return response;
         
})
       
);
     
});
   
}),
 
);
});

คุณสามารถอ่านเนื้อหาของคำตอบ/คำขอได้เพียงครั้งเดียวเพื่อให้ใช้หน่วยความจำได้อย่างมีประสิทธิภาพ โค้ดด้านบนใช้ .clone() เพื่อสร้างสำเนาเพิ่มเติมที่อ่านแยกกันได้

ใน trained-to-thrill เราใช้คำสั่งนี้เพื่อแคชรูปภาพ Flickr

Stale-while-revalidate

Stale-while-revalidate
Stale-while-revalidate

เหมาะสำหรับ: ทรัพยากรที่มีการอัปเดตบ่อยครั้งซึ่งไม่จำเป็นต้องใช้เวอร์ชันล่าสุด โดยรูปโปรไฟล์อาจจัดอยู่ในหมวดหมู่นี้

หากมีเวอร์ชันที่แคชไว้ ให้ใช้เวอร์ชันนั้น แต่ให้ดึงข้อมูลอัปเดตไว้ใช้ครั้งถัดไป

self.addEventListener('fetch', function (event) {
  event
.respondWith(
    caches
.open('mysite-dynamic').then(function (cache) {
     
return cache.match(event.request).then(function (response) {
       
var fetchPromise = fetch(event.request).then(function (networkResponse) {
          cache
.put(event.request, networkResponse.clone());
         
return networkResponse;
       
});
       
return response || fetchPromise;
     
});
   
}),
 
);
});

ซึ่งคล้ายกับ stale-while-revalidate ของ HTTP มาก

เมื่อมีข้อความ Push

เมื่อได้รับข้อความ Push
เปิดข้อความ Push

Push API เป็นฟีเจอร์อีกรายการหนึ่งที่สร้างขึ้นบน Service Worker ซึ่งช่วยให้ Service Worker ตื่นขึ้นเพื่อตอบสนองต่อข้อความจากบริการรับส่งข้อความของระบบปฏิบัติการ ซึ่งจะเกิดขึ้นแม้ว่าผู้ใช้จะไม่ได้เปิดแท็บไปยังเว็บไซต์ของคุณก็ตาม ระบบจะปลุกเฉพาะ Service Worker เท่านั้น คุณจะขอสิทธิ์ดำเนินการนี้จากหน้าเว็บได้ และระบบจะแสดงข้อความแจ้งให้ผู้ใช้ทราบ

เหมาะสําหรับ: เนื้อหาที่เกี่ยวข้องกับการแจ้งเตือน เช่น ข้อความแชท เรื่องราวข่าวด่วน หรืออีเมล รวมถึงเนื้อหาที่มีการเปลี่ยนแปลงไม่บ่อยนักซึ่งได้ประโยชน์จากการซิงค์ทันที เช่น การอัปเดตรายการสิ่งที่ต้องทำหรือการเปลี่ยนแปลงปฏิทิน

ผลลัพธ์สุดท้ายที่พบได้ทั่วไปคือการแจ้งเตือน ซึ่งเมื่อแตะแล้ว ระบบจะเปิด/โฟกัสหน้าที่เกี่ยวข้อง แต่การอัปเดตแคชก่อนที่จะเกิดเหตุการณ์นี้ขึ้นนั้นสำคัญอย่างยิ่ง ผู้ใช้อาจไม่ได้ออนไลน์เมื่อโต้ตอบกับการแจ้งเตือนในที่สุด จึงควรทำให้เนื้อหานี้พร้อมใช้งานแบบออฟไลน์

โค้ดนี้จะอัปเดตแคชก่อนที่จะแสดงการแจ้งเตือน

self.addEventListener('push', function (event) {
 
if (event.data.text() == 'new-email') {
    event
.waitUntil(
      caches
       
.open('mysite-dynamic')
       
.then(function (cache) {
         
return fetch('/inbox.json').then(function (response) {
            cache
.put('/inbox.json', response.clone());
           
return response.json();
         
});
       
})
       
.then(function (emails) {
          registration
.showNotification('New email', {
            body
: 'From ' + emails[0].from.name,
            tag
: 'new-email',
         
});
       
}),
   
);
 
}
});

self
.addEventListener('notificationclick', function (event) {
 
if (event.notification.tag == 'new-email') {
   
// Assume that all of the resources needed to render
   
// /inbox/ have previously been cached, e.g. as part
   
// of the install handler.
   
new WindowClient('/inbox/');
 
}
});

เกี่ยวกับการซิงค์ในเบื้องหลัง

เปิดการซิงค์ในเบื้องหลัง
เปิดการซิงค์ในเบื้องหลัง

การซิงค์ในเบื้องหลังเป็นฟีเจอร์อีกรายการหนึ่งที่สร้างขึ้นจาก Service Worker ซึ่งช่วยให้คุณส่งคําขอซิงค์ข้อมูลในเบื้องหลังแบบครั้งเดียวหรือเป็นช่วง (แบบเฮิวริสติกมาก) ได้ ซึ่งจะเกิดขึ้นแม้ว่าผู้ใช้จะไม่ได้เปิดแท็บเว็บไซต์ของคุณไว้ก็ตาม ระบบจะปลุกเฉพาะ Service Worker เท่านั้น คุณจะขอสิทธิ์ดำเนินการนี้ได้จากหน้าเว็บและระบบจะแสดงข้อความแจ้งให้ผู้ใช้ทราบ

เหมาะสําหรับ: การอัปเดตที่ไม่เร่งด่วน โดยเฉพาะการอัปเดตที่เกิดขึ้นเป็นประจำจนทำให้ผู้ใช้ได้รับข้อความ Push มากเกินไป เช่น ไทม์ไลน์โซเชียลหรือบทความข่าว

self.addEventListener('sync', function (event) {
 
if (event.id == 'update-leaderboard') {
    event
.waitUntil(
      caches
.open('mygame-dynamic').then(function (cache) {
       
return cache.add('/leaderboard.json');
     
}),
   
);
 
}
});

การเก็บรักษาแคช

ต้นทางจะมีพื้นที่ว่างจำนวนหนึ่งสำหรับดำเนินการตามต้องการ พื้นที่ว่างดังกล่าวจะแชร์กันระหว่างพื้นที่เก็บข้อมูลต้นทางทั้งหมด ได้แก่ พื้นที่เก็บข้อมูล(ในเครื่อง), IndexedDB, การเข้าถึงระบบไฟล์ และแน่นอน แคช

เราไม่ระบุจำนวนเงินที่คุณจะได้รับ ซึ่งจะแตกต่างกันไปตามอุปกรณ์และสภาพพื้นที่เก็บข้อมูล คุณดูจำนวนคะแนนที่ได้รับได้ผ่านช่องทางต่อไปนี้

if (navigator.storage && navigator.storage.estimate) {
 
const quota = await navigator.storage.estimate();
 
// quota.usage -> Number of bytes used.
 
// quota.quota -> Maximum number of bytes available.
 
const percentageUsed = (quota.usage / quota.quota) * 100;
  console
.log(`You've used ${percentageUsed}% of the available storage.`);
 
const remaining = quota.quota - quota.usage;
  console
.log(`You can write up to ${remaining} more bytes.`);
}

อย่างไรก็ตาม เช่นเดียวกับพื้นที่เก็บข้อมูลของเบราว์เซอร์ทั้งหมด เบราว์เซอร์มีสิทธิ์ทิ้งข้อมูลของคุณหากอุปกรณ์มีแรงกดดันด้านพื้นที่เก็บข้อมูล ขออภัย เบราว์เซอร์ไม่สามารถแยกความแตกต่างระหว่างภาพยนตร์ที่คุณต้องการเก็บไว้ไม่ว่าอย่างไรก็ตามกับเกมที่คุณไม่ได้สนใจมากนัก

หากต้องการแก้ปัญหานี้ ให้ใช้อินเทอร์เฟซ StorageManager ดังนี้

// From a page:
navigator
.storage.persist()
.then(function(persisted) {
 
if (persisted) {
   
// Hurrah, your data is here to stay!
 
} else {
   
// So sad, your data may get chucked. Sorry.
});

แน่นอนว่าผู้ใช้ต้องให้สิทธิ์ โปรดใช้ Permissions API

การทำให้ผู้ใช้เป็นส่วนหนึ่งของขั้นตอนนี้สำคัญอย่างยิ่ง เนื่องจากตอนนี้เราคาดหวังได้ว่าผู้ใช้จะควบคุมการลบได้ หากอุปกรณ์มีปริมาณพื้นที่เก็บข้อมูลเหลือน้อยและผู้ใช้ไม่สามารถแก้ปัญหาด้วยการล้างข้อมูลที่ไม่จำเป็น ผู้ใช้จะเป็นผู้ตัดสินใจว่าจะเก็บหรือนำรายการใดออก

ระบบปฏิบัติการต้องถือว่าต้นทางที่ "คงทน" เทียบเท่ากับแอปเฉพาะแพลตฟอร์มในการแจกแจงการใช้งานพื้นที่เก็บข้อมูลแทนการรายงานเบราว์เซอร์เป็นรายการเดียว เพื่อให้การรายงานนี้ใช้งานได้

การแสดงคําแนะนํา - การตอบสนองต่อคําขอ

ไม่ว่าคุณจะแคชมากแค่ไหน Service Worker จะไม่ใช้แคชเว้นแต่คุณจะบอกเวลาและวิธี รูปแบบการจัดการคําขอมีดังนี้

แคชเท่านั้น

แคชเท่านั้น
แคชเท่านั้น

เหมาะสำหรับ: ทุกอย่างที่คุณถือว่าคงที่สำหรับ "เวอร์ชัน" หนึ่งของเว็บไซต์ คุณควรแคชข้อมูลเหล่านี้ไว้ในเหตุการณ์การติดตั้งเพื่อให้มั่นใจว่าข้อมูลจะยังคงอยู่

self.addEventListener('fetch', function (event) {
 
// If a match isn't found in the cache, the response
 
// will look like a connection error
  event
.respondWith(caches.match(event.request));
});

…แม้ว่าคุณไม่จำเป็นต้องจัดการกับกรณีนี้โดยเฉพาะบ่อยนัก แต่แคชที่ใช้แทนเครือข่ายจะครอบคลุมกรณีนี้

เครือข่ายเท่านั้น

เครือข่ายเท่านั้น
เครือข่ายเท่านั้น

เหมาะสําหรับ: รายการที่ไม่มีรายการที่เทียบเท่าแบบออฟไลน์ เช่น คําสั่ง ping ของ Analytics คําขอที่ไม่ใช่ GET

self.addEventListener('fetch', function (event) {
  event
.respondWith(fetch(event.request));
 
// or simply don't call event.respondWith, which
 
// will result in default browser behavior
});

…แม้ว่าคุณไม่จำเป็นต้องจัดการกับกรณีนี้โดยเฉพาะบ่อยนัก แต่แคชที่ใช้แทนเครือข่ายจะครอบคลุมกรณีนี้

แคช กลับไปใช้เครือข่าย

แคช กลับไปใช้เครือข่าย
แคช กลับไปใช้เครือข่าย

เหมาะสําหรับ: การสร้างแบบออฟไลน์เป็นหลัก ในกรณีเช่นนี้ คุณควรจัดการคำขอส่วนใหญ่ด้วยวิธีนี้ รูปแบบอื่นๆ จะเป็นข้อยกเว้นตามคำขอขาเข้า

self.addEventListener('fetch', function (event) {
  event
.respondWith(
    caches
.match(event.request).then(function (response) {
     
return response || fetch(event.request);
   
}),
 
);
});

วิธีนี้จะช่วยให้คุณมีลักษณะการทํางานแบบ "แคชเท่านั้น" สําหรับรายการที่อยู่ในแคช และลักษณะการทํางานแบบ "เครือข่ายเท่านั้น" สําหรับรายการที่ไม่ได้แคช (ซึ่งรวมถึงคําขอที่ไม่ใช่ GET ทั้งหมด เนื่องจากไม่สามารถแคชได้)

การแข่งขันระหว่างแคชและเครือข่าย

แคชและเครือข่ายทำงานพร้อมกัน
การแข่งขันระหว่างแคชและเครือข่าย

เหมาะสำหรับชิ้นงานขนาดเล็กที่คุณต้องการเพิ่มประสิทธิภาพในอุปกรณ์ที่มีการเข้าถึงดิสก์ช้า

ในกรณีที่ใช้ฮาร์ดไดรฟ์รุ่นเก่า โปรแกรมสแกนไวรัส และการเชื่อมต่ออินเทอร์เน็ตที่เร็วขึ้น การเรียกใช้ทรัพยากรจากเครือข่ายอาจเร็วกว่าการเรียกใช้จากดิสก์ อย่างไรก็ตาม โปรดทราบว่าการไปที่เครือข่ายเมื่อผู้ใช้มีเนื้อหาในอุปกรณ์อาจทำให้สิ้นเปลืองอินเทอร์เน็ต

// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling. Let's make a proper
// race function:
function promiseAny(promises) {
 
return new Promise((resolve, reject) => {
   
// make sure promises are all promises
    promises
= promises.map((p) => Promise.resolve(p));
   
// resolve this promise as soon as one resolves
    promises
.forEach((p) => p.then(resolve));
   
// reject if all promises reject
    promises
.reduce((a, b) => a.catch(() => b)).catch(() => reject(Error('All failed')));
 
});
}

self
.addEventListener('fetch', function (event) {
  event
.respondWith(promiseAny([caches.match(event.request), fetch(event.request)]));
});

เครือข่ายใช้แคชสำรอง

เครือข่ายกลับไปใช้แคช
เครือข่ายเปลี่ยนกลับไปใช้แคช

เหมาะสำหรับการแก้ไขด่วนสำหรับทรัพยากรที่อัปเดตบ่อยครั้งนอก "เวอร์ชัน" ของเว็บไซต์ เช่น บทความ รูปโปรไฟล์ ไทม์ไลน์โซเชียลมีเดีย และลีดเดอร์บอร์ดของเกม

ซึ่งหมายความว่าคุณให้เนื้อหาล่าสุดแก่ผู้ใช้ออนไลน์ แต่ผู้ใช้ออฟไลน์จะได้รับเวอร์ชันที่แคชไว้เก่ากว่า หากคําขอเครือข่ายสําเร็จ คุณอาจต้องอัปเดตรายการแคช

อย่างไรก็ตาม วิธีนี้มีข้อบกพร่อง หากการเชื่อมต่อของผู้ใช้ขาดตอนหรือช้า ผู้ใช้จะต้องรอจนกว่าเครือข่ายจะใช้งานไม่ได้จึงจะได้รับเนื้อหาที่ยอมรับได้อย่างสมบูรณ์ในอุปกรณ์ ซึ่งอาจใช้เวลานานมากและทำให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ไม่ดี ดูรูปแบบถัดไปที่ชื่อแคชแล้วเครือข่ายเพื่อดูวิธีแก้ปัญหาที่ดีกว่า

self.addEventListener('fetch', function (event) {
  event
.respondWith(
    fetch
(event.request).catch(function () {
     
return caches.match(event.request);
   
}),
 
);
});

แคชแล้วเครือข่าย

แคช แล้วตามด้วยเครือข่าย
แคชแล้วเครือข่าย

เหมาะสำหรับเนื้อหาที่อัปเดตบ่อย เช่น บทความ ไทม์ไลน์โซเชียลมีเดีย และเกม ตารางอันดับ

ซึ่งทำให้หน้าเว็บต้องส่งคำขอ 2 รายการ ได้แก่ 1 รายการไปยังแคช และอีก 1 รายการไปยังเครือข่าย แนวคิดคือแสดงข้อมูลที่แคชไว้ก่อน จากนั้นอัปเดตหน้าเว็บเมื่อ/หากข้อมูลเครือข่ายมาถึง

บางครั้งคุณอาจแทนที่ข้อมูลปัจจุบันได้เมื่อมีข้อมูลใหม่เข้ามา (เช่น ตารางอันดับเกม) แต่วิธีนี้อาจรบกวนเนื้อหาขนาดใหญ่ โดยทั่วไปแล้ว อย่า "ซ่อน" เนื้อหาที่ผู้ใช้อาจกำลังอ่านหรือโต้ตอบอยู่

Twitter จะเพิ่มเนื้อหาใหม่ไว้เหนือเนื้อหาเก่าและปรับตำแหน่งการเลื่อนเพื่อให้ผู้ใช้เลื่อนดูเนื้อหาได้อย่างต่อเนื่อง ซึ่งเป็นไปได้เนื่องจาก Twitter ยังคงจัดเรียงเนื้อหาแบบเป็นเส้นตรงเป็นส่วนใหญ่ เราคัดลอกรูปแบบนี้สำหรับเนื้อหาที่สร้างความตื่นเต้นเพื่อให้เนื้อหาปรากฏบนหน้าจอโดยเร็วที่สุด พร้อมกับแสดงเนื้อหาล่าสุดทันทีที่ได้รับ

โค้ดในหน้าเว็บ:

var networkDataReceived = false;

startSpinner
();

// fetch fresh data
var networkUpdate = fetch('/data.json')
 
.then(function (response) {
   
return response.json();
 
})
 
.then(function (data) {
    networkDataReceived
= true;
    updatePage
(data);
 
});

// fetch cached data
caches
 
.match('/data.json')
 
.then(function (response) {
   
if (!response) throw Error('No data');
   
return response.json();
 
})
 
.then(function (data) {
   
// don't overwrite newer network data
   
if (!networkDataReceived) {
      updatePage
(data);
   
}
 
})
 
.catch(function () {
   
// we didn't get cached data, the network is our last hope:
   
return networkUpdate;
 
})
 
.catch(showErrorMessage)
 
.then(stopSpinner);

โค้ดใน Service Worker:

คุณควรไปที่เครือข่ายและอัปเดตแคชทุกครั้ง

self.addEventListener('fetch', function (event) {
  event
.respondWith(
    caches
.open('mysite-dynamic').then(function (cache) {
     
return fetch(event.request).then(function (response) {
        cache
.put(event.request, response.clone());
       
return response;
     
});
   
}),
 
);
});

ใน trained-to-thrill เราแก้ปัญหานี้โดยใช้ XHR แทน fetch และการใช้ส่วนหัว Accept ในทางที่ผิดเพื่อบอก Service Worker ว่าให้ดึงข้อมูลมาจากที่ใด (โค้ดหน้าเว็บ, โค้ด Service Worker)

วิดีโอสำรองทั่วไป

การแสดงผลสำรองทั่วไป
สำรองทั่วไป

หากแสดงเนื้อหาจากแคชและ/หรือเครือข่ายไม่ได้ คุณอาจต้องระบุเนื้อหาสำรองทั่วไป

เหมาะสำหรับรูปภาพรอง เช่น รูปโปรไฟล์ คำขอ POST ที่ดำเนินการไม่สำเร็จ และหน้า "ไม่พร้อมใช้งานขณะออฟไลน์"

self.addEventListener('fetch', function (event) {
  event
.respondWith(
   
// Try the cache
    caches
     
.match(event.request)
     
.then(function (response) {
       
// Fall back to network
       
return response || fetch(event.request);
     
})
     
.catch(function () {
       
// If both fail, show a generic fallback:
       
return caches.match('/offline.html');
       
// However, in reality you'd have many different
       
// fallbacks, depending on URL and headers.
       
// Eg, a fallback silhouette image for avatars.
     
}),
 
);
});

รายการที่คุณใช้แทนมีแนวโน้มจะเป็นข้อกำหนดในการติดตั้ง

หากหน้าเว็บโพสต์อีเมล Service Worker อาจเปลี่ยนไปจัดเก็บอีเมลใน "กล่องจดหมายออก" ของ IndexedDB และตอบสนองด้วยการแจ้งให้หน้าเว็บทราบว่าการส่งไม่สำเร็จ แต่ระบบเก็บข้อมูลไว้ได้สําเร็จ

เทมเพลตฝั่ง Service Worker

เทมเพลตฝั่ง ServiceWorker
เทมเพลตฝั่ง ServiceWorker

เหมาะสำหรับ: หน้าเว็บที่แคชการตอบกลับของเซิร์ฟเวอร์ไม่ได้

การแสดงผลหน้าเว็บบนเซิร์ฟเวอร์ทําให้ทุกอย่างรวดเร็ว แต่อาจหมายความว่ามีการรวมข้อมูลสถานะที่อาจไม่เหมาะสมไว้ในแคช เช่น "เข้าสู่ระบบในฐานะ…" หากหน้าเว็บของคุณควบคุมโดย Service Worker คุณอาจเลือกขอข้อมูล JSON พร้อมกับเทมเพลตและแสดงผลเทมเพลตแทน

importScripts('templating-engine.js');

self
.addEventListener('fetch', function (event) {
 
var requestURL = new URL(event.request.url);

  event
.respondWith(
   
Promise.all([
      caches
.match('/article-template.html').then(function (response) {
       
return response.text();
     
}),
      caches
.match(requestURL.path + '.json').then(function (response) {
       
return response.json();
     
}),
   
]).then(function (responses) {
     
var template = responses[0];
     
var data = responses[1];

     
return new Response(renderTemplate(template, data), {
        headers
: {
         
'Content-Type': 'text/html',
       
},
     
});
   
}),
 
);
});

การนำข้อมูลทั้งหมดมารวมกัน

คุณใช้วิธีใดวิธีหนึ่งเหล่านี้ก็ได้ อันที่จริง คุณอาจใช้หลายรายการ ทั้งนี้ขึ้นอยู่กับ URL คำขอ เช่น trained-to-thrill ใช้

เพียงดูคำขอและตัดสินใจว่าจะทำอย่างไร

self.addEventListener('fetch', function (event) {
 
// Parse the URL:
 
var requestURL = new URL(event.request.url);

 
// Handle requests to a particular host specifically
 
if (requestURL.hostname == 'api.example.com') {
    event
.respondWith(/* some combination of patterns */);
   
return;
 
}
 
// Routing for local URLs
 
if (requestURL.origin == location.origin) {
   
// Handle article URLs
   
if (/^\/article\//.test(requestURL.pathname)) {
      event
.respondWith(/* some other combination of patterns */);
     
return;
   
}
   
if (/\.webp$/.test(requestURL.pathname)) {
      event
.respondWith(/* some other combination of patterns */);
     
return;
   
}
   
if (request.method == 'POST') {
      event
.respondWith(/* some other combination of patterns */);
     
return;
   
}
   
if (/cheese/.test(requestURL.pathname)) {
      event
.respondWith(
       
new Response('Flagrant cheese error', {
          status
: 512,
       
}),
     
);
     
return;
   
}
 
}

 
// A sensible default pattern
  event
.respondWith(
    caches
.match(event.request).then(function (response) {
     
return response || fetch(event.request);
   
}),
 
);
});

…คุณคงเข้าใจแล้ว

เครดิต

…สำหรับไอคอนน่ารักๆ

และขอขอบคุณ Jeff Posnick ที่พบข้อผิดพลาดมากมายก่อนที่จะกด "เผยแพร่"

อ่านเพิ่มเติม