اذهب إلى المحتوى

التحسين التدريجي لتطبيقات الويب التقدمية PWA


Entesar Khaled

فاجأ نيك فينك Nick Finck وستيف شامبيون Steve Champeon عالم تصميم الويب بمفهوم "التحسين التدريجي" progressive enhancement في شهر مارس 2003، وهو مفهوم يعبر عن استراتيجية لتصميم الويب تركز على تنزيل المحتوى الأساسي لصفحة الويب أولًا، ثم تُضاف تدريجيًا الطبقات ذات التفاصيل والملامح الدقيقة على المحتوى الأساسي.

كان التحسين التدريجي عام 2003 يتعلق باستخدام ميزات CSS الحديثة وجافاسكربت الواضحة unobtrusive JavaScript ورسومات فكتور القابلة لتغيير الحجم Scalable Vector Graphics، لكن في 2020 وما بعده أصبح التحسين التدريجي يتعلق باستخدام إمكانيات المتصفح الحديثة.

01inclusive_web_design_slide.png

شرائح عرض: تصميم ويب متكامل مع التحسين التدريجي، (المصدر).

جافاسكربت الحديثة

عند الحديث عن JavaScript الحديثة، فإننا نستحضر دعم المتصفح لأحدث مميزات JavaScript ES 2015 الأساسية، إذتتضمن (ECMAScript 2015 (ES6 الوعود promises والوحدات modules والفئات classes وقالب النص template literals والدوال السهمية arrow functions والكلمتان المفتاحيتان let و const والمولِّدات generators والاسناد بالتفكيك destructuring assignment ومعاملَي البقية rest والنشر spread والنوعين map الخرائط وset الأطقم والنوعين الخرائط والأطقم ضعيفة الإشارة WeackMap وWeackSet الأطقم ومميزات أخرى كثيرة.

02ES6_browser_support.png

جدول دعم المتصفحات لمعايير ES6. (مصدر)

يمكن استخدام الدوال غير المتزامنة async والتي هي أحد مميزات ES 2017، في جميع المتصفحات الرئيسية.

تتيح الكلمتان المحجوزتان async و await كتابة السلوك غير المتزامن المعتمِد على الوعود بأسلوب نظيف، مع تجنب الحاجة إلى تكوين سلاسل وعود Promises chaining بشكل صريح.

03async_functions_browser_support.png

جدول دعم المتصفحات للدوال غير المتزامنة (المصدر).

كما وصل سريعًا دعم المتصفحات لإضافات ES 2020 الحديثة مثل التسلسل الإختياري optional chaining وعامل الاستبدال اللاغي nullish coalescing، لاحظ الشيفرة التالية.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0

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

سنعمل في هذا المقال مع تطبيق ويب تقدمي PWA بسيط اسمه Fugu Greetings (مستودع التطبيق على github). إنشاء هذا التطبيق كان محاولة لمنح الويب كامل قوة تطبيقات Android أو iOS أو سطح المكتب.

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

04fugu_greetings_sample_app.png

التطبيق المختار كنموذج Fugu Greetings

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

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

اقتباس

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

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

-- مساهمو MDN

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

الطريقة التقليدية التي يمكن أن تفكر فيها لاستيراد ملفات في أحد تطبيقاتك هي استخدام عنصر <input type=file>، حيث تُنشئ بداية العنصر، وتعين نوعه أو الخاصية type إلى'file' وتُضيف أنواع المعيار MIME إلى الخاصية accept، ثم تنقر عليه برمجيًا وتستمتع بالنتيجة.

يمكنك تجريب هذه الميزة في تطبيق Fugu Greetings باختيار صورة معينة إلى التطبيق وملاحظة كيف أنها تُرسم مباشرة في حاوية الرسم canvas.

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
}

وبوجود ميزة استيراد، فكر مطورو التطبيق بميزة تصدير أيضًا تُمكِّن المستخدمين من حفظ بطاقاتهم الترحيبية محليًا.

الطريقة التقليدية التي يمكنك فعلها لحفظ الملفات في تطبيقاتك، هي إنشاء رابط تنزيل بالخاصية download وبعنوان كائن البيانات الثنائية Blob 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 التي تعمل في الخلفية File System Access API لتطبيقك بفتح وإنشاء ملفات ومجلدات، وكذلك تعديل الملفات وحفظها، وذلك بالاستفادة من التابع ()window.chooseFileSystemEntries، لكن عند استخدام هذا التابع تحتاج إلى استيراد مشروط لشيفرات الاستيراد والتصدير (ملفات mjs في المثال التالي) بناء على كون التابع متاحًا أم لا، أو بعبارة أخرى بناء على كون المتصفح يدعم الواجهة File System Access أم لا. موضّح طريقة القيام بذلك في الشيفرة التالية.

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، والبدء باستخدام شيفراتها الخاصة باستيراد وتصدير الملفات،، لنرى سريعًا نمط التحسين التدريجي المتبع في الشيفرة السابقة.

عند طلب صفحة من موقع ويب في المتصفحات التي لا تدعم الواجهة البرمجية File System Access، يتم فقط تحميل السكريبتات القديمة import_image_legacy.mjs و export_image_legacy.mjs التي تحتوي شيفرات التصدير والاستيراد التقليدية. يمكنك رؤية نافذة الشبكة في Firefox و Safari أدناه.

05safar_inspector_network_tab.png

نافذة مراقبة الشبكة لمتصفح Safari.

06chrome_developer_tools_network.png

نافذة شبكة أدوات المطورين لمتصفح Firefox.

لكن لأن Chrome متصفح يدعم الواجهة File System Access، فيتم فقط تحميل السكربتات الجديدة import_image.mjs وexport_image.mjs التي تحتوي شيفرات متقدمة لميزات استيراد وتصدير الملفات في تطبيقات PWAs، بالاعتماد على الواجهة File System Access.

07firefox_developer_tools_network.png

نافذة شبكة أدوات المطورين لمتصفح Chrome.

واجهة الوصول إلى ملفات النظام

حان الآن الوقت لإلقاء نظرة على التنفيذ الفعلي للتحسين السابق، إذا فكرت باستيراد صورة باستخدام واجهة الوصول إلى ملفات النظام، فإنك ستستدعي التابع ()window.chooseFileSystemEntries وتمرر له الخاصية accept التي تعبر من خلالها أنك تريد ملفات صور عبر امتدادات الملفات أو أنواع 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);
  }
};

