إنشاء تطبيقات متوافقة مع المتصفّحات الحديثة وتحسينها تدريجيًا كما لو كان عام 2003
في آذار (مارس) 2003، فاجأ فينك و ستيف شاميون عالم تصميم الويب بمفهوم التحسين التدريجي، وهو استراتيجية لتصميم الويب تركّز على تحميل المحتوى الأساسي لصفحة الويب أولاً، ثم تضيف تدريجيًا طبقات أكثر دقة ودقة من الناحية الفنية للعرض والميزات فوق المحتوى. في عام 2003، كان التحسين التدريجي يدور حول استخدام ميزات CSS الحديثة ولغة JavaScript غير المزعجة، وحتى الرسومات الموجّهة التي يمكن تغيير حجمها. في العام 2020 وما بعده، يعتمد تحسين الأداء التدريجي على استخدام إمكانات المتصفّحات الحديثة.
لغة JavaScript الحديثة
في ما يتعلّق بـ JavaScript، يتوافق المتصفّح بشكلٍ رائع مع أحدث ميزات JavaScript الأساسية في ES 2015.
يتضمّن المعيار الجديد الوعود والوحدات والفئات والنصوص الثابتة للنماذج والدوالّ ذات الأسهم وlet
وconst
والمَعلمات التلقائية وأدوات إنشاء السلاسل وعمليات الربط غير القابلة للتغيير وعمليات التوسيع والتوزيع وMap
/Set
WeakMap
/WeakSet
وغير ذلك الكثير.
جميعها متوافقة.
يمكن استخدام الدوالّ غير المتزامنة، وهي إحدى ميزات ES 2017 وإحدى المفضّلات لديّ شخصيًا، في جميع المتصفحات الرئيسية.
تتيح الكلمات الرئيسية async
وawait
كتابة سلوك غير متزامن ومستند إلى الوعد
بأسلوب أنظف، ما يتجنّب الحاجة إلى ضبط سلاسل الوعود بشكل صريح.
وحتى الإضافات الحديثة جدًا في لغة 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
نموذج التطبيق: Fugu Greetings
في هذه المقالة، أستخدم تطبيقًا بسيطًا متوافقًا مع الأجهزة الجوّالة (PWA) يُسمى Fugu Greetings (GitHub). تم اختيار اسم هذا التطبيق تكريمًا لمشروع Fugu 🐡، وهو جهد يهدف إلى منح الويب كل الميزات الرائعة لتطبيقات Android/iOS/أجهزة الكمبيوتر المكتبي. يمكنك الاطّلاع على مزيد من المعلومات عن المشروع على صفحته المقصودة.
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 أدناه.
ومع ذلك، في متصفّح 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 قد أضاف إمكانات مشاركة الملفات.
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 الألمانية ولا أعرف كيفية كتابة اسمه. هذه مشكلة يمكن حلّها باستخدام واجهة برمجة التطبيقات 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 عمّا إذا كنت أريد السماح للتطبيق بالاطّلاع على النصوص والصور في الحافظة.
أخيرًا، بعد قبول الإذن، يتم لصق الصورة في التطبيق. وينطبق ذلك أيضًا على العكس. سنسخ بطاقة تهنئة إلى الحافظة. عندما أفتح "المعاينة" وأنقر على ملف ثم جديد من الحافظة، يتم لصق بطاقة المعايدة في صورة جديدة بلا عنوان.
واجهة برمجة التطبيقات 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');
}
في هذا المثال، رسمتُ الأرقام من واحد إلى سبعة باستخدام ضغطة قلم واحدة لكل رقم. أصبح عدد الشارات على الرمز الآن سبعة.
واجهة برمجة التطبيقات 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 Greetings، يمكنك العثور عليه وإنشاء نسخة منه على GitHub.
يعمل فريق Chromium جاهدًا على تحسين واجهات برمجة التطبيقات المتقدّمة في Fugu. من خلال تطبيق ميزة "التحسين التدريجي" في تطوير تطبيقي، أحرص على أن يحصل الجميع على تجربة أساسية جيدة وقوية، وعلى أن يحصل مستخدمو المتصفّحات التي تتوافق مع المزيد من واجهات برمجة التطبيقات لمنصّة الويب على تجربة أفضل. نحن نتطلّع إلى معرفة ما ستفعله باستخدام ميزة "التحسين التدريجي" في تطبيقاتك.
الشكر والتقدير
أشكر Christian Liebel و
Hemanth HM اللذَين ساهما في تطبيق Fugu Greetings.
تمت مراجعة هذه المقالة من قِبل جو ميدلي و
كايسي باسكيز.
ساعدني Jake Archibald في معرفة الحالة
باستخدام import()
الديناميكي في سياق worker الخدمة.