تحسين تطبيق الويب التقدّمي تدريجيًا

تم إنشاء تصميم متوافق مع المتصفحات الحديثة وتطويره تدريجيًا كما كان في عام 2003

في آذار (مارس) 2003، أذهل فينك وستيف شامبيون عالم تصميم الويب بمفهوم التحسين التدريجي، وهو استراتيجية لتصميم الويب تركّز على تحميل محتوى صفحة الويب الأساسية أولاً، ثم يضيف تدريجيًا إلى المحتوى طبقات عرض تقديمي وميزات أكثر دقةً ودقةً من الناحية الفنية. بينما في عام 2003، كان التحسين التدريجي يدور حول استخدام - في ذلك الوقت - ميزات CSS الحديثة ولغة JavaScript غير المزعجة وحتى الرسومات المتجهة القابلة للتطوير فقط. يتعلّق التحسين التدريجي في عام 2020 وما بعده باستخدام إمكانات المتصفّح الحديثة.

تصميم ويب شامل للمستقبل مع التحسين التدريجي. شريحة العنوان من العرض التقديمي الأصلي لـ "فينك" و"شامبون".
الشريحة: تصميم ويب شامل للمستقبل مع تحسين تدريجي. (المصدر)

JavaScript حديث

وبالحديث عن JavaScript، نعتبر أنّ حالة دعم المتصفّح لأحدث ميزات JavaScript الأساسية في ES 2015 رائعة. يشتمل المعيار الجديد على وعود ووحدات وفئات وقيَم حرفية للنماذج ودوال الأسهم وlet وconst والمعلَمات التلقائية وأدوات الإنشاء وتحديد المهام التخريبية والراحة والانتشار وMap/Set وWeakMap/WeakSet وغير ذلك الكثير. جميعها متوافقة.

يعرض جدول دعم CanIUse إمكانية استخدام ميزات ES6 في جميع المتصفحات الرئيسية.
جدول دعم المتصفّح ECMAScript 2015 (ES6) (المصدر)

يمكن استخدام الدوال غير المتزامنة، وهي إحدى ميزات ES 2017، وإحدى الميزات المفضلة لديّ في جميع المتصفحات الرئيسية. تتيح الكلمتان الرئيسيتان async وawait كتابة سلوك غير متزامن مستند إلى الوعود بأسلوب أوضح، ما يغني عن الحاجة إلى ضبط سلاسل الوعد بشكل صريح.

جدول الدعم CanIUse للدوال غير المتزامنة التي تُظهر الدعم في جميع المتصفحات الرئيسية.
جدول دعم المتصفح الخاص بالدوال غير المتزامنة. (المصدر)

وحتى الإضافات الحديثة للغة في ES 2020، مثل التسلسل الاختياري والدمج الفارغ، نجحت في الوصول إلى الدعم بسرعة كبيرة. يمكنك الاطّلاع على نموذج رمز أدناه. عندما يتعلق الأمر بميزات JavaScript الأساسية، لا يمكن أن تكون حالة العشب أكثر خضرة مما هي عليه اليوم.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
صورة خلفية العشب الأخضر في نظام التشغيل Windows XP.
تظهر خلفية خضراء عن ميزات JavaScript الأساسية. (لقطة شاشة لمنتج Microsoft، ويتم استخدامها مع الإذن).

نموذج التطبيق: Fugu Greetings

في هذه المقالة، أعمل باستخدام تطبيق ويب تقدّمي (PWA) بسيط يُطلق عليه اسم Fugu Greetings (GitHub). اسم هذا التطبيق هو تلميح لمشروع Project Fugu 🐡، وهو جهد لمنح الويب جميع إمكانات تطبيقات Android/iOS/سطح المكتب. يمكنك قراءة المزيد عن المشروع على الصفحة المقصودة.

