استكشاف إمكانات المتصفّح الجديدة والقادمة لتطبيق الويب التقدّمي: From Fugu With Love

1. قبل البدء

تطبيقات الويب التقدّمية (PWA) هي نوع من برامج التطبيقات التي يتم توفيرها من خلال الويب، وهي مصمَّمة باستخدام تكنولوجيات الويب الشائعة، بما في ذلك HTML وCSS وJavaScript. وهي مصمّمة للعمل على أي نظام أساسي يستخدم متصفّحًا متوافقًا مع المعايير.

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

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

المتطلبات الأساسية

في هذا الدرس التطبيقي، يجب أن تكون على دراية بلغة JavaScript الحديثة، وخاصةً الوعود وasync/await. بما أنّ بعض خطوات الدرس العملي غير متوافقة مع جميع المنصات، من المفيد أن يكون لديك أجهزة إضافية في متناول يدك لإجراء الاختبار، مثل هاتف Android أو كمبيوتر محمول يستخدم نظام تشغيل مختلفًا عن الجهاز الذي تعدّل فيه الرمز. كبديل للأجهزة الحقيقية، يمكنك محاولة استخدام محاكيات، مثل محاكي Android أو الخدمات على الإنترنت، مثل BrowserStack التي تتيح لك إجراء الاختبار من جهازك الحالي. وإلا، يمكنك أيضًا تخطّي أي خطوة، فهي لا تعتمد على بعضها البعض.

ما ستنشئه

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

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

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

المتطلبات

المتصفّحات المتوافقة تمامًا في الوقت الحالي هي:

ننصح باستخدام قناة الإصدارات التجريبية المحدّدة.

2. Project Fugu

تم تصميم تطبيقات الويب التقدّمية (PWA) وتحسينها باستخدام واجهات برمجة تطبيقات حديثة لتقديم إمكانات وموثوقية محسّنة وإمكانية التثبيت، مع إمكانية الوصول إلى أي مستخدم على الويب في أي مكان في العالم باستخدام أي نوع من الأجهزة.

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

لهذا السبب، فإنّ الاسم الرمزي الداخلي لمشروع "إمكانات الويب" (الذي تعمل الشركات المعنية على تطوير واجهات برمجة التطبيقات الجديدة فيه) هو Project Fugu.

تتيح إمكانات الويب للمؤسسات الكبيرة والصغيرة اليوم إنشاء حلول مستندة إلى المتصفح فقط، ما يتيح في كثير من الأحيان نشرًا أسرع بتكاليف تطوير أقل مقارنةً بالمسار الخاص بالمنصة.

3- البدء

نزِّل أحد المتصفّحَين، ثم اضبط علامة وقت التشغيل التالية 🚩 من خلال الانتقال إلى about://flags، وهي تعمل في كل من Chrome وEdge:

  • #enable-experimental-web-platform-features

بعد تفعيلها، أعِد تشغيل المتصفّح.

ستستخدم المنصة Glitch، لأنّها تتيح لك استضافة تطبيق الويب التقدّمي ولأنّها تتضمّن محررًا جيدًا. يتيح Glitch أيضًا الاستيراد والتصدير إلى GitHub، لذلك لا يتم فرض قيود على استخدام بائع معيّن. انتقِل إلى fugu-paint.glitch.me لتجربة التطبيق. هذا التطبيق هو تطبيق أساسي للرسم 🎨 ستحسّنه خلال تجربة البرمجة.

تطبيق ويب تقدّمي أساسي من Fugu Greetings يتضمّن لوحة قماشية كبيرة مرسوم عليها كلمة "Google".

بعد تجربة التطبيق، يمكنك إعادة مزجه لإنشاء نسختك الخاصة التي يمكنك تعديلها. سيبدو عنوان URL للريمكس الخاص بك على النحو التالي: glitch.com/edit/#!/bouncy-candytuft (سيكون "bouncy-candytuft" شيئًا آخر بالنسبة إليك). يمكن الوصول إلى هذا الريمكس مباشرةً في جميع أنحاء العالم. سجِّل الدخول إلى حسابك الحالي أو أنشِئ حسابًا جديدًا على Glitch لحفظ عملك. يمكنك الاطّلاع على تطبيقك من خلال النقر على الزر "🕶 عرض"، وسيكون عنوان URL للتطبيق المستضاف على النحو التالي: bouncy-candytuft.glitch.me (يُرجى ملاحظة .me بدلاً من .com كنطاق المستوى الأعلى).

