การสร้างสําหรับเบราว์เซอร์สมัยใหม่และการเพิ่มประสิทธิภาพอย่างต่อเนื่องเหมือนเป็นปี 2003
เมื่อเดือนมีนาคม 2003 Nick Finck และ Steve Champeon ได้สร้างความตื่นตะลึงให้กับวงการออกแบบเว็บด้วยแนวคิดการเพิ่มประสิทธิภาพแบบเป็นขั้นเป็นตอน ซึ่งเป็นกลยุทธ์การออกแบบเว็บที่เน้นการโหลดเนื้อหาหลักของหน้าเว็บก่อน จากนั้นจึงเพิ่มชั้นการแสดงผลและฟีเจอร์ที่ละเอียดยิ่งขึ้นและมีความซับซ้อนทางเทคนิคมากขึ้นบนเนื้อหา ขณะที่ในปี 2003 การปรับปรุงแบบเป็นขั้นเป็นตอนเกี่ยวข้องกับการใช้ฟีเจอร์ CSS ที่ทันสมัยในยุคนั้น, JavaScript ที่ไม่รบกวน และแม้แต่ Scalable Vector Graphics เท่านั้น การปรับปรุงแบบเป็นขั้นเป็นตอนในปี 2020 เป็นต้นไปเกี่ยวข้องกับการใช้ความสามารถของเบราว์เซอร์สมัยใหม่
![การออกแบบเว็บไซต์ที่ครอบคลุมสำหรับอนาคตด้วยการปรับปรุงแบบเป็นขั้นเป็นตอน สไลด์ชื่อจากงานนำเสนอต้นฉบับของ Finck และ Champeon](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/inclusive-web-design-the-8dd761f5f7ef6.png?hl=th)
JavaScript ที่ทันสมัย
เมื่อพูดถึง JavaScript สถานการณ์การรองรับเบราว์เซอร์สำหรับฟีเจอร์หลักล่าสุดของ JavaScript ES 2015 นั้นดีมาก
มาตรฐานใหม่นี้ประกอบด้วย Promise, โมดูล, คลาส, ลิเทอรัลเทมเพลต, ฟังก์ชันลูกศร, let
และ const
, พารามิเตอร์เริ่มต้น, เจนเนอเรเตอร์, การกำหนดค่าการจัดโครงสร้างใหม่, Rest และ Spread, Map
/Set
, WeakMap
/WeakSet
และอื่นๆ อีกมากมาย
รองรับทั้งหมด
![ตารางการรองรับของ CanIUse สำหรับฟีเจอร์ ES6 ที่แสดงการรองรับในเบราว์เซอร์หลักทั้งหมด](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-caniuse-support-table-fea500f90ffcf.png?hl=th)
ฟังก์ชัน Async ซึ่งเป็นฟีเจอร์ของ ES 2017 และเป็นหนึ่งในฟีเจอร์โปรดของฉันใช้ได้ในเบราว์เซอร์หลักทุกรุ่น
คีย์เวิร์ด async
และ await
ช่วยให้เขียนลักษณะการทำงานแบบแอซิงโครนัสที่อิงตามสัญญาในสไตล์ที่สะอาดขึ้นได้โดยไม่ต้องกำหนดค่าเชนสัญญาอย่างชัดเจน
![ตารางการรองรับของ CanIUse สำหรับฟังก์ชันการทำงานแบบแอสซิงค์ที่แสดงการรองรับในเบราว์เซอร์หลักๆ ทั้งหมด](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-caniuse-support-table-bec0941fba39c.png?hl=th)
และแม้แต่ภาษา 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](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-iconic-windows-xp-gre-52c72bfaafb9d.png?hl=th)
แอปตัวอย่าง: Fugu Greetings
ในบทความนี้ เราจะใช้ PWA ง่ายๆ ชื่อ Fugu Greetings (GitHub) ชื่อแอปนี้เป็นการยกย่อง Project Fugu 🐡 ซึ่งเป็นความพยายามที่จะทำให้เว็บมีความสามารถทั้งหมดของแอปพลิเคชัน Android/iOS/เดสก์ท็อป อ่านข้อมูลเพิ่มเติมเกี่ยวกับโปรเจ็กต์ได้ในหน้า Landing Page
Fugu Greetings เป็นแอปวาดภาพที่ช่วยให้คุณสร้างการ์ดอวยพรเสมือนจริงและส่งให้กับคนที่คุณรักได้ ซึ่งแสดงแนวคิดหลักของ PWA เครื่องมือนี้เชื่อถือได้และเปิดใช้โหมดออฟไลน์ได้อย่างเต็มที่ คุณจึงยังใช้เครื่องมือนี้ต่อไปได้แม้ว่าจะไม่มีเครือข่าย นอกจากนี้ คุณยังติดตั้งแอปพลิเคชันนี้ลงในหน้าจอหลักของอุปกรณ์และผสานรวมกับระบบปฏิบัติการได้อย่างราบรื่นในฐานะแอปพลิเคชันแบบสแตนด์อโลน
![PWA ของ Fugu Greetings ที่มีภาพวาดคล้ายกับโลโก้ชุมชน PWA](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-pwa-a-dra-964a41cbe2963.png?hl=th)
การเพิ่มประสิทธิภาพแบบต่อเนื่อง
เมื่อเข้าใจเรื่องนี้แล้ว เรามาพูดถึงการเพิ่มประสิทธิภาพแบบต่อเนื่องกัน พจนานุกรม MDN Web Docs ให้คำจำกัดความแนวคิดนี้ดังนี้
การปรับปรุงแบบเป็นขั้นเป็นตอนเป็นปรัชญาการออกแบบที่ระบุพื้นฐานของเนื้อหาและฟังก์ชันการทํางานที่จําเป็นสําหรับผู้ใช้จํานวนมากที่สุดเท่าที่จะเป็นไปได้ พร้อมกับมอบประสบการณ์การใช้งานที่ดีที่สุดแก่ผู้ใช้เบราว์เซอร์ที่ทันสมัยที่สุดซึ่งสามารถเรียกใช้โค้ดที่จําเป็นทั้งหมดได้เท่านั้น
โดยทั่วไปแล้ว การตรวจหาฟีเจอร์จะใช้เพื่อระบุว่าเบราว์เซอร์สามารถจัดการฟังก์ชันการทำงานที่ทันสมัยมากขึ้นได้หรือไม่ ส่วน Polyfill มักใช้เพื่อเพิ่มฟีเจอร์ที่ขาดหายไปด้วย JavaScript
[…]
การปรับปรุงแบบเป็นขั้นเป็นตอนเป็นเทคนิคที่มีประโยชน์ซึ่งช่วยให้นักพัฒนาเว็บมุ่งเน้นที่การพัฒนาเว็บไซต์ที่ดีที่สุดได้ขณะที่ทําให้เว็บไซต์เหล่านั้นทํางานได้บน User Agent ที่ไม่รู้จักหลายรายการ การลดระดับอย่างราบรื่นนั้นมีความเกี่ยวข้องกัน แต่ไม่ใช่สิ่งเดียวกัน และมักถูกมองว่าเป็นการดำเนินการที่สวนทางกับการปรับปรุงแบบเป็นขั้นเป็นตอน แต่อันที่จริงแล้ว แนวทางทั้ง 2 แนวทางนี้ใช้ได้จริงและมักช่วยเสริมซึ่งกันและกันได้
ผู้มีส่วนร่วมของ MDN
การเริ่มต้นการ์ดอวยพรแต่ละใบตั้งแต่ต้นอาจเป็นเรื่องยุ่งยาก
ดังนั้นทำไมถึงไม่สร้างฟีเจอร์ที่อนุญาตให้ผู้ใช้นำเข้ารูปภาพและเริ่มดำเนินการต่อจากตรงนั้น
หากใช้แนวทางแบบดั้งเดิม คุณต้องใช้องค์ประกอบ <input type=file>
เพื่อดำเนินการนี้
ก่อนอื่นให้สร้างองค์ประกอบ ตั้งค่า type
เป็น 'file'
และเพิ่มประเภท MIME ลงในพร็อพเพอร์ตี้ accept
จากนั้น "คลิก" องค์ประกอบแบบเป็นโปรแกรมและรอการเปลี่ยนแปลง
เมื่อเลือกรูปภาพ ระบบจะนำเข้ารูปภาพนั้นลงในผืนผ้าใบโดยตรง
const importImage = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
เมื่อมีฟีเจอร์นำเข้า ก็ควรมีฟีเจอร์ส่งออกด้วยเพื่อให้ผู้ใช้บันทึกการ์ดอวยพรไว้ในเครื่องได้
วิธีที่เก่าแก่ในการบันทึกไฟล์คือการสร้างลิงก์แอตทริบิวต์ download
ที่มี URL ของ BLOB เป็น href
นอกจากนี้ คุณยัง "คลิก" ไฟล์ดังกล่าวแบบเป็นโปรแกรมเพื่อเรียกให้ดาวน์โหลด และอย่าลืมเพิกถอน URL ออบเจ็กต์ Blob เพื่อไม่ให้หน่วยความจำรั่วไหล
const exportImage = async (blob) => {
const a = document.createElement('a');
a.download = 'fugu-greeting.png';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
แต่เดี๋ยวก่อน ในทางจิตวิทยา คุณไม่ได้ "ดาวน์โหลด" การ์ด แต่ได้ "บันทึก" การ์ดไว้ แทนที่จะแสดงกล่องโต้ตอบ "บันทึก" ให้คุณเลือกตำแหน่งที่จะวางไฟล์ เบราว์เซอร์จะดาวน์โหลดการ์ดอวยพรโดยตรงโดยที่ผู้ใช้ไม่ต้องโต้ตอบ และวางการ์ดดังกล่าวลงในโฟลเดอร์ดาวน์โหลดโดยตรง ซึ่งเป็นเรื่องที่ไม่ดี
จะเกิดอะไรขึ้นหากมีวิธีที่ดีกว่า จะเกิดอะไรขึ้นหากคุณเปิดไฟล์ในเครื่อง แก้ไขไฟล์ แล้วบันทึกการแก้ไขไปยังไฟล์ใหม่หรือกลับไปที่ไฟล์ต้นฉบับที่คุณเปิดไว้ตั้งแต่แรก ปรากฏว่ามีอยู่ File System Access API ช่วยให้คุณเปิดและสร้างไฟล์และไดเรกทอรี รวมถึงแก้ไขและบันทึกไฟล์และไดเรกทอรีได้
ฉันจะตรวจหาฟีเจอร์ของ API ได้อย่างไร
File System Access API แสดงเมธอดใหม่ window.chooseFileSystemEntries()
ดังนั้น เราจึงต้องโหลดโมดูลการนําเข้าและส่งออกที่แตกต่างกันแบบมีเงื่อนไข โดยขึ้นอยู่กับว่าวิธีการนี้พร้อมใช้งานหรือไม่ เราได้แสดงวิธีดำเนินการไว้ด้านล่าง
const loadImportAndExport = () => {
if ('chooseFileSystemEntries' in window) {
Promise.all([
import('./import_image.mjs'),
import('./export_image.mjs'),
]);
} else {
Promise.all([
import('./import_image_legacy.mjs'),
import('./export_image_legacy.mjs'),
]);
}
};
แต่ก่อนที่จะเจาะลึกรายละเอียดของ File System Access API เราขออธิบายรูปแบบการเพิ่มประสิทธิภาพแบบเป็นขั้นเป็นตอนคร่าวๆ สักนิด ในเบราว์เซอร์ที่ไม่รองรับ File System Access API ในปัจจุบัน เราจะโหลดสคริปต์เดิม คุณดูแท็บเครือข่ายของ Firefox และ Safari ได้ที่ด้านล่าง
![เครื่องมือตรวจสอบเว็บของ Safari แสดงการโหลดไฟล์เดิม](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/safari-web-inspector-show-38b32feb7b4f.png?hl=th)
![เครื่องมือสำหรับนักพัฒนาซอฟต์แวร์ของ Firefox ที่แสดงการโหลดไฟล์เดิม](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/firefox-developer-tools-s-60e8cb93aaa6b.png?hl=th)
อย่างไรก็ตาม ใน Chrome ซึ่งเป็นเบราว์เซอร์ที่รองรับ API ระบบจะโหลดเฉพาะสคริปต์ใหม่
การดำเนินการนี้เป็นไปได้อย่างราบรื่นด้วยimport()
แบบไดนามิกที่เบราว์เซอร์สมัยใหม่ทั้งหมดรองรับ
ตามที่ได้กล่าวไปก่อนหน้านี้ ช่วงนี้หญ้าเขียวดี
![เครื่องมือสำหรับนักพัฒนาเว็บใน Chrome ที่แสดงการโหลดไฟล์สมัยใหม่](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/chrome-devtools-showing-770ae149354f5.png?hl=th)
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 ที่มีกล่องโต้ตอบเปิดไฟล์](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-fil-e1040300ddcaf.png?hl=th)
![แอป Fugu Greetings พร้อมใช้งานรูปภาพที่นำเข้าแล้ว](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-with-56c3523778222.png?hl=th)
![แอป Fugu Greetings ที่มีรูปภาพที่แก้ไขแล้ว](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-the-m-1a86c627405ad.png?hl=th)
Web Share API และ Web Share Target API
นอกจากเก็บไว้ตลอดไปแล้ว ฉันอาจต้องการแชร์การ์ดอวยพรด้วย ซึ่ง Web Share API และ Web Share Target API ช่วยให้ฉันดำเนินการดังกล่าวได้ ระบบปฏิบัติการบนอุปกรณ์เคลื่อนที่และเดสก์ท็อปในปัจจุบันมีกลไกการแชร์ในตัว ตัวอย่างเช่น ด้านล่างนี้คือชีตการแชร์ของ Safari บนเดสก์ท็อปใน macOS ที่เรียกให้แสดงจากบทความในบล็อกของฉัน เมื่อคลิกปุ่มแชร์บทความ คุณจะแชร์ลิงก์ไปยังบทความกับเพื่อนได้ เช่น ผ่านแอปรับส่งข้อความของ macOS
![ชีตการแชร์ของ Safari บนเดสก์ท็อปใน macOS ที่เรียกให้แสดงจากปุ่มแชร์ของบทความ](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/desktop-safaris-share-sh-8fbd756c55ba8.png?hl=th)
โค้ดที่ใช้ดำเนินการนี้ค่อนข้างตรงไปตรงมา ฉันเรียกใช้ 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 แล้ววิดเจ็ตเครื่องมือเขียนอีเมลจะปรากฏขึ้นพร้อมแนบรูปภาพ
![ชีตการแชร์ระดับระบบปฏิบัติการที่แสดงแอปต่างๆ เพื่อแชร์รูปภาพ](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/os-level-share-sheet-show-3161a8aab13b2.png?hl=th)
![วิดเจ็ตการเขียนอีเมลของ Gmail พร้อมแนบรูปภาพ](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/gmails-email-compose-wid-fb4dcf2c7e4d4.png?hl=th)
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 คนในสมุดที่อยู่](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/contacts-picker-showing-4d7400f689224.png?hl=th)
![ชื่อของรายชื่อติดต่อ 2 คนที่เลือกไว้ก่อนหน้านี้ซึ่งวาดบนการ์ดอวยพร](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-names-the-previousl-58fa638399f8c.png?hl=th)
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 ที่แสดงข้อความแจ้งสิทธิ์ในคลิปบอร์ด](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-showin-2da915014bf66.png?hl=th)
สุดท้าย หลังจากยอมรับสิทธิ์แล้ว ระบบจะวางรูปภาพลงในแอปพลิเคชัน ในทางกลับกันก็ใช้ได้เช่นกัน เราขอคัดลอกการ์ดอวยพรไปยังคลิปบอร์ด เมื่อเปิดโปรแกรมแสดงตัวอย่างแล้วคลิกไฟล์ จากนั้นคลิกใหม่จากคลิปบอร์ด ระบบจะวางการ์ดอวยพรลงในรูปภาพใหม่ที่ไม่มีชื่อ
![แอปแสดงตัวอย่างของ macOS ที่มีรูปภาพที่ไม่มีชื่อซึ่งเพิ่งวาง](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-macos-preview-app-an-9ec120ebd7ad8.png?hl=th)
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
![ตัวเลข 1-7 ที่วาดบนการ์ดอวยพร โดยแต่ละตัวเลขวาดด้วยเส้นปากกาเพียงเส้นเดียว](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-numbers-one-seven-d-890d712e8df6d.png?hl=th)
![ไอคอนป้ายในแอป Fugu Greetings ที่แสดงหมายเลข 7](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/badge-icon-the-fugu-gree-bc1d070282039.png?hl=th)
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
![แอปการ์ดอวยพรของ Fugu ที่มีรูปภาพการ์ดอวยพรใหม่ประจำวัน](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-gr-d81b949b1fb1c.png?hl=th)
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 พร้อมข้อความแจ้งที่ถามผู้ใช้ว่าต้องการรับการช่วยเตือนให้ออกแบบการ์ดอวยพรให้เสร็จเมื่อใด](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-pro-5fd5c6e04511a.png?hl=th)
เมื่อการแจ้งเตือนที่ตั้งเวลาไว้ทริกเกอร์ใน Fugu Greetings การแจ้งเตือนจะแสดงเหมือนการแจ้งเตือนอื่นๆ แต่อย่างที่เราได้เขียนไว้ก่อนหน้านี้ การแจ้งเตือนนี้ไม่จำเป็นต้องมีการเชื่อมต่อเครือข่าย
![ศูนย์การแจ้งเตือนของ macOS ที่แสดงการแจ้งเตือนที่ทริกเกอร์จาก Fugu Greetings](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/macos-notification-center-87f383f0a103b.png?hl=th)
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 จะมีช่องทำเครื่องหมายนอนไม่หลับ ซึ่งเมื่อเลือกไว้ ระบบจะเปิดหน้าจอไว้
![ช่องทำเครื่องหมาย "นอนไม่หลับ" (หากเลือกไว้) จะทำให้หน้าจอสว่างอยู่เสมอ](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/the-insomnia-checkbox-c-fc49b7954974a.png?hl=th)
Idle Detection API
บางครั้ง แม้คุณจะจ้องมองหน้าจอเป็นเวลาหลายชั่วโมง แต่ก็ไม่ได้ช่วยให้คุณมีไอเดียสักนิดว่าควรทำอย่างไรกับการ์ดอวยพร Idle Detection API ช่วยให้แอปตรวจจับเวลาที่ไม่ได้ใช้งานของผู้ใช้ หากผู้ใช้ไม่ได้ใช้งานเป็นเวลานานเกินไป แอปจะรีเซ็ตเป็นสถานะเริ่มต้นและล้างผืนผ้าแคนวาส ปัจจุบัน API นี้อยู่ภายใต้สิทธิ์การแจ้งเตือนเนื่องจาก Use Case จำนวนมากในเวอร์ชันที่ใช้งานจริงของการตรวจหาการใช้งานอยู่เกี่ยวข้องกับการแจ้งเตือน เช่น เพื่อส่งการแจ้งเตือนไปยังอุปกรณ์ที่ผู้ใช้ใช้งานอยู่เท่านั้น
หลังจากตรวจสอบว่าได้ให้สิทธิ์การแจ้งเตือนแล้ว เราจะสร้างอินสแตนซ์ของโปรแกรมตรวจหาการใช้งานอยู่ ฉันลงทะเบียน Listener เหตุการณ์ที่คอยฟังการเปลี่ยนแปลงสถานะ "ไม่มีการใช้งาน" ซึ่งรวมถึงสถานะของผู้ใช้และหน้าจอ ผู้ใช้อาจใช้งานอยู่หรือไม่ได้ใช้งาน และหน้าจออาจปลดล็อกหรือล็อกอยู่ หากผู้ใช้ไม่มีความเคลื่อนไหว แคนวาสจะล้างข้อมูล ฉันกำหนดเกณฑ์ให้ตัวตรวจจับไม่มีการใช้งานไว้ที่ 60 วินาที
const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
const userState = idleDetector.userState;
const screenState = idleDetector.screenState;
console.log(`Idle change: ${userState}, ${screenState}.`);
if (userState === 'idle') {
clearCanvas();
}
});
await idleDetector.start({
threshold: 60000,
signal,
});
และเช่นเคย เราจะโหลดโค้ดนี้เฉพาะเมื่อเบราว์เซอร์รองรับ
if ('IdleDetector' in window) {
import('./idle_detection.mjs');
}
ในแอปการ์ดอวยพรของ Fugu ภาพวาดแคนวาสจะล้างออกเมื่อเลือกช่องทำเครื่องหมายชั่วคราวและผู้ใช้ไม่ได้ใช้งานเป็นเวลานานเกินไป
![แอปการ์ดอวยพรของ Fugu ที่มีภาพวาดที่ล้างออกแล้วหลังจากที่ผู้ใช้ไม่ได้ใช้งานเป็นเวลานานเกินไป](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-app-a-cle-559af8955ecad.png?hl=th)
เปิดจากขอบ
เฮ้อ อะไรจะขนาดนั้น API มากมายขนาดนี้ในตัวอย่างแอปเพียงแอปเดียว และอย่าลืมว่าเราไม่เคยเรียกเก็บค่าดาวน์โหลดจากผู้ใช้สำหรับฟีเจอร์ที่เบราว์เซอร์ของผู้ใช้ไม่รองรับ การใช้การเพิ่มประสิทธิภาพแบบต่อเนื่องช่วยให้มั่นใจได้ว่าระบบจะโหลดเฉพาะโค้ดที่เกี่ยวข้อง และเนื่องจาก HTTP/2 มีการส่งคำขอที่ประหยัด รูปแบบนี้จึงควรใช้ได้กับแอปพลิเคชันจำนวนมาก แม้ว่าคุณอาจต้องพิจารณาเครื่องมือรวมสำหรับแอปขนาดใหญ่มาก
![แผงเครือข่ายของ Chrome DevTools ที่แสดงเฉพาะคําขอไฟล์ที่มีโค้ดที่เบราว์เซอร์ปัจจุบันรองรับ](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/chrome-devtools-network-p-c51d72a3bad2.png?hl=th)
แอปอาจดูแตกต่างออกไปเล็กน้อยในเบราว์เซอร์แต่ละประเภท เนื่องจากแพลตฟอร์มบางแพลตฟอร์มไม่รองรับฟีเจอร์บางรายการ แต่ฟังก์ชันหลักจะยังคงอยู่เสมอ โดยจะได้รับการปรับปรุงอย่างต่อเนื่องตามความสามารถของเบราว์เซอร์แต่ละประเภท โปรดทราบว่าความสามารถเหล่านี้อาจเปลี่ยนแปลงได้แม้ในเบราว์เซอร์เดียวกัน ขึ้นอยู่กับว่าแอปทำงานเป็นแอปที่ติดตั้งหรือในแท็บเบราว์เซอร์
![Fugu Greetings ที่ทำงานบน Chrome ของ Android ซึ่งแสดงฟีเจอร์ต่างๆ ที่มีให้ใช้งาน](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-running-a-058338175547c.png?hl=th)
![Fugu Greetings ที่ทำงานบน Safari บนเดสก์ท็อป ซึ่งแสดงฟีเจอร์ที่มีให้ใช้งานน้อยลง](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-running-d-446d17fc11442.png?hl=th)
![Fugu Greetings ที่ทำงานบน Chrome บนเดสก์ท็อป ซึ่งแสดงฟีเจอร์ต่างๆ ที่มีให้ใช้งาน](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-running-d-9006f53b391af.png?hl=th)
หากสนใจแอป Fugu Greetings ให้ค้นหาและแยกแอปนี้ใน GitHub
![ที่เก็บ Fugu Greetings ใน GitHub](https://web.developers.google.cn/static/articles/progressively-enhance-your-pwa/image/fugu-greetings-repo-gith-f95acb5949892.png?hl=th)
ทีม Chromium กำลังพยายามอย่างเต็มที่เพื่อทำให้หญ้าเขียวขึ้นเมื่อพูดถึง Fugu API ขั้นสูง การใช้การปรับปรุงแบบเป็นขั้นเป็นตอนในการพัฒนาแอปช่วยให้มั่นใจได้ว่าทุกคนจะได้รับประสบการณ์การใช้งานพื้นฐานที่ดีและมั่นคง แต่ผู้ใช้เบราว์เซอร์ที่รองรับ API ของแพลตฟอร์มเว็บจำนวนมากขึ้นจะได้รับประสบการณ์การใช้งานที่ดีขึ้นไปอีก เราหวังว่าจะได้เห็นสิ่งที่คุณทำกับ Progressive Enhancement ในแอป
ขอขอบคุณ
ขอขอบคุณ Christian Liebel และ Hemanth HM ที่ได้มีส่วนร่วมใน Fugu Greetings
บทความนี้ผ่านการตรวจสอบโดย Joe Medley และ Kayce Basques
Jake Archibald ช่วยให้ฉันทราบสถานการณ์เกี่ยวกับimport()
แบบไดนามิกในบริบทของ Service Worker