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

ساخت برای مرورگرهای مدرن و بهبود تدریجی مانند سال ۲۰۰۳

منتشر شده: ۲۹ ژوئن ۲۰۲۰

در مارس ۲۰۰۳، نیک فینک و استیو چمپون دنیای طراحی وب را با مفهوم بهبود تدریجی (progressive enhancement) شگفت‌زده کردند، استراتژی‌ای برای طراحی وب که ابتدا بر بارگذاری محتوای اصلی صفحه وب تأکید دارد و سپس به تدریج لایه‌های ظریف‌تر و از نظر فنی دقیق‌تری از نمایش و ویژگی‌ها را به محتوا اضافه می‌کند. در حالی که در سال ۲۰۰۳، بهبود تدریجی در مورد استفاده از ویژگی‌های مدرن CSS، جاوا اسکریپت ساده و حتی گرافیک برداری مقیاس‌پذیر (Scalable Vector Graphics) بود. بهبود تدریجی در سال ۲۰۲۰ و پس از آن در مورد استفاده از قابلیت‌های مرورگرهای مدرن است.

طراحی وب فراگیر برای آینده با بهبودهای تدریجی. اسلاید عنوان از ارائه اصلی فینک و چمپون .

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

صحبت از جاوا اسکریپت شد، وضعیت پشتیبانی مرورگرها از جدیدترین ویژگی‌های اصلی جاوا اسکریپت ES 2015 عالی است. استاندارد جدید شامل promiseها، ماژول‌ها، کلاس‌ها، template literals، توابع arrow، let و const ، پارامترهای پیش‌فرض، generatorها، destructuring assignment، rest و spread، Map / Set ، WeakMap / WeakSet و بسیاری موارد دیگر می‌شود. همه پشتیبانی می‌شوند .

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

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

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

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

برای مثال:

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

برنامه نمونه: تبریک فوگو

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

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

PWA با درود فوگو و طرحی شبیه به لوگوی انجمن PWA.
نمونه اپلیکیشن تبریک فوگو .

بهبود تدریجی

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

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

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

[…]

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

مشارکت‌کنندگان 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 و یک آدرس اینترنتی blob به عنوان href آن است. همچنین می‌توانید به صورت برنامه‌نویسی روی آن "کلیک" کنید تا دانلود آغاز شود و برای جلوگیری از نشت حافظه، امیدواریم که فراموش نکنید آدرس اینترنتی شیء 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 را از طریق feature-detect شناسایی کنم؟ API دسترسی به سیستم فایل، متد جدیدی به نام window.chooseFileSystemEntries() ارائه می‌دهد. در نتیجه، باید ماژول‌های import و export مختلف را بسته به اینکه آیا این متد در دسترس است یا خیر، به صورت شرطی بارگذاری کنم.

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 پشتیبانی می‌کند، فقط اسکریپت‌های جدید بارگذاری می‌شوند. این امر به لطف تابع import() که همه مرورگرهای مدرن از آن پشتیبانی می‌کنند ، به طرز زیبایی امکان‌پذیر شده است. همانطور که قبلاً گفتم، این روزها اوضاع کاملاً روبراه است.

ابزارهای توسعه کروم (Chrome DevTools) نحوه بارگذاری فایل‌های مدرن را نشان می‌دهد.
تب شبکه‌ی ابزارهای توسعه‌ی کروم.

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

حالا که به این موضوع پرداختم، وقت آن است که پیاده‌سازی واقعی مبتنی بر API دسترسی به سیستم فایل را بررسی کنیم. برای وارد کردن یک تصویر، من window.chooseFileSystemEntries() را فراخوانی می‌کنم و یک ویژگی accepts به آن ارسال می‌کنم که در آن می‌گویم فایل‌های تصویر را می‌خواهم. هم پسوند فایل و هم انواع MIME پشتیبانی می‌شوند. این منجر به یک file handle می‌شود که می‌توانم با فراخوانی getFile() فایل واقعی را از آن دریافت کنم.

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

خروجی گرفتن از یک تصویر تقریباً مشابه است، اما این بار باید پارامتر نوع 'save-file' را به متد chooseFileSystemEntries() ارسال کنم. از این طریق، یک کادر محاوره‌ای ذخیره فایل دریافت می‌کنم. با باز کردن فایل، این کار لازم نبود زیرا 'open-file' پیش‌فرض است. پارامتر accepts را مشابه قبل تنظیم می‌کنم، اما این بار فقط به تصاویر PNG محدود می‌شود. دوباره یک فایل دریافت می‌کنم، اما به جای دریافت فایل، این بار با فراخوانی createWritable() یک جریان قابل نوشتن ایجاد می‌کنم. سپس، blob را که تصویر کارت تبریک من است، در فایل می‌نویسم. در نهایت، جریان قابل نوشتن را می‌بندم.

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

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

