จัดการเหตุการณ์ด้วย Service Worker

บทแนะนำที่ครอบคลุมแนวคิดเกี่ยวกับโปรแกรมทำงานของบริการส่วนขยาย

ภาพรวม

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

  • ลงทะเบียน Service Worker และนำเข้าโมดูล
  • แก้ไขข้อบกพร่องของโปรแกรมทำงานของบริการส่วนขยาย
  • จัดการรัฐและจัดการเหตุการณ์
  • เรียกเหตุการณ์ตามช่วงเวลา
  • สื่อสารกับสคริปต์เนื้อหา

ก่อนจะเริ่ม

คู่มือนี้มีสมมติฐานว่าคุณมีประสบการณ์ในการพัฒนาเว็บขั้นพื้นฐาน เราขอแนะนำให้ดูส่วนขยาย 101 และ Hey World สำหรับข้อมูลเบื้องต้นเกี่ยวกับการพัฒนาส่วนขยาย

สร้างส่วนขยาย

เริ่มด้วยการสร้างไดเรกทอรีใหม่ชื่อ quick-api-reference เพื่อเก็บไฟล์ส่วนขยาย หรือดาวน์โหลดซอร์สโค้ดจากที่เก็บตัวอย่าง GitHub

ขั้นตอนที่ 1: ลงทะเบียน Service Worker

สร้างไฟล์ไฟล์ Manifest ในรูทของโปรเจ็กต์และเพิ่มโค้ดต่อไปนี้

manifest.json:

{
  "manifest_version": 3,
  "name": "Open extension API reference",
  "version": "1.0.0",
  "icons": {
    "16": "images/icon-16.png",
    "128": "images/icon-128.png"
  },
  "background": {
    "service_worker": "service-worker.js",
  },
}

ส่วนขยายจะลงทะเบียน Service Worker ในไฟล์ Manifest ซึ่งต้องใช้ไฟล์ JavaScript เพียงไฟล์เดียวเท่านั้น ไม่จำเป็นต้องเรียกใช้ navigator.serviceWorker.register() เหมือนกับในหน้าเว็บ

สร้างโฟลเดอร์ images จากนั้นดาวน์โหลดไอคอนลงในโฟลเดอร์นั้น

ดูขั้นตอนแรกของบทแนะนำเวลาอ่านเพื่อดูข้อมูลเพิ่มเติมเกี่ยวกับข้อมูลเมตาและไอคอนของส่วนขยายในไฟล์ Manifest

ขั้นตอนที่ 2: นำเข้าโมดูล Service Worker หลายรายการ

โปรแกรมทำงานของบริการของเราใช้ฟีเจอร์ 2 อย่าง เราจะนำแต่ละฟีเจอร์ไปใช้ในโมดูลที่แยกกันเพื่อให้บำรุงรักษาได้ดียิ่งขึ้น ก่อนอื่น เราต้องประกาศ Service Worker เป็นโมดูล ES ในไฟล์ Manifest ซึ่งช่วยให้เรานำเข้าโมดูลในโปรแกรมทำงานของบริการได้ ดังนี้

manifest.json:

{
 "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
}

สร้างไฟล์ service-worker.js และนำเข้าโมดูล 2 รายการดังนี้

import './sw-omnibox.js';
import './sw-tips.js';

สร้างไฟล์เหล่านี้และเพิ่มบันทึกคอนโซลให้กับแต่ละไฟล์

sw-omnibox.js:

console.log("sw-omnibox.js")

sw-tips.js:

console.log("sw-tips.js")

ดูการนำเข้าสคริปต์เพื่อดูวิธีอื่นๆ ในการนำเข้าไฟล์หลายไฟล์ในโปรแกรมทำงานของบริการ

ไม่บังคับ: การแก้ไขข้อบกพร่อง Service Worker

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

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

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

ตอนนี้ให้แยกส่วนขยายเพื่อดูว่าจะหาข้อผิดพลาดได้จากที่ใด วิธีหนึ่งคือการลบ ".js" ออกจากการนำเข้า './sw-omnibox.js' ในไฟล์ service-worker.js Chrome จะลงทะเบียน Service Worker ไม่ได้

กลับไปที่ chrome://extensions แล้วรีเฟรชส่วนขยาย คุณจะเห็นข้อผิดพลาด 2 อย่างดังนี้

Service worker registration failed. Status code: 3.

An unknown error occurred when fetching the script.

โปรดดูวิธีอื่นๆ ในการแก้ไขข้อบกพร่องของโปรแกรมทำงานของบริการส่วนขยายได้ที่การแก้ไขข้อบกพร่องของส่วนขยาย

ขั้นตอนที่ 4: เริ่มต้นสถานะ

