สำรวจความสามารถใหม่ๆ ของเบราว์เซอร์ที่กำลังจะเปิดตัวสำหรับ PWA: From Fugu With Love

1. ก่อนเริ่มต้น

Progressive Web Application (PWA) เป็นซอฟต์แวร์แอปพลิเคชันประเภทหนึ่งที่ให้บริการผ่านเว็บ ซึ่งสร้างขึ้นโดยใช้เทคโนโลยีเว็บทั่วไป ได้แก่ HTML, CSS และ JavaScript โดยออกแบบมาให้ทำงานบนแพลตฟอร์มใดก็ได้ที่ใช้เบราว์เซอร์ที่เป็นไปตามมาตรฐาน

ในโค้ดแล็บนี้ คุณจะเริ่มต้นด้วย PWA พื้นฐาน จากนั้นสำรวจความสามารถใหม่ๆ ของเบราว์เซอร์ที่จะช่วยให้ PWA ของคุณมีพลังวิเศษ 🦸 ในที่สุด

ความสามารถใหม่ๆ ของเบราว์เซอร์เหล่านี้หลายอย่างยังอยู่ระหว่างการพัฒนาและยังคงอยู่ระหว่างการกำหนดมาตรฐาน ดังนั้นในบางครั้งคุณจะต้องตั้งค่าสถานะของเบราว์เซอร์เพื่อใช้งาน

ข้อกำหนดเบื้องต้น

สำหรับ Codelab นี้ คุณควรคุ้นเคยกับ JavaScript สมัยใหม่ โดยเฉพาะ Promise และ async/await เนื่องจากแพลตฟอร์มบางแพลตฟอร์มไม่รองรับบางขั้นตอนของโค้ดแล็บ การมีอุปกรณ์เพิ่มเติมไว้ในมือจึงช่วยในการทดสอบได้ เช่น โทรศัพท์ Android หรือแล็ปท็อปที่ใช้ระบบปฏิบัติการอื่นที่ไม่ใช่อุปกรณ์ที่คุณใช้แก้ไขโค้ด คุณสามารถลองใช้โปรแกรมจำลอง เช่น โปรแกรมจำลอง Android หรือบริการออนไลน์ เช่น BrowserStack ที่ให้คุณทดสอบจากอุปกรณ์ที่ใช้อยู่แทนการใช้อุปกรณ์จริง หรือคุณจะข้ามขั้นตอนใดก็ได้ เนื่องจากขั้นตอนต่างๆ ไม่ได้ขึ้นอยู่กับกัน

สิ่งที่คุณจะสร้าง

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

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

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

สิ่งที่คุณต้องมี

เบราว์เซอร์ที่รองรับอย่างเต็มรูปแบบในขณะนี้ ได้แก่

  • Chrome
  • และ Edge ที่พัฒนาบน Chromium

เราขอแนะนำให้ใช้ช่อง Dev ที่เฉพาะเจาะจง

2. Project Fugu

Progressive Web App (PWA) สร้างและเพิ่มประสิทธิภาพด้วย API ที่ทันสมัยเพื่อมอบความสามารถที่ดียิ่งขึ้น ความน่าเชื่อถือ และการติดตั้งได้ ในขณะที่เข้าถึงได้ทุกคนบนเว็บ ทุกที่ในโลก และใช้อุปกรณ์ทุกประเภท

API บางรายการมีประสิทธิภาพสูงมาก และหากจัดการไม่ถูกต้อง อาจเกิดข้อผิดพลาดได้ เช่นเดียวกับปลาปักเป้า 🐡: หากตัดอย่างถูกต้องก็จะเป็นอาหารรสเลิศ แต่หากตัดผิดก็อาจเป็นอันตรายถึงชีวิต (แต่ไม่ต้องกังวล ในโค้ดแล็บนี้ไม่มีอะไรเสียหายจริงๆ)

ด้วยเหตุนี้ ชื่อรหัสภายในของโปรเจ็กต์ความสามารถของเว็บ (ที่บริษัทที่เกี่ยวข้องกำลังพัฒนา API ใหม่เหล่านี้) จึงเป็นโปรเจ็กต์ Fugu

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

3. เริ่มต้นใช้งาน

ดาวน์โหลดเบราว์เซอร์ใดเบราว์เซอร์หนึ่ง แล้วตั้งค่าสถานะรันไทม์ต่อไปนี้ 🚩 โดยไปที่ about://flags ซึ่งใช้ได้ทั้งใน Chrome และ Edge

  • #enable-experimental-web-platform-features

