ปรับปรุง Progressive Web App อย่างต่อเนื่อง

การสร้างสําหรับเบราว์เซอร์สมัยใหม่และการเพิ่มประสิทธิภาพอย่างต่อเนื่องเหมือนเป็นปี 2003

เมื่อเดือนมีนาคม 2003 Nick Finck และ Steve Champeon สร้างความตื่นตะลึงให้กับวงการออกแบบเว็บด้วยแนวคิดการเพิ่มประสิทธิภาพแบบเป็นขั้นเป็นตอน ซึ่งเป็นกลยุทธ์การออกแบบเว็บที่เน้นการโหลดเนื้อหาหลักของหน้าเว็บก่อน จากนั้นจึงเพิ่มชั้นการแสดงผลและฟีเจอร์ที่ละเอียดยิ่งขึ้นและมีความซับซ้อนทางเทคนิคมากขึ้นบนเนื้อหา ขณะที่ในปี 2003 การปรับปรุงแบบเป็นขั้นเป็นตอนเกี่ยวข้องกับการใช้ฟีเจอร์ CSS ที่ทันสมัยในยุคนั้น, JavaScript ที่ไม่รบกวน และแม้แต่ Scalable Vector Graphics เท่านั้น การปรับปรุงแบบเป็นขั้นเป็นตอนในปี 2020 เป็นต้นไปเกี่ยวข้องกับการใช้ความสามารถของเบราว์เซอร์สมัยใหม่

การออกแบบเว็บไซต์ที่ครอบคลุมสำหรับอนาคตด้วยการเพิ่มประสิทธิภาพแบบเป็นขั้นเป็นตอน สไลด์ชื่อจากงานนำเสนอต้นฉบับของ Finck และ Champeon
สไลด์: การออกแบบเว็บที่ครอบคลุมสำหรับอนาคตด้วยการเพิ่มประสิทธิภาพแบบต่อเนื่อง (แหล่งที่มา)

JavaScript ที่ทันสมัย

เมื่อพูดถึง JavaScript สถานการณ์การรองรับเบราว์เซอร์สำหรับฟีเจอร์หลักล่าสุดของ JavaScript ES 2015 นั้นดีมาก มาตรฐานใหม่นี้ประกอบด้วยพรอมิส โมดูล คลาส นิพจน์เทมเพลต ฟังก์ชันลูกศร let และ const, พารามิเตอร์เริ่มต้น เจนเนอเรเตอร์ การกำหนดค่าการแยกโครงสร้าง ที่เหลือและกระจาย Map/Set, WeakMap/WeakSet และอื่นๆ อีกมากมาย รองรับทั้งหมด

ตารางการรองรับของ CanIUse สำหรับฟีเจอร์ ES6 ที่แสดงการรองรับในเบราว์เซอร์หลักทั้งหมด
ตารางการรองรับเบราว์เซอร์ ECMAScript 2015 (ES6) (แหล่งที่มา)

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

ตารางการรองรับของ CanIUse สำหรับฟังก์ชันการทำงานแบบแอสซิงค์ที่แสดงการรองรับในเบราว์เซอร์หลักๆ ทั้งหมด
ตารางการรองรับเบราว์เซอร์ของฟังก์ชัน Async (แหล่งที่มา)

และแม้แต่ภาษา ES 2020 ที่เพิ่งเพิ่มเข้ามาอย่างการเชนแบบไม่บังคับและการรวมค่า Nullก็ได้รับการรองรับอย่างรวดเร็ว คุณดูตัวอย่างโค้ดได้ที่ด้านล่าง ในส่วนของฟีเจอร์หลักของ JavaScript นั้น ทุกอย่างก็ดีอยู่แล้ว

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
ภาพพื้นหลังหญ้าสีเขียวอันเป็นเอกลักษณ์ของ Windows XP
ข้อดีของ JavaScript อยู่ที่ฟีเจอร์หลัก (ภาพหน้าจอผลิตภัณฑ์ Microsoft ที่ใช้โดยมีสิทธิ์)

แอปตัวอย่าง: Fugu Greetings