أنت الآن جاهز لتعديل تطبيقك وتحسينه. عند إجراء تغييرات، ستتم إعادة تحميل التطبيق وستظهر التغييرات مباشرةً.

بيئة التطوير المتكاملة Glitch تعرض تعديل مستند HTML.

يُفضّل إكمال المهام التالية بالترتيب، ولكن كما ذكرنا أعلاه، يمكنك دائمًا تخطّي خطوة إذا لم يكن لديك جهاز متوافق. تذكَّر أنّ كل مهمة تحمل إما الرمز 🐟، وهو سمكة مياه عذبة غير ضارة، أو الرمز 🐡، وهو سمكة فوغو "يجب التعامل معها بحذر"، لتنبيهك إلى مدى تجريبية الميزة.

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

يتم تسجيل توافق واجهة برمجة التطبيقات في "وحدة التحكّم" ضمن "أدوات مطوّري البرامج".

4. 🐟 إضافة دعم Web Share API

إنّ إنشاء رسومات رائعة لا فائدة منه إذا لم يكن هناك من يقدّرها. أضِف ميزة تتيح للمستخدمين مشاركة رسوماتهم مع العالم في شكل بطاقات تهنئة.

تتيح Web Share API مشاركة الملفات، وكما تعلم، فإنّ File هو مجرد نوع معيّن من Blob. لذلك، في الملف المسمّى share.mjs، استورِد زر المشاركة ودالة ملائمة toBlob() تحوّل محتوى لوحة الرسم إلى كائن ثنائي كبير وأضِف وظيفة المشاركة وفقًا للرمز البرمجي أدناه.

إذا نفّذت ذلك ولكن لم يظهر لك الزر، يعود السبب إلى أنّ المتصفّح الذي تستخدمه لا ينفّذ Web Share API.

import { shareButton, toBlob } from './script.mjs';

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);
  }
};

shareButton.style.display = 'block';
shareButton.addEventListener('click', async () => {
  return share('Fugu Greetings', 'From Fugu With Love', await toBlob());
});

5- 🐟 إضافة دعم Web Share Target API

يمكن للمستخدمين الآن مشاركة بطاقات التهنئة التي تم إنشاؤها باستخدام التطبيق، ولكن يمكنك أيضًا السماح للمستخدمين بمشاركة الصور مع تطبيقك وتحويلها إلى بطاقات تهنئة. يمكنك استخدام Web Share Target API لإجراء ذلك.

في ملف Web Application Manifest، عليك إخبار التطبيق بنوع الملفات التي يمكنك قبولها وعنوان URL الذي يجب أن يستدعيه المتصفّح عند مشاركة ملف واحد أو عدة ملفات. يوضّح المقتطف أدناه من الملف manifest.webmanifest ذلك.

{
  "share_target": {
    "action": "./share-target/",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "files": [
        {
          "name": "image",
          "accept": ["image/jpeg", "image/png", "image/webp", "image/gif"]
        }
      ]
    }
  }
}

بعد ذلك، يتعامل عامل الخدمة مع الملفات المستلَمة. عنوان URL ./share-target/ غير متوفّر في الواقع، بل يتصرّف التطبيق بناءً عليه في معالج fetch ويعيد توجيه الطلب إلى عنوان URL الجذر من خلال إضافة مَعلمة طلب بحث ?share-target:

self.addEventListener('fetch', (fetchEvent) => {
  /* 🐡 Start Web Share Target */
  if (
    fetchEvent.request.url.endsWith('/share-target/') &&
    fetchEvent.request.method === 'POST'
  ) {
    return fetchEvent.respondWith(
      (async () => {
        const formData = await fetchEvent.request.formData();
        const image = formData.get('image');
        const keys = await caches.keys();
        const mediaCache = await caches.open(
          keys.filter((key) => key.startsWith('media'))[0],
        );
        await mediaCache.put('shared-image', new Response(image));
        return Response.redirect('./?share-target', 303);
      })(),
    );
  }
  /* 🐡 End Web Share Target */

  /* ... */
});

