برنامه وب پیشرو خود را به تدریج ارتقا دهید

برای مرورگرهای مدرن ساخته شده و به تدریج مانند سال 2003 بهبود می یابد

در مارس 2003، نیک فینک و استیو چمپئون دنیای طراحی وب را با مفهوم پیشرفت پیشرونده مبهوت کردند، یک استراتژی برای طراحی وب که ابتدا بر بارگذاری محتوای اصلی صفحه وب تأکید می کند و سپس به تدریج لایه های ظریف و فنی دقیق تری از ارائه و ویژگی ها را در بالای محتوا اضافه می کند. در حالی که در سال 2003، پیشرفت تدریجی در مورد استفاده از ویژگی های CSS مدرن، جاوا اسکریپت محجوب و حتی گرافیک های برداری مقیاس پذیر بود. بهبود پیشرونده در سال 2020 و پس از آن در مورد استفاده از قابلیت های مرورگر مدرن است.

طراحی وب فراگیر برای آینده با پیشرفت تدریجی. اسلاید عنوان از ارائه اصلی Finck و Champeon.
اسلاید: طراحی وب جامع برای آینده با پیشرفت پیشرو. ( منبع )

جاوا اسکریپت مدرن

وقتی صحبت از جاوا اسکریپت شد، وضعیت پشتیبانی مرورگر برای آخرین ویژگی های جاوا اسکریپت هسته ES 2015 عالی است. استاندارد جدید شامل وعده‌ها، ماژول‌ها، کلاس‌ها، الفاظ قالب، توابع پیکان، let و const ، پارامترهای پیش‌فرض، ژنراتورها، تخصیص تخریب، استراحت و گسترش، Map / Set ، WeakMap / WeakSet و بسیاری موارد دیگر است. همه پشتیبانی می شوند .

جدول پشتیبانی CanIUse برای ویژگی های ES6 که پشتیبانی از همه مرورگرهای اصلی را نشان می دهد.
جدول پشتیبانی مرورگر ECMAScript 2015 (ES6). ( منبع )

توابع Async، یکی از ویژگی های ES 2017 و یکی از موارد دلخواه شخصی من، می تواند در همه مرورگرهای اصلی استفاده شود . کلمات کلیدی async و await این امکان را فراهم می کند که رفتار ناهمزمان و مبتنی بر قول به سبک تمیزتر نوشته شود و از نیاز به پیکربندی صریح زنجیره های وعده اجتناب شود.

جدول پشتیبانی CanIUse برای توابع ناهمگام که پشتیبانی را در تمام مرورگرهای اصلی نشان می دهد.
جدول پشتیبانی مرورگر توابع Async. ( منبع )

و حتی زبان‌های فوق‌العاده اخیر ES 2020 مانند زنجیره‌سازی اختیاری و ادغام بی‌حساب به سرعت به پشتیبانی رسیده‌اند. نمونه کد را می توانید در زیر مشاهده کنید. وقتی صحبت از ویژگی‌های اصلی جاوا اسکریپت می‌شود، چمن نمی‌تواند خیلی سبزتر از امروز باشد.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
تصویر پس زمینه چمن سبز نمادین ویندوز XP.
وقتی صحبت از ویژگی‌های اصلی جاوا اسکریپت می‌شود، چمن سبز است. (نمایش محصول مایکروسافت، با اجازه استفاده شده است.)

برنامه نمونه: Fugu Greetings

برای این مقاله، من با یک PWA ساده به نام Fugu Greetings ( GitHub ) کار می کنم. نام این برنامه نوک کلاهی است برای Project Fugu 🐡، تلاشی برای دادن تمام قدرت های برنامه های Android/iOS/desktop به وب. می توانید اطلاعات بیشتری در مورد این پروژه در صفحه فرود آن بخوانید.

Fugu Greetings یک برنامه نقاشی است که به شما امکان می دهد کارت تبریک مجازی ایجاد کنید و آنها را برای عزیزان خود ارسال کنید. این نمونه مفاهیم اصلی PWA است. قابل اعتماد است و کاملاً آفلاین فعال است، بنابراین حتی اگر شبکه ندارید، همچنان می توانید از آن استفاده کنید. همچنین قابل نصب بر روی صفحه اصلی دستگاه است و به عنوان یک برنامه مستقل به طور یکپارچه با سیستم عامل ادغام می شود.

Fugu Greetings PWA با طرحی که شبیه آرم انجمن PWA است.
برنامه نمونه Fugu Greetings .

افزایش پیشرونده

با خارج شدن از این راه، وقت آن است که در مورد بهبود پیشرونده صحبت کنیم. واژه نامه MDN Web Docs این مفهوم را به صورت زیر تعریف می کند :

ارتقای پیشرو یک فلسفه طراحی است که مبنایی از محتوا و عملکرد ضروری را برای هرچه بیشتر کاربران فراهم می‌کند، در حالی که بهترین تجربه ممکن را فقط برای کاربران مدرن‌ترین مرورگرها ارائه می‌کند که می‌توانند تمام کدهای مورد نیاز را اجرا کنند.

تشخیص ویژگی عموماً برای تعیین اینکه آیا مرورگرها می توانند عملکردهای مدرن تری را انجام دهند یا خیر استفاده می شود، در حالی که از polyfills اغلب برای افزودن ویژگی های گمشده با جاوا اسکریپت استفاده می شود.

[…]

بهبود پیشرونده یک تکنیک مفید است که به توسعه دهندگان وب اجازه می دهد تا روی توسعه بهترین وب سایت های ممکن تمرکز کنند در حالی که باعث می شود آن وب سایت ها روی چندین عامل کاربر ناشناس کار کنند. تنزل برازنده مرتبط است، اما یک چیز نیست و اغلب در جهت مخالف پیشرفت پیشرونده دیده می شود. در واقع، هر دو رویکرد معتبر هستند و اغلب می توانند مکمل یکدیگر باشند.

مشارکت کنندگان 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'),
    ]);
  }
};

اما قبل از اینکه به جزئیات API دسترسی به فایل سیستم بپردازم، اجازه دهید به سرعت الگوی بهبود پیشرونده را در اینجا برجسته کنم. در مرورگرهایی که در حال حاضر از File System Access API پشتیبانی نمی‌کنند، اسکریپت‌های قدیمی را بارگیری می‌کنم. در زیر می توانید تب های شبکه فایرفاکس و سافاری را مشاهده کنید.

Safari Web Inspector که فایل‌های قدیمی را در حال بارگذاری نشان می‌دهد.
تب شبکه Safari Web Inspector.
ابزارهای توسعه دهنده فایرفاکس که فایل های قدیمی در حال بارگذاری را نشان می دهد.
تب شبکه ابزارهای توسعه دهنده فایرفاکس.

با این حال، در کروم، مرورگری که از API پشتیبانی می کند، فقط اسکریپت های جدید بارگیری می شوند. این امر به لطف import() که همه مرورگرهای مدرن از آن پشتیبانی می کنند ، به زیبایی امکان پذیر شده است. همانطور که قبلاً گفتم، این روزها چمن بسیار سبز است.

Chrome DevTools که فایل‌های مدرن در حال بارگیری را نشان می‌دهد.
تب شبکه Chrome DevTools.

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() یک جریان قابل نوشتن ایجاد می کنم. بعد، حباب را که تصویر کارت تبریک من است، روی فایل می نویسم. در نهایت، جریان قابل نوشتن را می بندم.

همه چیز همیشه ممکن است خراب شود: فضای دیسک ممکن است خالی باشد، ممکن است خطای نوشتن یا خواندن وجود داشته باشد، یا شاید کاربر به سادگی گفتگوی فایل را لغو کند. به همین دلیل است که من همیشه تماس ها را در یک عبارت 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 با تصویر اصلاح شده.
ذخیره تصویر اصلاح شده در یک فایل جدید.

Web Share و Web Share Target API

جدای از ذخیره برای ابدیت، شاید واقعاً بخواهم کارت تبریک خود را به اشتراک بگذارم. این چیزی است که Web Share API و Web Share Target API به من اجازه انجام آن را می دهند. سیستم عامل های موبایل و اخیراً دسکتاپ مکانیسم های اشتراک گذاری داخلی را به دست آورده اند. برای مثال، در زیر برگه اشتراک سافاری دسکتاپ در macOS است که از مقاله ای در وبلاگ من راه اندازی شده است. وقتی روی دکمه اشتراک‌گذاری مقاله کلیک می‌کنید، می‌توانید پیوندی به مقاله را با یک دوست به اشتراک بگذارید، به عنوان مثال، از طریق برنامه پیام‌های macOS.

برگه اشتراک سافاری دسکتاپ در macOS از دکمه اشتراک‌گذاری مقاله راه‌اندازی می‌شود
Web Share API در Safari دسکتاپ در macOS.

کد برای تحقق این امر بسیار ساده است. من navigator.share() را فراخوانی می‌کنم و آن را یک title اختیاری، text و url در یک شی ارسال می‌کنم. اما اگر بخواهم تصویری را ضمیمه کنم چه؟ سطح 1 از Web Share API هنوز از این پشتیبانی نمی کند. خبر خوب این است که Web Share Level 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 متشکل از یک لکه و سپس یک title و یک text آماده کنم. در مرحله بعد، به عنوان بهترین روش، من از متد new 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 از طریق dynamic import() بارگذاری می کنم. در مرورگرهایی مانند سافاری موبایل که فقط یکی از دو شرط را برآورده می‌کنند، عملکرد را بارگیری نمی‌کنم.

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

در Fugu Greetings، اگر روی دکمه اشتراک‌گذاری در مرورگر پشتیبانی‌کننده مانند Chrome در اندروید ضربه بزنم، برگه اشتراک‌گذاری داخلی باز می‌شود. برای مثال می‌توانم Gmail را انتخاب کنم و ویجت ایمیل آهنگساز با تصویر پیوست شده ظاهر می‌شود.

صفحه اشتراک‌گذاری در سطح سیستم‌عامل که برنامه‌های مختلفی را برای اشتراک‌گذاری تصویر نشان می‌دهد.
انتخاب برنامه ای برای اشتراک گذاری فایل.
ویجت نوشتن ایمیل Gmail با تصویر پیوست شده است.
فایل به ایمیل جدیدی در کامپوزر Gmail پیوست می شود.

Contact Picker API

بعد، می‌خواهم در مورد مخاطبین صحبت کنم، یعنی دفترچه آدرس دستگاه یا برنامه مدیریت مخاطبین. وقتی یک کارت تبریک می نویسید، ممکن است نوشتن نام یک نفر به درستی همیشه آسان نباشد. به عنوان مثال، من یک دوست سرگئی دارم که ترجیح می دهد نامش با حروف سیریلیک نوشته شود. من از صفحه کلید QWERTZ آلمانی استفاده می کنم و نمی دانم چگونه نام آنها را تایپ کنم. این مشکلی است که Contact Picker API می تواند آن را حل کند. از آنجایی که من دوستم را در برنامه مخاطبین تلفنم ذخیره کرده ام، از طریق Contacts Picker 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، وقتی روی دکمه Contacts ضربه می زنم و دو دوست برتر خود را انتخاب می کنم، Sergey Mihaylovich Brin و劳伦斯·爱德华·"拉里"·佩奇، می توانید ببینید که چگونه انتخابگر مخاطبین فقط نام آنها را نشان می دهد، اما اطلاعات دیگر آنها مانند آدرس تلفن، شماره ایمیل آنها را نشان نمی دهد. سپس نام آنها بر روی کارت تبریک من کشیده می شود.