ในบทความนี้ เราจะใช้ PWA ง่ายๆ ชื่อ Fugu Greetings (GitHub) ชื่อแอปนี้เป็นการยกย่อง Project Fugu 🐡 ซึ่งเป็นความพยายามที่จะทำให้เว็บมีความสามารถทั้งหมดของแอปพลิเคชัน Android/iOS/เดสก์ท็อป อ่านข้อมูลเพิ่มเติมเกี่ยวกับโปรเจ็กต์ได้ในหน้า Landing Page

Fugu Greetings เป็นแอปวาดภาพที่ช่วยให้คุณสร้างการ์ดอวยพรเสมือนจริงและส่งให้กับคนที่คุณรักได้ ซึ่งแสดงแนวคิดหลักของ PWA เครื่องมือนี้เชื่อถือได้และเปิดใช้โหมดออฟไลน์ได้อย่างเต็มที่ คุณจึงยังใช้เครื่องมือนี้ต่อไปได้แม้ว่าจะไม่มีเครือข่าย นอกจากนี้ คุณยังติดตั้งแอปพลิเคชันนี้ลงในหน้าจอหลักของอุปกรณ์และผสานรวมกับระบบปฏิบัติการได้อย่างราบรื่นในฐานะแอปพลิเคชันแบบสแตนด์อโลน

PWA ของ Fugu Greetings ที่มีภาพวาดคล้ายกับโลโก้ชุมชน PWA
แอปตัวอย่าง Fugu Greetings

การเพิ่มประสิทธิภาพแบบต่อเนื่อง

เมื่อเข้าใจเรื่องนี้แล้ว เรามาพูดถึงการเพิ่มประสิทธิภาพแบบต่อเนื่องกัน อภิธานศัพท์ของ MDN Web Docs ให้คำจำกัดความแนวคิดนี้ดังนี้

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

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

[…]

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

ผู้มีส่วนร่วมของ MDN

การเริ่มต้นการ์ดอวยพรแต่ละใบตั้งแต่ต้นอาจเป็นเรื่องยุ่งยาก เหตุใดจึงไม่สร้างฟีเจอร์ที่อนุญาตให้ผู้ใช้นำเข้ารูปภาพและเริ่มดำเนินการต่อจากตรงนั้น แนวทางแบบดั้งเดิมคือคุณต้องใช้องค์ประกอบ <input type=file> เพื่อดำเนินการนี้ ก่อนอื่นให้สร้างองค์ประกอบ ตั้งค่า type เป็น 'file' และเพิ่มประเภท MIME ลงในพร็อพเพอร์ตี้ accept จากนั้น "คลิก" องค์ประกอบนั้นแบบเป็นโปรแกรมและรอการเปลี่ยนแปลง เมื่อเลือกรูปภาพ ระบบจะนำเข้ารูปภาพนั้นลงในผืนผ้าใบโดยตรง

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

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

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

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

จะเกิดอะไรขึ้นหากมีวิธีที่ดีกว่า จะเกิดอะไรขึ้นหากคุณเปิดไฟล์ในเครื่อง แก้ไขไฟล์ แล้วบันทึกการแก้ไขไปยังไฟล์ใหม่หรือกลับไปที่ไฟล์ต้นฉบับที่คุณเปิดไว้ตั้งแต่แรก ปรากฏว่ามีอยู่ File System Access API ช่วยให้คุณเปิดและสร้างไฟล์และไดเรกทอรี รวมถึงแก้ไขและบันทึกไฟล์และไดเรกทอรีได้

ฉันจะตรวจหาฟีเจอร์ของ API ได้อย่างไร File System Access API แสดงเมธอดใหม่ window.chooseFileSystemEntries() ดังนั้น เราจึงต้องโหลดโมดูลการนําเข้าและส่งออกที่แตกต่างกันแบบมีเงื่อนไข โดยขึ้นอยู่กับว่าวิธีการนี้พร้อมใช้งานหรือไม่ เราได้แสดงวิธีดำเนินการไว้ด้านล่าง

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