عند تحميل التطبيق، يتحقّق مما إذا تم ضبط مَعلمة طلب البحث هذه، وإذا كان الأمر كذلك، يرسم الصورة المشترَكة على لوحة العرض ويمحوها من ذاكرة التخزين المؤقت. يحدث كل هذا في script.mjs:

const restoreImageFromShare = async () => {
  const mediaCache = await getMediaCache();
  const image = await mediaCache.match('shared-image');
  if (image) {
    const blob = await image.blob();
    await drawBlob(blob);
    await mediaCache.delete('shared-image');
  }
};

يتم استخدام هذه الدالة بعد ذلك عند بدء تشغيل التطبيق.

if (location.search.includes('share-target')) {
  restoreImageFromShare();
} else {
  drawDefaultImage();
}

6. 🐟 إضافة إمكانية استيراد الصور

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

أولاً، اطّلِع على وظيفة drawImage() في لوحة العرض. بعد ذلك، تعرَّف على عنصر <​input
type=file>
.

باستخدام هذه المعلومات، يمكنك تعديل الملف المسمّى import_image_legacy.mjs وإضافة المقتطف التالي. في أعلى الملف الذي تستورده، يظهر زر الاستيراد ودالة ملائمة drawBlob() تتيح لك رسم كائن ثنائي كبير الحجم على لوحة العرض.

import { importButton, drawBlob } from './script.mjs';

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

importButton.style.display = 'block';
importButton.addEventListener('click', async () => {
  const file = await importImage();
  if (file) {
    await drawBlob(file);
  }
});

7. 🐟 إضافة إمكانية تصدير الصور

كيف سيحفظ المستخدم ملفًا تم إنشاؤه في التطبيق على جهازه؟ في السابق، كان يتم تحقيق ذلك باستخدام عنصر <​a
download>
.

في الملف export_image_legacy.mjs، أضِف المحتوى كما يلي. استورِد زر التصدير ودالة toBlob() المريحة التي تحوّل محتوى لوحة الرسم إلى كائن ثنائي كبير الحجم.

import { exportButton, toBlob } from './script.mjs';

export const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    a.remove();
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  setTimeout(() => a.click(), 0);
};

exportButton.style.display = 'block';
exportButton.addEventListener('click', async () => {
  exportImage(await toBlob());
});

8. 🐟 إضافة دعم File System Access API

المشاركة مهمة، ولكن من المحتمل أنّ المستخدمين يريدون حفظ أفضل أعمالهم على أجهزتهم. أضِف ميزة تتيح للمستخدمين حفظ رسوماتهم (وإعادة فتحها).

في السابق، كنت تستخدم <​input type=file> طريقة قديمة لاستيراد الملفات و<​a download> طريقة قديمة لتصدير الملفات. ستستخدم الآن File System Access API لتحسين التجربة.

تتيح واجهة برمجة التطبيقات هذه فتح الملفات وحفظها من نظام الملفات الخاص بنظام التشغيل. عدِّل الملفَين import_image.mjs وexport_image.mjs على التوالي عن طريق إضافة المحتوى أدناه. لكي يتم تحميل هذه الملفات، عليك إزالة رموز الإيموجي 🐡 من script.mjs.

استبدِل هذا السطر:

// Remove all the emojis for this feature test to succeed.
if ('show🐡Open🐡File🐡Picker' in window) {
  /* ... */
}

...بالسطر التالي:

if ('showOpenFilePicker' in window) {
  /* ... */
}

في import_image.mjs:

import { importButton, drawBlob } from './script.mjs';