انتخابگر مخاطبین که نام دو مخاطب را در دفترچه آدرس نشان می دهد.
انتخاب دو نام با انتخابگر مخاطب از دفترچه آدرس.
نام دو مخاطبی که قبلاً انتخاب شده بود روی کارت تبریک کشیده شده است.
سپس این دو نام بر روی کارت تبریک کشیده می شود.

API کلیپ بورد ناهمزمان

مرحله بعدی کپی و چسباندن است. یکی از عملیات های مورد علاقه ما به عنوان توسعه دهندگان نرم افزار، کپی و چسباندن است. به عنوان نویسنده کارت تبریک، گاهی اوقات ممکن است بخواهم همین کار را انجام دهم. ممکن است بخواهم یک تصویر را در کارت تبریکی که روی آن کار می‌کنم بچسبانم، یا کارت تبریک خود را کپی کنم تا بتوانم آن را از جای دیگری ویرایش کنم. Async Clipboard API از متن و تصاویر پشتیبانی می کند. اجازه دهید نحوه اضافه کردن پشتیبانی کپی و جایگذاری را به برنامه Fugu Greetings توضیح دهم.

برای کپی کردن چیزی در کلیپ بورد سیستم، باید روی آن بنویسم. متد navigator.clipboard.write() آرایه ای از آیتم های کلیپ بورد را به عنوان پارامتر می گیرد. هر مورد کلیپ بورد اساساً یک شی است که یک حباب به عنوان مقدار و نوع لکه به عنوان کلید دارد.

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 که درخواست مجوز کلیپ بورد را نشان می دهد.
درخواست مجوز کلیپ بورد.

در نهایت، پس از پذیرش مجوز، تصویر در برنامه جایگذاری می شود. برعکس هم کار می کند. اجازه دهید یک کارت تبریک را در کلیپ بورد کپی کنم. وقتی پیش‌نمایش را باز می‌کنم و روی File و سپس New from Clipboard کلیک می‌کنم، کارت تبریک در یک تصویر بدون عنوان جدید قرار می‌گیرد.

برنامه پیش‌نمایش macOS با یک تصویر بدون عنوان و فقط چسبانده شده است.
تصویری که در برنامه macOS Preview چسبانده شده است.

Badging API

یکی دیگر از APIهای مفید Badging API است. به عنوان یک PWA قابل نصب، Fugu Greetings البته دارای یک نماد برنامه است که کاربران می توانند آن را روی داک برنامه یا صفحه اصلی قرار دهند. یک راه سرگرم کننده و آسان برای نشان دادن API این است که از آن در Fugu Greetings به عنوان شمارنده ضربه های قلم استفاده کنید. من یک شنونده رویداد اضافه کرده ام که شمارشگر ضربه های قلم را هر زمان که رویداد 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 با استفاده از هفت قلم.
نماد نشان در برنامه Fugu Greetings که عدد 7 را نشان می دهد.
شمارنده ضربه های قلم به شکل نشان نماد برنامه.

API Periodic Background Sync

آیا می خواهید هر روز خود را با چیزی جدید شروع کنید؟ یکی از ویژگی های نرم افزار Fugu Greetings این است که می تواند هر روز صبح با یک تصویر پس زمینه جدید برای شروع کارت تبریک خود الهام بخش شما باشد. این برنامه برای رسیدن به این هدف از API همگام‌سازی پس‌زمینه دوره‌ای استفاده می‌کند.

اولین قدم ثبت یک رویداد همگام سازی دوره ای در ثبت نام کارگر خدمات است. این تگ همگام سازی به نام 'image-of-the-day' را گوش می دهد و حداقل یک روز فاصله دارد، بنابراین کاربر می تواند هر 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);
  }
};

مرحله دوم گوش دادن به رویداد periodicsync در سرویس‌کار است. اگر تگ رویداد '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() پویا (که هنوز در زمینه 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، فشار دادن دکمه Wallpaper تصویر کارت تبریک روز را نشان می دهد که هر روز از طریق Periodic Background Sync API به روز می شود.

برنامه Fugu Greetings با تصویر کارت تبریک جدید روز.
با فشار دادن دکمه Wallpaper تصویر روز نمایش داده می شود.

Notification Triggers API

گاهی اوقات حتی با الهام زیاد، برای پایان دادن به کارت تبریک شروع شده به یک تلنگر نیاز دارید. این یک ویژگی است که توسط Notification Triggers API فعال شده است. به عنوان یک کاربر، می‌توانم زمانی را وارد کنم که می‌خواهم برای پایان دادن به کارت تبریک به من ضربه بزنند. وقتی آن زمان فرا رسید، یک اعلان دریافت می کنم که کارت تبریک من منتظر است.

پس از درخواست زمان مورد نظر، برنامه اعلان را با یک showTrigger برنامه ریزی می کند. این می تواند TimestampTrigger با تاریخ هدف انتخاب شده قبلی باشد. اعلان یادآوری به صورت محلی فعال می شود، هیچ شبکه یا سمت سرور مورد نیاز نیست.

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

مانند هر چیز دیگری که تا کنون نشان داده ام، این یک پیشرفت پیشرونده است، بنابراین کد فقط به صورت مشروط بارگذاری می شود.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

وقتی کادر انتخاب یادآوری را در Fugu Greetings علامت می‌زنم، یک پیام از من می‌پرسد که چه زمانی می‌خواهم به من یادآوری شود که کارت تبریک خود را تمام کنم.

برنامه Fugu Greetings با درخواستی که از کاربر می‌پرسد چه زمانی می‌خواهد به او یادآوری شود که کارت تبریک خود را تمام کند.
برنامه ریزی یک اعلان محلی برای یادآوری به پایان رساندن کارت تبریک.

هنگامی که یک اعلان برنامه ریزی شده در Fugu Greetings فعال می شود، درست مانند هر اعلان دیگری نشان داده می شود، اما همانطور که قبلاً نوشتم، نیازی به اتصال شبکه نداشت.

مرکز اعلان macOS یک اعلان فعال شده از Fugu Greetings را نشان می دهد.
اعلان فعال شده در مرکز اعلان macOS ظاهر می شود.

Wake Lock API

همچنین می‌خواهم Wake Lock API را هم اضافه کنم. گاهی اوقات لازم است به اندازه کافی به صفحه خیره شوید تا زمانی که الهام شما را ببوسد. بدترین اتفاقی که ممکن است بیفتد این است که صفحه نمایش خاموش شود. Wake Lock API می تواند از این اتفاق جلوگیری کند.

اولین قدم این است که با navigator.wakelock.request method() یک wake lock بدست آورید. من آن را روی رشته 'screen' می گذارم تا قفل بیدار شدن صفحه نمایش را به دست بیاورم. سپس یک شنونده رویداد اضافه می‌کنم تا از زمان آزاد شدن 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، یک چک باکس Insomnia وجود دارد که با علامت زدن، صفحه را بیدار نگه می دارد.

چک باکس بی خوابی، اگر علامت زده شود، صفحه را بیدار نگه می دارد.
چک باکس Insomnia برنامه را بیدار نگه می دارد.

Idle Detection API

گاهی اوقات، حتی اگر ساعت ها به صفحه نمایش خیره شوید، فقط بی فایده است و نمی توانید کوچکترین ایده ای داشته باشید که با کارت تبریک خود چه کار کنید. Idle Detection API به برنامه اجازه می دهد تا زمان بیکاری کاربر را تشخیص دهد. اگر کاربر برای مدت طولانی بیکار باشد، برنامه به حالت اولیه بازنشانی می شود و بوم را پاک می کند. این API در حال حاضر پشت مجوز اعلان‌ها قرار دارد، زیرا بسیاری از موارد استفاده تولیدی از شناسایی بی‌حرکت مربوط به اعلان‌ها هستند، برای مثال، فقط برای ارسال اعلان به دستگاهی که کاربر در حال حاضر فعالانه از آن استفاده می‌کند.

پس از اطمینان از اینکه مجوز اعلان‌ها اعطا شده است، ردیاب بیکار را نمونه‌سازی می‌کنم. من یک شنونده رویداد را ثبت می کنم که به تغییرات غیرفعال گوش می دهد که شامل کاربر و وضعیت صفحه می شود. کاربر می تواند فعال یا بیکار باشد و صفحه نمایش را می توان باز یا قفل کرد. اگر کاربر بیکار باشد، بوم پاک می شود. من به آشکارساز بیکار آستانه 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، زمانی که چک باکس Ephemeral علامت زده شود و کاربر برای مدت طولانی بیکار باشد، بوم پاک می شود.

برنامه Fugu Greetings با یک بوم پاک شده پس از مدت طولانی بیکار بودن کاربر.
هنگامی که چک باکس Ephemeral علامت زده می شود و کاربر برای مدت طولانی بیکار بوده است، بوم پاک می شود.

بسته شدن

فوو، چه سواری. API های بسیار زیادی فقط در یک برنامه نمونه. و، به یاد داشته باشید، من هرگز کاربر را مجبور نمی‌کنم هزینه دانلود را برای ویژگی‌ای که مرورگر او پشتیبانی نمی‌کند، بپردازد. با استفاده از بهبود پیشرونده، مطمئن می‌شوم که فقط کد مربوطه بارگذاری می‌شود. و از آنجایی که با HTTP/2، درخواست‌ها ارزان هستند، این الگو باید برای بسیاری از برنامه‌ها به خوبی کار کند، اگرچه ممکن است بخواهید یک باندلر برای برنامه‌های واقعاً بزرگ در نظر بگیرید.

پانل شبکه Chrome DevTools فقط درخواست‌هایی را برای فایل‌هایی با کدی که مرورگر فعلی پشتیبانی می‌کند نشان می‌دهد.
برگه Chrome DevTools Network فقط درخواست‌هایی را برای فایل‌هایی با کدی که مرورگر فعلی پشتیبانی می‌کند نشان می‌دهد.

این برنامه ممکن است در هر مرورگر کمی متفاوت به نظر برسد زیرا همه پلتفرم‌ها از همه ویژگی‌ها پشتیبانی نمی‌کنند، اما عملکرد اصلی همیشه وجود دارد - با توجه به قابلیت‌های مرورگر خاص به تدریج بهبود می‌یابد. توجه داشته باشید که بسته به اینکه برنامه به عنوان یک برنامه نصب شده یا در برگه مرورگر اجرا می شود، این قابلیت ها ممکن است حتی در یک مرورگر تغییر کند.

Fugu Greetings در Android Chrome اجرا می شود و بسیاری از ویژگی های موجود را نشان می دهد.
Fugu Greetings در حال اجرا در اندروید کروم.
Fugu Greetings در سافاری دسکتاپ اجرا می‌شود و ویژگی‌های موجود کمتری را نشان می‌دهد.
Fugu Greetings در حال اجرا در سافاری دسکتاپ.
Fugu Greetings در حال اجرا در کروم دسکتاپ است و بسیاری از ویژگی‌های موجود را نشان می‌دهد.
Fugu Greetings در حال اجرا در کروم دسکتاپ.