หลังจากเปิดใช้แล้ว ให้รีสตาร์ทเบราว์เซอร์

คุณจะใช้แพลตฟอร์ม Glitch เนื่องจากแพลตฟอร์มนี้ช่วยให้คุณโฮสต์ PWA ได้และมีโปรแกรมแก้ไขที่ใช้งานได้ดี นอกจากนี้ Glitch ยังรองรับการนำเข้าและส่งออกไปยัง GitHub ด้วย จึงไม่มีการล็อกอินของผู้ให้บริการ ไปที่ fugu-paint.glitch.me เพื่อลองใช้แอปพลิเคชัน ซึ่งเป็นแอปวาดภาพพื้นฐาน 🎨 ที่คุณจะปรับปรุงในระหว่างการทำโค้ดแล็บ

PWA พื้นฐานของ Fugu Greetings ที่มี Canvas ขนาดใหญ่ซึ่งมีคำว่า "Google" วาดอยู่

หลังจากลองใช้แอปพลิเคชันแล้ว ให้รีมิกซ์แอปเพื่อสร้างสำเนาของคุณเองซึ่งคุณสามารถแก้ไขได้ URL ของรีมิกซ์จะมีลักษณะคล้ายกับ glitch.com/edit/#!/bouncy-candytuft (คำว่า "bouncy-candytuft" จะเป็นคำอื่นสำหรับคุณ) โดยรีมิกซ์นี้จะเข้าถึงได้โดยตรงทั่วโลก ลงชื่อเข้าใช้บัญชีที่มีอยู่หรือสร้างบัญชีใหม่ใน Glitch เพื่อบันทึกงาน คุณดูแอปได้โดยคลิกปุ่ม "🕶 แสดง" และ URL ของแอปที่โฮสต์จะมีลักษณะคล้าย bouncy-candytuft.glitch.me (โปรดสังเกต .me แทน .com เป็นโดเมนระดับบนสุด)

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

IDE ของ Glitch แสดงการแก้ไขเอกสาร HTML

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

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

ความเข้ากันได้ของ API ที่บันทึกไว้ในคอนโซลในเครื่องมือสำหรับนักพัฒนาเว็บ

4. 🐟 เพิ่มการรองรับ Web Share API

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

Web Share API รองรับการแชร์ไฟล์ และคุณอาจจำได้ว่า File เป็นเพียงBlob ประเภทหนึ่ง ดังนั้น ในไฟล์ที่ชื่อ share.mjs ให้นำเข้าปุ่มแชร์และฟังก์ชันอำนวยความสะดวก toBlob() ที่แปลงเนื้อหาของ Canvas เป็น Blob และเพิ่มฟังก์ชันการแชร์ตามโค้ดด้านล่าง

หากคุณติดตั้งใช้งานแล้วแต่ไม่เห็นปุ่มดังกล่าว แสดงว่าเบราว์เซอร์ไม่ได้ติดตั้งใช้งาน Web Share API

import { shareButton, toBlob } from './script.mjs';

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!navigator.canShare(data)) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

shareButton.style.display = 'block';
shareButton.addEventListener('click', async () => {
  return share('Fugu Greetings', 'From Fugu With Love', await toBlob());
});

5. 🐟 เพิ่มการรองรับ Web Share Target API

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

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

{
  "share_target": {
    "action": "./share-target/",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "files": [
        {
          "name": "image",
          "accept": ["image/jpeg", "image/png", "image/webp", "image/gif"]
        }
      ]
    }
  }
}

จากนั้น Service Worker จะจัดการไฟล์ที่ได้รับ URL ./share-target/ ไม่มีอยู่จริง แต่แอปจะดำเนินการกับ URL ดังกล่าวในตัวแฮนเดิลอร์ fetch และเปลี่ยนเส้นทางการร้องขอไปยัง URL รูทโดยการเพิ่มพารามิเตอร์การค้นหา ?share-target

self.addEventListener('fetch', (fetchEvent) => {
  /* 🐡 Start Web Share Target */
  if (
    fetchEvent.request.url.endsWith('/share-target/') &&
    fetchEvent.request.method === 'POST'
  ) {
    return fetchEvent.respondWith(
      (async () => {
        const formData = await fetchEvent.request.formData();
        const image = formData.get('image');
        const keys = await caches.keys();
        const mediaCache = await caches.open(
          keys.filter((key) => key.startsWith('media'))[0],
        );
        await mediaCache.put('shared-image', new Response(image));
        return Response.redirect('./?share-target', 303);
      })(),
    );
  }
  /* 🐡 End Web Share Target */

  /* ... */
});