Chrome จะปิดโปรแกรมทำงานของบริการหากไม่จำเป็น เราใช้ API ของ chrome.storage เพื่อคงสถานะไว้ในเซสชันของ Service Worker สำหรับการเข้าถึงพื้นที่เก็บข้อมูล เราจำเป็นต้องขอสิทธิ์ในไฟล์ Manifest

manifest.json:

{
  ...
  "permissions": ["storage"],
}

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

sw-omnibox.js:

...
// Save default API suggestions
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'install') {
    chrome.storage.local.set({
      apiSuggestions: ['tabs', 'storage', 'scripting']
    });
  }
});

Service Worker ไม่มีสิทธิ์เข้าถึงออบเจ็กต์หน้าต่างโดยตรง ดังนั้นจึงใช้ window.localStorage เพื่อจัดเก็บค่าไม่ได้ นอกจากนี้ โปรแกรมทำงานของบริการยังเป็นสภาพแวดล้อมการดำเนินการที่มีระยะเวลาสั้น โดยระบบจะสิ้นสุดการทำงานซ้ำๆ ตลอดเซสชันเบราว์เซอร์ของผู้ใช้ ซึ่งทำให้โปรแกรมทำงานบนตัวแปรร่วมไม่ได้ แต่ให้ใช้ chrome.storage.local ซึ่งจัดเก็บข้อมูลไว้ในเครื่องแทน

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

ขั้นตอนที่ 5: บันทึกกิจกรรม

Listener เหตุการณ์ทั้งหมดจะต้องลงทะเบียนแบบคงที่ในขอบเขตรวมของ Service Worker กล่าวคือ Listener เหตุการณ์ไม่ควรฝังอยู่ในฟังก์ชันที่ไม่พร้อมกัน ซึ่งจะช่วยให้ Chrome มั่นใจได้ว่าตัวแฮนเดิลเหตุการณ์ทั้งหมดจะได้รับการคืนค่าในกรณีที่เริ่มการทำงานของ Service Worker

ในตัวอย่างนี้ เราจะใช้ API ของ chrome.omnibox แต่ก่อนอื่นเราต้องประกาศทริกเกอร์คีย์เวิร์ดของแถบอเนกประสงค์ในไฟล์ Manifest ดังนี้

manifest.json:

{
  ...
  "minimum_chrome_version": "102",
  "omnibox": {
    "keyword": "api"
  },
}

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

sw-omnibox.js:

...
const URL_CHROME_EXTENSIONS_DOC =
  'https://developer.chrome.com/docs/extensions/reference/';
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

// Display the suggestions after user starts typing
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
  await chrome.omnibox.setDefaultSuggestion({
    description: 'Enter a Chrome API or choose from past searches'
  });
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  const suggestions = apiSuggestions.map((api) => {
    return { content: api, description: `Open chrome.${api} API` };
  });
  suggest(suggestions);
});

หลังจากที่ผู้ใช้เลือกคำแนะนำแล้ว onInputEntered() จะเปิดหน้าข้อมูลอ้างอิงของ Chrome API ที่เกี่ยวข้อง

sw-omnibox.js:

...
// Open the reference page of the chosen API
chrome.omnibox.onInputEntered.addListener((input) => {
  chrome.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
  // Save the latest keyword
  updateHistory(input);
});

ฟังก์ชัน updateHistory() จะรับการป้อนข้อมูลในแถบอเนกประสงค์และบันทึกไว้ใน storage.local วิธีนี้จะทำให้ระบบใช้ข้อความค้นหาล่าสุดเป็นคำแนะนำของแถบอเนกประสงค์ได้ในภายหลัง

sw-omnibox.js:

...
async function updateHistory(input) {
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  apiSuggestions.unshift(input);
  apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
  return chrome.storage.local.set({ apiSuggestions });
}

ขั้นตอนที่ 6: ตั้งค่ากิจกรรมที่เกิดซ้ำ

เมธอด setTimeout() หรือ setInterval() มักใช้เพื่อทำงานที่ล่าช้าหรือเป็นระยะๆ อย่างไรก็ตาม API เหล่านี้อาจล้มเหลวเนื่องจากเครื่องจัดตารางเวลาจะยกเลิกตัวจับเวลาเมื่อโปรแกรมทำงานของบริการสิ้นสุดลง แต่ส่วนขยายจะใช้ API ของ chrome.alarms แทนได้

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

manifest.json:

{
  ...
  "permissions": ["storage", "alarms"],
  "permissions": ["storage"],
  "host_permissions": ["https://extension-tips.glitch.me/*"],
}

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

sw-tips.js:

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://extension-tips.glitch.me/tips.json');
  const tips = await response.json();
  const randomIndex = Math.floor(Math.random() * tips.length);
  return chrome.storage.local.set({ tip: tips[randomIndex] });
};

