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

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

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

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

JavaScript ที่ทันสมัย

เมื่อพูดถึง JavaScript สถานการณ์การรองรับเบราว์เซอร์สำหรับฟีเจอร์หลักล่าสุดของ JavaScript ES 2015 นั้นดีมาก มาตรฐานใหม่นี้ประกอบด้วย Promise, โมดูล, คลาส, ลิเทอรัลเทมเพลต, ฟังก์ชันลูกศร, let และ const, พารามิเตอร์เริ่มต้น, เจนเนอเรเตอร์, การกำหนดค่าการจัดโครงสร้างใหม่, Rest และ Spread, 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 ให้คำจำกัดความแนวคิดนี้ดังนี้

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

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

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 Preview ซึ่งมีรูปภาพที่ไม่ได้ตั้งชื่อและเพิ่งวาง
รูปภาพที่วางลงในแอปแสดงตัวอย่างของ macOS

Badging API

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

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 ครั้งต่อตัวเลข ตอนนี้ตัวนับป้ายบนไอคอนอยู่ที่ 7

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

Periodic Background Sync API

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

ขั้นตอนแรกคือลงทะเบียนเหตุการณ์การซิงค์เป็นระยะในการลงทะเบียน 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() และระบบจะเผยแพร่ผลลัพธ์ไปยังไคลเอ็นต์ทั้งหมด เพื่อให้อัปเดต Canvas และแคชได้

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

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

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

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

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

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

ที่เก็บข้อความทักทายของ Fugu ใน GitHub
แอป Fugu Greetings ใน GitHub

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

กิตติกรรมประกาศ

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