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

إنشاء تطبيقات متوافقة مع المتصفّحات الحديثة وتحسينها تدريجيًا كما لو كان عام 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 للدوال غير المتزامنة التي تعرض التوافق مع جميع المتصفحات الرئيسية
جدول دعم المتصفحات للدوال غير المتزامنة. (المصدر)

وسرعان ما أصبحت حتى الإضافات الحديثة للغاية في إسبانيا 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). تم اختيار اسم هذا التطبيق تكريمًا لمشروع Fugu 🐡، وهو جهد يهدف إلى منح الويب كل الميزات الرائعة لتطبيقات Android/iOS/أجهزة الكمبيوتر المكتبي. يمكنك قراءة المزيد عن المشروع على صفحته المقصودة.

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

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

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

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

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

يتم استخدام ميزة رصد الميزات بشكل عام لتحديد ما إذا كان بإمكان المتصفّحات معالجة وظائف أكثر حداثة، في حين يتم استخدام العناصر القابلة للاستبدال غالبًا لإضافة ميزات غير متوفّرة باستخدام 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 للملفّات المصغّرة ك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 Web Inspector تعرِض الملفات القديمة التي يتم تحميلها
علامة التبويب "الشبكة" في Safari Web Inspector
أدوات المطوّرين في Firefox تعرض الملفات القديمة التي يتم تحميلها
علامة التبويب "الشبكة" في "أدوات مطوّري برامج Firefox"

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

أدوات مطوّري البرامج في Chrome تعرض الملفات الحديثة التي يتم تحميلها
علامة التبويب "الشبكة" في "أدوات مطوّري البرامج في 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(). بعد ذلك، أكتب العنصر المصغّر، وهو صورة بطاقة المعايدة، في الملف. أخيرًا، أغلق ساحة المشاركات القابلة للكتابة.

يمكن أن يتعذّر إكمال أي إجراء: قد لا يتوفّر مساحة كافية على القرص، أو قد يحدث خطأ في الكتابة أو القراءة، أو قد يلغي المستخدم مربّع حوار الملف. لهذا السبب، أُغلق دائمًا المكالمات بعبارة 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 Target API

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

لوحة المشاركة في Safari على أجهزة الكمبيوتر المكتبي التي تعمل بنظام التشغيل macOS والتي يتم تفعيلها من زر المشاركة في المقالة
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 يتألف من عنصر واحد، ثم title وtext. بعد ذلك، وبصفتها من أفضل الممارسات، أستخدم طريقة navigator.canShare() الجديدة التي تؤدي إلى ما يشير إليه اسمها: تُعلمني ما إذا كان بإمكان المتصفّح مشاركة عنصر data الذي أحاول مشاركته من الناحية الفنية. إذا أخبرني "navigator.canShare()" أنّه يمكن مشاركة البيانات، سأكون مستعدًا للاتصال بـ "navigator.share()" كما في السابق. لأنّه يمكن أن يتعذّر تنفيذ أي إجراء، سأستخدم مرة أخرى العنصر try...catch.

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

وكما في السابق، أستخدم أسلوب التحسين التدريجي. إذا كان كل من 'share' و'canShare' متوفّرَين في عنصر navigator، عندئذٍ فقط أمضي قدمًا وأحمل share.mjs من خلال import() الديناميكي. في المتصفحات التي تستوفي شرطًا واحدًا فقط من الشرطَين، مثل Safari على الأجهزة الجوّالة، لا يتم تحميل الوظيفة.

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

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

ورقة مشاركة على مستوى نظام التشغيل تعرض تطبيقات مختلفة لمشاركة الصورة معها
اختيار تطبيق لمشاركة الملف معه
أداة إنشاء الرسائل الإلكترونية في 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);
  }
};

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

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

في تطبيق Fugu Greeting، عندما أنقر على الزر جهات الاتصال وأختار أفضل صديقَين لي، Сергей Михайлович Брин و劳伦斯·爱德华·"拉里"·佩奇، يمكنك ملاحظة أنّه يقتصر على عرض أسمائهم فقط، وليس عناوين بريدهم الإلكتروني أو معلومات أخرى مثل أرقام هواتفهم. بعد ذلك، أرسم أسمائهم على بطاقة المعايدة.

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

واجهة برمجة التطبيقات Async Clipboard API

الخطوة التالية هي النسخ واللصق. إنّ عملية النسخ واللصق هي إحدى العمليات المفضّلة لدينا كمطوّرين للبرامج. بصفتي مؤلف بطاقات تهنئة، قد أحتاج في بعض الأحيان إلى الشيء نفسه. قد أريد لصق صورة في بطاقة تهنئة أعمل عليها، أو نسخ بطاقة التهنئة لأتمكّن من مواصلة تعديلها من مكان آخر. Async Clipboard API، تتيح استخدام النصوص والصور. اسمحوا لي أن أشرح لكم كيفية إضافة دعم النسخ واللصق إلى تطبيق Fugu Geetings.

لنسخ محتوى إلى الحافظة في النظام، يجب كتابته. تأخذ الطريقة 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() الخاصة بعنصر الحافظة، مع تمرير نوع ملف getType() الذي حصلت عليه من قبل.

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، وسأ copied إلى الحافظة. عندما أنقر على لصق، يسألني تطبيق Fugu Greetings عمّا إذا كنت أريد السماح للتطبيق بالاطّلاع على النصوص والصور في الحافظة.

تطبيق Fugu Greetings يعرض طلب إذن الوصول إلى الحافظة
إشعار بإذن الوصول إلى الحافظة.

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

تطبيق &quot;معاينة&quot; في نظام التشغيل macOS مع صورة تم لصقها للتو بدون عنوان
صورة تم لصقها في تطبيق Preview (معاينة) على نظام التشغيل macOS

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

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

واجهة برمجة التطبيقات Periodic Background Sync API

هل تريد بدء كل يوم بنشاط جديد؟ من الميزات الرائعة في تطبيق Fugu Greetings أنّه يمكن أن يلهمك كل صباح بصورة خلفية جديدة لبدء بطاقة المعايدة. ويستخدم التطبيق Periodic Background Sync API لتحقيق ذلك.

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

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

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(). أُرسل إليه السلسلة '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، يتوفّر مربّع الاختيار الأرق الذي يبقي الشاشة في الوضع النشط عند وضع علامة في المربّع.

يؤدي وضع علامة في مربّع الاختيار &quot;الأرق&quot; إلى إبقاء الشاشة قيد التشغيل.
يؤدي وضع علامة في مربّع الاختيار Insomnia (الأرق) إلى إبقاء التطبيق قيد التشغيل.

واجهة برمجة التطبيقات Idle Detection API

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

تطبيق Fugu Greetings مع لوحة تم محو محتواها بعد أن ظل المستخدم غير نشط لفترة طويلة جدًا
عند وضع علامة في مربّع الاختيار مؤقت وتوقف المستخدم عن التفاعل لفترة طويلة جدًا، يتم محو اللوحة.

الخاتمة

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

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

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

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

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

مستودع Fugu Greetings على GitHub
تطبيق Fugu Greetings على GitHub

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

الشكر والتقدير

أشكر Christian Liebel و Hemanth HM اللذَين ساهما في تطبيق Fugu Greetings. تمت مراجعة هذه المقالة من قِبل جو ميديلي وكايس باسك. ساعدني جيك أرتشيبالد في معرفة الموقف الذي واجهه بسبب import() الديناميكي في سياق مشغّلي الخدمات.