تصميم مواقع إلكترونية متوافقة مع المتصفّحات الحديثة وتحسينها تدريجيًا كما كان الحال في عام 2003
تاريخ النشر: 29 حزيران (يونيو) 2020
في آذار (مارس) 2003، أذهل فينك وستيف تشامبيون عالم تصميم الويب بمفهوم التحسين التدريجي، وهو استراتيجية لتصميم الويب تركّز على تحميل المحتوى الأساسي لصفحة الويب أولاً، ثم إضافة طبقات أكثر دقة وصعوبة من الناحية الفنية للعرض والميزات فوق المحتوى. في عام 2003، كان التحسين التدريجي يركّز على استخدام ميزات CSS الحديثة في ذلك الوقت، ولغة JavaScript غير المزعجة، وحتى الرسومات الموجّهة القابلة للتوسيع. في عام 2020 وما بعده، يركّز التحسين التدريجي على استخدام إمكانات المتصفّحات الحديثة.
JavaScript الحديثة
بالحديث عن JavaScript، إنّ حالة توافق المتصفّح مع أحدث ميزات JavaScript الأساسية ES 2015 رائعة. يتضمّن المعيار الجديد الوعود والوحدات والفئات والعبارات النموذجية الحرفية ووظائف الأسهم وlet وconst والمعلمات التلقائية والمولّدات وتعيين التفكيك وعمليات الباقي والتوزيع وMap/Set وWeakMap/WeakSet وغير ذلك الكثير.
جميعها متاحة.
يمكن استخدام الدوال غير المتزامنة، وهي إحدى ميزات ES 2017 المفضّلة لديّ، في جميع المتصفحات الرئيسية.
تتيح الكلمتان الرئيسيتان async وawait كتابة سلوك غير متزامن يستند إلى الوعود بأسلوب أكثر وضوحًا، ما يغني عن الحاجة إلى إعداد سلاسل الوعود بشكل صريح.
وحتى الإضافات الحديثة جدًا إلى لغة ES 2020، مثل optional chaining و nullish coalescing أصبحت متاحة بسرعة كبيرة. عندما يتعلق الأمر بميزات JavaScript الأساسية، لا يمكن أن تكون أفضل مما هي عليه.
على سبيل المثال:
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah',
},
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
تطبيق العيّنة: Fugu Greetings
في هذا المستند، سأستخدم تطبيق ويب تقدّميًا يُسمى Fugu Greetings (GitHub). اسم هذا التطبيق هو إشارة إلى مشروع Fugu 🐡، وهو جهد يهدف إلى منح الويب جميع إمكانات تطبيقات Android وiOS وأجهزة الكمبيوتر. يمكنك الاطّلاع على مزيد من المعلومات حول المشروع على صفحته المقصودة.
Fugu Greetings هو تطبيق للرسم يتيح لك إنشاء بطاقات معايدة افتراضية وإرسالها إلى أحبائك. وهي تجسّد المفاهيم الأساسية لتطبيقات الويب التقدّمية. يتميّز هذا التطبيق بأنّه موثوق ويمكن استخدامه بلا اتصال بالإنترنت، لذا يمكنك الاستفادة منه حتى في حال عدم توفّر شبكة. يمكن أيضًا تثبيتها على الشاشة الرئيسية للجهاز، كما أنّها تتكامل بسلاسة مع نظام التشغيل كتطبيق مستقل.
التحسين التدريجي
بعد أن أوضحنا هذه النقطة، حان الوقت للتحدّث عن التحسين التدريجي. يعرّف مسرد مصطلحات MDN Web Docs هذا المفهوم على النحو التالي:
التحسين التدريجي هو فلسفة تصميم توفّر أساسًا من المحتوى والوظائف الأساسية لأكبر عدد ممكن من المستخدمين، مع تقديم أفضل تجربة ممكنة فقط لمستخدمي المتصفحات الأحدث التي يمكنها تشغيل جميع الرموز المطلوبة.
يتم استخدام عملية رصد الميزات بشكل عام لتحديد ما إذا كان بإمكان المتصفّحات التعامل مع وظائف أكثر حداثة، بينما يتم غالبًا استخدام الرموز البرمجية polyfill لإضافة الميزات الناقصة باستخدام JavaScript.
[…]
التحسين التدريجي هو أسلوب مفيد يتيح لمطوّري الويب التركيز على تطوير أفضل المواقع الإلكترونية الممكنة مع الحرص على أن تعمل هذه المواقع الإلكترونية على العديد من وكلاء المستخدمين غير المعروفين. التكيّف مع الإصدارات الأقدم هو مفهوم ذو صلة، لكنّه ليس الشيء نفسه، وغالبًا ما يُنظر إليه على أنّه يسير في الاتجاه المعاكس للتحسين المتدرّج. في الواقع، كلا النهجَين صالحان ويمكن أن يكمّل أحدهما الآخر في كثير من الأحيان.
المساهمون في شبكة مطوّري Mozilla
قد يكون إنشاء بطاقة تهنئة من البداية أمرًا شاقًا.
لماذا لا نقدّم ميزة تتيح للمستخدمين استيراد صورة والبدء من هناك؟
في الطريقة التقليدية، كان عليك استخدام العنصر
<input type=file>
لتحقيق ذلك.
أولاً، عليك إنشاء العنصر، وضبط type على 'file'، وإضافة أنواع MIME إلى السمة accept، ثم النقر عليه برمجيًا والاستماع إلى التغييرات. عند اختيار صورة، يتم استيرادها مباشرةً إلى لوحة العرض.
const importImage = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
عند توفّر ميزة الاستيراد، من المفترض أن تتوفّر ميزة التصدير
ليتمكّن المستخدمون من حفظ بطاقات التهنئة محليًا.
الطريقة التقليدية لحفظ الملفات هي إنشاء رابط ارتساء باستخدام السمة download، مع استخدام عنوان URL الخاص بالملفات الثنائية الكبيرة (blob) كقيمة href.
يمكنك أيضًا "النقر" عليه آليًا لتفعيل التنزيل،
ولمنع تسرّب الذاكرة، نأمل ألا تنسى إبطال عنوان URL الخاص بكائن blob.
const exportImage = async (blob) => {
const a = document.createElement('a');
a.download = 'fugu-greeting.png';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
ولكن مهلاً. من الناحية الذهنية، أنت لم "تنزّل" بطاقة تهنئة، بل "حفظتها". بدلاً من عرض مربّع حوار "حفظ" يتيح لك اختيار مكان حفظ الملف، نزّل المتصفّح بطاقة التهنئة مباشرةً بدون أي تفاعل من المستخدم، ووضعها في مجلد "عمليات التنزيل". هذا ليس جيدًا.
ماذا لو كانت هناك طريقة أفضل؟ ماذا لو كان بإمكانك فتح ملف محلي وتعديله ثم حفظ التعديلات، إما في ملف جديد أو في الملف الأصلي الذي فتحته في البداية؟ اتّضح أنّ هناك واجهة برمجة تطبيقات تتيح ذلك، وهي File System Access API التي تتيح لك فتح الملفات والأدلة وإنشائها وتعديلها وحفظها.
إذًا، كيف يمكنني رصد ميزة في واجهة برمجة تطبيقات؟
تعرض واجهة برمجة التطبيقات 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، يتم تحميل النصوص البرمجية القديمة.
ومع ذلك، في Chrome، وهو متصفّح يتوافق مع واجهة برمجة التطبيقات، يتم تحميل النصوص البرمجية الجديدة فقط.
أصبح ذلك ممكنًا بشكل أنيق بفضل
الاستيراد الديناميكي import() الذي تتيحه جميع المتصفحات الحديثة.
كما ذكرتُ سابقًا، تبدو الأمور واعدة هذه الأيام.
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، يمكنني فتح ملف كما كان من قبل. يتم رسم الملف المستورَد مباشرةً على لوحة العرض. يمكنني إجراء التعديلات وحفظها أخيرًا باستخدام مربّع حوار الحفظ الفعلي، حيث يمكنني اختيار اسم الملف وموقع التخزين. أصبح الملف الآن جاهزًا للحفظ إلى الأبد.
واجهتا Web Share وWeb Share Target API

بعيدًا عن تخزينها إلى الأبد، قد أرغب في مشاركة بطاقة التهنئة. تتيح لي Web Share API وWeb Share Target API تنفيذ ذلك، وقد أتاحت أنظمة التشغيل على الأجهزة الجوّالة، ومؤخرًا على أجهزة الكمبيوتر، آليات مشاركة مضمّنة.
على سبيل المثال، يتم تشغيل ورقة المشاركة في متصفّح Safari على أجهزة الكمبيوتر المكتبي التي تعمل بنظام التشغيل macOS عندما ينقر المستخدم على مشاركة المقالة على مدونتي. يمكنك مشاركة رابط المقالة مع صديق باستخدام تطبيق "الرسائل" على macOS.
لتحقيق ذلك، أستدعي navigator.share() وأمرّر إليه title وtext وurl اختيارية في عنصر.
ولكن ماذا لو أردت إرفاق صورة؟ لا يتيح المستوى 1 من Web Share API ذلك بعد.
الخبر السار هو أنّ المستوى 2 من Web Share أضاف إمكانات مشاركة الملفات.
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، وستظهر أداة إنشاء الرسائل الإلكترونية مع إرفاق الصورة.
Contact Picker API
أريد الآن التحدّث عن جهات الاتصال، أي دفتر العناوين على الجهاز أو تطبيق إدارة جهات الاتصال. عند كتابة بطاقة تهنئة، قد لا يكون من السهل دائمًا كتابة اسم شخص بشكل صحيح. على سبيل المثال، لديّ صديق اسمه "سيرغي" يفضّل كتابة اسمه بأحرف كيريلية. أستخدم لوحة مفاتيح ألمانية QWERTZ ولا أعرف كيف أكتب اسمه. هذه مشكلة يمكن أن تحلّها واجهة برمجة التطبيقات "أداة اختيار جهات الاتصال". بما أنّ صديقي محفوظ في تطبيق جهات الاتصال على هاتفي، يمكنني الوصول إلى جهات الاتصال من الويب باستخدام واجهة برمجة التطبيقات 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 استخدام النصوص والصور. سأشرح لك كيف أضفت ميزة النسخ واللصق إلى تطبيق 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 ما إذا كنت أريد السماح للتطبيق بالاطّلاع على النصوص والصور في الحافظة.
أخيرًا، بعد قبول الإذن، يتم لصق الصورة في التطبيق. والعكس صحيح أيضًا. أريد نسخ بطاقة تهنئة إلى الحافظة. عندما أفتح تطبيق "المعاينة" وأضغط على ملف (File) ثم على جديد من الحافظة (New from Clipboard)، يتم لصق بطاقة التهنئة في صورة جديدة بلا عنوان.
Badging API
Badging API هي واجهة برمجة تطبيقات أخرى مفيدة.
بما أنّ Fugu Greetings هو تطبيق ويب تقدّمي قابل للتثبيت، يتضمّن بالطبع رمز تطبيق يمكن للمستخدمين وضعه على شريط التطبيقات أو الشاشة الرئيسية.
إحدى الطرق الممتعة لتوضيح كيفية عمل واجهة برمجة التطبيقات هي استخدامها في 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');
}
في هذا المثال، رسمتُ الأرقام من واحد إلى سبعة باستخدام ضربة قلم واحدة لكل رقم. أصبح عدد الشارات على الأيقونة الآن سبعًا.
Periodic Background Sync API
هل تريد أن تبدأ كل يوم بشيء جديد؟ من الميزات الرائعة في تطبيق Fugu Greetings أنّه يمكنه أن يقدّم لك الإلهام كل صباح من خلال صورة خلفية جديدة لبدء بطاقة التهنئة. يستخدم التطبيق Periodic Background Sync API لتحقيق ذلك.
الخطوة الأولى هي تسجيل حدث مزامنة دورية في تسجيل عامل الخدمة. تستمع هذه الخدمة إلى علامة مزامنة باسم 'image-of-the-day'
ويبلغ الحد الأدنى للفاصل الزمني يومًا واحدًا،
ما يتيح للمستخدم الحصول على صورة خلفية جديدة كل 24 ساعة.
const registerPeriodicBackgroundSync = async () => {
const registration = await navigator.serviceWorker.ready;
try {
registration.periodicSync.register('image-of-the-day-sync', {
// An interval of one day.
minInterval: 24 * 60 * 60 * 1000,
});
} catch (err) {
console.error(err.name, err.message);
}
};
الخطوة الثانية هي الاستماع إلى حدث periodicsync في عامل الخدمة.
إذا كانت علامة الحدث هي 'image-of-the-day'، أي العلامة التي تم تسجيلها من قبل،
يتم استرداد صورة اليوم باستخدام الدالة getImageOfTheDay()،
ويتم نشر النتيجة إلى جميع العملاء، حتى يتمكّنوا من تعديل لوحات العرض
وذاكرات التخزين المؤقت.
self.addEventListener('periodicsync', (syncEvent) => {
if (syncEvent.tag === 'image-of-the-day-sync') {
syncEvent.waitUntil(
(async () => {
const blob = await getImageOfTheDay();
const clients = await self.clients.matchAll();
clients.forEach((client) => {
client.postMessage({
image: blob,
});
});
})()
);
}
});
مرة أخرى، هذه تحسينات تدريجية، لذا لا يتم تحميل الرمز إلا عندما يتيح المتصفح استخدام واجهة برمجة التطبيقات.
وينطبق ذلك على كلّ من رمز العميل ورمز عامل الخدمة.
في المتصفّحات غير المتوافقة، لا يتم تحميل أي منهما.
لاحظ كيف أنّني أستخدم 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.
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، يتم عرضه مثل أي إشعار آخر، ولكن كما ذكرتُ سابقًا، لا يتطلّب ذلك اتصالاً بالشبكة.
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، يتوفّر مربّع اختيار الأرق، وعند وضع علامة فيه، تبقى الشاشة نشطة.
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، تتم إزالة محتوى لوحة الرسم عندما يتم وضع علامة في مربّع الاختيار مؤقت ويبقى المستخدم غير نشط لفترة طويلة جدًا.
الخاتمة
يا لها من رحلة. تتضمّن عيّنة التطبيق هذه الكثير من واجهات برمجة التطبيقات، وتذكَّر أنّني لا أفرض على المستخدم أبدًا دفع تكلفة التنزيل للحصول على ميزة لا يتوافق معها متصفّحه. باستخدام التحسين التدريجي، أتأكّد من تحميل الرمز ذي الصلة فقط. وبما أنّ الطلبات غير مكلفة مع HTTP/2، من المفترض أن يكون هذا النمط مناسبًا للعديد من التطبيقات، مع أنّه قد يكون من الأفضل استخدام أداة تجميع للتطبيقات الكبيرة جدًا.
قد يبدو التطبيق مختلفًا بعض الشيء على كل متصفّح لأنّ بعض الأنظمة الأساسية لا تتيح جميع الميزات، ولكن الوظائف الأساسية تكون متاحة دائمًا، ويتم تحسينها تدريجيًا وفقًا لإمكانات المتصفّح المعيّن. وقد تتغيّر هذه الإمكانات حتى في المتصفّح نفسه، اعتمادًا على ما إذا كان التطبيق يعمل كتطبيق مثبَّت أو في علامة تبويب في المتصفّح.
يمكنك إنشاء نسخة من Fugu على GitHub.
يعمل فريق Chromium جاهدًا على تحسين واجهات Fugu API المتقدّمة. من خلال تطبيق التحسين التدريجي عند إنشاء تطبيقي، أضمن حصول الجميع على تجربة أساسية جيدة وثابتة، ولكن يحصل المستخدمون الذين يستعملون متصفحات تتوافق مع المزيد من واجهات برمجة التطبيقات لمنصة الويب على تجربة أفضل. أتطلّع إلى رؤية ما ستفعله باستخدام التحسين التدريجي في تطبيقاتك.
الإقرارات
أودّ أن أشكر كريستيان ليبيل وهيمانث إتش إم اللذين ساهم كل منهما في تطوير Fugu Greetings.
راجع هذا المستند جو ميدلي وكايس باسكس.
ساعدني جيك أرشيبالد في معرفة حالة import() الديناميكية في سياق عامل الخدمة.