Fugu Greetings هو تطبيق رسم يتيح لك إنشاء بطاقات معايدة افتراضية وإرسالها إلى أحبائك. وهو يبيّن مفاهيم PWA الأساسية. وهذه الشبكة موثوقة ويتم تفعيلها بالكامل بلا اتصال بالإنترنت، وبالتالي حتى إذا لم تكن لديك شبكة، لا يزال بإمكانك استخدامها. إنه أيضًا قابل للتثبيت على الشاشة الرئيسية للجهاز ويتكامل بسلاسة مع نظام التشغيل كتطبيق مستقل.

تطبيق Fugu Greetings PWA برسم يشبه شعار منتدى PWA.
نموذج تطبيق Fugu Greetings

التحسين التدريجي

بعد ذلك، حان الوقت للحديث عن التحسين التدريجي. يعرّف مسرد مصطلحات مستندات الويب MDN المفهوم على النحو التالي:

والتحسين التدريجي هو فلسفة تصميم توفّر أساسًا للمحتوى والوظائف الأساسية لأكبر عدد ممكن من المستخدمين، مع تقديم أفضل تجربة ممكنة فقط لمستخدمي أحدث المتصفحات التي يمكنها تشغيل جميع الرموز المطلوبة.

يُستخدم رصد الميزات بشكل عام لتحديد ما إذا كان بإمكان المتصفّحات معالجة وظائف أكثر حداثة، في حين يتم استخدام polyfill غالبًا لإضافة الميزات غير المتوفّرة في 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();
  });
};

عند توفير ميزة import، يجب أن تتوفر ميزة import حتى يتمكن المستخدمون من حفظ بطاقات الترحيب محليًا. والطريقة التقليدية لحفظ الملفات هي من خلال إنشاء رابط ارتساء باستخدام السمة download مع عنوان URL لكائن ثنائي كبير باعتباره 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 تتيح لك فتح الملفات والدلائل وإنشائها، بالإضافة إلى تعديلها وحفظها.

إذًا، كيف يمكنني اكتشاف الميزات في واجهة برمجة التطبيقات؟ تعرض واجهة برمجة التطبيقات File System Access API طريقة جديدة window.chooseFileSystemEntries(). وبالتالي، أحتاج إلى تحميل وحدات استيراد وتصدير مختلفة بشكل مشروط استنادًا إلى ما إذا كانت هذه الطريقة متاحة أم لا. وقد أوضحنا لك كيفية إجراء ذلك أدناه.

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

ولكن قبل أن أتعمق في تفاصيل واجهة برمجة التطبيقات File System Access API، دعني أسلط الضوء بسرعة على نمط التحسين التدريجي هنا. على المتصفّحات التي لا تتيح حاليًا استخدام File System Access API، أحمّل النصوص البرمجية القديمة. يمكنك الاطلاع على علامات تبويب الشبكة في Firefox وSafari أدناه.

أداة فحص الويب في Safari تعرض الملفات القديمة التي يتم تحميلها
علامة تبويب شبكة "أداة فحص الويب على Safari".
أدوات المطوّرين في Firefox تعرض الملفات القديمة التي يتم تحميلها
علامة تبويب شبكة "أدوات المطوّرين" في Firefox.

ومع ذلك، في Chrome، وهو متصفّح متوافق مع واجهة برمجة التطبيقات، يتم تحميل النصوص البرمجية الجديدة فقط. أصبح هذا ممكنًا بفضل import() الديناميكي، الذي تتيحه جميع المتصفحات الحديثة. كما ذكرت سابقًا، العشب أخضر جميل هذه الأيام.

&quot;أدوات مطوري البرامج في Chrome&quot; تعرض الملفات الحديثة التي يتم تحميلها
علامة تبويب "شبكة أدوات مطوّري البرامج في Chrome"

واجهة برمجة التطبيقات File System Access API

والآن بعد أن تم حل هذه المشكلة، حان الوقت للاطّلاع على عملية التنفيذ الفعلية استنادًا إلى File System Access API. لاستيراد صورة، أسميها window.chooseFileSystemEntries() وأمنحها السمة accepts حيث أقول أريد ملفات صور. يمكن استخدام كل من امتدادات الملفات وأنواع MIME. ينتج عن ذلك مؤشر ملف يمكنني من خلاله الحصول على الملف من خلال استدعاء getFile().

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

