เทคนิคทั่วไปในการสร้างแอปพลิเคชันที่ทำงานแบบออฟไลน์

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

ดูการสาธิตการทำงานของรูปแบบเหล่านี้ได้ที่ Trained-to-thrill

ควรจัดเก็บทรัพยากรเมื่อใด

Browser Support

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Source

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

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

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

Service Worker API จะให้เหตุการณ์ install แก่คุณ คุณใช้ฟีเจอร์นี้เพื่อเตรียม สิ่งต่างๆ ที่ต้องพร้อมก่อนที่คุณจะจัดการกิจกรรมอื่นๆ ได้ ในระหว่าง install Service Worker เวอร์ชันก่อนหน้าจะยังคงทำงานและแสดงหน้าเว็บ ต่อไป ไม่ว่าคุณจะทำอะไรในขณะนี้ก็ไม่ควรขัดขวาง 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 ฉันใช้สิ่งนี้เพื่อ แคชเนื้อหาแบบคงที่

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

เมื่อติดตั้ง ไม่ใช่เป็นส่วนขึ้นอยู่

ซึ่งคล้ายกับการติดตั้งเป็นทรัพยากร 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 Synchronization API สามารถจัดการกรณีเช่นนี้ รวมถึงการดาวน์โหลดขนาดใหญ่ เช่น ภาพยนตร์

Browser Support

  • Chrome: 40.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 11.1.

Source

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

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

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

เมื่อติดตั้ง 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);
      });
  });
});

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

Browser Support

  • Chrome: 40.
  • Edge: 16.
  • Firefox: 41.
  • Safari: 11.1.

Source

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

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

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

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

หากดำเนินการนี้กับ 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

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

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

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 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/');
  }
});

เกี่ยวกับ Background Sync

ใน background-sync

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

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

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

self.addEventListener('fetch', function (event) {
  event.respondWith(fetch(event.request));
  // or 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 rejects when a promise rejects before fulfilling.
// To make a 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)]));
});

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

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

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

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

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

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

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

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

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

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

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

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 แทนการดึงข้อมูล และใช้ส่วนหัว 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

การใช้เทมเพลตฝั่ง Service Worker

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

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