تُطبَّق الفكرة نفسها عند تنفيذ تصدير الصورة، لكن هذه المرة تحتاج تمرير المعامل type بالقيمة 'save-file' إلى التابع ()ChooseFileSystemEntries، بهذا تحصل على مربع حوار لحفظ الملف، لم يكن هذا ضروريًا عند فتح الملف (استيراد صورة) لأن القيمة 'open-file' هي الافتراضية للمعامل type.

ستعين هنا المعامل accept بشكل مشابه لاستيراد ملف، لكن في التصدير العملية تقتصر على صور PNG فقط، وبدلاً من جلب الملف من المقبض، فإنك هنا تنشئ مجرى قابل للكتابة writable stream عن طريق التابع ()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، فإنه بإمكانك استيراد ملف صورة إلى التطبيق، وتُرسم تلك الصورة مباشرة في حاوية الرسم canvas، كما يمكنك إجراء تعديلاتك على الصورة وحفظها باستخدام مربع حوار حفظ يتيح لك تحديد اسم للملف الذي تريد حفظه وموقع لتخزينه، وذلك كله فقط للمتصفحات التي تدعم الواجهة File System Access.

08file_open_dialog.png

مربع حوار فتح ملف.

09mported_image.png

الصورة المستوردة.

10saving_modified_image.png

حفظ الصورة المعدلة إلى ملف جديد.