แต่ก่อนที่จะเจาะลึกรายละเอียดของ File System Access API เราขออธิบายรูปแบบการเพิ่มประสิทธิภาพแบบเป็นขั้นเป็นตอนคร่าวๆ สักนิด เราจะโหลดสคริปต์เดิมในเบราว์เซอร์ที่ไม่รองรับ File System Access API ในขณะนี้ คุณดูแท็บเครือข่ายของ Firefox และ Safari ได้ที่ด้านล่าง

เครื่องมือตรวจสอบเว็บของ Safari แสดงการโหลดไฟล์เดิม
แท็บเครือข่ายของเครื่องมือตรวจสอบเว็บ Safari
เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ของ Firefox ที่แสดงการโหลดไฟล์เดิม
แท็บเครือข่ายของเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ Firefox

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

เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome ที่แสดงการโหลดไฟล์สมัยใหม่
แท็บเครือข่ายของเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

File System Access API

ตอนนี้เรามาพูดถึงการใช้งานจริงตาม File System Access API สําหรับการนําเข้ารูปภาพ ฉันเรียกใช้ window.chooseFileSystemEntries() และส่งพร็อพเพอร์ตี้ accepts ไปให้ ซึ่งฉันบอกว่าต้องการไฟล์รูปภาพ ระบบรองรับทั้งนามสกุลไฟล์และประเภท MIME การดำเนินการนี้จะทำให้เกิดตัวแฮนเดิลไฟล์ ซึ่งฉันจะเรียกใช้ไฟล์จริงได้โดยเรียก getFile()

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

การส่งออกรูปภาพเกือบจะเหมือนกัน แต่ครั้งนี้ฉันต้องส่งพารามิเตอร์ประเภท 'save-file' ไปยังเมธอด chooseFileSystemEntries() จากการดำเนินการนี้ ฉันได้รับกล่องโต้ตอบการบันทึกไฟล์ เมื่อเปิดไฟล์แล้ว การดำเนินการนี้ไม่จำเป็นเนื่องจาก 'open-file' เป็นค่าเริ่มต้น เราตั้งค่าพารามิเตอร์ accepts ในลักษณะเดียวกับก่อนหน้านี้ แต่ครั้งนี้จำกัดไว้เฉพาะรูปภาพ PNG เราได้รับตัวแฮนเดิลไฟล์อีกครั้ง แต่ครั้งนี้เราจะสร้างสตรีมแบบเขียนได้โดยการเรียกใช้ createWritable() แทนที่จะรับไฟล์ ถัดไป เราจะเขียน Blob ซึ่งเป็นรูปภาพการ์ดอวยพรลงในไฟล์ สุดท้าย ฉันจะปิดสตรีมแบบเขียนได้

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

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

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

แอป Fugu Greetings ที่มีกล่องโต้ตอบเปิดไฟล์
กล่องโต้ตอบเปิดไฟล์
แอป Fugu Greetings พร้อมใช้งานรูปภาพที่นำเข้าแล้ว
รูปภาพที่นําเข้า
แอป Fugu Greetings ที่มีรูปภาพที่แก้ไขแล้ว
การบันทึกรูปภาพที่แก้ไขแล้วลงในไฟล์ใหม่

Web Share API และ Web Share Target API

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

ชีตการแชร์ของ Safari บนเดสก์ท็อปใน macOS ที่เรียกให้แสดงจากปุ่มแชร์ของบทความ
Web Share API ใน Safari บนเดสก์ท็อปใน macOS

โค้ดที่ใช้ดำเนินการนี้ค่อนข้างตรงไปตรงมา ฉันเรียกใช้ navigator.share() และส่ง title, text และ url ที่ไม่บังคับในออบเจ็กต์ แต่หากฉันต้องการแนบรูปภาพล่ะ ระดับ 1 ของ Web Share API ยังไม่รองรับการดำเนินการนี้ ข่าวดีคือ Web Share ระดับ 2 มีความสามารถในการแชร์ไฟล์เพิ่มขึ้น

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

