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

برای مرورگرهای مدرن ساخته شده و به تدریج مانند سال 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 APIs

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

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

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() را در زمینه کارمند سرویس پیدا کنم.