واجهتَي مشاركة الويب

قد ترغب ذات مرة بمشاركة بطاقتك الترحيبية، هذا ما أتاحه مطورو Fugu Greetings بالاستفادة من الواجهتين البرمجيتين Web Share (مشاركة الويب) و Web Share Target (مشاركة وُجْهة الويب).

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

11web_share_API_on_safari.png

واجهة مشاركة الويب في متصفح Safari على macOS.

إذا أردت تحقيق ميزة المشاركة هذه لأحد تطبيقاتك، فالشيفرة لفعل ذلك واضحة ومباشِرة، بداية تستدعي التابع ()navigator.share وتعطيه معاملات اختيارية title وtext وurl في كائن كمعامل.

هذا إذا أردت مشاركة رابط المصدر، لكن ماذا لو أردت مشاركة صورة؟ المستوى الأول من واجهة مشاركة الويب لا يدعم هذا حتى الآن، لكن لحسن الحظ فإن المستوى الثاني من واجهة مشاركة الويب أضاف إمكانيات مشاركة الملفات.

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 Greetings لمشاركة الصور هي بداية إعداد كائن data بمصفوفة ملفات files تتكون من كائن بيانات ثنائي واحد، وعنوان title ونص text، بعدها تم استخدام التابع ()navigator.canShare يخبرنا ما إذا كان كائن data الذي نحاول مشاركته يمكن مشاركته تقنيًا عبر المتصفح أم لا (هل المتصفح حديث ويدعم المشاركة أم لا)، إذا أفاد التابع بإمكانية مشاركة الملفات، يمكن استدعاء التابع ()navigator.share كما فعلنا في مشاركة الروابط.

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، فإن تطبيق Fugu Greetings يستدعي الشيفرة السابقة (على فرض أنها محفوظة في ملف بالاسم share.mjs) عبر دالة الاستيراد الديناميكي ()import، وفي المتصفحات التي تحقق شرط واحد فقط من الشرطين مثل نسخ الهاتف المحمول من متصفح Safari، فيُستَخدم الشرط المتاح فقط.

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

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

12choosing_app_to_share_file_to.png

اختيار تطبيق لمشاركة الملف إليه.

13file_attached_to_new_email.png

أُرفِق الملف المشارك برسالة بريد الكتروني جديدة في منشئ الـ Gmail.

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

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

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

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

وكعادة أي تحسين تدريجي، لا تستورد شيفرة انتقاء جهات الاتصال إلا في حال كان المتصفح يدعم بالفعل الواجهة البرمجية Contact Picker.

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

يمكنك تجريب الميزة التي توفرها الواجهة البرمجية Contact Picker في تطبيق Fugu Greetings بالضغط على زر جهات الاتصال contacts في التطبيق ثم اختيار أسماء جهات اتصالك التي تريد أن تُرسم في بطاقتك الترحيبية. لاحظ كيف يقتصر منتقي جهات الاتصال في تطبيق Fugu Greetings على جلب أسماء جهات الاتصال المختارة فقط دون عناوين بريدها أو معلومات أخرى مثل أرقام هواتفها.

14Selecting_2names_with_contact_picker.png

اختيار اسمين من قائمة جهات الاتصال عبر مُنتقي جهات الاتصال.

15the_2names_drawn_onto_card.png

رُسمَ الاسمين في بطاقة الترحيب.

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

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

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

يمكن لكل عنصر من عناصر الحافظة ClipboardItem الاحتفاظ بمحتوياته في أنواع MIME مختلفة، لذلك فإنك للحصول على كائن blob لعنصر الحافظة فإنك بحاجة للمرور على قائمة الأنواع المتاحة افتراضيًا، ولكل نوع تستدعي الدالة ()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);
  }
};

وبالطبع كأي ميزة تحسين تدريجي، تفعل هذا فقط للمتصفحات الداعمة للواجهة البرمجية Clipboard:

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

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

16clipboard_permission_prompt.png