เราขอแสดงวิธีทําให้แอปพลิเคชันการ์ดอวยพรของ Fugu ทํางานกับฟีเจอร์นี้ ก่อนอื่น เราต้องเตรียมออบเจ็กต์ data ที่มีอาร์เรย์ files ซึ่งประกอบด้วย Blob 1 รายการ แล้วจึงเตรียม title และ text ถัดไป แนวทางปฏิบัติแนะนำคือการใช้เมธอด navigator.canShare() ใหม่ ซึ่งจะทําตามชื่อที่ระบุไว้ กล่าวคือ จะบอกให้ฉันทราบว่าเบราว์เซอร์แชร์ออบเจ็กต์ data ที่ฉันพยายามแชร์ได้หรือไม่ในทางเทคนิค หาก navigator.canShare() แจ้งว่าแชร์ข้อมูลได้ เราพร้อมโทรหา navigator.share() เหมือนเดิม เนื่องจากทุกอย่างอาจไม่สำเร็จ เราจึงใช้บล็อก try...catch อีกครั้ง

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

เราใช้การเพิ่มประสิทธิภาพแบบต่อเนื่องตามเดิม หากทั้ง 'share' และ 'canShare' อยู่ในออบเจ็กต์ navigator ฉันจึงจะดำเนินการต่อและโหลด share.mjs ผ่าน import() แบบไดนามิก ในเบราว์เซอร์อย่าง Safari บนอุปกรณ์เคลื่อนที่ที่เป็นไปตามเงื่อนไขข้อใดข้อหนึ่งเท่านั้น เราจะไม่โหลดฟังก์ชันการทำงาน

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

ใน Fugu Greetings หากฉันแตะปุ่มแชร์ในเบราว์เซอร์ที่รองรับ เช่น Chrome ใน Android ชีตการแชร์ในตัวจะเปิดขึ้น เช่น ฉันเลือก Gmail แล้ววิดเจ็ตเครื่องมือเขียนอีเมลจะปรากฏขึ้นพร้อมแนบรูปภาพ

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

Contact Picker API

ต่อไปเราจะพูดถึงรายชื่อติดต่อ ซึ่งหมายถึงสมุดที่อยู่ของอุปกรณ์หรือแอปตัวจัดการรายชื่อติดต่อ เมื่อเขียนการ์ดอวยพร การเขียนชื่อให้ถูกต้องอาจไม่ใช่เรื่องง่ายเสมอไป ตัวอย่างเช่น ฉันมีเพื่อนชื่อ Sergey ที่ต้องการให้สะกดชื่อเป็นอักษรซีริลลิก ฉันใช้แป้นพิมพ์ QWERTZ ของเยอรมันและไม่รู้วิธีพิมพ์ชื่อ ปัญหานี้แก้ไขได้ด้วย Contact Picker API เนื่องจากฉันจัดเก็บข้อมูลเพื่อนไว้ในแอปรายชื่อติดต่อของโทรศัพท์ ฉันจึงเข้าถึงรายชื่อติดต่อจากเว็บได้ผ่าน Contacts Picker API

ก่อนอื่น เราต้องระบุรายการพร็อพเพอร์ตี้ที่ต้องการเข้าถึง ในกรณีนี้ ฉันต้องการเฉพาะชื่อ แต่สำหรับกรณีการใช้งานอื่นๆ ฉันอาจสนใจหมายเลขโทรศัพท์ อีเมล ไอคอนรูปโปรไฟล์ หรือที่อยู่จริง ถัดไป ฉันกําหนดค่าออบเจ็กต์ options และตั้งค่า multiple เป็น true เพื่อให้เลือกรายการได้มากกว่า 1 รายการ สุดท้ายนี้ ฉันจะเรียกใช้ navigator.contacts.select() ซึ่งจะแสดงพร็อพเพอร์ตี้ที่ต้องการสำหรับรายชื่อติดต่อที่ผู้ใช้เลือก

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

และตอนนี้คุณอาจทราบรูปแบบแล้ว ฉันจะโหลดไฟล์ก็ต่อเมื่อระบบรองรับ API นั้นจริงๆ

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

