التعرّف على إمكانات المتصفح الجديدة والمقبلة لتطبيق الويب التقدّمي (PWA): من شركة Fugu With Love

1. قبل البدء

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

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

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

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

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

العناصر التي سيتم إنشاؤها

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

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

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

الأشياء التي تحتاج إليها

المتصفحات المتوافقة بالكامل في الوقت الحالي:

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

2. مشروع فوغو

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

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

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

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

3- البدء

يمكنك تنزيل أي من المتصفحين، ثم وضع علامة وقت التشغيل التالية 🔎 من خلال الانتقال إلى about://flags، التي تعمل في كل من Chrome وEdge.

  • #enable-experimental-web-platform-features

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

ستستخدم منصّة Glitch، لأنها تسمح لك باستضافة تطبيق الويب التقدّمي (PWA)، ولأنّه يحتوي على محرِّر جيد. يدعم glgl أيضًا الاستيراد والتصدير إلى GitHub، وبالتالي لا يتوفر حظر المورد. انتقل إلى fugu-paint.glitch.me لتجربة التطبيق. إنه تطبيق رسم أساسي 🎨 لتحسينه أثناء الدرس التطبيقي حول الترميز.

يشكّل تطبيق Fugu Greetings PWA مرجعًا يحتوي على لوحة كبيرة مكتوب عليها الكلمة “Google&rdquo.

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

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

يعرض GLgl IDE تعديل مستند HTML.

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

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

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

4. 🐟 إضافة دعم واجهة برمجة التطبيقات لمشاركة الويب

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

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

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

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. 🐟 إضافة دعم واجهة برمجة التطبيقات المستهدفة لمشاركة الويب

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

في بيان تطبيق الويب، يجب إخبار التطبيق بنوع الملفات التي يمكنك قبولها وعنوان 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() تتيح لك رسم كائن ثنائي كبير (blob) على اللوحة.

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- 🐟 إضافة دعم واجهة برمجة التطبيقات للوصول إلى نظام الملفات

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

استخدمت سابقًا نهجًا قديمًا <​input type=file> لاستيراد الملفات ومنهجًا قديمًا لـ <​a download> لتصدير الملفات. والآن، ستستخدم واجهة برمجة التطبيقات للوصول إلى نظام الملفات لتحسين التجربة.

تسمح واجهة برمجة التطبيقات هذه بفتح الملفات وحفظها من نظام الملفات في نظام التشغيل. عدِّل الملفين، 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- 🐟 إضافة دعم واجهة برمجة التطبيقات لأداة اختيار جهات الاتصال

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

على جهاز Android أو iOS، تتيح لك واجهة برمجة تطبيقات منتقي جهات الاتصال اختيار جهات الاتصال من تطبيق مدير جهات الاتصال على الجهاز وإرجاعها إلى التطبيق. عدِّل الملف 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 صور 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- 🐟 إضافة دعم واجهة برمجة تطبيقات الشارة

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

يمكنك إضافة ميزة تُحتسب الشارة عندما يُجري المستخدم فرشاة جديدة. تسمح واجهة برمجة التطبيقات للشارات بإعداد شارة رقمية على رمز التطبيق. يمكنك تعديل الشارة عند وقوع حدث 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- 🐟 إضافة دعم واجهة برمجة تطبيقات قفل الشاشة

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

ابحث عن الملف 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- 🐟 إضافة دعم دوري لواجهة برمجة التطبيقات للمزامنة في الخلفية

قد تكون لوحة الرسم الفارغة مملة. يمكنك استخدام واجهة برمجة تطبيقات المزامنة في الخلفية الدورية لإعداد المستخدمين، وصورة جديدة يوميًا، على سبيل المثال، صورة يومية لـ UnSpot&#99.

وهذا يتطلب ملفَين، وهما ملف 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- 🐟 إضافة واجهة برمجة التطبيقات لاكتشاف الأشكال

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

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

ابحث عن الملف 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 إجراء ذلك تمامًا.

سيكون عليك تسجيل تطبيق الويب التقدّمي (PWA) باعتباره معالج ملفات الصور. ويحدث ذلك في بيان تطبيق الويب، ويظهر المقتطف أدناه من الملف 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. تهانينا

🎉 تهانينا،

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

للاطّلاع على معلومات مفصّلة أو لمعرفة المزيد من المعلومات، يمكنك متابعة جهات النشر على موقعنا web.dev.

الصفحة المقصودة للقسم &ldquo;Capability&rdquo؛ بالموقع الإلكتروني web.dev.

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

الموقع الإلكتروني لأداة تتبّع Fugu API

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