طلب الإذن بالوصول إلى محتوى الحافظة.

واجهة الشارات

كون تطبيق Fugu Greetings قابلًا للتثبيت installable، له أيقونة خاصة يمكن للمستخدمين تثبيتها في شريط التطبيقات app dock أو الشاشة الرئيسية، استغل مطورو Fugu Greetings هذه الأيقونة لإظهار شارة انطلاقًا منها، تحتوي عدّاد نقرات قلم الرسم pen strokes counter، وذلك بالاستفادة من الواجهة البرمجية Padging التي تعمل في الخلفية Badging API.

إذا أردت إضافة شارات في تطبيقاتك، ففيما يلي طريقة استخدام الواجهة البرمجية Padging لفعل ذلك، إذ يمكنك ببساطة إضافة مستمع حدث event listener يعيِّن شارة الأيقونة بقيمة عدّاد نقرات القلم التي تزيد عند تحقق الحدث pointerdown (ينطلق هذا الحدث عندما يصبح مؤشر الرسم فعال)، وعند مسح محتوى حاوية الرسم يمكنك إعادة تعيين العداد إلى صفر لتُزال الشارة.

let strokes = 0;

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

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

وكعادة أي تحسين تدريجي، حمّل هذا المنطق (الشيفرة) فقط للمتصفحات التي تدعم الواجهة البرمجية Badging:

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

ولتجريب ذلك قمت في هذا المثال برسم الأرقام من واحد إلى سبعة، باستخدام ضغطة قلم واحدة لكل رقم، لذا فإن عداد الشارة على الأيقونة الآن هو 7.

17drawing_numbers_from_1_to_7.png

رسم الأرقام من 1 إلى 7 بضغطة قلم واحدة لكل رقم.

18pen_strokes_counter_as_app_icon_badge.png

عدّاد ضغطات القلم ممثّل شارة لأيقونة التطبيق.

واجهة المزامنة الدورية للخلفية

هل تريد أن تبدأ كل يومٍ جديدٍ بمحتوى جديد؟ من الميزات الرائعة لتطبيق Fugu Greetings أنه يمكن أن يلهمك كل يوم بصورة خلفية جديدة لإنشاء بطاقتك، يستخدم التطبيق لهذه الميزة الواجهة البرمجية Periodic Background Sync التي تعمل في الخلفية Periodic Background Sync API، وإذا أردت فعل هذا لتطبيقاتك فإن عليك إضافة حدث مزامنة دوري periodic sync event في تسجيل عامل الخدمة service worker registration عبر التابع ()register، عملية تسجيل الحدث تتطلب وسم tag يستمع عامل الخدمة إليه، وحد أدنى للفاصل الزمني للمزامنة interval. ممرر هنا حدث المزامنة بوسم اسمه '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، وتُنشر النتيجة لجميع العملاء، لتُحدَّث حاويات رسمهم canvases وذاكراتهم المؤقته caches بها.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

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

// في طرف العميل
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// في عامل الخدمة
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

وبتجريب هذه الميزة في تطبيق Fugu Greetings، يؤدي الضغط على زر Wallpaper إلى إظهار صورة بطاقة الترحيب لهذا اليوم التي تُحدَّث يوميًا عبر الواجهة البرمجية Periodic Background Sync.

19pressing_wallpaper_button.png

نقر زر Wallpaper يؤدي لعرض صورة اليوم.

واجهة مُطْلِق الإشعارات

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

تمكن مطورو Fugu Greeting من إضافة هذه الميزة من خلال الواجهة البرمجية Notification Triggers التي تعمل في الخلفية 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، وذلك بتفعيل خيار Reminder (تذكير) الذي يَطلب منك إدخال متى تريد أن تُنبّه بشأن إنهاء بطاقتك، عندما يحين ذلك الموعد ستحصل بالفعل على تنبيه لإنهائها.

20Scheduling_local_reminder.png