ใน Fugu Greeting เมื่อฉันแตะปุ่มรายชื่อติดต่อและเลือกเพื่อนสนิท 2 คน นั่นคือ Сергей Михайлович Брин และ 劳伦斯·爱德华·"拉里"·佩奇 คุณจะเห็นได้ว่าเครื่องมือเลือกรายชื่อติดต่อจำกัดให้แสดงเฉพาะชื่อเท่านั้น โดยไม่แสดงอีเมลหรือข้อมูลอื่นๆ เช่น หมายเลขโทรศัพท์ จากนั้นเราจะวาดชื่อของบุคคลเหล่านั้นลงบนการ์ดอวยพร

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

Asynchronous Clipboard API

ต่อไปคือการคัดลอกและวาง การดำเนินการที่เราชื่นชอบอย่างหนึ่งในฐานะนักพัฒนาซอฟต์แวร์คือการคัดลอกและวาง ในฐานะผู้เขียนการ์ดอวยพร ฉันก็อาจต้องการทำเช่นนั้นในบางครั้ง ฉันอาจต้องการวางรูปภาพลงในการ์ดอวยพรที่กําลังแก้ไข หรือคัดลอกการ์ดอวยพรเพื่อแก้ไขต่อจากที่อื่น Async Clipboard API รองรับทั้งข้อความและรูปภาพ เราขออธิบายวิธีเพิ่มการรองรับการคัดลอกและวางลงในแอป Fugu Greetings

หากต้องการคัดลอกข้อมูลไปยังคลิปบอร์ดของระบบ ฉันต้องเขียนลงในคลิปบอร์ด เมธอด navigator.clipboard.write() จะรับอาร์เรย์ของรายการคลิปบอร์ดเป็นพารามิเตอร์ รายการในคลิปบอร์ดแต่ละรายการคือออบเจ็กต์ที่มี Blob เป็นค่า และคีย์คือประเภทของ Blob

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

หากต้องการวาง เราต้องวนผ่านรายการคลิปบอร์ดที่ได้รับโดยการเรียกใช้ navigator.clipboard.read() สาเหตุคือรายการในคลิปบอร์ดอาจมีหลายรายการในการแสดงผลที่แตกต่างกัน รายการในคลิปบอร์ดแต่ละรายการมีช่อง types ที่บอกประเภท MIME ของทรัพยากรที่ใช้ได้ ฉันเรียกใช้เมธอด getType() ของรายการคลิปบอร์ด โดยส่งประเภท MIME ที่ได้ก่อนหน้านี้

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

และแทบไม่ต้องบอกแล้วว่า เราดำเนินการนี้ในเบราว์เซอร์ที่รองรับเท่านั้น

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

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

แอป Fugu Greetings ที่แสดงข้อความแจ้งสิทธิ์ในคลิปบอร์ด
ข้อความแจ้งสิทธิ์เข้าถึงคลิปบอร์ด

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

แอปแสดงตัวอย่างของ macOS ที่มีรูปภาพที่ไม่มีชื่อซึ่งเพิ่งวาง
รูปภาพที่วางลงในแอปแสดงตัวอย่างของ macOS

Badging API

อีก API ที่มีประโยชน์คือ Badging API Fugu Greetings เป็นแอป PWA ที่ติดตั้งได้ จึงมีไอคอนแอปที่ผู้ใช้วางไว้ในแท่นชาร์จแอปหรือหน้าจอหลักได้ วิธีสนุกๆ และง่ายในการสาธิต API คือการใช้ (ในทางที่ผิด) ใน Fugu Greetings เป็นตัวนับจำนวนการลากเส้นด้วยปากกา เราได้เพิ่ม Listener เหตุการณ์ที่จะเพิ่มตัวนับการเขียนด้วยปากกาทุกครั้งที่เหตุการณ์ pointerdown เกิดขึ้น แล้วตั้งค่าป้ายไอคอนที่อัปเดตแล้ว เมื่อล้างภาพพิมพ์แคนวาสแล้ว ระบบจะรีเซ็ตตัวนับและนำป้ายออก

let strokes = 0;

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

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