const ALARM_NAME = 'tip';

// Check if alarm exists to avoid resetting the timer.
// The alarm might be removed when the browser session restarts.
async function createAlarm() {
  const alarm = await chrome.alarms.get(ALARM_NAME);
  if (typeof alarm === 'undefined') {
    chrome.alarms.create(ALARM_NAME, {
      delayInMinutes: 1,
      periodInMinutes: 1440
    });
    updateTip();
  }
}

createAlarm();

// Update tip once a day
chrome.alarms.onAlarm.addListener(updateTip);

ขั้นตอนที่ 7: สื่อสารกับบริบทอื่นๆ

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

เริ่มต้นด้วยการประกาศสคริปต์เนื้อหาในไฟล์ Manifest และเพิ่มรูปแบบการจับคู่ที่สอดคล้องกับเอกสารอ้างอิงของ Chrome API

manifest.json:

{
  ...
  "content_scripts": [
    {
      "matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
      "js": ["content.js"]
    }
  ]
}

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

content.js:

(async () => {
  // Sends a message to the service worker and receives a tip in response
  const { tip } = await chrome.runtime.sendMessage({ greeting: 'tip' });

  const nav = document.querySelector('.upper-tabs > nav');
  
  const tipWidget = createDomElement(`
    <button type="button" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0 12px; height: 36px;">
      <span style="display: block; font: var(--devsite-link-font,500 14px/20px var(--devsite-primary-font-family));">Tip</span>
    </button>
  `);

  const popover = createDomElement(
    `<div id='tip-popover' popover style="margin: auto;">${tip}</div>`
  );

  document.body.append(popover);
  nav.append(tipWidget);
})();

function createDomElement(html) {
  const dom = new DOMParser().parseFromString(html, 'text/html');
  return dom.body.firstElementChild;
}

ขั้นตอนสุดท้ายคือการเพิ่มเครื่องจัดการข้อความลงใน Service Worker ที่ส่งการตอบกลับสคริปต์เนื้อหาพร้อมเคล็ดลับประจำวัน

sw-tips.js:

...
// Send tip to content script via messaging
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.greeting === 'tip') {
    chrome.storage.local.get('tip').then(sendResponse);
    return true;
  }
});

ทดสอบว่าใช้งานได้

ตรวจสอบว่าโครงสร้างไฟล์ของโปรเจ็กต์มีลักษณะดังนี้

เนื้อหาของโฟลเดอร์ส่วนขยาย ได้แก่ โฟลเดอร์รูปภาพ, Manifest.json, service-worker.js, sw-omnibox.js, sw-tips.js และ content.js

โหลดส่วนขยายในเครื่อง

หากต้องการโหลดส่วนขยายที่คลายการแพคแล้วในโหมดนักพัฒนาซอฟต์แวร์ ให้ทำตามขั้นตอนใน Hello world

เปิดหน้าข้อมูลอ้างอิง

  1. ป้อนคีย์เวิร์ด "api" ในแถบที่อยู่ของเบราว์เซอร์
  2. กด "Tab" หรือ "Space"
  3. ป้อนชื่อเต็มของ API
    • หรือเลือกจากรายการการค้นหาที่ผ่านมา
  4. หน้าใหม่จะเปิดหน้าข้อมูลอ้างอิง API ของ Chrome

ซึ่งควรมีลักษณะดังนี้

การอ้างอิง API ด่วน เปิดการอ้างอิง API รันไทม์
ส่วนขยาย API ด่วนที่เปิด Runtime API

เปิดเคล็ดลับประจำวัน

คลิกปุ่ม เคล็ดลับ บนแถบนำทางเพื่อเปิดเคล็ดลับส่วนขยาย

เปิดเคล็ดลับประจำวันใน
ส่วนขยาย API ด่วนที่เปิดเคล็ดลับของวันนี้

🎯 การเพิ่มประสิทธิภาพที่เป็นไปได้

จากสิ่งที่คุณได้เรียนรู้ในวันนี้ ลองทำอย่างใดอย่างหนึ่งต่อไปนี้:

  • สำรวจอีกวิธีหนึ่งในการนำคำแนะนำของแถบอเนกประสงค์ไปใช้
  • สร้างโมดัลที่กำหนดเองสำหรับแสดงเคล็ดลับส่วนขยาย
  • เปิดหน้าเพิ่มเติมเพื่อไปยังหน้า API อ้างอิงส่วนขยายเว็บของ MDN

สร้างชุมชนให้เติบโตไปเรื่อยๆ

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

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

สำรวจต่อ

เราขอแนะนำให้อ่านบทความต่อไปนี้เพื่อศึกษาเส้นทางการเรียนรู้ของโปรแกรมทำงานของบริการส่วนขยายต่อ