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

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

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

และแม้แต่ภาษา 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

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

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


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

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



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


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


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

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

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

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 การแจ้งเตือนจะแสดงเหมือนการแจ้งเตือนอื่นๆ แต่อย่างที่เราได้เขียนไว้ก่อนหน้านี้ การแจ้งเตือนนี้ไม่จำเป็นต้องมีการเชื่อมต่อเครือข่าย

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

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

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

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



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

ทีม Chromium กำลังพยายามอย่างเต็มที่เพื่อทำให้หญ้าเขียวขึ้นเมื่อพูดถึง Fugu API ขั้นสูง การใช้การปรับปรุงแบบเป็นขั้นเป็นตอนในการพัฒนาแอปช่วยให้มั่นใจได้ว่าทุกคนจะได้รับประสบการณ์การใช้งานพื้นฐานที่ดีและมั่นคง แต่ผู้ใช้เบราว์เซอร์ที่รองรับ API ของแพลตฟอร์มเว็บจำนวนมากขึ้นจะได้รับประสบการณ์การใช้งานที่ดีขึ้นไปอีก เราหวังว่าจะได้เห็นสิ่งที่คุณทำกับ Progressive Enhancement ในแอป
ขอขอบคุณ
ขอขอบคุณ Christian Liebel และ Hemanth HM ที่ได้มีส่วนร่วมใน Fugu Greetings
บทความนี้ผ่านการตรวจสอบโดย Joe Medley และ Kayce Basques
Jake Archibald ช่วยให้ฉันทราบสถานการณ์เกี่ยวกับimport()
แบบไดนามิกในบริบทของ Service Worker