ฟีเจอร์นี้เป็นการเพิ่มประสิทธิภาพแบบค่อยเป็นค่อยไป ดังนั้นตรรกะการโหลดจึงเป็นไปตามปกติ

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

ในตัวอย่างนี้ เราวาดตัวเลข 1-7 โดยใช้การลากเส้นด้วยปากกา 1 ครั้งต่อตัวเลข 1 ตัว ตอนนี้ตัวนับป้ายบนไอคอนอยู่ที่ 7

ตัวเลข 1-7 ที่วาดบนการ์ดอวยพร โดยแต่ละตัวเลขวาดด้วยเส้นปากกาเพียงเส้นเดียว
การวาดตัวเลข 1-7 โดยใช้เส้นจากปากกา 7 เส้น
ไอคอนป้ายในแอป Fugu Greetings ที่แสดงหมายเลข 7
ตัวนับการเขียนด้วยปากกาในรูปแบบป้ายไอคอนแอป

Periodic Background Sync API

หากต้องการเริ่มต้นวันใหม่ด้วยสิ่งใหม่ๆ ฟีเจอร์ที่น่าสนใจของแอป Fugu Greetings คือแอปสามารถช่วยสร้างแรงบันดาลใจให้คุณในทุกเช้าด้วยภาพพื้นหลังใหม่เพื่อเริ่มต้นสร้างการ์ดอวยพร แอปใช้ Periodic Background Sync API เพื่อดำเนินการนี้

ขั้นตอนแรกคือการregisterเหตุการณ์การซิงค์เป็นระยะในการลงทะเบียน Service Worker โดยจะรอแท็กการซิงค์ชื่อ 'image-of-the-day' และมีช่วงเวลาขั้นต่ำ 1 วัน เพื่อให้ผู้ใช้ได้รับภาพพื้นหลังใหม่ทุก 24 ชั่วโมง

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

ขั้นตอนที่ 2 คือรอรับเหตุการณ์ periodicsync ใน Service Worker หากแท็กเหตุการณ์คือ 'image-of-the-day' ซึ่งเป็นแท็กที่ลงทะเบียนไว้ก่อนหน้านี้ ระบบจะดึงข้อมูลรูปภาพของวันนั้นผ่านฟังก์ชัน getImageOfTheDay() และเผยแพร่ผลลัพธ์ไปยังไคลเอ็นต์ทั้งหมดเพื่อให้อัปเดตภาพพิมพ์แคนวาสและแคชได้

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

อีกครั้ง นี่เป็นการปรับปรุงแบบค่อยเป็นค่อยไปอย่างแท้จริง ระบบจะโหลดโค้ดก็ต่อเมื่อเบราว์เซอร์รองรับ API เท่านั้น ซึ่งมีผลกับทั้งโค้ดไคลเอ็นต์และโค้ด Service Worker ส่วนในเบราว์เซอร์ที่ไม่รองรับ ระบบจะไม่โหลดรายการใดเลย โปรดสังเกตว่าใน Service Worker เราใช้ import() แบบคลาสสิก (ยังไม่มี) แทน import() แบบไดนามิก (ซึ่งบริบท Service Worker ยังไม่รองรับ)importScripts()

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

ใน Fugu Greetings การกดปุ่มวอลเปเปอร์จะแสดงรูปภาพการ์ดอวยพรประจำวันซึ่งอัปเดตทุกวันผ่าน Periodic Background Sync API

แอปการ์ดอวยพรของ Fugu ที่มีรูปภาพการ์ดอวยพรใหม่ประจำวัน
การกดปุ่มวอลเปเปอร์จะแสดงรูปภาพประจำวัน

Notification Triggers API

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

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

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

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

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

เมื่อฉันเลือกช่องทำเครื่องหมายการช่วยเตือนใน Fugu Greetings ระบบจะแสดงข้อความแจ้งให้ฉันเลือกเวลาที่ต้องการได้รับการช่วยเตือนให้ทำการ์ดอวยพรให้เสร็จ

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

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

ศูนย์การแจ้งเตือนของ macOS ที่แสดงการแจ้งเตือนที่ทริกเกอร์จาก Fugu Greetings
การแจ้งเตือนที่ทริกเกอร์จะปรากฏในศูนย์การแจ้งเตือนของ macOS