اگر به برنامه Fugu Greetings علاقه دارید، آن را در GitHub پیدا کنید و آن را تقسیم کنید .

مخزن Fugu Greetings در GitHub.
برنامه Fugu Greetings در GitHub.

تیم Chromium سخت در حال کار بر روی سبزتر کردن چمن در مورد API های پیشرفته Fugu است. با اعمال پیشرفت تدریجی در توسعه برنامه‌ام، مطمئن می‌شوم که همه یک تجربه پایه خوب و مستحکم دارند، اما افرادی که از مرورگرهایی استفاده می‌کنند که APIهای پلتفرم وب بیشتری را پشتیبانی می‌کنند، تجربه بهتری دارند. من مشتاقانه منتظر این هستم که ببینم با پیشرفت تدریجی برنامه های خود چه می کنید.

قدردانی ها

من از کریستین لیبل و هیمانث اچ ام که هر دو در Fugu Greetings همکاری داشته اند سپاسگزارم. این مقاله توسط Joe Medley و Kayce Basques بررسی شده است. جیک آرچیبالد به من کمک کرد تا وضعیت import() در زمینه کارمند سرویس پیدا کنم.

،

برای مرورگرهای مدرن ساخته شده و به تدریج مانند سال 2003 بهبود می یابد

در مارس 2003، نیک فینک و استیو چمپئون دنیای طراحی وب را با مفهوم پیشرفت پیشرونده مبهوت کردند، یک استراتژی برای طراحی وب که ابتدا بر بارگذاری محتوای اصلی صفحه وب تأکید می کند و سپس به تدریج لایه های ظریف و فنی دقیق تری از ارائه و ویژگی ها را در بالای محتوا اضافه می کند. در حالی که در سال 2003، پیشرفت تدریجی در مورد استفاده از ویژگی های CSS مدرن، جاوا اسکریپت محجوب و حتی گرافیک های برداری مقیاس پذیر بود. بهبود پیشرونده در سال 2020 و پس از آن در مورد استفاده از قابلیت های مرورگر مدرن است.

طراحی وب فراگیر برای آینده با پیشرفت تدریجی. اسلاید عنوان از ارائه اصلی Finck و Champeon.
اسلاید: طراحی وب جامع برای آینده با پیشرفت پیشرو. ( منبع )

جاوا اسکریپت مدرن

وقتی صحبت از جاوا اسکریپت شد، وضعیت پشتیبانی مرورگر برای آخرین ویژگی های جاوا اسکریپت هسته ES 2015 عالی است. استاندارد جدید شامل وعده‌ها، ماژول‌ها، کلاس‌ها، الفاظ قالب، توابع پیکان، let و const ، پارامترهای پیش‌فرض، ژنراتورها، تخصیص تخریب، استراحت و گسترش، Map / Set ، WeakMap / WeakSet و بسیاری موارد دیگر است. همه پشتیبانی می شوند .

جدول پشتیبانی CanIUse برای ویژگی های ES6 که پشتیبانی از همه مرورگرهای اصلی را نشان می دهد.
جدول پشتیبانی مرورگر ECMAScript 2015 (ES6). ( منبع )

توابع Async، یکی از ویژگی های ES 2017 و یکی از موارد دلخواه شخصی من، می تواند در همه مرورگرهای اصلی استفاده شود . کلمات کلیدی async و await این امکان را فراهم می کند که رفتار ناهمزمان و مبتنی بر قول به سبک تمیزتر نوشته شود و از نیاز به پیکربندی صریح زنجیره های وعده اجتناب شود.

جدول پشتیبانی CanIUse برای توابع ناهمگام که پشتیبانی را در تمام مرورگرهای اصلی نشان می دهد.
جدول پشتیبانی مرورگر توابع Async. ( منبع )

و حتی زبان‌های فوق‌العاده اخیر ES 2020 مانند زنجیره‌سازی اختیاری و ادغام بی‌حساب به سرعت به پشتیبانی رسیده‌اند. نمونه کد را می توانید در زیر مشاهده کنید. وقتی صحبت از ویژگی‌های اصلی جاوا اسکریپت می‌شود، چمن نمی‌تواند خیلی سبزتر از امروز باشد.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
تصویر پس زمینه چمن سبز نمادین ویندوز XP.
وقتی صحبت از ویژگی‌های اصلی جاوا اسکریپت می‌شود، چمن سبز است. (نمایش محصول مایکروسافت، با اجازه استفاده شده است.)

برنامه نمونه: Fugu Greetings

برای این مقاله، من با یک PWA ساده به نام Fugu Greetings ( GitHub ) کار می کنم. نام این برنامه نوک کلاهی است برای Project Fugu 🐡، تلاشی برای دادن تمام قدرت های برنامه های Android/iOS/desktop به وب. می توانید اطلاعات بیشتری در مورد این پروژه در صفحه فرود آن بخوانید.

Fugu Greetings یک برنامه نقاشی است که به شما امکان می دهد کارت تبریک مجازی ایجاد کنید و آنها را برای عزیزان خود ارسال کنید. این نمونه مفاهیم اصلی PWA است. قابل اعتماد است و کاملاً آفلاین فعال است، بنابراین حتی اگر شبکه ندارید، همچنان می توانید از آن استفاده کنید. همچنین قابل نصب بر روی صفحه اصلی دستگاه است و به عنوان یک برنامه مستقل به طور یکپارچه با سیستم عامل ادغام می شود.

Fugu Greetings PWA با طرحی که شبیه آرم انجمن PWA است.
برنامه نمونه Fugu Greetings .

افزایش پیشرونده

با خارج شدن از این راه، وقت آن است که در مورد بهبود پیشرونده صحبت کنیم. واژه نامه MDN Web Docs این مفهوم را به صورت زیر تعریف می کند :

ارتقای پیشرو یک فلسفه طراحی است که مبنایی از محتوا و عملکرد ضروری را برای هرچه بیشتر کاربران فراهم می‌کند، در حالی که بهترین تجربه ممکن را فقط برای کاربران مدرن‌ترین مرورگرها ارائه می‌کند که می‌توانند تمام کدهای مورد نیاز را اجرا کنند.

تشخیص ویژگی عموماً برای تعیین اینکه آیا مرورگرها می توانند عملکردهای مدرن تری را انجام دهند یا خیر استفاده می شود، در حالی که از polyfills اغلب برای افزودن ویژگی های گمشده با جاوا اسکریپت استفاده می شود.

[…]

بهبود پیشرونده یک تکنیک مفید است که به توسعه دهندگان وب اجازه می دهد تا روی توسعه بهترین وب سایت های ممکن تمرکز کنند در حالی که باعث می شود آن وب سایت ها روی چندین عامل کاربر ناشناس کار کنند. تنزل برازنده مرتبط است، اما یک چیز نیست و اغلب در جهت مخالف پیشرفت پیشرونده دیده می شود. در واقع، هر دو رویکرد معتبر هستند و اغلب می توانند مکمل یکدیگر باشند.

مشارکت کنندگان 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'),
    ]);
  }
};

اما قبل از اینکه به جزئیات API دسترسی به فایل سیستم بپردازم، اجازه دهید به سرعت الگوی بهبود پیشرونده را در اینجا برجسته کنم. در مرورگرهایی که در حال حاضر از File System Access API پشتیبانی نمی‌کنند، اسکریپت‌های قدیمی را بارگیری می‌کنم. در زیر می توانید تب های شبکه فایرفاکس و سافاری را مشاهده کنید.

Safari Web Inspector که فایل‌های قدیمی را در حال بارگذاری نشان می‌دهد.
تب شبکه Safari Web Inspector.
ابزارهای توسعه دهنده فایرفاکس که فایل های قدیمی در حال بارگذاری را نشان می دهد.
تب شبکه ابزارهای توسعه دهنده فایرفاکس.

با این حال، در کروم، مرورگری که از API پشتیبانی می کند، فقط اسکریپت های جدید بارگیری می شوند. این امر به لطف import() که همه مرورگرهای مدرن از آن پشتیبانی می کنند ، به زیبایی امکان پذیر شده است. همانطور که قبلاً گفتم، این روزها چمن بسیار سبز است.

Chrome DevTools که فایل‌های مدرن در حال بارگیری را نشان می‌دهد.
تب شبکه Chrome DevTools.

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() یک جریان قابل نوشتن ایجاد می کنم. بعد، حباب را که تصویر کارت تبریک من است، روی فایل می نویسم. در نهایت، جریان قابل نوشتن را می بندم.

همه چیز همیشه ممکن است خراب شود: فضای دیسک ممکن است خالی باشد، ممکن است خطای نوشتن یا خواندن وجود داشته باشد، یا شاید کاربر به سادگی گفتگوی فایل را لغو کند. به همین دلیل است که من همیشه تماس ها را در یک عبارت 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 اکنون با یک تصویر وارداتی.
تصویر وارداتی
برنامه سلام Fugu با تصویر اصلاح شده.
ذخیره تصویر اصلاح شده در یک فایل جدید.

API های هدف اشتراک وب و وب اشتراک گذاری وب

جدا از ذخیره برای ابدیت ، شاید من واقعاً می خواهم کارت تبریک خود را به اشتراک بگذارم. این کاری است که وب API به اشتراک گذاری وب و API هدف اشتراک وب به من اجازه می دهد انجام دهم. سیستم عامل های دسک تاپ موبایل و اخیراً مکانیسم های اشتراک گذاری داخلی را به دست آورده اند. به عنوان مثال ، در زیر برگه اشتراک گذاری دسک تاپ Safari در MacOS که از مقاله ای در وبلاگ من ساخته شده است. هنگامی که روی دکمه اشتراک مقاله کلیک می کنید ، می توانید به عنوان مثال از طریق برنامه پیام MACOS پیوندی به مقاله را با یک دوست به اشتراک بگذارید.

برگه اشتراک دسک تاپ Safari در MACOS از دکمه اشتراک مقاله ایجاد شده است
وب به اشتراک گذاری API در دسک تاپ سافاری در MACOS.

کدی که این اتفاق بیفتد بسیار ساده است. من با navigator.share() تماس می گیرم و به آن title ، text و url اختیاری را در یک شی منتقل می کنم. اما اگر بخواهم یک تصویر ضمیمه کنم چه می شود؟ سطح 1 API اشتراک وب هنوز از این پشتیبانی نمی کند. خبر خوب این است که اشتراک وب سطح 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 متشکل از یک حباب تهیه کنم ، و سپس یک title و یک text . در مرحله بعد ، به عنوان بهترین روش ، من از روش جدید navigator.canShare() استفاده می کنم که آنچه را که نام آن را نشان می دهد انجام می دهد: این به من می گوید اگر شیء data ای که من سعی می کنم به اشتراک بگذارم می تواند از لحاظ فنی توسط مرورگر به اشتراک گذاشته شود. اگر navigator.canShare() به من می گوید داده ها را می توان به اشتراک گذاشت ، من آماده هستم که مانند گذشته با navigator.share() تماس بگیرم. از آنجا که همه چیز می تواند شکست بخورد ، من دوباره از یک try...catch Block.

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 ، اگر روی دکمه اشتراک گذاری روی یک مرورگر پشتیبانی مانند Chrome on Android ضربه بزنم ، برگه اشتراک داخلی باز می شود. به عنوان مثال می توانم Gmail را انتخاب کنم و ویجت آهنگساز ایمیل با تصویر پیوست شده ظاهر می شود.