const importImage = async () => {
  try {
    const [handle] = await window.showOpenFilePicker({
      types: [
        {
          description: 'Image files',
          accept: {
            'image/*': ['.png', '.jpg', '.jpeg', '.avif', '.webp', '.svg'],
          },
        },
      ],
    });
    return await handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

importButton.style.display = 'block';
importButton.addEventListener('click', async () => {
  const file = await importImage();
  if (file) {
    await drawBlob(file);
  }
});

في export_image.mjs:

import { exportButton, toBlob } from './script.mjs';

const exportImage = async () => {
  try {
    const handle = await window.showSaveFilePicker({
      suggestedName: 'fugu-greetings.png',
      types: [
        {
          description: 'Image file',
          accept: {
            'image/png': ['.png'],
          },
        },
      ],
    });
    const blob = await toBlob();
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

exportButton.style.display = 'block';
exportButton.addEventListener('click', async () => {
  await exportImage();
});

9- 🐟 إضافة توافق واجهة برمجة التطبيقات Contact Picker

قد يريد المستخدمون إضافة رسالة إلى بطاقة التهنئة وتوجيهها إلى شخص معيّن. أضِف ميزة تتيح للمستخدمين اختيار جهة اتصال واحدة (أو عدّة جهات اتصال) من جهات الاتصال المحلية وإضافة أسمائهم إلى رسالة المشاركة.

على جهاز Android أو iOS، تتيح لك واجهة برمجة التطبيقات Contact Picker API اختيار جهات اتصال من تطبيق إدارة جهات الاتصال على الجهاز وإرجاعها إلى التطبيق. عدِّل الملف contacts.mjs وأضِف الرمز أدناه.

import { contactsButton, ctx, canvas } from './script.mjs';

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);
  }
};

contactsButton.style.display = 'block';
contactsButton.addEventListener('click', async () => {
  const contacts = await getContacts();
  if (contacts) {
    ctx.font = '1em Comic Sans MS';
    contacts.forEach((contact, index) => {
      ctx.fillText(contact.name.join(), 20, 16 * ++index, canvas.width);
    });
  }
});

10. 🐟 إضافة دعم Async Clipboard API

قد يريد المستخدمون لصق صورة من تطبيق آخر في تطبيقك، أو نسخ رسم من تطبيقك إلى تطبيق آخر. أضِف ميزة تتيح للمستخدمين نسخ الصور ولصقها داخل تطبيقك وخارجه. تتوافق Async Clipboard API مع صور PNG، لذا يمكنك الآن قراءة بيانات الصور وكتابتها في الحافظة.

ابحث عن الملف clipboard.mjs وأضِف ما يلي:

import { copyButton, pasteButton, toBlob, drawImage } from './script.mjs';

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      /* global ClipboardItem */
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

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);
  }
};

copyButton.style.display = 'block';
copyButton.addEventListener('click', async () => {
  await copy(await toBlob());
});

pasteButton.style.display = 'block';
pasteButton.addEventListener('click', async () => {
  const image = new Image();
  image.addEventListener('load', () => {
    drawImage(image);
  });
  image.src = URL.createObjectURL(await paste());
});

11. 🐟 إضافة دعم Badging API

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

أضِف ميزة تحتسب عدد الشارات كلما رسم المستخدم ضربة فرشاة جديدة. تتيح Badging API ضبط شارة رقمية على رمز التطبيق. يمكنك تعديل الشارة كلما حدث حدث pointerdown (أي عند حدوث ضربة فرشاة)، وإعادة ضبط الشارة عند محو لوحة الرسم.

ضَع الرمز أدناه في الملف badge.mjs:

import { canvas, clearButton } from './script.mjs';

let strokes = 0;

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

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

12. 🐟 إضافة دعم Screen Wake Lock API

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

ابحث عن الملف wake_lock.mjs وأضِف المحتوى أدناه. لاختبار ما إذا كان ذلك يعمل، اضبط شاشة التوقف على الظهور بعد دقيقة واحدة.

import { wakeLockInput, wakeLockLabel } from './script.mjs';

let wakeLock = null;