Wake Lock API

ฉันต้องการรวม Wake Lock API ด้วย บางครั้งคุณแค่ต้องจ้องมองหน้าจอนานพอจนกว่าจะได้รับแรงบันดาลใจ สิ่งเลวร้ายที่สุดที่อาจเกิดขึ้นได้คือหน้าจอจะปิดลง Wake Lock API สามารถป้องกันไม่ให้เหตุการณ์นี้เกิดขึ้นได้

ขั้นตอนแรกคือรับ Wake Lock ด้วย navigator.wakelock.request method() ฉันส่งสตริง 'screen' ไปให้เพื่อรับ Wake Lock หน้าจอ จากนั้นฉันจะเพิ่ม Listener เหตุการณ์เพื่อรับการแจ้งเตือนเมื่อปลดล็อกการปลุก เหตุการณ์นี้อาจเกิดขึ้นได้ เช่น เมื่อระดับการแชร์แท็บมีการเปลี่ยนแปลง หากเกิดกรณีเช่นนี้ เมื่อแท็บปรากฏขึ้นอีกครั้ง ฉันจะรับการล็อกการปลุกอีกครั้งได้

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

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

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

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

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

ใน Fugu Greetings จะมีช่องทำเครื่องหมายนอนไม่หลับ ซึ่งเมื่อเลือกไว้ ระบบจะเปิดหน้าจอไว้

ช่องทำเครื่องหมาย &quot;นอนไม่หลับ&quot; (หากเลือกไว้) จะทำให้หน้าจอสว่างอยู่เสมอ
ช่องทำเครื่องหมาย Insomnia จะทำให้แอปทำงานต่อไป

Idle Detection API

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

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

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

และเช่นเคย เราจะโหลดโค้ดนี้เฉพาะเมื่อเบราว์เซอร์รองรับ

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

ในแอปการ์ดอวยพรของ Fugu ภาพวาดแคนวาสจะล้างออกเมื่อเลือกช่องทำเครื่องหมายชั่วคราวไว้และผู้ใช้ไม่ได้ใช้งานเป็นเวลานานเกินไป

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

เปิดจากขอบ

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

แผงเครือข่ายของ Chrome DevTools ที่แสดงเฉพาะคําขอไฟล์ที่มีโค้ดที่เบราว์เซอร์ปัจจุบันรองรับ
แท็บเครือข่ายของเครื่องมือสำหรับนักพัฒนาเว็บ Chrome ที่แสดงเฉพาะคำขอไฟล์ที่มีโค้ดที่เบราว์เซอร์ปัจจุบันรองรับ

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

Fugu Greetings ที่ทำงานบน Chrome ของ Android ซึ่งแสดงฟีเจอร์ต่างๆ ที่มีให้ใช้งาน
Fugu Greetings ทำงานบน Chrome ของ Android
Fugu Greetings ที่ทำงานบน Safari บนเดสก์ท็อป ซึ่งแสดงฟีเจอร์ที่มีให้ใช้งานน้อยลง
Fugu Greetings ทำงานบน Safari บนเดสก์ท็อป
Fugu Greetings ที่ทำงานบน Chrome บนเดสก์ท็อป ซึ่งแสดงฟีเจอร์ต่างๆ ที่มีให้ใช้งาน
Fugu Greetings ทำงานบน Chrome บนเดสก์ท็อป

หากสนใจแอป Fugu Greetings ให้ค้นหาและแยกแอปนี้ใน GitHub

ที่เก็บ Fugu Greetings ใน GitHub
แอป Fugu Greetings ใน GitHub

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

ขอขอบคุณ

ขอขอบคุณ Christian Liebel และ Hemanth HM ที่ได้มีส่วนร่วมในฟีเจอร์คําอวยพรของ Fugu บทความนี้ผ่านการตรวจสอบโดย Joe Medley และ Kayce Basques Jake Archibald ช่วยให้ฉันทราบสถานการณ์เกี่ยวกับimport()แบบไดนามิกในบริบทของ Service Worker