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

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

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

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

JavaScript สมัยใหม่

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

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

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

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

แม้แต่ส่วนเพิ่มเติมล่าสุดในภาษา ES 2020 ที่เพิ่มเข้ามา เช่น ตัวเลือกการทำเชน (ไม่บังคับ) และการรวมข้อมูลแบบ Nullish ก็เข้าถึงการสนับสนุนได้อย่างรวดเร็วมาก คุณดูตัวอย่างโค้ดได้ที่ด้านล่าง เมื่อพูดถึงฟีเจอร์หลักของ 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

ในบทความนี้ เราทํางานร่วมกับ PWA แบบง่ายที่เรียกว่า Fugu Greetings (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();
  });
};

เมื่อมีฟีเจอร์import ควรมีฟีเจอร์importเพื่อให้ผู้ใช้บันทึกการ์ดอวยพรของตนไว้ในเครื่องได้ วิธีการบันทึกไฟล์แบบดั้งเดิมคือการสร้างลิงก์ Anchor ด้วยแอตทริบิวต์ 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 Web Inspector
เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ Firefox ที่แสดงการโหลดไฟล์เดิม
แท็บเครือข่ายเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ Firefox

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

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 ยังไม่รองรับฟีเจอร์นี้ ข่าวดีคือ 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 ที่ประกอบด้วย 1 Blob แล้วตามด้วย 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 ถ้าฉันแตะปุ่มแชร์ในเบราว์เซอร์ที่รองรับ เช่น Chrome บน Android ชีตการแชร์ในตัวจะเปิดขึ้น ตัวอย่างเช่น ผมสามารถเลือก Gmail แล้ววิดเจ็ตเครื่องมือเขียนอีเมลก็จะปรากฏขึ้นพร้อมรูปภาพที่แนบ

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

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

ต่อไปฉันจะพูดถึงรายชื่อติดต่อ ซึ่งหมายถึงสมุดที่อยู่ของอุปกรณ์หรือแอปโปรแกรมจัดการรายชื่อติดต่อ เมื่อคุณเขียนการ์ดอวยพร การเขียนชื่อคนอื่นอย่างถูกต้อง อาจไม่ใช่เรื่องง่ายเสมอไป ตัวอย่างเช่น ฉันมีเพื่อนที่ชื่อ Sergey ต้องการให้ชื่อของเขาสะกดเป็นตัวอักษรซิริลลิก ฉันใช้แป้นพิมพ์ QWERTZ ภาษาเยอรมันอยู่และไม่รู้ว่าจะพิมพ์ชื่ออย่างไร ซึ่งเป็นปัญหาที่ Contact Picker API สามารถแก้ไขได้ เนื่องจากฉันเก็บเพื่อนไว้ในแอปรายชื่อติดต่อในโทรศัพท์ ผ่าน 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 เมื่อแตะปุ่มรายชื่อติดต่อแล้วเลือกเพื่อนสนิท 2 คน серค่ะей การจัดจำแนก เก็บไว้ นอกจากนี้ เราเคยดูเพียงชื่ออื่นๆ ของคำว่า 劳伦斯·爱德 Security·"拉里"·佩奇 คุณจะเห็นว่าข้อมูลผู้ติดต่อนั้นถูกจำกัดไว้เพียงชื่อ แล้วระบบจะวาดชื่อของพวกเขาไว้บนการ์ดอวยพรของฉัน

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

แอป Fugu Greetings แสดงข้อความแจ้งสิทธิ์ของคลิปบอร์ด
ข้อความแจ้งสิทธิ์ของคลิปบอร์ด

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

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

API การติดป้าย

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

ตัวเลขจาก 1 ถึง 7 วาดลงบนบัตรอวยพร ซึ่งแต่ละใบวาดด้วยปากกา 1 เส้น
การวาดตัวเลขตั้งแต่ 1 ถึง 7 โดยใช้ปากกา 7 เส้น
ไอคอนป้ายในแอปทักทาย Fugu แสดงเลข 7
ตัวนับการลากปากกาในรูปแบบของป้ายไอคอนแอป

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 ในโปรแกรมทำงานของบริการ หากแท็กเหตุการณ์คือ '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 เท่านั้น ซึ่งจะมีผลกับทั้งรหัสไคลเอ็นต์และรหัสโปรแกรมทำงานของบริการ ในเบราว์เซอร์ที่ไม่รองรับ จะไม่มีทั้งสองโหลด โปรดสังเกตวิธีใน Service Worker แทน 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 การกดปุ่มวอลเปเปอร์จะแสดงรูปภาพการ์ดอวยพรประจำวันที่มีการอัปเดตทุกวันผ่าน Periodic Background Sync API

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

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

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

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

ศูนย์การแจ้งเตือนของ macOS แสดงการแจ้งเตือนที่มีการทริกเกอร์จากคำทักทาย Fugu
การแจ้งเตือนที่ทริกเกอร์จะปรากฏในศูนย์การแจ้งเตือนของ 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 จะมีช่องทำเครื่องหมายนอนไม่หลับซึ่งเมื่อเลือกช่องนี้จะทำให้หน้าจอยังเปิดอยู่ตลอด

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

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

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

เปิดจากขอบ

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

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

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

คำทักทาย Fugu กำลังทำงานบน Chrome ของ Android โดยแสดงฟีเจอร์ที่ใช้ได้มากมาย
คำทักทายของฟูกูที่ทำงานใน Android Chrome
ข้อความทักทาย Fugu กำลังทำงานบน Safari บนเดสก์ท็อป โดยแสดงฟีเจอร์ที่ใช้ได้น้อยลง
Fugu Greetings ที่ทำงานใน Safari บนเดสก์ท็อป
ข้อความทักทาย Fugu กำลังทำงานบน Chrome บนเดสก์ท็อป โดยแสดงฟีเจอร์ที่พร้อมใช้งานมากมาย
คำทักทายของฟูกูที่ทำงานใน 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