با استفاده از بهبود تدریجی با رابط برنامه‌نویسی کاربردی دسترسی به سیستم فایل (File System Access API)، می‌توانم یک فایل را مانند قبل باز کنم. فایل وارد شده مستقیماً روی بوم رسم می‌شود. می‌توانم ویرایش‌هایم را انجام دهم و در نهایت آنها را با یک پنجره ذخیره واقعی ذخیره کنم، جایی که می‌توانم نام و محل ذخیره فایل را انتخاب کنم. اکنون فایل آماده است تا برای همیشه ذخیره شود.

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

رابط‌های برنامه‌نویسی کاربردی (API) اشتراک‌گذاری وب و هدف اشتراک‌گذاری وب

تلاش-راست

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

برای مثال، صفحه اشتراک‌گذاری سافاری دسکتاپ در macOS زمانی فعال می‌شود که کاربر روی «اشتراک‌گذاری مقاله در وبلاگ من» کلیک کند. شما می‌توانید با استفاده از برنامه پیام‌های macOS، لینک مقاله را با یک دوست به اشتراک بگذارید.

برای اینکه این اتفاق بیفتد، من تابع navigator.share() را فراخوانی می‌کنم و یک title ، text و url اختیاری را در یک شیء به آن ارسال می‌کنم. اما اگر بخواهم یک تصویر را پیوست کنم چه؟ سطح ۱ از Web Share API هنوز از این پشتیبانی نمی‌کند. خبر خوب این است که Web Share سطح ۲ قابلیت‌های اشتراک‌گذاری فایل را اضافه کرده است.

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

بگذارید به شما نشان دهم که چگونه این کار را با برنامه کارت تبریک Fugu انجام دهید. ابتدا، باید یک شیء data با آرایه‌ای files که شامل یک blob و سپس یک title و یک text است، آماده کنم. سپس، به عنوان بهترین روش، از متد جدید navigator.canShare() استفاده می‌کنم که همانطور که از نامش پیداست عمل می‌کند: این متد به من می‌گوید که آیا شیء data که می‌خواهم به اشتراک بگذارم، از نظر فنی می‌تواند توسط مرورگر به اشتراک گذاشته شود یا خیر. اگر navigator.canShare() به من بگوید که داده‌ها می‌توانند به اشتراک گذاشته شوند، آماده‌ام که navigator.share() مانند قبل فراخوانی کنم. از آنجا که همه چیز ممکن است با شکست مواجه شود، دوباره از یک بلوک try...catch استفاده می‌کنم.

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

مانند قبل، من از بهبود تدریجی استفاده می‌کنم. اگر هر دو 'share' و 'canShare' در شیء navigator وجود داشته باشند، فقط در آن صورت به جلو می‌روم و share.mjs با استفاده از dynamic import() بارگذاری می‌کنم. در مرورگرهایی مانند سافاری موبایل که فقط یکی از دو شرط را برآورده می‌کنند، این قابلیت را بارگذاری نمی‌کنم.

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

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

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

API انتخاب مخاطب

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

ابتدا باید لیست ویژگی‌هایی را که می‌خواهم به آنها دسترسی داشته باشم، مشخص کنم. در این مورد، فقط نام‌ها را می‌خواهم، اما برای موارد دیگر ممکن است به شماره تلفن‌ها، ایمیل‌ها، آیکون‌های آواتار یا آدرس‌های فیزیکی علاقه‌مند باشم. در مرحله بعد، یک شیء options را پیکربندی می‌کنم و multiple روی true تنظیم می‌کنم تا بتوانم بیش از یک ورودی را انتخاب کنم. در نهایت، می‌توانم navigator.contacts.select() فراخوانی کنم که ویژگی‌های ایده‌آل را برای مخاطبین انتخاب شده توسط کاربر برمی‌گرداند.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

و تا الان احتمالاً الگو را یاد گرفته‌اید: من فقط زمانی فایل را بارگذاری می‌کنم که API واقعاً پشتیبانی شود.

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