برگه به ​​اشتراک گذاری سطح سیستم عامل برنامه های مختلفی را برای به اشتراک گذاشتن تصویر نشان می دهد.
انتخاب یک برنامه برای به اشتراک گذاشتن پرونده در.
ایمیل Gmail ویجت را با تصویر پیوست ساخته شده است.
پرونده به یک ایمیل جدید در آهنگساز Gmail وصل می شود.

API Contact Picker

در مرحله بعد ، من می خواهم در مورد مخاطبین صحبت کنم ، به معنای کتاب آدرس دستگاه یا برنامه مدیر مخاطبین. وقتی کارت تبریک می نویسید ، ممکن است همیشه به راحتی نام کسی را بنویسید. به عنوان مثال ، من یک دوست سرگئی دارم که ترجیح می دهد نام خود را در حروف سیریلیک بیان کند. من از یک صفحه کلید qwertz آلمانی استفاده می کنم و هیچ ایده ای برای تایپ نام آنها ندارم. این مشکلی است که API Contact Picker می تواند آن را حل کند. از آنجا که من دوستم را در برنامه مخاطبین تلفن خود ذخیره کرده ام ، از طریق API Contacts Picker ، می توانم از طریق وب به مخاطبین خود ضربه بزنم.

ابتدا باید لیست خصوصیاتی را که می خواهم به آن دسترسی پیدا کنم مشخص کنم. در این حالت ، من فقط نام ها را می خواهم ، اما برای سایر موارد استفاده ممکن است به شماره تلفن ، ایمیل ، نمادهای آواتار یا آدرس های فیزیکی علاقه مند باشم. در مرحله بعد ، من یک شیء 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 ، هنگامی که من به دکمه مخاطبین ضربه می زنم و دو بهترین صفحه اصلی خود را انتخاب می کنم ، серGей михайлович و劳伦斯 · 爱德华 · "拉里" · 佩奇 佩奇، می بینید که چگونه انتخاب کننده مخاطب فقط به نام خود نشان می دهد ، اما نه آدرس های ایمیل آنها ، یا اطلاعات دیگری مانند شماره تلفن خود. نام آنها سپس به کارت تبریک من کشیده می شود.

مخاطب انتخاب کننده نام دو مخاطب را در کتاب آدرس نشان می دهد.
انتخاب دو نام با انتخاب کننده مخاطب از کتاب آدرس.
نام این دو مخاطب که قبلاً انتخاب شده بودند روی کارت تبریک کشیده شده اند.
این دو نام سپس به کارت تبریک کشیده می شوند.

API کلیپ بورد ناهمزمان

در مرحله بعدی کپی و چسباندن است. یکی از عملیات مورد علاقه ما به عنوان توسعه دهندگان نرم افزار کپی و چسباندن است. به عنوان یک نویسنده کارت تبریک ، در بعضی مواقع ، ممکن است بخواهم همین کار را انجام دهم. ممکن است بخواهم یک تصویر را در کارت تبریک که روی آن کار می کنم ، چسبانده ام ، یا کارت تبریک خود را کپی کنم تا بتوانم ویرایش آن را از جایی دیگر ادامه دهم. API Clipboard Async ، از متن و تصاویر پشتیبانی می کند. بگذارید شما را از طریق نحوه اضافه کردن کپی و چسباندن پشتیبانی به برنامه تبریک Fugu پیاده روی کنم.

برای کپی کردن چیزی روی کلیپ بورد سیستم ، باید برای آن بنویسم. روش navigator.clipboard.write() مجموعه ای از موارد کلیپ بورد را به عنوان یک پارامتر می گیرد. هر مورد کلیپ بورد در اصل یک شی با حباب به عنوان یک مقدار و نوع حباب به عنوان کلید است.

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 مختلفی است که انواع منابع موجود را به من می گوید. من با روش 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 باز کرده و آن را در کلیپ بورد کپی می کنم. هنگامی که من روی Paste کلیک کردم ، برنامه تبریک Fugu سپس از من می پرسد که آیا می خواهم به برنامه اجازه دهم متن و تصاویر موجود در کلیپ بورد را ببینم.

برنامه سلام Fugu که نشان دهنده سریع اجازه کلیپ بورد است.
سریع اجازه کلیپ بورد.

سرانجام ، پس از پذیرش مجوز ، تصویر سپس در برنامه چسبانده می شود. راه دیگر نیز کار می کند. بگذارید یک کارت تبریک را در کلیپ بورد کپی کنم. هنگامی که من پیش نمایش را باز می کنم و روی File و سپس New از Clipboard کلیک می کنم ، کارت تبریک به یک تصویر جدید بدون عنوان چسب می شود.

برنامه پیش نمایش MACOS با یک تصویر بدون عنوان و بدون عنوان.
تصویری که در برنامه پیش نمایش MACOS قرار گرفته است.

API نشان دادن

API مفید دیگر API نشان دادن است. به عنوان یک PWA قابل نصب ، تبریک Fugu البته دارای یک آیکون برنامه است که کاربران می توانند در حوض برنامه یا صفحه اصلی قرار دهند. یک روش سرگرم کننده و آسان برای نشان دادن API استفاده از آن در تبریک فوگو به عنوان پیشخوان قلم است. من یک شنونده رویداد را اضافه کرده ام که هر زمان که رویداد 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 ترسیم کنید.
نماد نشان در برنامه تبریک Fugu که شماره 7 را نشان می دهد.
ضربات قلم به شکل نشان نماد برنامه پیش می رود.

API همگام سازی پس زمینه دوره ای

آیا می خواهید هر روز را با چیز جدیدی تازه شروع کنید؟ یکی از ویژگی های شسته و رفته در برنامه تبریک Fugu این است که می تواند هر روز صبح با یک تصویر پس زمینه جدید از شما الهام بگیرد تا کارت تبریک خود را شروع کند. برنامه از API همگام سازی پس زمینه دوره ای برای دستیابی به این هدف استفاده می کند.

اولین قدم ثبت یک رویداد همگام سازی دوره ای در ثبت نام کارگر است. این برچسب همگام سازی به نام 'image-of-the-day' گوش می دهد و حداقل یک روز یک روز دارد ، بنابراین کاربر می تواند هر 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);
  }
};

مرحله دوم گوش دادن به رویداد periodicsync در کارگر سرویس است. اگر برچسب رویداد '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 توسط مرورگر بارگیری می شود. این امر هم در مورد کد مشتری و هم برای کد سرویس دهنده اعمال می شود. در مرورگرهای غیر پشتیبانی ، هیچ یک از آنها بارگیری نشده است. توجه داشته باشید که چگونه در کارگر سرویس ، به جای 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 ، با فشار دادن دکمه تصویر زمینه تصویر کارت تبریک روز را که هر روز از طریق API همگام سازی پس زمینه دوره ای به روز می شود ، نشان می دهد.

برنامه سلام Fugu با یک تصویر کارت تبریک جدید روز.
با فشار دادن دکمه کاغذ دیواری تصویر روز را نشان می دهد.

اعلان باعث API می شود

بعضی اوقات حتی با الهام زیاد ، برای به پایان رساندن کارت تبریک شروع به یک گنگ نیاز دارید. این ویژگی ای است که توسط API Notification باعث ایجاد 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 با یک سریع از کاربر که می خواهد به آنها یادآوری شود برای پایان دادن به کارت تبریک خود.
برنامه ریزی یک اطلاعیه محلی برای یادآوری کارت تبریک.

هنگامی که یک اعلان برنامه ریزی شده در تبریک های Fugu ایجاد می شود ، دقیقاً مانند هر اعلان دیگر نشان داده می شود ، اما همانطور که قبلاً نوشتم ، نیازی به اتصال شبکه ندارد.

مرکز اطلاع رسانی MACOS که یک اعلان محرک از تبریک Fugu را نشان می دهد.
اعلان تحریک شده در مرکز اعلان MACOS ظاهر می شود.

API قفل بیداری

من همچنین می خواهم API قفل Wake را درج کنم. بعضی اوقات فقط باید به اندازه کافی طولانی به صفحه خیره شوید تا اینکه الهام شما را ببوسد. بدترین چیزی که می تواند اتفاق بیفتد این است که صفحه نمایش خاموش شود. API قفل Wake می تواند از وقوع این امر جلوگیری کند.

اولین قدم بدست آوردن قفل بیدار با navigator.wakelock.request method() است. من آن را به 'screen' منتقل می کنم تا قفل بیداری صفحه را بدست آورم. سپس یک شنونده رویداد را اضافه می کنم تا هنگام انتشار قفل بیدار شود. این می تواند ، به عنوان مثال ، هنگامی که دید برگه تغییر می کند ، اتفاق بیفتد. اگر این اتفاق بیفتد ، من می توانم ، هنگامی که برگه دوباره قابل مشاهده است ، دوباره قفل بیداری را باز کنید.

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 ، یک کادر انتخاب بی خوابی وجود دارد که هنگام بررسی ، صفحه را بیدار نگه می دارد.

کادر انتخاب بی خوابی ، در صورت بررسی ، صفحه را بیدار نگه می دارد.
کادر انتخاب Insomnia برنامه را بیدار نگه می دارد.

API تشخیص بیکار

در بعضی مواقع ، حتی اگر ساعت ها به صفحه خیره شوید ، فقط بی فایده است و نمی توانید با کمترین ایده با کارت تبریک خود چه کاری انجام دهید. API تشخیص بیکار به برنامه اجازه می دهد تا زمان بیکار کاربر را تشخیص دهد. اگر کاربر بیش از حد بیکار باشد ، برنامه به حالت اولیه بازگردانده می شود و بوم را پاک می کند. این API در حال حاضر در پشت مجوز اعلان ها قرار دارد ، زیرا بسیاری از موارد استفاده از تولید از تشخیص بیکار مربوط به اعلان ها هستند ، به عنوان مثال ، فقط برای ارسال اعلان به دستگاهی که کاربر در حال حاضر به طور فعال از آن استفاده می کند ، ارسال می شود.

پس از اطمینان از اعطای مجوز اعلان ها ، من آشکارساز بیکار را فوری می کنم. من یک شنونده رویداد را ثبت می کنم که برای تغییرات بیکار گوش می دهد ، که شامل کاربر و وضعیت صفحه نمایش است. کاربر می تواند فعال یا بیکار باشد و صفحه را می توان قفل یا قفل کرد. اگر کاربر بیکار باشد ، بوم پاک می شود. من آستانه 60 ثانیه به آشکارساز بیکار می دهم.

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