const requestWakeLock = async () => {
  try {
    wakeLock = await navigator.wakeLock.request('screen');
    wakeLock.addEventListener('release', () => {
      console.log('Wake Lock was released');
    });
    console.log('Wake Lock is active');
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);

wakeLockInput.style.display = 'block';
wakeLockLabel.style.display = 'block';
wakeLockInput.addEventListener('change', async () => {
  if (wakeLockInput.checked) {
    await requestWakeLock();
  } else {
    wakeLock.release();
  }
});

13. ‫🐟 إضافة توافق واجهة برمجة التطبيقات Periodic Background Sync

قد يكون البدء بلوحة فارغة أمرًا مملًا. يمكنك استخدام Periodic Background Sync API لضبط لوحة الرسم الخاصة بالمستخدمين على صورة جديدة كل يوم، مثل صورة سمكة الفوغو اليومية من Unsplash.

يتطلّب ذلك ملفَين، أحدهما periodic_background_sync.mjs لتسجيل ميزة "المزامنة الدورية في الخلفية" والآخر image_of_the_day.mjs للتعامل مع تنزيل صورة اليوم.

في periodic_background_sync.mjs:

import { periodicBackgroundSyncButton, drawBlob } from './script.mjs';

const getPermission = async () => {
  const status = await navigator.permissions.query({
    name: 'periodic-background-sync',
  });
  return status.state === 'granted';
};

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);
  }
};

navigator.serviceWorker.addEventListener('message', async (event) => {
  const fakeURL = event.data.image;
  const mediaCache = await getMediaCache();
  const response = await mediaCache.match(fakeURL);
  drawBlob(await response.blob());
});

const getMediaCache = async () => {
  const keys = await caches.keys();
  return await caches.open(keys.filter((key) => key.startsWith('media'))[0]);
};

periodicBackgroundSyncButton.style.display = 'block';
periodicBackgroundSyncButton.addEventListener('click', async () => {
  if (await getPermission()) {
    await registerPeriodicBackgroundSync();
  }
  const mediaCache = await getMediaCache();
  let blob = await mediaCache.match('./assets/background.jpg');
  if (!blob) {
    blob = await mediaCache.match('./assets/fugu_greeting_card.jpg');
  }
  drawBlob(await blob.blob());
});

في image_of_the_day.mjs:

const getImageOfTheDay = async () => {
  try {
    const fishes = ['blowfish', 'pufferfish', 'fugu'];
    const fish = fishes[Math.floor(fishes.length * Math.random())];
    const response = await fetch(`https://source.unsplash.com/daily?${fish}`);
    if (!response.ok) {
      throw new Error('Response was', response.status, response.statusText);
    }
    return await response.blob();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

const getMediaCache = async () => {
  const keys = await caches.keys();
  return await caches.open(keys.filter((key) => key.startsWith('media'))[0]);
};

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        try {
          const blob = await getImageOfTheDay();
          const mediaCache = await getMediaCache();
          const fakeURL = './assets/background.jpg';
          await mediaCache.put(fakeURL, new Response(blob));
          const clients = await self.clients.matchAll();
          clients.forEach((client) => {
            client.postMessage({
              image: fakeURL,
            });
          });
        } catch (err) {
          console.error(err.name, err.message);
        }
      })(),
    );
  }
});

14. 🐟 إضافة دعم Shape Detection API

في بعض الأحيان، قد تحتوي رسومات المستخدمين أو صور الخلفية المستخدَمة على معلومات مفيدة، مثل الرموز الشريطية. تتيح لك Shape Detection API، وتحديدًا Barcode Detection API، استخراج هذه المعلومات. أضِف ميزة تحاول رصد الرموز الشريطية من رسومات المستخدمين. حدِّد موقع الملف barcode.mjs وأضِف المحتوى أدناه. لاختبار هذه الميزة، ما عليك سوى تحميل صورة تتضمّن رمزًا شريطيًا أو لصقها على لوحة العرض. يمكنك نسخ رمز شريطي نموذجي من بحث بالصور عن رموز الاستجابة السريعة.

/* global BarcodeDetector */
import {
  scanButton,
  clearButton,
  canvas,
  ctx,
  CANVAS_BACKGROUND,
  CANVAS_COLOR,
  floor,
} from './script.mjs';

const barcodeDetector = new BarcodeDetector();

const detectBarcodes = async (canvas) => {
  return await barcodeDetector.detect(canvas);
};

