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

สร้างขึ้นสำหรับเบราว์เซอร์ที่ทันสมัย และปรับปรุงอย่างต่อเนื่องให้เหมือนกับปี 2003

ย้อนกลับไปในเดือนมีนาคมปี 2003 Nick Finck และ Steve Champeon ทำให้โลกแห่งการออกแบบเว็บตะลึง ที่มีแนวคิดเกี่ยวกับ การปรับปรุงแบบก้าวหน้า กลยุทธ์การออกแบบเว็บ ที่เน้นการโหลดเนื้อหาหลักของหน้าเว็บก่อน จากนั้นจะค่อยๆ เพิ่มความละเอียด และชั้นเชิงเทคนิค ของการนำเสนอและคุณลักษณะต่างๆ ที่ด้านบนของเนื้อหา ขณะที่ในปี 2003 การเพิ่มประสิทธิภาพแบบก้าวหน้าเกี่ยวข้องกับการใช้ความทันสมัย คุณลักษณะ CSS, JavaScript ที่ไม่ก่อให้เกิดความรำคาญ และแม้กระทั่งกราฟิกเวกเตอร์ที่รองรับการปรับขนาด การเพิ่มประสิทธิภาพแบบก้าวหน้าในปี 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 ที่เพิ่งเพิ่มเข้ามา เช่น การทำเชนที่ไม่บังคับ และ การทำงานร่วมกันที่ไม่มีข้อมูล ได้ติดต่อทีมสนับสนุนอย่างรวดเร็ว ดูตัวอย่างโค้ดได้ที่ด้านล่าง เมื่อพูดถึงฟีเจอร์ 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 แบบง่ายชื่อ สวัสดีชาวฟูกู (GitHub) ชื่อของแอปนี้เรียกได้ว่าเป็นเคล็ดลับของ Project Fugu 🐡 ซึ่งเป็นความพยายามในการทำให้ทุกคนบนเว็บ ประสิทธิภาพของแอปพลิเคชัน Android/iOS/เดสก์ท็อป คุณสามารถอ่านเพิ่มเติมเกี่ยวกับโครงการได้ใน หน้า Landing Page

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

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

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

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

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

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

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

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

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

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

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

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

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

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

API เครื่องมือเลือกรายชื่อติดต่อ

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

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

API คลิปบอร์ดแบบอะซิงโครนัส

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

ในการคัดลอกเนื้อหาลงในคลิปบอร์ดของระบบ ฉันต้องเขียนสิ่งนั้นไว้ในคลิปบอร์ด เมธอด 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 Preview และ คัดลอกไปยังคลิปบอร์ด เมื่อคลิกวาง แอป Fugu Greetings จะขอให้ฉัน ว่าจะอนุญาตให้แอปดูข้อความและรูปภาพในคลิปบอร์ดไหม

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

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

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

Badging API

API ที่มีประโยชน์อีกอย่างหนึ่งคือ Badging API แน่นอนว่า Fugu Greetings เป็น PWA ที่ติดตั้งได้ จึงจะมีไอคอนแอป ที่ผู้ใช้สามารถวางบนแถบแอปหรือหน้าจอหลักได้ วิธีที่สนุกและง่ายในการสาธิต API คือการใช้ 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 ขีด
วาดตัวเลขจาก 1 ถึง 7 โดยใช้ปากกา 7 เส้น
ไอคอนป้ายในแอป Fugu Greetings แสดงเลข 7
ปากกาจะลากตัวนับในรูปแบบของป้ายไอคอนแอป

API การซิงค์ในเบื้องหลังตามระยะเวลา

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

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

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 ป้องกันไม่ให้เกิดเหตุการณ์นี้ได้

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

API การตรวจจับเมื่อไม่มีการใช้งาน

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

หลังจากตรวจสอบว่าได้ให้สิทธิ์การแจ้งเตือนแล้ว เราจะสร้างอินสแตนซ์ ตัวตรวจจับเมื่อไม่มีการใช้งาน ฉันลงทะเบียน 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 Greetings แคนวาสจะหายไปเมื่อช่องทำเครื่องหมายชั่วคราว และผู้ใช้ไม่มีการใช้งานนานเกินไป

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

เปิดจากขอบ

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

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

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

วันที่ Fugu Greetings ทำงานใน Chrome สำหรับ Android และแสดงฟีเจอร์ที่พร้อมใช้งานมากมาย
Fugu Greetings กำลังทำงานใน Chrome ของ Android
Fugu Greetings ทำงานบน Safari ในเดสก์ท็อป แสดงฟีเจอร์ที่ใช้ได้น้อยลง
คำทักทายจาก Fuugu ทำงานบน Safari บนเดสก์ท็อป
คำทักทายของ Fugu กำลังทำงานใน Chrome บนเดสก์ท็อป พร้อมแสดงฟีเจอร์มากมายที่ใช้ได้
คำอวยพรจากฟูกูกำลังทำงานใน Chrome บนเดสก์ท็อป

หากคุณสนใจแอปคำทักทายผู้สร้างสรรค์ฟูก ค้นหาและแยกออกบน GitHub

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

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

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

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