و مثل همیشه ، من فقط وقتی مرورگر از آن پشتیبانی می کند ، این کد را بارگیری می کنم.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

در برنامه تبریک Fugu ، بوم هنگامی که کادر چک زودگذر بررسی می شود ، پاک می شود و کاربر برای مدت طولانی بیکار است.

برنامه سلام Fugu با بوم پاک شده پس از مدت طولانی کاربر بیکار است.
هنگامی که کادر انتخاب زودگذر بررسی می شود و کاربر بیش از حد بیکار بوده است ، بوم پاک می شود.

بسته شدن

پسر ، چه سواری. بسیاری از API ها فقط در یک برنامه نمونه. و به یاد داشته باشید ، من هرگز باعث نمی شود کاربر هزینه بارگیری را برای ویژگی ای که مرورگر آنها از آن پشتیبانی نمی کند ، پرداخت کند. با استفاده از پیشرفت مترقی ، من اطمینان می دهم که فقط کد مربوطه بارگیری می شود. و از آنجا که با HTTP/2 ، درخواست ها ارزان هستند ، این الگوی باید برای بسیاری از برنامه های کاربردی خوب کار کند ، اگرچه ممکن است بخواهید یک بسته را برای برنامه های واقعاً بزرگ در نظر بگیرید.

پانل شبکه Chrome DevTools فقط درخواست هایی را برای پرونده هایی با کد نشان می دهد که مرورگر فعلی از آن پشتیبانی می کند.
برگه شبکه Chrome DevTools فقط درخواست های پرونده هایی با کد را نشان می دهد که مرورگر فعلی از آن پشتیبانی می کند.

این برنامه ممکن است در هر مرورگر کمی متفاوت به نظر برسد زیرا همه سیستم عامل ها از همه ویژگی ها پشتیبانی نمی کنند ، اما عملکرد اصلی همیشه در آنجا وجود دارد - با توجه به قابلیت های مرورگر خاص ، پیشرفته است. توجه داشته باشید که بسته به اینکه برنامه به عنوان یک برنامه نصب شده یا در برگه مرورگر در حال اجرا باشد ، ممکن است این قابلیت ها حتی در یک و یک مرورگر یکسان تغییر کند.

سلام Fugu در حال اجرا بر روی Android Chrome است و بسیاری از ویژگی های موجود را نشان می دهد.
سلام Fugu در حال اجرا در Android Chrome.
تبریک Fugu که روی دسک تاپ سافاری اجرا می شود ، و ویژگی های کمتری را نشان می دهد.
سلام Fugu که روی دسک تاپ سافاری اجرا می شود.
سلام Fugu که روی دسک تاپ کروم اجرا می شود و بسیاری از ویژگی های موجود را نشان می دهد.
سلام Fugu در حال اجرا بر روی دسک تاپ Chrome.

اگر به برنامه تبریک Fugu علاقه دارید ، آن را در GitHub پیدا کرده و چنگ بزنید .

repo سلام Fugu در GitHub.
برنامه سلام Fugu در GitHub.

تیم Chromium هنگام صحبت از API های Advanced Fugu ، روی سبزتر شدن چمن سبزتر تلاش می کند. با استفاده از پیشرفت مترقی در توسعه برنامه من ، اطمینان می دهم که همه یک تجربه پایه خوب و محکم دارند ، اما افرادی که از مرورگرهایی استفاده می کنند که از API های پلت فرم وب بیشتری پشتیبانی می کنند ، تجربه بهتری دارند. من مشتاقانه منتظر هستم که ببینم شما با پیشرفت مترقی در برنامه های خود چه کاری انجام می دهید.

قدردانی ها

من از کریستین لیبل و Hemanth HM سپاسگزارم که هر دو در تبریک فوگو نقش داشته اند. این مقاله توسط جو مدلی و کیس باسک بررسی شده است. جیک Archibald به من کمک کرد تا وضعیت import() در یک زمینه کارگر خدمات پیدا کنم.

،

ساختمان برای مرورگرهای مدرن و به تدریج مانند 2003

در مارس 2003 ، نیک فینک و استیو چمپون با مفهوم پیشرفت مترقی ، دنیای طراحی وب را متحیر کردند ، یک استراتژی برای طراحی وب که ابتدا بر بارگیری محتوای صفحه اصلی وب تأکید می کند ، و سپس به تدریج لایه های ظریف تر و فنی دقیق تر از ارائه و ویژگی ها را در بالای مطالب اضافه می کند. در حالی که در سال 2003 ، پیشرفت مترقی در مورد استفاده از ویژگی های CSS Modern ، JavaScript بی نظیر و حتی گرافیک های بردار مقیاس پذیر بود. پیشرفت مترقی در سال 2020 و بعد از آن در مورد استفاده از قابلیت های مرورگر مدرن است.

طراحی وب فراگیر برای آینده با پیشرفت مترقی. عنوان از ارائه اصلی Finck و Champeon.
اسلاید: طراحی وب فراگیر برای آینده با پیشرفت مترقی. ( منبع )

جاوا اسکریپت مدرن

صحبت از JavaScript ، وضعیت پشتیبانی مرورگر برای جدیدترین ویژگی های JavaScript Core ES 2015 بسیار عالی است. استاندارد جدید شامل وعده ها ، ماژول ها ، کلاس ها ، الگوی الگوی ، توابع فلش ، let و const ، پارامترهای پیش فرض ، ژنراتورها ، تکالیف تخریب ، استراحت و گسترش ، Map / Set ، WeakMap / WeakSet و موارد دیگر است. همه پشتیبانی می شوند

جدول پشتیبانی Caniuse برای ویژگی های ES6 که نشان دهنده پشتیبانی در تمام مرورگرهای اصلی است.
جدول پشتیبانی مرورگر ECMAScript 2015 (ES6). ( منبع )

توابع Async ، یک ویژگی ES 2017 و یکی از موارد دلخواه شخصی من ، می تواند در کلیه مرورگرهای اصلی استفاده شود . کلمات کلیدی async و await باعث می شود که رفتار ناهمزمان و مبتنی بر قول به سبک پاک تر نوشته شود و از نیاز به پیکربندی صریح زنجیره های وعده جلوگیری شود.

جدول پشتیبانی Caniuse برای توابع ASYNC که پشتیبانی از همه مرورگرهای اصلی را نشان می دهد.
جدول پشتیبانی مرورگر ASYNC. ( منبع )

و حتی موارد فوق العاده اخیر ES 2020 زبان مانند زنجیره ای اختیاری و همبستگی تهی بسیار سریع به پشتیبانی رسیده است. می توانید یک نمونه کد را در زیر مشاهده کنید. وقتی صحبت از ویژگی های اصلی جاوا اسکریپت می شود ، چمن نمی تواند خیلی سبزتر از امروز باشد.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
تصویر پس زمینه Green Windows XP Windows.
چمن وقتی به ویژگی های اصلی جاوا اسکریپت می رسد سبز است. (تصویر محصول مایکروسافت ، با اجازه استفاده می شود.)

برنامه نمونه: سلام Fugu

برای این مقاله ، من با یک PWA ساده ، به نام Fugu Wometings ( Github ) کار می کنم. نام این برنامه نوک کلاه برای Project Fugu است ، تلاشی برای ارائه همه قدرت برنامه های Android/iOS/Desktop. می توانید اطلاعات بیشتر در مورد این پروژه را در صفحه فرود آن بخوانید.

سلام Fugu یک برنامه طراحی است که به شما امکان می دهد کارت تبریک مجازی ایجاد کنید و آنها را برای عزیزان خود بفرستید. این مثال مفاهیم اصلی PWA را نشان می دهد. این قابل اطمینان و کاملاً آفلاین است ، بنابراین حتی اگر شبکه ندارید ، هنوز هم می توانید از آن استفاده کنید. همچنین در صفحه اصلی دستگاه قابل نصب است و یکپارچه با سیستم عامل به عنوان یک برنامه مستقل ادغام می شود.

سلام Fugu PWA را با نقاشی که شبیه آرم جامعه PWA است ، سلام می کند.
برنامه نمونه سلام Fugu .

افزایش پیشرونده

با این کار ، وقت آن است که در مورد پیشرفت مترقی صحبت کنیم. واژه نامه MDN Web Docs این مفهوم را به شرح زیر تعریف می کند :

پیشرفت مترقی یک فلسفه طراحی است که پایه ای از محتوای و کارکردهای اساسی را برای هرچه بیشتر کاربران فراهم می کند ، در حالی که بهترین تجربه ممکن را فقط به کاربران مدرن ترین مرورگرها ارائه می دهد که می توانند تمام کد مورد نیاز را اجرا کنند.

تشخیص ویژگی به طور کلی برای تعیین اینکه آیا مرورگرها می توانند عملکردهای مدرن تری داشته باشند ، استفاده می شود ، در حالی که اغلب از پلی فیل ها برای اضافه کردن ویژگی های مفقود شده با JavaScript استفاده می شود.

[…]

پیشرفت پیشرو یک تکنیک مفید است که به توسعه دهندگان وب اجازه می دهد تا ضمن اینکه این وب سایت ها روی چندین عامل کاربر ناشناخته کار می کنند ، بر توسعه بهترین وب سایت های ممکن تمرکز کنند. تخریب برازنده مرتبط است ، اما یک چیز مشابه نیست و اغلب به عنوان در جهت مخالف به پیشرفت مترقی دیده می شود. در واقعیت ، هر دو رویکرد معتبر هستند و اغلب می توانند یکدیگر را تکمیل کنند.

همکاران 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();
};

اما یک دقیقه صبر کنید. از نظر ذهنی ، شما کارت تبریک را "بارگیری نکرده اید" ، آن را ذخیره کرده اید. مرورگر به جای اینکه به شما یک گفتگوی "ذخیره" نشان دهد که به شما امکان می دهد پرونده را در کجا قرار دهید ، کارت تبریک را بدون تعامل کاربر بارگیری کرده و مستقیماً در پوشه بارگیری خود قرار داده است. این عالی نیست

اگر راه بهتری وجود داشته باشد چه می شود؟ چه می شود اگر فقط بتوانید یک فایل محلی را باز کنید ، آن را ویرایش کنید ، و سپس اصلاحات را در یک پرونده جدید ذخیره کنید ، یا به پرونده اصلی که در ابتدا باز کرده اید بازگردید؟ معلوم است وجود دارد. API دسترسی به سیستم فایل به شما امکان می دهد پرونده ها و دایرکتوری ها را باز و ایجاد کنید و همچنین آنها را اصلاح و ذخیره کنید.

بنابراین چگونه می توانم یک API را مشخص کنم؟ 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'),
    ]);
  }
};

اما قبل از اینکه وارد جزئیات API API سیستم دسترسی به سیستم فایل شوم ، اجازه دهید به سرعت الگوی پیشرفت مترقی را در اینجا برجسته کنم. در مرورگرهایی که در حال حاضر از API دسترسی به سیستم فایل پشتیبانی نمی کنند ، اسکریپت های میراث را بارگیری می کنم. می توانید زبانه های شبکه Firefox و Safari را در زیر مشاهده کنید.

