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

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

สำหรับการสาธิตการทำงานของรูปแบบเหล่านี้บางส่วน โปรดดูฝึกฝนเพื่อลุ้นระทึก และวิดีโอนี้จะแสดงผลกระทบด้านประสิทธิภาพ

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

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

เมื่อติดตั้ง - เป็นทรัพยากร Dependency

เมื่อติดตั้ง - เป็นทรัพยากร 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 ต้องรับประกันระยะเวลาและความสำเร็จของการติดตั้ง หากคำปฏิเสธคำปฏิเสธ การติดตั้งจะถือว่าล้มเหลว และ Service Worker นี้จะถูกยกเลิกการทำงาน (หากเวอร์ชันเก่าทำงานอยู่ การทำงานดังกล่าวจะยังคงอยู่) caches.open() และ cache.addAll() สัญญาว่าจะรีเทิร์น หากดึงทรัพยากรไม่ได้ การเรียกใช้ cache.addAll() จะถูกปฏิเสธ

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

เมื่อติดตั้ง ไม่ใช่เป็นทรัพยากร Dependency

เมื่อติดตั้ง - ไม่ใช่เป็นทรัพยากร Dependency
เมื่อติดตั้ง - ไม่ใช่เป็นทรัพยากร Dependency

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

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

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 ความเร็ว Syncation API จะรองรับกรณีเช่นนี้ รวมถึงการดาวน์โหลดขนาดใหญ่ เช่น ภาพยนตร์ ปัจจุบัน API ดังกล่าว รองรับเฉพาะใน Chromium Fork เท่านั้น

เมื่อเปิดใช้งาน

เมื่อเปิดใช้งาน
เมื่อเปิดใช้งาน

เหมาะอย่างยิ่งสำหรับ: การล้างข้อมูลและการย้ายข้อมูล

เมื่อติดตั้ง 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 พร้อมใช้งานจากหน้าเว็บและโปรแกรมทำงานของบริการ ซึ่งหมายความว่าคุณจะเพิ่มไปยังแคชได้โดยตรงจากหน้าเว็บ

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

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

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

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

หากคุณทำเช่นนี้กับ 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

ไม่มีอัปเดตขณะตรวจสอบใหม่

ไม่มีอัปเดตขณะตรวจสอบใหม่
ไม่อัปเดตขณะตรวจสอบใหม่

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

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

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;
      });
    }),
  );
});

ซึ่งคล้ายกับ "ไม่อัปเดตขณะตรวจสอบใหม่" ของ HTTP เป็นอย่างมาก

ในข้อความพุช

ในข้อความพุช
ในข้อความ Push

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

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

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

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

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, การเข้าถึงระบบไฟล์ และแคช

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

navigator.storageQuota.queryInfo('temporary').then(function (info) {
  console.log(info.quota);
  // Result: <quota in bytes>
  console.log(info.usage);
  // Result: <used data in 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

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

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

คำแนะนำการแสดงผล—ตอบกลับคำขอ

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

แคชเท่านั้น

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

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

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 ของข้อมูลวิเคราะห์, คำขอที่ไม่ใช่ 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);
    }),
  );
});

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

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

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

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

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

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

โค้ดในหน้า:

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 แทนการดึงข้อมูล และใช้ส่วนหัว "ยอมรับ" ในทางที่ผิดเพื่อบอก 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.
      }),
  );
});

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

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

เทมเพลตฝั่งพนักงานบริการ

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

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

การแสดงผลหน้าบนเซิร์ฟเวอร์ทำให้ดำเนินการต่างๆ ได้อย่างรวดเร็ว แต่อาจหมายถึงการรวมข้อมูลสถานะที่อาจไม่สมเหตุสมผลในแคช เช่น "เข้าสู่ระบบในชื่อ..." หากโปรแกรมทำงานของบริการควบคุมหน้าเว็บของคุณ คุณอาจเลือกขอข้อมูล 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 จำนวนมากตาม 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 ที่ตรวจจับข้อผิดพลาดมากมาย ก่อนผมกด "เผยแพร่" ได้

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