يتشابه تصدير الصورة تقريبًا، ولكن هذه المرة أحتاج إلى تمرير معلمة النوع 'save-file' إلى الطريقة chooseFileSystemEntries(). من هذا المنطلق، أحصل على مربع حوار حفظ الملف. عندما يكون الملف مفتوحًا، لم يكن هذا ضروريًا لأنّ 'open-file' هو الإعداد التلقائي. لقد ضبطتُ معلَمة accepts على نحو مشابه لما سبق، ولكن هذه المرة تقتصر على صور PNG فقط. مرة أخرى، أسترجع مؤشر ملف، ولكن بدلاً من الحصول على الملف، أنشئ بثًا قابلاً للكتابة هذه المرة عن طريق استدعاء createWritable(). بعد ذلك، أكتب blob، وهي صورة بطاقة الترحيب، إلى الملف. وأخيرًا، أغلق ساحة المشاركات القابلة للكتابة.

قد يفشل كل شيء دائمًا: فقد يكون القرص نفدت المساحة، أو قد يكون هناك خطأ في الكتابة أو القراءة، أو ربما يلغي المستخدم ببساطة مربع حوار الملف. لهذا السبب، أغلِق المكالمات دائمًا في عبارة try...catch.

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

باستخدام التحسين التدريجي باستخدام File System Access API، يمكنني فتح ملف كما كان من قبل. يتم رسم الملف المستورد مباشرةً على لوحة الرسم. يمكنني إجراء تعديلاتي وحفظها أخيرًا باستخدام مربع حوار حفظ حقيقي حيث يمكنني اختيار اسم الملف وموقع تخزينه. والآن أصبح الملف جاهزًا لحفظه إلى الأبد.

تطبيق Fugu Greetings يحتوي على مربّع حوار فتح ملف
مربّع حوار فتح الملف.
تطبيق Fugu Greetings الآن مع صورة تم استيرادها
الصورة التي تم استيرادها.
تطبيق Fugu Greetings بالصورة المعدّلة
جارٍ حفظ الصورة المعدَّلة في ملف جديد.

واجهات برمجة التطبيقات لاستهداف مشاركة الويب والمشاركة على الويب

بخلاف التخزين للأبد، ربما أرغب في مشاركة بطاقة الترحيب الخاصة بي. وهذا إجراء تتيح لي واجهة برمجة التطبيقات Web Share API وواجهة Web Share API تنفيذ ذلك. وأصبحت أنظمة التشغيل على الأجهزة الجوّالة والكمبيوتر المكتبي مؤخرًا تضم آليات مشاركة مُدمجة. على سبيل المثال، أدناه تظهر صفحة مشاركة Safari لأجهزة الكمبيوتر المكتبي على نظام التشغيل macOS الناتجة عن مقالة في مدونتي. عند النقر على الزر مشاركة المقالة، يمكنك مشاركة رابط المقالة مع صديق، على سبيل المثال، عبر تطبيق "الرسائل" في نظام التشغيل macOS.

صفحة المشاركة في متصفّح Safari لأجهزة الكمبيوتر المكتبي على نظام التشغيل macOS يتم تشغيلها من خلال الزر &quot;مشاركة&quot; في المقالة
Web Share API على متصفّح Safari لأجهزة الكمبيوتر المكتبي على نظام التشغيل macOS