بازرس وب سافاری که فایلهای میراث را بارگیری می کند ، نشان می دهد.
برگه شبکه بازرس وب سافاری.
ابزارهای توسعه دهنده Firefox نشان می دهد که پرونده های میراث بارگیری می شوند.
برگه Network Firefox Developer Tools.

با این حال ، در Chrome ، مرورگر که از API پشتیبانی می کند ، فقط اسکریپت های جدید بارگیری می شوند. این امر به لطف import() ، که همه مرورگرهای مدرن از آن پشتیبانی می کنند ، به طرز ظریف امکان پذیر است. همانطور که قبلاً گفتم ، چمن این روزها بسیار سبز است.

Chrome DevTools که نشان می دهد پرونده های مدرن بارگیری می شوند.
برگه شبکه Chrome DevTools.

API دسترسی به سیستم فایل

بنابراین اکنون که من به این موضوع پرداخته ام ، وقت آن رسیده است که بر اساس 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);
  }
};

با استفاده از پیشرفت پیشرونده با API دسترسی به سیستم فایل ، می توانم مانند گذشته پرونده ای را باز کنم. پرونده وارداتی درست روی بوم کشیده شده است. من می توانم ویرایش های خود را تهیه کنم و در نهایت آنها را با یک کادر گفتگوی ذخیره واقعی ذخیره کنم که در آن می توانم نام و محل ذخیره فایل را انتخاب کنم. اکنون پرونده آماده است تا برای ابدیت حفظ شود.

برنامه سلام Fugu با یک گفتگوی باز پرونده.
گفتگوی باز پرونده.
برنامه سلام Fugu اکنون با یک تصویر وارداتی.
تصویر وارداتی
برنامه سلام Fugu با تصویر اصلاح شده.
ذخیره تصویر اصلاح شده در یک فایل جدید.

API های هدف اشتراک وب و وب اشتراک گذاری وب

جدا از ذخیره برای ابدیت ، شاید من واقعاً می خواهم کارت تبریک خود را به اشتراک بگذارم. این کاری است که وب API به اشتراک گذاری وب و API هدف اشتراک وب به من اجازه می دهد انجام دهم. سیستم عامل های دسک تاپ موبایل و اخیراً مکانیسم های اشتراک گذاری داخلی را به دست آورده اند. به عنوان مثال ، در زیر برگه اشتراک گذاری دسک تاپ Safari در MacOS که از مقاله ای در وبلاگ من ساخته شده است. هنگامی که روی دکمه اشتراک مقاله کلیک می کنید ، می توانید به عنوان مثال از طریق برنامه پیام MACOS پیوندی به مقاله را با یک دوست به اشتراک بگذارید.

برگه اشتراک دسک تاپ Safari در MACOS از دکمه اشتراک مقاله ایجاد شده است
وب به اشتراک گذاری API در دسک تاپ سافاری در MACOS.

کدی که این اتفاق بیفتد بسیار ساده است. من با navigator.share() تماس می گیرم و به آن title ، text و url اختیاری را در یک شی منتقل می کنم. اما اگر بخواهم یک تصویر ضمیمه کنم چه می شود؟ سطح 1 API اشتراک وب هنوز از این پشتیبانی نمی کند. خبر خوب این است که اشتراک وب سطح 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 متشکل از یک حباب تهیه کنم ، و سپس یک title و یک text . در مرحله بعد ، به عنوان بهترین روش ، من از روش جدید navigator.canShare() استفاده می کنم که آنچه را که نام آن را نشان می دهد انجام می دهد: این به من می گوید اگر شیء data ای که من سعی می کنم به اشتراک بگذارم می تواند از لحاظ فنی توسط مرورگر به اشتراک گذاشته شود. اگر navigator.canShare() به من می گوید داده ها را می توان به اشتراک گذاشت ، من آماده هستم که مانند گذشته با navigator.share() تماس بگیرم. از آنجا که همه چیز می تواند شکست بخورد ، من دوباره از یک try...catch Block.

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 ، اگر روی دکمه اشتراک گذاری روی یک مرورگر پشتیبانی مانند Chrome on Android ضربه بزنم ، برگه اشتراک داخلی باز می شود. به عنوان مثال می توانم Gmail را انتخاب کنم و ویجت آهنگساز ایمیل با تصویر پیوست شده ظاهر می شود.

برگه به ​​اشتراک گذاری سطح سیستم عامل برنامه های مختلفی را برای به اشتراک گذاشتن تصویر نشان می دهد.
انتخاب یک برنامه برای به اشتراک گذاشتن پرونده در.
ایمیل Gmail ویجت را با تصویر پیوست ساخته شده است.
پرونده به یک ایمیل جدید در آهنگساز Gmail وصل می شود.

API Contact Picker

در مرحله بعد ، من می خواهم در مورد مخاطبین صحبت کنم ، به معنای کتاب آدرس دستگاه یا برنامه مدیر مخاطبین. وقتی کارت تبریک می نویسید ، ممکن است همیشه به راحتی نام کسی را بنویسید. به عنوان مثال ، من یک دوست سرگئی دارم که ترجیح می دهد نام خود را در حروف سیریلیک بیان کند. من از یک صفحه کلید qwertz آلمانی استفاده می کنم و هیچ ایده ای برای تایپ نام آنها ندارم. این مشکلی است که API Contact Picker می تواند آن را حل کند. از آنجا که من دوستم را در برنامه مخاطبین تلفن خود ذخیره کرده ام ، از طریق API Contacts Picker ، می توانم از طریق وب به مخاطبین خود ضربه بزنم.

ابتدا باید لیست خصوصیاتی را که می خواهم به آن دسترسی پیدا کنم مشخص کنم. در این حالت ، من فقط نام ها را می خواهم ، اما برای سایر موارد استفاده ممکن است به شماره تلفن ، ایمیل ، نمادهای آواتار یا آدرس های فیزیکی علاقه مند باشم. در مرحله بعد ، من یک شیء 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 ، هنگامی که من به دکمه مخاطبین ضربه می زنم و دو بهترین صفحه اصلی خود را انتخاب می کنم ، серGей михайлович و劳伦斯 · 爱德华 · "拉里" · 佩奇 佩奇، می بینید که چگونه انتخاب کننده مخاطب فقط به نام خود نشان می دهد ، اما نه آدرس های ایمیل آنها ، یا اطلاعات دیگری مانند شماره تلفن خود. نام آنها سپس به کارت تبریک من کشیده می شود.

مخاطب انتخاب کننده نام دو مخاطب را در کتاب آدرس نشان می دهد.
انتخاب دو نام با انتخاب کننده مخاطب از کتاب آدرس.
نام این دو مخاطب که قبلاً انتخاب شده بودند روی کارت تبریک کشیده شده اند.
این دو نام سپس به کارت تبریک کشیده می شوند.

API کلیپ بورد ناهمزمان

در مرحله بعدی کپی و چسباندن است. یکی از عملیات مورد علاقه ما به عنوان توسعه دهندگان نرم افزار کپی و چسباندن است. به عنوان یک نویسنده کارت تبریک ، در بعضی مواقع ، ممکن است بخواهم همین کار را انجام دهم. ممکن است بخواهم یک تصویر را در کارت تبریک که روی آن کار می کنم ، چسبانده ام ، یا کارت تبریک خود را کپی کنم تا بتوانم ویرایش آن را از جایی دیگر ادامه دهم. API Clipboard Async ، از متن و تصاویر پشتیبانی می کند. بگذارید شما را از طریق نحوه اضافه کردن کپی و چسباندن پشتیبانی به برنامه تبریک Fugu پیاده روی کنم.

برای کپی کردن چیزی روی کلیپ بورد سیستم ، باید برای آن بنویسم. روش navigator.clipboard.write() مجموعه ای از موارد کلیپ بورد را به عنوان یک پارامتر می گیرد. هر مورد کلیپ بورد در اصل یک شی با حباب به عنوان یک مقدار و نوع حباب به عنوان کلید است.

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 مختلفی است که انواع منابع موجود را به من می گوید. من با روش 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 باز کرده و آن را در کلیپ بورد کپی می کنم. هنگامی که من روی Paste کلیک کردم ، برنامه تبریک Fugu سپس از من می پرسد که آیا می خواهم به برنامه اجازه دهم متن و تصاویر موجود در کلیپ بورد را ببینم.

برنامه سلام Fugu که نشان دهنده سریع اجازه کلیپ بورد است.
سریع اجازه کلیپ بورد.

سرانجام ، پس از پذیرش مجوز ، تصویر سپس در برنامه چسبانده می شود. راه دیگر نیز کار می کند. بگذارید یک کارت تبریک را در کلیپ بورد کپی کنم. هنگامی که من پیش نمایش را باز می کنم و روی File و سپس New از Clipboard کلیک می کنم ، کارت تبریک به یک تصویر جدید بدون عنوان چسب می شود.

برنامه پیش نمایش MACOS با یک تصویر بدون عنوان و بدون عنوان.
تصویری که در برنامه پیش نمایش MACOS قرار گرفته است.

API نشان دادن

API مفید دیگر API نشان دادن است. به عنوان یک PWA قابل نصب ، تبریک Fugu البته دارای یک آیکون برنامه است که کاربران می توانند در حوض برنامه یا صفحه اصلی قرار دهند. یک روش سرگرم کننده و آسان برای نشان دادن API استفاده از آن در تبریک فوگو به عنوان پیشخوان قلم است. من یک شنونده رویداد را اضافه کرده ام که هر زمان که رویداد 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 ترسیم کنید.
نماد نشان در برنامه تبریک Fugu که شماره 7 را نشان می دهد.
ضربات قلم به شکل نشان نماد برنامه پیش می رود.

API همگام سازی پس زمینه دوره ای

آیا می خواهید هر روز را با چیز جدیدی تازه شروع کنید؟ یکی از ویژگی های شسته و رفته در برنامه تبریک Fugu این است که می تواند هر روز صبح با یک تصویر پس زمینه جدید از شما الهام بگیرد تا کارت تبریک خود را شروع کند. برنامه از API همگام سازی پس زمینه دوره ای برای دستیابی به این هدف استفاده می کند.

اولین قدم ثبت یک رویداد همگام سازی دوره ای در ثبت نام کارگر است. این برچسب همگام سازی به نام 'image-of-the-day' گوش می دهد و حداقل یک روز یک روز دارد ، بنابراین کاربر می تواند هر 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);
  }
};

مرحله دوم گوش دادن به رویداد periodicsync در کارگر سرویس است. اگر برچسب رویداد '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 توسط مرورگر بارگیری می شود. این امر هم در مورد کد مشتری و هم برای کد سرویس دهنده اعمال می شود. در مرورگرهای غیر پشتیبانی ، هیچ یک از آنها بارگیری نشده است. توجه داشته باشید که چگونه در کارگر سرویس ، به جای 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');
}

In Fugu Greetings, pressing the Wallpaper button reveals the greeting card image of the day that is updated every day via the Periodic Background Sync API.

Fugu Greetings app with a new greeting card image of the day.
Pressing the Wallpaper button displays the image of the day.

Notification Triggers API