เมื่อแอปโหลดแล้ว แอปจะตรวจสอบว่ามีการตั้งค่าพารามิเตอร์การค้นหานี้หรือไม่ หากมีการตั้งค่า แอปจะวาดรูปภาพที่แชร์ลงใน Canvas แล้วลบออกจากแคช โดยทั้งหมดนี้จะเกิดขึ้นใน script.mjs ดังนี้

const restoreImageFromShare = async () => {
  const mediaCache = await getMediaCache();
  const image = await mediaCache.match('shared-image');
  if (image) {
    const blob = await image.blob();
    await drawBlob(blob);
    await mediaCache.delete('shared-image');
  }
};

จากนั้นจะใช้ฟังก์ชันนี้เมื่อแอปเริ่มต้น

if (location.search.includes('share-target')) {
  restoreImageFromShare();
} else {
  drawDefaultImage();
}

6. 🐟 เพิ่มการรองรับการนำเข้ารูปภาพ

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

ก่อนอื่นให้อ่านฟังก์ชัน drawImage() ของ Canvas จากนั้นทำความคุ้นเคยกับองค์ประกอบ <​input
type=file>

เมื่อมีความรู้ดังกล่าวแล้ว คุณจะแก้ไขไฟล์ที่ชื่อ import_image_legacy.mjs และเพิ่มข้อมูลโค้ดต่อไปนี้ได้ ที่ด้านบนของไฟล์ที่คุณนำเข้าจะมีปุ่มนำเข้าและฟังก์ชันอำนวยความสะดวก drawBlob() ที่ให้คุณวาด Blob ลงใน Canvas

import { importButton, drawBlob } from './script.mjs';

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/png, image/jpeg, image/*';
    input.addEventListener('change', () => {
      const file = input.files[0];
      input.remove();
      return resolve(file);
    });
    input.click();
  });
};

importButton.style.display = 'block';
importButton.addEventListener('click', async () => {
  const file = await importImage();
  if (file) {
    await drawBlob(file);
  }
});

7. 🐟 เพิ่มการรองรับการส่งออกรูปภาพ

ผู้ใช้จะบันทึกไฟล์ที่สร้างในแอปไปยังอุปกรณ์ได้อย่างไร โดยปกติแล้วจะทำได้ด้วยองค์ประกอบ <​a
download>

ในไฟล์ export_image_legacy.mjs ให้เพิ่มเนื้อหาตามที่ระบุไว้ด้านล่าง นำเข้าปุ่มส่งออกและtoBlob()ฟังก์ชันอำนวยความสะดวกที่แปลงเนื้อหา Canvas เป็น Blob

import { exportButton, toBlob } from './script.mjs';

export const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    a.remove();
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  setTimeout(() => a.click(), 0);
};

exportButton.style.display = 'block';
exportButton.addEventListener('click', async () => {
  exportImage(await toBlob());
});

8. 🐟 เพิ่มการรองรับ File System Access API

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

ก่อนหน้านี้คุณใช้<​input type=file>วิธีการเดิมในการนำเข้าไฟล์และ<​a download>วิธีการเดิมในการส่งออกไฟล์ ตอนนี้คุณจะใช้ File System Access API เพื่อปรับปรุงประสบการณ์การใช้งาน

API นี้อนุญาตให้เปิดและบันทึกไฟล์จากระบบไฟล์ของระบบปฏิบัติการ แก้ไขไฟล์ 2 ไฟล์ ได้แก่ import_image.mjs และ export_image.mjs ตามลำดับ โดยเพิ่มเนื้อหาด้านล่าง หากต้องการให้ไฟล์เหล่านี้โหลดได้ ให้นำอิโมจิ 🐡 ออกจาก script.mjs

แทนที่บรรทัดนี้

// Remove all the emojis for this feature test to succeed.
if ('show🐡Open🐡File🐡Picker' in window) {
  /* ... */
}

...ด้วยบรรทัดนี้

if ('showOpenFilePicker' in window) {
  /* ... */
}

ใน import_image.mjs

import { importButton, drawBlob } from './script.mjs';