التعليمات البرمجية لتحقيق ذلك واضحة جدًا. أطلق على navigator.share() وأعرض له title وtext وurl اختيارية في كائن. ولكن ماذا لو أردت إرفاق صورة؟ لا يمكن حتى الآن استخدام المستوى 1 من Web Share 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 يتألّف من كائن ثنائي كبير (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 من خلال import() الديناميكية. في المتصفحات مثل Safari على الأجهزة الجوّالة التي تستوفي أحد الشرطين فقط، لا أحمّل الوظيفة.

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

في Fugu Greetings، إذا نقرت على الزر Share (مشاركة) على متصفح متوافق مثل Chrome على Android، سيتم فتح صفحة المشاركة المدمجة. يمكنني على سبيل المثال اختيار Gmail، وستظهر أداة إنشاء الرسائل الإلكترونية مع إرفاق الصورة.

ورقة مشاركة على مستوى نظام التشغيل تعرض تطبيقات مختلفة لمشاركة الصورة معها.
اختيار تطبيق لمشاركة الملف معه.
أداة إنشاء الرسائل الإلكترونية في Gmail مع الصورة المرفقة.
يتم إرفاق الملف برسالة إلكترونية جديدة في أداة إنشاء الرسائل في Gmail.

واجهة برمجة تطبيقات منتقي جهات الاتصال

بعد ذلك، أريد أن أتحدث عن جهات الاتصال، أي دفتر عناوين الجهاز أو تطبيق مدير جهات الاتصال. وعند كتابة بطاقة تهنئة، قد لا يكون من السهل دائمًا كتابة اسم شخص ما بشكل صحيح. على سبيل المثال، لدي صديق سيرغي الذي يفضل كتابة اسمه بالأحرف السيريلية. أستخدم لوحة مفاتيح QWERTZ ألمانية وليس لدي أي فكرة عن كيفية كتابة أسمائها. هذه مشكلة يمكن أن تحلها واجهة برمجة تطبيقات أداة اختيار جهات الاتصال. نظرًا لتخزين صديقي في تطبيق جهات اتصال هاتفي، عبر واجهة برمجة تطبيقات 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);
  }
};

وربما تكون قد تعلمت الآن النمط: لا أحمّل الملف إلا عندما تكون واجهة برمجة التطبيقات متوافقة بالفعل.

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

في Fugu Greeting، عندما أنقر على زر جهات الاتصال وأختار أفضل اثنين من أصدقائي، ергео ока ليلاًловиш рин و劳伦斯·爱德升·"拉里"·, يمكنك رؤية كيف أن منتقي عناوين بريدهم الإلكتروني ثم يتم رسم أسمائهم على بطاقة الترحيب.

منتقي جهات الاتصال يُظهر اسمَي جهتَي اتصال في دفتر العناوين.
اختيار اسمين باستخدام أداة اختيار جهات الاتصال من دفتر العناوين.
أسماء جهتي الاتصال اللتين تم اختيارهما سابقًا والمرسومة على بطاقة الترحيب.
بعد ذلك، يُرسم الاسمان على بطاقة الترحيب.

واجهة برمجة التطبيقات للحافظة غير المتزامنة

التالي هو النسخ واللصق. إحدى العمليات المفضلة لدينا كمطوري البرامج هي النسخ واللصق. بصفتي مؤلف بطاقة تهنئة، قد أرغب في بعض الأحيان في القيام بنفس الشيء. قد أرغب في لصق صورة في بطاقة معايدة أعمل عليها، أو نسخ بطاقة الترحيب حتى أتمكن من متابعة تحريرها من مكان آخر. تتوافق واجهة برمجة تطبيقات حافظة غير متزامنة مع النصوص والصور. اسمح لي أن أطلعك على كيفية إضافة دعم النسخ واللصق إلى تطبيق 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 ونسخها إلى الحافظة. عند النقر على لصق، يسألني تطبيق Fugu Greetings بعد ذلك ما إذا كنت أريد السماح للتطبيق بمشاهدة النصوص والصور الموجودة على الحافظة.

تطبيق Fugu Greetings يعرض طلب إذن الحافظة
رسالة المطالبة بإذن الحافظة.

وأخيرًا، بعد قبول الإذن، يتم لصق الصورة في التطبيق. العكس صحيح أيضًا. دعني أنسخ بطاقة تهنئة إلى الحافظة. عندما أفتح بعد ذلك "معاينة" ونقر فوق ملف ثم جديد من الحافظة، يتم لصق بطاقة الترحيب في صورة جديدة بلا عنوان.

تطبيق المعاينة في نظام التشغيل macOS يتضمّن صورة تم لصقها للتو بلا عنوان
صورة تم لصقها في تطبيق المعاينة من نظام التشغيل macOS