در برنامه‌ی تبریک فوگو، وقتی روی دکمه‌ی مخاطبین ضربه می‌زنم و دو تا از بهترین دوستانم، سِرگی میچَیلِویچ بِرین و劳伦斯·爱德华·"拉里"·佩奇، را انتخاب می‌کنم، می‌بینید که انتخابگر مخاطبین فقط به نمایش نام آنها محدود می‌شود، اما آدرس ایمیل یا اطلاعات دیگری مانند شماره تلفن آنها را نمایش نمی‌دهد. سپس نام آنها روی کارت تبریک من کشیده می‌شود.

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

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

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

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

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

برای چسباندن، باید روی آیتم‌های کلیپ‌بورد که با فراخوانی navigator.clipboard.read() به دست آورده‌ام، حلقه‌ای ایجاد کنم. دلیل این امر این است که ممکن است چندین آیتم کلیپ‌بورد با نمایش‌های مختلف در کلیپ‌بورد باشند. هر آیتم کلیپ‌بورد یک فیلد types دارد که انواع MIME منابع موجود را به من می‌گوید. من متد getType() آیتم کلیپ‌بورد را فراخوانی می‌کنم و نوع MIME که قبلاً به دست آورده‌ام را به آن ارسال می‌کنم.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

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

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

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

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

در نهایت، پس از پذیرش مجوز، تصویر در برنامه پیست می‌شود. روش دیگر نیز کار می‌کند. فرض کنید یک کارت تبریک را در کلیپ‌بورد کپی می‌کنم. وقتی پیش‌نمایش را باز می‌کنم و روی فایل و سپس «جدید از کلیپ‌بورد» کلیک می‌کنم، کارت تبریک در یک تصویر بدون عنوان جدید پیست می‌شود.

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

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

در این مثال، اعداد یک تا هفت را با استفاده از یک حرکت قلم برای هر عدد رسم کرده‌ام. شمارنده‌ی نشان روی آیکون اکنون روی عدد هفت است.

اعداد یک تا هفت که روی کارت تبریک کشیده شده‌اند، هر کدام فقط با یک حرکت قلم.
رسم اعداد از ۱ تا ۷، با استفاده از هفت حرکت قلم.
نماد نشان در برنامه تبریک فوگو که عدد ۷ را نشان می‌دهد.
ضربات قلم به شکل نشان آیکون برنامه، شمارنده می‌شوند.

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

آیا می‌خواهید هر روز را با چیزی جدید شروع کنید؟ یکی از ویژگی‌های جالب برنامه 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() بازیابی می‌شود و نتیجه به همه کلاینت‌ها ارسال می‌شود تا بتوانند canvasها و cacheهای خود را به‌روزرسانی کنند.

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 Greetings، فشردن دکمه‌ی Wallpaper تصویر کارت تبریک آن روز را که هر روز با Periodic Background Sync API به‌روزرسانی می‌شود، نشان می‌دهد.

با فشردن دکمه‌ی «تصویر زمینه»، تصویر روز نمایش داده می‌شود.

API مربوط به اعلان‌ها

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

پس از درخواست زمان مورد نظر، برنامه اعلان را با یک 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 فعال می‌شود، درست مانند هر اعلان دیگری نمایش داده می‌شود، اما همانطور که قبلاً نوشتم، نیازی به اتصال به شبکه نداشت.

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

API قفل بیداری

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

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

اگر گزینه بی‌خوابی تیک خورده باشد، صفحه نمایش را بیدار نگه می‌دارد.
تیک گزینه بی‌خوابی، برنامه را بیدار نگه می‌دارد.

API تشخیص حالت بی‌کاری

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

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

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 تیک می‌خورد و کاربر برای مدت طولانی بیکار می‌ماند، بوم پاک می‌شود.

اپلیکیشن تبریک فوگو با یک بوم نقاشی پاک‌شده پس از اینکه کاربر برای مدت طولانی بیکار بوده است.
وقتی گزینه‌ی Ephemeral تیک خورده باشد و کاربر برای مدت طولانی بیکار مانده باشد، بوم نقاشی پاک می‌شود.

اختتامیه

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

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

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

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

می‌توانید فوگو را در گیت‌هاب فورک کنید.

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

تقدیرنامه‌ها

من از کریستین لیبل و همنت اچ‌ام که هر دو در Fugu Greetings مشارکت داشته‌اند، سپاسگزارم. این سند توسط جو مدلی و کیس باسک بررسی شده است. جیک آرچیبالد به من کمک کرد تا وضعیت تابع import() پویا را در یک زمینه‌ی سرویس ورکرها پیدا کنم.