scanButton.style.display = 'block';
let seenBarcodes = [];
clearButton.addEventListener('click', () => {
  seenBarcodes = [];
});
scanButton.addEventListener('click', async () => {
  const barcodes = await detectBarcodes(canvas);
  if (barcodes.length) {
    barcodes.forEach((barcode) => {
      const rawValue = barcode.rawValue;
      if (seenBarcodes.includes(rawValue)) {
        return;
      }
      seenBarcodes.push(rawValue);
      ctx.font = '1em Comic Sans MS';
      ctx.textAlign = 'center';
      ctx.fillStyle = CANVAS_BACKGROUND;
      const boundingBox = barcode.boundingBox;
      const left = boundingBox.left;
      const top = boundingBox.top;
      const height = boundingBox.height;
      const oneThirdHeight = floor(height / 3);
      const width = boundingBox.width;
      ctx.fillRect(left, top + oneThirdHeight, width, oneThirdHeight);
      ctx.fillStyle = CANVAS_COLOR;
      ctx.fillText(
        rawValue,
        left + floor(width / 2),
        top + floor(height / 2),
        width,
      );
    });
  }
});

15. 🐡 إضافة دعم Idle Detection API

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

ابحث عن الملف idle_detection.mjs والصِق المحتوى أدناه فيه.

import { ephemeralInput, ephemeralLabel, clearCanvas } from './script.mjs';

let controller;

ephemeralInput.style.display = 'block';
ephemeralLabel.style.display = 'block';

ephemeralInput.addEventListener('change', async () => {
  if (ephemeralInput.checked) {
    const state = await IdleDetector.requestPermission();
    if (state !== 'granted') {
      ephemeralInput.checked = false;
      return alert('Idle detection permission must be granted!');
    }
    try {
      controller = new AbortController();
      const idleDetector = new IdleDetector();
      idleDetector.addEventListener('change', (e) => {
        const { userState, screenState } = e.target;
        console.log(`idle change: ${userState}, ${screenState}`);
        if (userState === 'idle') {
          clearCanvas();
        }
      });
      idleDetector.start({
        threshold: 60000,
        signal: controller.signal,
      });
    } catch (err) {
      console.error(err.name, err.message);
    }
  } else {
    console.log('Idle detection stopped.');
    controller.abort();
  }
});

16. 🐡 إضافة دعم File Handling API

ماذا لو كان بإمكان المستخدمين النقر مرّتين على ملف صورة وسيظهر تطبيقك؟ تتيح لك واجهة برمجة تطبيقات معالجة الملفات إجراء ذلك.

عليك تسجيل تطبيق الويب التقدّمي كمعالج ملفات للصور. يحدث ذلك في بيان تطبيق الويب، ويظهر ذلك في المقتطف أدناه من الملف manifest.webmanifest. (هذا الجزء مضمّن في ملف البيان، ولا داعي لإضافته بنفسك).

{
  "file_handlers": [
    {
      "action": "./",
      "accept": {
        "image/*": [".jpg", ".jpeg", ".png", ".webp", ".svg"]
      }
    }
  ]
}

للتعامل فعليًا مع الملفات المفتوحة، أضِف الرمز البرمجي أدناه إلى الملف file-handling.mjs:

import { drawBlob } from './script.mjs';

const handleLaunchFiles = () => {
  window.launchQueue.setConsumer((launchParams) => {
    if (!launchParams.files.length) {
      return;
    }
    launchParams.files.forEach(async (handle) => {
      const file = await handle.getFile();
      drawBlob(file);
    });
  });
};

handleLaunchFiles();

17. تهانينا

🎉 أحسنت، لقد أنجزت المهمة!

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

للحصول على مزيد من المعلومات، يمكنك متابعة منشوراتنا على موقعنا الإلكتروني web.dev.

الصفحة المقصودة لقسم &quot;الإمكانات&quot; في الموقع الإلكتروني web.dev

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

موقع إلكتروني لتتبُّع واجهة برمجة تطبيقات Fugu

تمت كتابة هذا الدرس العملي على الويب بواسطة "توماس شتاينر" (‎@tomayac)، ويسرّني الإجابة عن أسئلتك وأتطلّع إلى قراءة ملاحظاتك. نتوجّه بالشكر إلى "هيمانث إتش إم" (GNUmanth@) و"كريستيان ليبيل" (christianliebel@) و"سفين ماي" (Svenmay@) و"لارس كنودسن" (larsgk@) و"جاكي هان" (hanguokai@) الذين ساهموا في إعداد هذا الدرس العملي.