Sometimes even with a lot of inspiration, you need a nudge to finish a started greeting card. This is a feature that is enabled by the Notification Triggers API . As a user, I can enter a time when I want to be nudged to finish my greeting card. When that time comes, I will get a notification that my greeting card is waiting.

After prompting for the target time, the application schedules the notification with a showTrigger . This can be a TimestampTrigger with the previously selected target date. The reminder notification will be triggered locally, no network or server side is needed.

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),
  });
}

As with everything else I have shown so far, this is a progressive enhancement, so the code is only conditionally loaded.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

When I check the Reminder checkbox in Fugu Greetings, a prompt asks me when I want to be reminded to finish my greeting card.

Fugu Greetings app with a prompt asking the user when they want to be reminded to finish their greeting card.
Scheduling a local notification to be reminded to finish a greeting card.

When a scheduled notification triggers in Fugu Greetings, it is shown just like any other notification, but as I wrote before, it didn't require a network connection.

macOS Notification Center showing a triggered notification from Fugu Greetings.
The triggered notification appears in the macOS Notification Center.

The Wake Lock API

I also want to include the Wake Lock API . Sometimes you just need to stare long enough at the screen until inspiration kisses you. The worst that can happen then is for the screen to turn off. The Wake Lock API can prevent this from happening.

The first step is to obtain a wake lock with the navigator.wakelock.request method() . I pass it the string 'screen' to obtain a screen wake lock. I then add an event listener to be informed when the wake lock is released. This can happen, for example, when the tab visibility changes. If this happens, I can, when the tab becomes visible again, re-obtain the 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);

Yes, this is a progressive enhancement, so I only need to load it when the browser supports the API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

In Fugu Greetings, there's an Insomnia checkbox that, when checked, keeps the screen awake.

The insomnia checkbox, if checked, keeps the screen awake.
The Insomnia checkbox keeps app awake.

The Idle Detection API

At times, even if you stare at the screen for hours, it's just useless and you can't come up with the slightest idea what to do with your greeting card. The Idle Detection API allows the app to detect user idle time. If the user is idle for too long, the app resets to the initial state and clears the canvas. This API is currently gated behind the notifications permission , since a lot of production use cases of idle detection are notifications-related, for example, to only send a notification to a device the user is currently actively using.

After making sure that the notifications permission is granted, I then instantiate the idle detector. I register an event listener that listens for idle changes, which includes the user and the screen state. The user can be active or idle, and the screen can be unlocked or locked. If the user is idle, the canvas clears. I give the idle detector a threshold of 60 seconds.

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,
});

And as always, I only load this code when the browser supports it.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

In the Fugu Greetings app, the canvas clears when the Ephemeral checkbox is checked and the user is idle for for too long.

Fugu Greetings app with a cleared canvas after the user has been idle for too long.
When the Ephemeral checkbox is checked and the user has been idle for too long, the canvas is cleared.

بسته شدن

Phew, what a ride. So many APIs in just one sample app. And, remember, I never make the user pay the download cost for a feature that their browser doesn't support. By using progressive enhancement, I make sure only the relevant code gets loaded. And since with HTTP/2, requests are cheap, this pattern should work well for a lot of applications, although you might want to consider a bundler for really large apps.

Chrome DevTools Network panel showing only requests for files with code that the current browser supports.
Chrome DevTools Network tab showing only requests for files with code that the current browser supports.

The app may look a little different on each browser since not all platforms support all features, but the core functionality is always there—progressively enhanced according to the particular browser's capabilities. Note that these capabilities may change even in one and the same browser, depending on whether the app is running as an installed app or in a browser tab.

Fugu Greetings running on Android Chrome, showing many available features.
Fugu Greetings running on Android Chrome.
Fugu Greetings running on desktop Safari, showing fewer available features.
Fugu Greetings running on desktop Safari.
Fugu Greetings running on desktop Chrome, showing many available features.
Fugu Greetings running on desktop Chrome.

If you're interested in the Fugu Greetings app, go find and fork it on GitHub .

Fugu Greetings repo on GitHub.
Fugu Greetings app on GitHub.

The Chromium team is working hard on making the grass greener when it comes to advanced Fugu APIs. By applying progressive enhancement in the development of my app, I make sure that everybody gets a good, solid baseline experience, but that people using browsers that support more Web platform APIs get an even better experience. I'm looking forward to seeing what you do with progressive enhancement in your apps.

قدردانی ها

I'm grateful to Christian Liebel and Hemanth HM who both have contributed to Fugu Greetings. This article was reviewed by Joe Medley and Kayce Basques . Jake Archibald helped me find out the situation with dynamic import() in a service worker context.

،

Building for modern browsers and progressively enhancing like it's 2003

Back in March 2003, Nick Finck and Steve Champeon stunned the web design world with the concept of progressive enhancement , a strategy for web design that emphasizes loading core web page content first, and that then progressively adds more nuanced and technically rigorous layers of presentation and features on top of the content. While in 2003, progressive enhancement was about using—at the time—modern CSS features, unobtrusive JavaScript, and even just Scalable Vector Graphics. Progressive enhancement in 2020 and beyond is about using modern browser capabilities .

Inclusive web design for the future with progressive enhancement. Title slide from Finck and Champeon's original presentation.
Slide: Inclusive Web Design for the Future With Progressive Enhancement. ( منبع )

جاوا اسکریپت مدرن

Speaking of JavaScript, the browser support situation for the latest core ES 2015 JavaScript features is great. The new standard includes promises, modules, classes, template literals, arrow functions, let and const , default parameters, generators, the destructuring assignment, rest and spread, Map / Set , WeakMap / WeakSet , and many more. All are supported .

The CanIUse support table for ES6 features showing support across all major browsers.
The ECMAScript 2015 (ES6) browser support table. ( منبع )

Async functions, an ES 2017 feature and one of my personal favorites, can be used in all major browsers. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

The CanIUse support table for async functions showing support across all major browsers.
The Async functions browser support table. ( منبع )

And even super recent ES 2020 language additions like optional chaining and nullish coalescing have reached support really quickly. You can see a code sample below. When it comes to core JavaScript features, the grass couldn't be much greener than it is today.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
The iconic Windows XP green grass background image.
The grass is green when it comes to core JavaScript features. (Microsoft product screenshot, used with permission .)

The sample app: Fugu Greetings

For this article, I work with a simple PWA, called Fugu Greetings ( GitHub ). The name of this app is a tip of the hat to Project Fugu 🐡, an effort to give the web all the powers of Android/iOS/desktop applications. You can read more about the project on its landing page .

Fugu Greetings is a drawing app that lets you create virtual greeting cards, and send them to your loved ones. It exemplifies PWA's core concepts . It's reliable and fully offline enabled, so even if you don't have a network, you can still use it. It's also Installable to a device's home screen and integrates seamlessly with the operating system as a stand-alone application.

Fugu Greetings PWA with a drawing that resembles the PWA community logo.
The Fugu Greetings sample app.

افزایش پیشرونده

With this out of the way, it's time to talk about progressive enhancement . The MDN Web Docs Glossary defines the concept as follows:

Progressive enhancement is a design philosophy that provides a baseline of essential content and functionality to as many users as possible, while delivering the best possible experience only to users of the most modern browsers that can run all the required code.

Feature detection is generally used to determine whether browsers can handle more modern functionality, while polyfills are often used to add missing features with JavaScript.

[…]

Progressive enhancement is a useful technique that allows web developers to focus on developing the best possible websites while making those websites work on multiple unknown user agents. Graceful degradation is related, but is not the same thing and is often seen as going in the opposite direction to progressive enhancement. In reality, both approaches are valid and can often complement one another.

MDN contributors

Starting each greeting card from scratch can be really cumbersome. So why not have a feature that allows users to import an image, and start from there? With a traditional approach, you'd have used an <input type=file> element to make this happen. First, you'd create the element, set its type to 'file' and add MIME types to the accept property, and then programmatically "click" it and listen for changes. When you select an image, it is imported straight onto the canvas.

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();
  });
};

When there's an import feature, there probably should be an export feature so users can save their greeting cards locally. The traditional way of saving files is to create an anchor link with a download attribute and with a blob URL as its href . You'd also programmatically "click" it to trigger the download, and, to prevent memory leaks, hopefully not forget to revoke the blob object URL.

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();
};

اما یک دقیقه صبر کنید. Mentally, you haven't "downloaded" a greeting card, you have "saved" it. Rather than showing you a "save" dialog that lets you choose where to put the file, the browser has directly downloaded the greeting card without user interaction and has put it straight into your Downloads folder. This isn't great.

What if there were a better way? What if you could just open a local file, edit it, and then save the modifications, either to a new file, or back to the original file that you had initially opened? معلوم است وجود دارد. The File System Access API allows you to open and create files and directories, as well as modify and save them .

So how do I feature-detect an API? The File System Access API exposes a new method window.chooseFileSystemEntries() . Consequently, I need to conditionally load different import and export modules depending on whether this method is available. I've shown how to do this below.

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'),
    ]);
  }
};

But before I dive into the File System Access API details, let me just quickly highlight the progressive enhancement pattern here. On browsers that currently don't support the File System Access API, I load the legacy scripts. You can see the network tabs of Firefox and Safari below.

Safari Web Inspector showing the legacy files getting loaded.
Safari Web Inspector network tab.
Firefox Developer Tools showing the legacy files getting loaded.
Firefox Developer Tools network tab.

However, on Chrome, a browser that supports the API, only the new scripts are loaded. This is made elegantly possible thanks to dynamic import() , which all modern browsers support . As I said earlier, the grass is pretty green these days.

Chrome DevTools showing the modern files getting loaded.
Chrome DevTools network tab.

The File System Access API

So now that I have addressed this, it's time to look at the actual implementation based on the File System Access API. For importing an image, I call window.chooseFileSystemEntries() and pass it an accepts property where I say I want image files. Both file extensions as well as MIME types are supported. This results in a file handle, from which I can get the actual file by calling 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);
  }
};

Exporting an image is almost the same, but this time I need to pass a type parameter of 'save-file' to the chooseFileSystemEntries() method. From this I get a file save dialog. With file open, this wasn't necessary since 'open-file' is the default. I set the accepts parameter similarly to before, but this time limited to just PNG images. Again I get back a file handle, but rather than getting the file, this time I create a writable stream by calling createWritable() . Next, I write the blob, which is my greeting card image, to the file. Finally, I close the writable stream.

Everything can always fail: The disk could be out of space, there could be a write or read error, or maybe simply the user cancels the file dialog. This is why I always wrap the calls in a try...catch statement.

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);
  }
};

Using progressive enhancement with the File System Access API, I can open a file as before. The imported file is drawn right onto the canvas. I can make my edits and finally save them with a real save dialog box where I can choose the name and storage location of the file. Now the file is ready to be preserved for eternity.

Fugu Greetings app with a file open dialog.
The file open dialog.
Fugu Greetings app now with an imported image.
The imported image.
Fugu Greetings app with the modified image.
Saving the modified image to a new file.

The Web Share and Web Share Target APIs

Apart from storing for eternity, maybe I actually want to share my greeting card. This is something that the Web Share API and Web Share Target API allow me to do. Mobile, and more recently desktop operating systems have gained built-in sharing mechanisms. For example, below is desktop Safari's share sheet on macOS triggered from an article on my blog . When you click the Share Article button, you can share a link to the article with a friend, for example, via the macOS Messages app.

