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

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

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

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

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

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

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

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

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

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

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

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

أشكر Christian Liebel و Hemanth HM اللذَين ساهما في إنشاء تطبيق Fugu Greetings. تمت مراجعة هذه المقالة من قِبل جو ميدلي و كايسي باسكيز. ساعدني Jake Archibald في معرفة الحالة باستخدام import() الديناميكي في سياق worker الخدمة.