جدولة إشعار محلي للتذكير بإنهاء بطاقة التهنئة.

عندما يُطلَق إشعار مجدول في Fugu Greetings، يُعرَض تمامًا مثل أي إشعار آخر، لكن الفرق أنه لا يتطلب اتصالاً بالشبكة كما قلنا.

21triggered_notification_in_notification_center.png

الإشعار المطلَق يظهر في مركز إشعارات macOS.

واجهة قفل الإيقاظ

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

إذا أردت إضافة هذه الميزة لتطبيقاتك، فإن أول ما عليك فعله هو الحصول على قفل إيقاظ باستخدام التابع ()navigator.wakelock.request، وبالتحديد للحصول على قفل إيقاظ للشاشة، يمكنك تمرير نص '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);

وكعادة التحسين التدريجي، تُحمِّل شيفرة هذه الميزة فقط إذا كان المتصفح يدعم الواجهة البرمجية Wake Lock.

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

هذه الواجهة مستخدمة في Fugu Greetings لتطبيق ميزة إبقاء الشاشة يقظة إذا طلب المستخدم ذلك، يمكنك تجريب هذه الميزة عبر تفعيل مربع الاختيار Insomnia الذي يُبقي الشاشة يقظة عند ابتعادك عن التطبيق بدلًا من إيقاف تشغيلها.

22insomnia_checkbox_keeps_app_awake.png

مربع اختيار إبقاء الشاشة يقظة.

واجهة كشف التوقف

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

تسمح الواجهة البرمجية Idle Detection التي تعمل في الخلفية Idle Detection API بالكشف عن توقف استخدام تطبيقك.

على صعيد Fugu Greetings إذا كان المستخدم في وضع عدم استخدام التطبيق لفترة طويلة جدًا، فإنه يُعاد تعيين التطبيق إلى الحالة الابتدائية وتُمسح حاوية الرسم canvas.

إذا أردت استخدام هذه الميزة لتطبيقاتك فعليك كخطوة أولى لاستخدام الواجهة Idle Detection أن تتأكد من منح المتصفح الإذن للكشف عن توقف استخدام التطبيق 'idle-detection'، وإلا فأنت بحاجة إلى طلب ذلك الإذن عبر التابع ()IdleDetector.requestPermission، لاحظ أن استدعاء هذه التابع يتطلب قبول من المستخدم.

// 'idle-detection' تأكد من الحصول على الإذن
const state = await IdleDetector.requestPermission();
if (state !== 'granted') {
  // Need to request permission first.
  return console.log('Idle detection permission not granted.');
}

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

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

23phemeral_checked_and_user_is_idle.png

حاوية الرسم ممسوحة كون خيار Ephemeral مفعّل والمستخدم متوقف عن استخدام التطبيق لفترة طويلة.

الخاتمة

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

24request_only_files_with_code_supported.png

قد يبدو تطبيق Fugu Greetings مختلفًا قليلاً في كل متصفح نظرًا لعدم دعم جميع المنصات لجميع الميزات السابقة، ولكن الوظائف الأساسية للتطبيق موجودة في جميع المتصفحات ويتم تحسينها تدريجيًا وفقًا لإمكانيات كل متصفح.

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

25fugu_greetings_on_android_chrome.png

نسخة Android من تطبيق Fugu Greetings في متصفح Chrome.

26fugu_greetings_on_desktop_safari.png

نسخة سطح المكتب من تطبيق Fugu Greetings في متصفح Safari.

27fugu_greetings_on_desktop_chrome.png

نسخة سطح المكتب من تطبيق Fugu Greetings في متصفح Chrome.

إذا كنت مهتمًا بتطبيق Fugu Greetings، يمكنك الاطلاع على شيفرته كاملةً على منصة GitHub وتشكيل بطاقتك الترحيبية باستخدامه.

28fugu_greetings_gitHub.png

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

ترجمة -وبتصرف- للمقال Progressively enhance your Progressive Web App من موقع web.dev.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...