const importImage = async () => {
  try {
    const [handle] = await window.showOpenFilePicker({
      types: [
        {
          description: 'Image files',
          accept: {
            'image/*': ['.png', '.jpg', '.jpeg', '.avif', '.webp', '.svg'],
          },
        },
      ],
    });
    return await handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

importButton.style.display = 'block';
importButton.addEventListener('click', async () => {
  const file = await importImage();
  if (file) {
    await drawBlob(file);
  }
});

ใน export_image.mjs

import { exportButton, toBlob } from './script.mjs';

const exportImage = async () => {
  try {
    const handle = await window.showSaveFilePicker({
      suggestedName: 'fugu-greetings.png',
      types: [
        {
          description: 'Image file',
          accept: {
            'image/png': ['.png'],
          },
        },
      ],
    });
    const blob = await toBlob();
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

exportButton.style.display = 'block';
exportButton.addEventListener('click', async () => {
  await exportImage();
});

9. 🐟 เพิ่มการรองรับ Contact Picker API

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

ในอุปกรณ์ Android หรือ iOS Contact Picker API จะช่วยให้คุณเลือกรายชื่อติดต่อจากแอปตัวจัดการรายชื่อติดต่อของอุปกรณ์และส่งกลับไปยังแอปพลิเคชันได้ แก้ไขไฟล์ contacts.mjs และเพิ่มโค้ดด้านล่าง

import { contactsButton, ctx, canvas } from './script.mjs';

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

contactsButton.style.display = 'block';
contactsButton.addEventListener('click', async () => {
  const contacts = await getContacts();
  if (contacts) {
    ctx.font = '1em Comic Sans MS';
    contacts.forEach((contact, index) => {
      ctx.fillText(contact.name.join(), 20, 16 * ++index, canvas.width);
    });
  }
});

10. 🐟 เพิ่มการรองรับ Async Clipboard API

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

ค้นหาไฟล์ clipboard.mjs แล้วเพิ่มข้อมูลต่อไปนี้

import { copyButton, pasteButton, toBlob, drawImage } from './script.mjs';

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      /* global ClipboardItem */
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

copyButton.style.display = 'block';
copyButton.addEventListener('click', async () => {
  await copy(await toBlob());
});

pasteButton.style.display = 'block';
pasteButton.addEventListener('click', async () => {
  const image = new Image();
  image.addEventListener('load', () => {
    drawImage(image);
  });
  image.src = URL.createObjectURL(await paste());
});

11. 🐟 เพิ่มการรองรับ Badging API

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

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

ใส่โค้ดด้านล่างในไฟล์ badge.mjs

import { canvas, clearButton } from './script.mjs';

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

12. 🐟 เพิ่มการรองรับ Screen Wake Lock API

บางครั้งผู้ใช้อาจต้องใช้เวลาสักครู่เพื่อจ้องมองภาพวาดนานพอที่จะเกิดแรงบันดาลใจ เพิ่มฟีเจอร์ที่ทำให้หน้าจอสว่างอยู่เสมอและหยุดไม่ให้โปรแกรมรักษาหน้าจอทำงาน Screen Wake Lock API จะป้องกันไม่ให้หน้าจอของผู้ใช้เข้าสู่โหมดสลีป ระบบจะปล่อย Wake Lock โดยอัตโนมัติเมื่อเกิดเหตุการณ์การเปลี่ยนแปลงระดับการเข้าถึงตามที่กำหนดโดย Page Visibility ดังนั้นจึงต้องขอ Wake Lock อีกครั้งเมื่อหน้าเว็บกลับมาปรากฏในมุมมอง

ค้นหาไฟล์ wake_lock.mjs แล้วเพิ่มเนื้อหาด้านล่าง หากต้องการทดสอบว่าวิธีนี้ใช้ได้หรือไม่ ให้กำหนดค่าโปรแกรมพักหน้าจอให้แสดงหลังจากผ่านไป 1 นาที

import { wakeLockInput, wakeLockLabel } from './script.mjs';

let wakeLock = null;

const requestWakeLock = async () => {
  try {
    wakeLock = await navigator.wakeLock.request('screen');
    wakeLock.addEventListener('release', () => {
      console.log('Wake Lock was released');
    });
    console.log('Wake Lock is active');
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);

wakeLockInput.style.display = 'block';
wakeLockLabel.style.display = 'block';
wakeLockInput.addEventListener('change', async () => {
  if (wakeLockInput.checked) {
    await requestWakeLock();
  } else {
    wakeLock.release();
  }
});

13. 🐟 เพิ่มการรองรับ Periodic Background Sync API

การเริ่มต้นด้วยผืนผ้าใบเปล่าอาจน่าเบื่อ คุณสามารถใช้ Periodic Background Sync API เพื่อเริ่มต้นใช้งาน Canvas ของผู้ใช้ด้วยรูปภาพใหม่ทุกวันได้ เช่น รูปภาพปลาปักเป้าประจำวันของ Unsplash

ซึ่งต้องใช้ไฟล์ 2 ไฟล์ ได้แก่ ไฟล์ periodic_background_sync.mjs ที่ลงทะเบียนการซิงค์ข้อมูลเป็นระยะๆ ในเบื้องหลัง และไฟล์ image_of_the_day.mjs อีกไฟล์ที่จัดการการดาวน์โหลดรูปภาพประจำวัน

ใน periodic_background_sync.mjs

import { periodicBackgroundSyncButton, drawBlob } from './script.mjs';

const getPermission = async () => {
  const status = await navigator.permissions.query({
    name: 'periodic-background-sync',
  });
  return status.state === 'granted';
};

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

navigator.serviceWorker.addEventListener('message', async (event) => {
  const fakeURL = event.data.image;
  const mediaCache = await getMediaCache();
  const response = await mediaCache.match(fakeURL);
  drawBlob(await response.blob());
});

const getMediaCache = async () => {
  const keys = await caches.keys();
  return await caches.open(keys.filter((key) => key.startsWith('media'))[0]);
};

periodicBackgroundSyncButton.style.display = 'block';
periodicBackgroundSyncButton.addEventListener('click', async () => {
  if (await getPermission()) {
    await registerPeriodicBackgroundSync();
  }
  const mediaCache = await getMediaCache();
  let blob = await mediaCache.match('./assets/background.jpg');
  if (!blob) {
    blob = await mediaCache.match('./assets/fugu_greeting_card.jpg');
  }
  drawBlob(await blob.blob());
});

ใน image_of_the_day.mjs

const getImageOfTheDay = async () => {
  try {
    const fishes = ['blowfish', 'pufferfish', 'fugu'];
    const fish = fishes[Math.floor(fishes.length * Math.random())];
    const response = await fetch(`https://source.unsplash.com/daily?${fish}`);
    if (!response.ok) {
      throw new Error('Response was', response.status, response.statusText);
    }
    return await response.blob();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const getMediaCache = async () => {
  const keys = await caches.keys();
  return await caches.open(keys.filter((key) => key.startsWith('media'))[0]);
};

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        try {
          const blob = await getImageOfTheDay();
          const mediaCache = await getMediaCache();
          const fakeURL = './assets/background.jpg';
          await mediaCache.put(fakeURL, new Response(blob));
          const clients = await self.clients.matchAll();
          clients.forEach((client) => {
            client.postMessage({
              image: fakeURL,
            });
          });
        } catch (err) {
          console.error(err.name, err.message);
        }
      })(),
    );
  }
});

14. 🐟 เพิ่มการรองรับ Shape Detection API

บางครั้งภาพวาดของผู้ใช้หรือรูปภาพพื้นหลังที่ใช้ อาจมีข้อมูลที่เป็นประโยชน์ เช่น บาร์โค้ด Shape Detection API และโดยเฉพาะอย่างยิ่ง Barcode Detection API ช่วยให้คุณดึงข้อมูลนี้ได้ เพิ่มฟีเจอร์ที่พยายามตรวจหาบาร์โค้ดจากภาพวาดของผู้ใช้ ค้นหาไฟล์ barcode.mjs แล้วเพิ่มเนื้อหาด้านล่าง หากต้องการทดสอบฟีเจอร์นี้ เพียงโหลดหรือวางรูปภาพที่มีบาร์โค้ดลงใน Canvas คุณสามารถคัดลอกบาร์โค้ดตัวอย่างจากการค้นหารูปภาพสำหรับคิวอาร์โค้ด

/* global BarcodeDetector */
import {
  scanButton,
  clearButton,
  canvas,
  ctx,
  CANVAS_BACKGROUND,
  CANVAS_COLOR,
  floor,
} from './script.mjs';

const barcodeDetector = new BarcodeDetector();

const detectBarcodes = async (canvas) => {
  return await barcodeDetector.detect(canvas);
};

scanButton.style.display = 'block';
let seenBarcodes = [];
clearButton.addEventListener('click', () => {
  seenBarcodes = [];
});
scanButton.addEventListener('click', async () => {
  const barcodes = await detectBarcodes(canvas);
  if (barcodes.length) {
    barcodes.forEach((barcode) => {
      const rawValue = barcode.rawValue;
      if (seenBarcodes.includes(rawValue)) {
        return;
      }
      seenBarcodes.push(rawValue);
      ctx.font = '1em Comic Sans MS';
      ctx.textAlign = 'center';
      ctx.fillStyle = CANVAS_BACKGROUND;
      const boundingBox = barcode.boundingBox;
      const left = boundingBox.left;
      const top = boundingBox.top;
      const height = boundingBox.height;
      const oneThirdHeight = floor(height / 3);
      const width = boundingBox.width;
      ctx.fillRect(left, top + oneThirdHeight, width, oneThirdHeight);
      ctx.fillStyle = CANVAS_COLOR;
      ctx.fillText(
        rawValue,
        left + floor(width / 2),
        top + floor(height / 2),
        width,
      );
    });
  }
});

15. 🐡 เพิ่มการรองรับ Idle Detection API

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

ค้นหาไฟล์ idle_detection.mjs แล้ววางเนื้อหาด้านล่าง

import { ephemeralInput, ephemeralLabel, clearCanvas } from './script.mjs';

let controller;

ephemeralInput.style.display = 'block';
ephemeralLabel.style.display = 'block';

ephemeralInput.addEventListener('change', async () => {
  if (ephemeralInput.checked) {
    const state = await IdleDetector.requestPermission();
    if (state !== 'granted') {
      ephemeralInput.checked = false;
      return alert('Idle detection permission must be granted!');
    }
    try {
      controller = new AbortController();
      const idleDetector = new IdleDetector();
      idleDetector.addEventListener('change', (e) => {
        const { userState, screenState } = e.target;
        console.log(`idle change: ${userState}, ${screenState}`);
        if (userState === 'idle') {
          clearCanvas();
        }
      });
      idleDetector.start({
        threshold: 60000,
        signal: controller.signal,
      });
    } catch (err) {
      console.error(err.name, err.message);
    }
  } else {
    console.log('Idle detection stopped.');
    controller.abort();
  }
});

16. 🐡 เพิ่มการรองรับ File Handling API

จะเกิดอะไรขึ้นหากผู้ใช้เพียงแค่ดับเบิลคลิกไฟล์รูปภาพ แล้วแอปของคุณก็ปรากฏขึ้น File Handling API ช่วยให้คุณทำสิ่งนี้ได้

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

{
  "file_handlers": [
    {
      "action": "./",
      "accept": {
        "image/*": [".jpg", ".jpeg", ".png", ".webp", ".svg"]
      }
    }
  ]
}

หากต้องการจัดการไฟล์ที่เปิดอยู่จริง ให้เพิ่มโค้ดด้านล่างลงในไฟล์ file-handling.mjs

import { drawBlob } from './script.mjs';

const handleLaunchFiles = () => {
  window.launchQueue.setConsumer((launchParams) => {
    if (!launchParams.files.length) {
      return;
    }
    launchParams.files.forEach(async (handle) => {
      const file = await handle.getFile();
      drawBlob(file);
    });
  });
};

handleLaunchFiles();

17. ขอแสดงความยินดี

🎉 ไชโย คุณทำได้แล้ว

API ของเบราว์เซอร์ที่น่าตื่นเต้นมากมายกำลังได้รับการพัฒนาในบริบทของโปรเจ็กต์ Fugu 🐡 ทำให้ Codelab นี้แทบจะไม่ได้แตะพื้นผิวเลย

หากต้องการดูข้อมูลเพิ่มเติม โปรดติดตามสิ่งพิมพ์ของเราในเว็บไซต์ web.dev

หน้า Landing Page ของส่วน &quot;ความสามารถ&quot; ของเว็บไซต์ web.dev

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

เว็บไซต์เครื่องมือติดตาม Fugu API

Codelab นี้เขียนโดย Thomas Steiner (@tomayac) เรายินดีที่จะตอบคำถามของคุณและหวังว่าจะได้อ่านความคิดเห็นของคุณ ขอขอบคุณ Hemanth H.M (@GNUmanth), Christian Liebel (@christianliebel), Sven May (@Svenmay), Lars Knudsen (@larsgk) และ Jackie Han (@hanguokai) ที่ช่วยสร้าง Codelab นี้