واجهة برمجة تطبيقات الشارات

من واجهات برمجة التطبيقات المفيدة الأخرى أيضًا واجهة برمجة تطبيقات الشارات. كتطبيق PWA قابل للتثبيت، يحتوي Fugu Greetings بالطبع على رمز تطبيق يمكن للمستخدمين وضعه على شريط التطبيقات أو الشاشة الرئيسية. هناك طريقة ممتعة وسهلة لإثبات واجهة برمجة التطبيقات وهي (ab) استخدامها في 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
عدّاد ضربات القلم على شكل شارة رمز التطبيق

واجهة برمجة التطبيقات الدورية لمزامنة الخلفية

هل تريد أن تبدأ كل يوم من جديد مع شيء جديد؟ من الميزات الأنيقة لتطبيق Fugu Greetings أنه يمكن أن يُلهمك كل صباح بصورة خلفية جديدة لبدء بطاقة الترحيب. يستخدم التطبيق واجهة برمجة التطبيقات التي تستخدم ميزة "مزامنة الخلفية الدورية" لتحقيق ذلك.

الخطوة الأولى هي register حدث مزامنة دوري في عملية تسجيل مشغّل الخدمات. وتستمع إلى علامة مزامنة تُسمى '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,
          });
        });
      })()
    );
  }
});

ومرة أخرى، يعد هذا تحسينًا تدريجيًّا، وبالتالي لا يتم تحميل الرمز إلّا إذا كانت واجهة برمجة التطبيقات متوافقة مع المتصفّح. وينطبق ذلك على كلّ من رمز العميل ورمز مشغّل الخدمات. وعلى المتصفِّحات غير المتوافقة، لا يتم تحميل أيٍّ منهما. لاحِظ كيف أستخدمُ الإصدار الكلاسيكي importScripts() في إطار مشغّل الخدمات، بدلاً من استخدام import() الديناميكي (غير متوافق في سياق مشغّل الخدمات حتى الآن).

// 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، يؤدي الضغط على الزر Background (الخلفية) إلى عرض صورة بطاقة الترحيب لليوم التي يتم تحديثها كل يوم عبر واجهة برمجة التطبيقات Regionic Background Sync API.

تطبيق Fugu Greetings مع صورة بطاقة معايدة جديدة لهذا اليوم.
يؤدي الضغط على زر الخلفية إلى عرض صورة اليوم.

واجهة برمجة التطبيقات لمشغِّلات الإشعارات

في بعض الأحيان، حتى مع الكثير من الإلهام، تحتاج إلى تذكير لإنهاء بطاقة الترحيب التي بدأت. هذه ميزة يتمّ تفعيلها من خلال واجهة برمجة التطبيقات لعوامل تشغيل الإشعارات. بصفتي مستخدمًا، يمكنني إدخال وقت أريد تلقّي تذكير تلقائي لإنهاء بطاقة الترحيب. وعندما يحين ذلك الوقت، سأتلقّى إشعارًا بأنّ بطاقة الترحيب قيد الانتظار.

بعد طلب الوقت المستهدَف، يجدول التطبيق الإشعار باستخدام 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 Notification Center.

واجهة برمجة تطبيقات Wake Lock

أريد أيضًا تضمين Wake Lock 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);

نعم، هذا تحسين تدريجي، لذلك أحتاج إلى تحميله فقط عندما يتوافق المتصفّح مع واجهة برمجة التطبيقات.

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

في Fugu Greetings، يظهر مربّع اختيار الأرق الذي يؤدي إلى إبقاء الشاشة نشطة عند وضع علامة عليه.

وفي حال وضع علامة في مربّع اختيار الأرق، يتم إبقاء الشاشة نشطة.
يؤدي مربّع الاختيار الأرق إلى إبقاء التطبيق نشطًا.

واجهة برمجة التطبيقات لرصد عدم النشاط لفترة قصيرة