Desktop Safari's share sheet on macOS triggered from an article's Share button
Web Share API on desktop Safari on macOS.

The code to make this happen is pretty straightforward. I call navigator.share() and pass it an optional title , text , and url in an object. But what if I want to attach an image? Level 1 of the Web Share API doesn't support this yet. The good news is that Web Share Level 2 has added file sharing capabilities.

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);
}

Let me show you how to make this work with the Fugu Greeting card application. First, I need to prepare a data object with a files array consisting of one blob, and then a title and a text . Next, as a best practice, I use the new navigator.canShare() method which does what its name suggests: It tells me if the data object I'm trying to share can technically be shared by the browser. If navigator.canShare() tells me the data can be shared, I'm ready to call navigator.share() as before. Because everything can fail, I'm again using a try...catch block.

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);
  }
};

As before, I use progressive enhancement. If both 'share' and 'canShare' exist on the navigator object, only then I go forward and load share.mjs via dynamic import() . On browsers like mobile Safari that only fulfill one of the two conditions, I don't load the functionality.

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

In Fugu Greetings, if I tap the Share button on a supporting browser like Chrome on Android, the built-in share sheet opens. I can, for example, choose Gmail, and the email composer widget pops up with the image attached.

OS-level share sheet showing various apps to share the image to.
Choosing an app to share the file to.
Gmail's email compose widget with the image attached.
The file gets attached to a new email in Gmail's composer.

The Contact Picker API

Next, I want to talk about contacts, meaning a device's address book or contacts manager app. When you write a greeting card, it may not always be easy to correctly write someone's name. For example, I have a friend Sergey who prefers his name to be spelled in Cyrillic letters. I'm using a German QWERTZ keyboard and have no idea how to type their name. This is a problem that the Contact Picker API can solve. Since I have my friend stored in my phone's contacts app, via the Contacts Picker API, I can tap into my contacts from the web.

First, I need to specify the list of properties I want to access. In this case, I only want the names, but for other use cases I might be interested in telephone numbers, emails, avatar icons, or physical addresses. Next, I configure an options object and set multiple to true , so that I can select more than one entry. Finally, I can call navigator.contacts.select() , which returns the desired properties for the user-selected contacts.

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);
  }
};

And by now you've probably learned the pattern: I only load the file when the API is actually supported.

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

In Fugu Greeting, when I tap the Contacts button and select my two best pals, Сергей Михайлович Брин and劳伦斯·爱德华·"拉里"·佩奇, you can see how the contacts picker is limited to only show their names, but not their email addresses, or other information like their phone numbers. Their names are then drawn onto my greeting card.

Contacts picker showing the names of two contacts in the address book.
Selecting two names with the contact picker from the address book.
The names of the two previously picked contacts drawn on the greeting card.
The two names then get drawn onto the greeting card.

API کلیپ بورد ناهمزمان

Up next is copying and pasting. One of our favorite operations as software developers is copy and paste. As a greeting card author, at times, I may want to do the same. I may want to either paste an image into a greeting card I'm working on, or copy my greeting card so I can continue editing it from somewhere else. The Async Clipboard API , supports both text and images. Let me walk you through how I added copy and paste support to the Fugu Greetings app.

In order to copy something onto the system's clipboard, I need to write to it. The navigator.clipboard.write() method takes an array of clipboard items as a parameter. Each clipboard item is essentially an object with a blob as a value, and the blob's type as the key.

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

To paste, I need to loop over the clipboard items that I obtain by calling navigator.clipboard.read() . The reason for this is that multiple clipboard items might be on the clipboard in different representations. Each clipboard item has a types field that tells me the MIME types of the available resources. I call the clipboard item's getType() method, passing the MIME type I obtained before.

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);
  }
};

And it's almost needless to say by now. I only do this on supporting browsers.

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

پس این در عمل چگونه کار می کند؟ I have an image open in the macOS Preview app and copy it to the clipboard. When I click Paste , the Fugu Greetings app then asks me whether I want to allow the app to see text and images on the clipboard.

Fugu Greetings app showing the clipboard permission prompt.
The clipboard permission prompt.

Finally, after accepting the permission, the image is then pasted into the application. The other way round works, too. Let me copy a greeting card to the clipboard. When I then open Preview and click File and then New from Clipboard , the greeting card gets pasted into a new untitled image.

The macOS Preview app with an untitled, just pasted image.
An image pasted into the macOS Preview app.

The Badging API

Another useful API is the Badging API . As an installable PWA, Fugu Greetings of course does have an app icon that users can place on the app dock or the home screen. A fun and easy way to demonstrate the API is to (ab)use it in Fugu Greetings as a pen strokes counter. I have added an event listener that increments the pen strokes counter whenever the pointerdown event occurs and then sets the updated icon badge. Whenever the canvas gets cleared, the counter resets, and the badge is removed.

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

This feature is a progressive enhancement, so the loading logic is as usual.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

In this example, I have drawn the numbers from one to seven, using one pen stroke per number. The badge counter on the icon is now at seven.

The numbers from one to seven drawn onto the greeting card, each with just one pen stroke.
Drawing the numbers from 1 to 7, using seven pen strokes.
Badge icon on the Fugu Greetings app showing the number 7.
The pen strokes counter in the form of the app icon badge.

The Periodic Background Sync API

Want to start each day fresh with something new? A neat feature of the Fugu Greetings app is that it can inspire you each morning with a new background image to start your greeting card. The app uses the Periodic Background Sync API to achieve this.

The first step is to register a periodic sync event in the service worker registration. It listens for a sync tag called 'image-of-the-day' and has a minimum interval of one day, so the user can get a new background image every 24 hours.

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);
  }
};

The second step is to listen for the periodicsync event in the service worker. If the event tag is 'image-of-the-day' , that is, the one that was registered before, the image of the day is retrieved via the getImageOfTheDay() function, and the result propagated to all clients, so they can update their canvases and caches.

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,
          });
        });
      })()
    );
  }
});

Again this is truly a progressive enhancement, so the code is only loaded when the API is supported by the browser. This applies to both the client code and the service worker code. On non-supporting browsers, neither of them is loaded. Note how in the service worker, instead of a dynamic import() (that isn't supported in a service worker context yet ), I use the classic 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');
}

In Fugu Greetings, pressing the Wallpaper button reveals the greeting card image of the day that is updated every day via the Periodic Background Sync API.

Fugu Greetings app with a new greeting card image of the day.
Pressing the Wallpaper button displays the image of the day.

Notification Triggers API

Sometimes even with a lot of inspiration, you need a nudge to finish a started greeting card. This is a feature that is enabled by the Notification Triggers API . As a user, I can enter a time when I want to be nudged to finish my greeting card. When that time comes, I will get a notification that my greeting card is waiting.

After prompting for the target time, the application schedules the notification with a showTrigger . This can be a TimestampTrigger with the previously selected target date. The reminder notification will be triggered locally, no network or server side is needed.

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),
  });
}

As with everything else I have shown so far, this is a progressive enhancement, so the code is only conditionally loaded.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

When I check the Reminder checkbox in Fugu Greetings, a prompt asks me when I want to be reminded to finish my greeting card.

Fugu Greetings app with a prompt asking the user when they want to be reminded to finish their greeting card.
Scheduling a local notification to be reminded to finish a greeting card.

When a scheduled notification triggers in Fugu Greetings, it is shown just like any other notification, but as I wrote before, it didn't require a network connection.

macOS Notification Center showing a triggered notification from Fugu Greetings.
The triggered notification appears in the macOS Notification Center.

The Wake Lock API

I also want to include the Wake Lock API . Sometimes you just need to stare long enough at the screen until inspiration kisses you. The worst that can happen then is for the screen to turn off. The Wake Lock API can prevent this from happening.

The first step is to obtain a wake lock with the navigator.wakelock.request method() . I pass it the string 'screen' to obtain a screen wake lock. I then add an event listener to be informed when the wake lock is released. This can happen, for example, when the tab visibility changes. If this happens, I can, when the tab becomes visible again, re-obtain the 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);

Yes, this is a progressive enhancement, so I only need to load it when the browser supports the API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

In Fugu Greetings, there's an Insomnia checkbox that, when checked, keeps the screen awake.

The insomnia checkbox, if checked, keeps the screen awake.
The Insomnia checkbox keeps app awake.

The Idle Detection API

At times, even if you stare at the screen for hours, it's just useless and you can't come up with the slightest idea what to do with your greeting card. The Idle Detection API allows the app to detect user idle time. If the user is idle for too long, the app resets to the initial state and clears the canvas. This API is currently gated behind the notifications permission , since a lot of production use cases of idle detection are notifications-related, for example, to only send a notification to a device the user is currently actively using.

After making sure that the notifications permission is granted, I then instantiate the idle detector. I register an event listener that listens for idle changes, which includes the user and the screen state. The user can be active or idle, and the screen can be unlocked or locked. If the user is idle, the canvas clears. I give the idle detector a threshold of 60 seconds.

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,
});

And as always, I only load this code when the browser supports it.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

In the Fugu Greetings app, the canvas clears when the Ephemeral checkbox is checked and the user is idle for for too long.

Fugu Greetings app with a cleared canvas after the user has been idle for too long.
When the Ephemeral checkbox is checked and the user has been idle for too long, the canvas is cleared.

بسته شدن

Phew, what a ride. So many APIs in just one sample app. And, remember, I never make the user pay the download cost for a feature that their browser doesn't support. By using progressive enhancement, I make sure only the relevant code gets loaded. And since with HTTP/2, requests are cheap, this pattern should work well for a lot of applications, although you might want to consider a bundler for really large apps.

Chrome DevTools Network panel showing only requests for files with code that the current browser supports.
Chrome DevTools Network tab showing only requests for files with code that the current browser supports.

The app may look a little different on each browser since not all platforms support all features, but the core functionality is always there—progressively enhanced according to the particular browser's capabilities. Note that these capabilities may change even in one and the same browser, depending on whether the app is running as an installed app or in a browser tab.

Fugu Greetings running on Android Chrome, showing many available features.
Fugu Greetings running on Android Chrome.
Fugu Greetings running on desktop Safari, showing fewer available features.
Fugu Greetings running on desktop Safari.
Fugu Greetings running on desktop Chrome, showing many available features.
Fugu Greetings running on desktop Chrome.

If you're interested in the Fugu Greetings app, go find and fork it on GitHub .

Fugu Greetings repo on GitHub.
Fugu Greetings app on GitHub.

The Chromium team is working hard on making the grass greener when it comes to advanced Fugu APIs. By applying progressive enhancement in the development of my app, I make sure that everybody gets a good, solid baseline experience, but that people using browsers that support more Web platform APIs get an even better experience. I'm looking forward to seeing what you do with progressive enhancement in your apps.

قدردانی ها

I'm grateful to Christian Liebel and Hemanth HM who both have contributed to Fugu Greetings. This article was reviewed by Joe Medley and Kayce Basques . Jake Archibald helped me find out the situation with dynamic import() in a service worker context.