في بعض الأحيان، حتى إذا كنت تحدق في الشاشة لساعات، فلا يكون الأمر مفيدًا ولا يمكنك الخروج بأدنى فكرة عما تفعله ببطاقة الترحيب. تسمح Idle Detection 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 بلوحة رسم تم محوها بعد أن يكون المستخدم غير نشط لفترة طويلة جدًا.
عندما يتم وضع علامة في مربّع الاختيار مؤقت ويكون المستخدم غير نشط لفترة طويلة جدًا، يتم محو اللوحة.

الخاتمة

أوه، يا لها من رحلة. كان هناك العديد من واجهات برمجة التطبيقات في نموذج تطبيق واحد فقط. ولا أجعل المستخدم أبدًا يدفع تكلفة التنزيل مقابل ميزة لا يدعمها المتصفّح الذي يستخدمه. باستخدام التحسين التدريجي، أتأكد من تحميل التعليمة البرمجية ذات الصلة فقط. ونظرًا لأن الطلبات مع HTTP/2 رخيصة، من المفترض أن يعمل هذا النمط بشكل جيد مع كثير من التطبيقات، بالرغم من أنك قد تحتاج إلى التفكير في استخدام أداة حزم للتطبيقات الكبيرة جدًا.

لا تعرض لوحة &quot;شبكة أدوات مطوّري البرامج في Chrome&quot; سوى طلبات الملفات التي تتضمّن رموزًا برمجية يتوافق معها المتصفِّح الحالي.
لا تعرض علامة التبويب "شبكة أدوات مطوري البرامج في Chrome" سوى طلبات الملفات التي تتضمّن رموزًا برمجية يتوافق معها المتصفّح الحالي.

قد يبدو التطبيق مختلفًا بعض الشيء في كل متصفح، نظرًا لعدم دعم جميع الأنظمة الأساسية لجميع الميزات، إلا أن الوظائف الأساسية تكون دائمًا موجودة، حيث يتم تحسينها تدريجيًا وفقًا لإمكانات المتصفح المحدد. تجدر الإشارة إلى أنّ هذه الإمكانات قد تتغير حتى في متصفح واحد ونفس ذلك، يعتمد ذلك على تشغيل التطبيق كتطبيق مثبَّت أو في علامة تبويب في المتصفح.

تطبيق Fugu Greetings يعمل على متصفّح Android Chrome ويعرض العديد من الميزات المتاحة
Fugu Greetings التي يتم تشغيلها على Android Chrome.
يتم تشغيل Fugu Greetings على متصفّح Safari لأجهزة الكمبيوتر المكتبي، وتعرض عددًا أقل من الميزات المتاحة.
يتم تشغيل Fugu Greetings على متصفّح Safari لأجهزة الكمبيوتر المكتبي.
رسائل Fugu Greetings تعمل على متصفّح Chrome لأجهزة الكمبيوتر المكتبي، وتعرض العديد من الميزات المتاحة.
يتم تشغيل Fugu Greetings على Chrome لأجهزة الكمبيوتر المكتبية.

إذا كنت مهتمًا بتطبيق Fugu Greetings، فابحث عنه وتشعبه على GitHub.

مستودع Fugu Greetings على GitHub.
Fugu Greetings على GitHub.

يعمل فريق Chromium بجد لجعل العشب أكثر خضراء عندما يتعلق الأمر بواجهات برمجة تطبيقات Fugu API. من خلال تطبيق التحسين التدريجي في تطوير تطبيقي، أتأكد من حصول الجميع على تجربة أساسية جيدة وقوية، ولكن الأشخاص الذين يستخدمون متصفحات تدعم المزيد من واجهات برمجة تطبيقات النظام الأساسي للويب يحصلون على تجربة أفضل. أتطلع إلى معرفة ما يمكنك فعله باستخدام التحسين التدريجي في تطبيقاتك.

شكر وتقدير

أنا ممتن للغاية لكل من كريستيان ليبيل وهيمانث إم اللذَين ساهما في نشر تحية Fugu Greetings. راجع هذه المقالة جو ميدلي وكايس باسك. ساعدني جيك أرشيبالد في معرفة الموقف باستخدام import() الديناميكي في سياق عاملي الخدمات.