अपने प्रोग्रेसिव वेब ऐप्लिकेशन को धीरे-धीरे और बेहतर बनाएं

आधुनिक ब्राउज़र बनाने और साल 2003 की तरह ही बेहतर होने की सुविधाएं

मार्च 2003 में, निक फ़िंक और स्टीव चैंपियन ने प्रोग्रेसिव एन्हैंसमेंट के कॉन्सेप्ट से वेब डिज़ाइन की दुनिया को हैरान किया. यह वेब डिज़ाइन के लिए एक ऐसी रणनीति है जिसमें पहले मुख्य वेब पेज का कॉन्टेंट लोड करने पर ज़ोर दिया जाता है. इसके बाद, कॉन्टेंट में बारीक और तकनीकी रूप से बेहतर सुविधाएं जोड़ी जाती हैं. 2003 में, प्रोग्रेसिव एन्हैंसमेंट का मकसद उस समय—आधुनिक सीएसएस सुविधाओं, रुकावट न डालने वाले JavaScript, और यहां तक कि सिर्फ़ स्केलेबल वेक्टर ग्राफ़िक का इस्तेमाल करना था. साल 2020 और उसके बाद में किए जाने वाले सुधारों का मतलब है, ब्राउज़र की आधुनिक क्षमताओं का इस्तेमाल करना.

प्रगतिशील सुधारों के साथ भविष्य के लिए इनक्लूसिव वेब डिज़ाइन. Finck और Champeon के मूल प्रज़ेंटेशन की टाइटल स्लाइड.
स्लाइड: प्रोग्रेसिव एन्हैंसमेंट के साथ, भविष्य के लिए इन्क्लूसिव वेब डिज़ाइन. (सोर्स)

मॉडर्न JavaScript

JavaScript की बात करें, तो नवीनतम मुख्य ES 2015 JavaScript सुविधाओं के लिए ब्राउज़र सहायता स्थिति शानदार है. नए स्टैंडर्ड में प्रॉमिस, मॉड्यूल, क्लास, टेंप्लेट लिटरल, ऐरो फ़ंक्शन, let और const, डिफ़ॉल्ट पैरामीटर, जनरेटर, नुकसान पहुंचाने वाला असाइनमेंट, रेस्ट और स्प्रेड, Map/Set, WeakMap/WeakSet वगैरह शामिल हैं. सभी काम करते हैं.

ES6 की सुविधाओं के लिए CanIUse सहायता टेबल, जो सभी मुख्य ब्राउज़र के साथ काम करती है.
ECMAScript 2015 (ES6) ब्राउज़र के लिए सहायता टेबल. (सोर्स)

एक साथ काम नहीं करने वाले फ़ंक्शन, ES 2017 की सुविधा और मेरी पसंद की एक सुविधा, सभी बड़े ब्राउज़र में इस्तेमाल की जा सकती हैं. async और await कीवर्ड, एसिंक्रोनस और प्रॉमिस पर आधारित व्यवहार को बेहतर तरीके से लिखने की सुविधा देते हैं. इससे प्रॉमिस चेन को सटीक तरीके से कॉन्फ़िगर करने की ज़रूरत नहीं पड़ती.

एक साथ काम नहीं करने वाले फ़ंक्शन के लिए CanIUse सहायता टेबल, जो सभी मुख्य ब्राउज़र पर काम करती है.
ब्राउज़र के लिए, Async फ़ंक्शन वाला सहायता टेबल. (सोर्स)

इतना ही नहीं, ES2020 में हाल ही में जोड़ी गई भाषाओं, जैसे कि वैकल्पिक चेन और शून्य कोलेसिंग को भी तेज़ी से सहायता मिली. नीचे दिया गया कोड सैंपल देखा जा सकता है. जब JavaScript की मुख्य सुविधाओं की बात आती है, तो पहले से कहीं ज़्यादा हरी-भरी घास नहीं हो सकती थी.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Windows XP की हरी घास वाली बैकग्राउंड की इमेज.
JavaScript की मुख्य सुविधाओं के मामले में, घास का रंग हरा होता है. (Microsoft प्रॉडक्ट का स्क्रीनशॉट, जिसका इस्तेमाल अनुमति के साथ किया गया है.)

सैंपल ऐप्लिकेशन: Fugu Greetings

इस लेख के लिए, मैं एक सामान्य PWA के साथ काम करता/करती हूं, जिसे Fugu Greetings (GitHub) कहते हैं. इस ऐप्लिकेशन का नाम Project Fugu 🐡 के सुझाव के तौर पर बताया गया है, ताकि वेब को Android/iOS/डेस्कटॉप ऐप्लिकेशन की सभी सुविधाएं दी जा सकें. प्रोजेक्ट के बारे में ज़्यादा जानकारी पाने के लिए, लैंडिंग पेज पर जाएं.

Fugu Greetings एक ड्रॉइंग ऐप्लिकेशन है, जिससे आप वर्चुअल ग्रीटिंग कार्ड बना सकते हैं और उन्हें अपने करीबियों को भेज सकते हैं. यह PWA के मुख्य कॉन्सेप्ट का उदाहरण देता है. यह भरोसेमंद है और ऑफ़लाइन मोड में इस्तेमाल किया जा सकता है. इसलिए, अगर आपके पास नेटवर्क नहीं है, तब भी आप इसे इस्तेमाल कर सकते हैं. इसे डिवाइस की होम स्क्रीन पर इंस्टॉल किया जा सकता है. साथ ही, यह ऑपरेटिंग सिस्टम के साथ स्टैंड-अलोन ऐप्लिकेशन के तौर पर आसानी से इंटिग्रेट हो जाता है.

PWA का ग्रीटिंग मैसेज, PWA के कम्यूनिटी लोगो की तरह दिखता है.
Fugu Greetings सैंपल ऐप्लिकेशन.

प्रोग्रेसिव एन्हैंसमेंट

हालांकि, अब बेहतर बनाने की सुविधा के बारे में बात की जा सकती है. एमडीएन वेब दस्तावेज़ की ग्लॉसरी में कॉन्सेप्ट को इस तरह तय किया गया है:

प्रोग्रेसिव एन्हैंसमेंट, डिज़ाइन से जुड़ी ऐसी प्रोसेस है जो ज़्यादा से ज़्यादा उपयोगकर्ताओं को ज़रूरी कॉन्टेंट और फ़ंक्शन उपलब्ध कराने की बुनियादी जानकारी देती है. साथ ही, यह सबसे बेहतर अनुभव देने वाले सबसे आधुनिक ब्राउज़र के उपयोगकर्ताओं को ही, सभी ज़रूरी कोड इस्तेमाल करने की सुविधा देती है.

आम तौर पर, सुविधा की पहचान का इस्तेमाल यह तय करने के लिए किया जाता है कि ब्राउज़र ज़्यादा आधुनिक सुविधाओं का इस्तेमाल कर सकते हैं या नहीं. वहीं, पॉलीफ़िल का इस्तेमाल अक्सर JavaScript की मदद से उन सुविधाओं को जोड़ने के लिए किया जाता है जो मौजूद नहीं हैं.

[…]

प्रोग्रेसिव बेहतर बनाने की सुविधा, एक काम की तकनीक है. इसकी मदद से, वेब डेवलपर सबसे अच्छी वेबसाइटों को डेवलप करने पर फ़ोकस कर सकते हैं. साथ ही, वे वेबसाइटें कई अनजान उपयोगकर्ता एजेंट पर काम कर सकती हैं. ग्रेसफ़ुल डिग्रेडेशन एक-दूसरे से जुड़ा हुआ है. हालांकि, यह एक ही चीज़ नहीं है. इसे अक्सर बेहतर तरीके से बेहतर बनाने के लिए विपरीत दिशा में होने के तौर पर देखा जाता है. असल में, दोनों तरीके मान्य हैं और अक्सर एक-दूसरे की मदद कर सकते हैं.

एमडीएन में योगदान देने वाले

हर ग्रीटिंग कार्ड को शुरुआत से शुरू करना बहुत मुश्किल हो सकता है. आइए जानते हैं कि आपके पास कोई ऐसी सुविधा क्यों नहीं है जिससे उपयोगकर्ता इमेज इंपोर्ट कर सकें और वहां से शुरुआत कर सकें? परंपरागत तरीके से, ऐसा करने के लिए आपने <input type=file> एलिमेंट का इस्तेमाल किया होगा. सबसे पहले, आपको एलिमेंट बनाना होगा, उसके type को 'file' पर सेट करना होगा और accept प्रॉपर्टी में MIME टाइप जोड़ना होगा. इसके बाद, प्रोग्राम के हिसाब से, उस पर "क्लिक" करना होगा और बदलावों के बारे में जानना होगा. किसी इमेज को चुनने पर, उसे सीधे कैनवस पर इंपोर्ट कर लिया जाता है.

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

अगर import करने की सुविधा दी जाती है, तो उसमें import करने की सुविधा ज़रूर होनी चाहिए, ताकि लोग अपने ग्रीटिंग कार्ड को अपने डिवाइस पर सेव कर सकें. फ़ाइलें सेव करने का पारंपरिक तरीका download एट्रिब्यूट और href के तौर पर ब्लॉब यूआरएल वाला ऐंकर लिंक बनाना है. डाउनलोड को ट्रिगर करने के लिए, आप प्रोग्राम के हिसाब से "क्लिक" भी करेंगे. साथ ही, मेमोरी लीक होने से बचाने के लिए, ब्लॉब ऑब्जेक्ट यूआरएल को रद्द करना न भूलें.

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 काम नहीं करता है उन पर लेगसी स्क्रिप्ट लोड होती हैं. आप नीचे Firefox और Safari के नेटवर्क टैब देख सकते हैं.

Safari Web Inspector, लेगसी फ़ाइलों को लोड होते हुए दिखा रहा है.
Safari वेब इंस्पेक्टर नेटवर्क टैब.
लेगसी फ़ाइलों को लोड होते हुए दिखाने वाले Firefox डेवलपर टूल.
Firefox के डेवलपर टूल नेटवर्क टैब पर.

हालांकि, एपीआई के साथ काम करने वाले ब्राउज़र Chrome पर, सिर्फ़ नई स्क्रिप्ट लोड होती हैं. ऐसा डाइनैमिक import() की मदद से किया गया है. यह सभी मॉडर्न ब्राउज़र पर काम करता है. जैसा कि मैंने पहले बताया था, इन दिनों घास बहुत हरी है.

Chrome DevTools, जिसमें आधुनिक फ़ाइलों को लोड होते हुए दिखाया गया है.
Chrome DevTools नेटवर्क टैब.

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

इमेज को एक्सपोर्ट करने का तरीका करीब-करीब एक जैसा है, लेकिन इस बार मुझे chooseFileSystemEntries() तरीके में 'save-file' टाइप पैरामीटर पास करना होगा. यहां से मुझे फ़ाइल सेव करने का डायलॉग बॉक्स मिला है. फ़ाइल खुली होने पर, इसकी ज़रूरत नहीं थी, क्योंकि 'open-file' डिफ़ॉल्ट है. मैंने accepts पैरामीटर को पहले की तरह ही सेट किया है, लेकिन इस समय सिर्फ़ PNG इमेज ही सेट की गई हैं. फिर से मुझे फ़ाइल हैंडल वापस मिल जाता है, लेकिन इस बार फ़ाइल पाने के बजाय, मैं createWritable() को कॉल करके एक ऐसी स्ट्रीम बनाता हूँ जिसे पढ़ने में मदद मिलती है. इसके बाद, मैं फ़ाइल में ब्लॉब लिखता हूं, जो मेरे ग्रीटिंग कार्ड की इमेज है. आखिर में, लिखने वाली स्ट्रीम को बंद कर दिया जाता है.

हर बार कुछ गड़बड़ी हो सकती है: डिस्क में जगह कम हो, लिखने या पढ़ने में गड़बड़ी हो सकती है या फिर हो सकता है कि उपयोगकर्ता ने फ़ाइल डायलॉग को रद्द कर दिया हो. इसलिए, मैं कॉल को हमेशा try...catch स्टेटमेंट में रैप करता हूं.

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

File System Access API के साथ प्रोग्रेसिव एन्हैंसमेंट की सुविधा का इस्तेमाल करके, फ़ाइल को पहले की तरह खोला जा सकता है. इंपोर्ट की गई फ़ाइल, कैनवस पर सीधे खींची जाती है. बदलाव करके, आखिर में उन्हें सेव करने के लिए एक असल डायलॉग बॉक्स में सेव किया जा सकता है. इस बॉक्स में फ़ाइल का नाम और स्टोरेज की जगह को चुना जा सकता है. अब यह फ़ाइल हमेशा के लिए सुरक्षित रखने के लिए तैयार है.

फ़ाइल खुलने वाले डायलॉग के साथ Fugu Greetings ऐप्लिकेशन.
फ़ाइल खुलने का डायलॉग बॉक्स.
इंपोर्ट की गई इमेज के साथ अब Fugu Greetings ऐप्लिकेशन की इमेज.
इंपोर्ट की गई इमेज.
बदली गई इमेज के साथ Fugu Greetings ऐप्लिकेशन.
बदली गई इमेज को नई फ़ाइल में सेव करना.

वेब शेयर और वेब शेयर टारगेट एपीआई

हमेशा के लिए सेव करने के अलावा, शायद मुझे अपना ग्रीटिंग कार्ड शेयर करना पसंद हो. यह कुछ ऐसा है जो मुझे Web Share API और वेब शेयर टारगेट एपीआई करने की अनुमति देता है. मोबाइल और हाल ही में, डेस्कटॉप ऑपरेटिंग सिस्टम में, शेयर करने के तरीके पहले से ही मौजूद हैं. उदाहरण के लिए, यहां macOS पर डेस्कटॉप Safari की शेयर शीट दी गई है, जो मेरे ब्लॉग पर एक लेख से ट्रिगर हुई है. लेख शेयर करें बटन पर क्लिक करके, अपने दोस्त के साथ लेख का लिंक शेयर किया जा सकता है. उदाहरण के लिए, macOS के Messages ऐप्लिकेशन से लेख का लिंक शेयर किया जा सकता है.

macOS पर डेस्कटॉप Safari की शेयर शीट, एक लेख के शेयर बटन से ट्रिगर हुई
macOS पर डेस्कटॉप Safari पर Web Share API.

इसे करने का कोड बहुत आसान है. मैं navigator.share() को कॉल करता/करती हूं और उसे किसी ऑब्जेक्ट में वैकल्पिक title, text, और url पास करता/करती हूं. लेकिन अगर मैं कोई इमेज अटैच करना चाहूं, तो क्या होगा? 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 ग्रीटिंग कार्ड ऐप्लिकेशन से यह कैसे काम किया जा सकता है. सबसे पहले, मुझे एक files कलेक्शन वाला data ऑब्जेक्ट तैयार करना होगा. इसमें एक ब्लॉब और उसके बाद एक 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);
  }
};

पहले की तरह, मैं प्रोग्रेसिव एन्हैंसमेंट का इस्तेमाल करता हूं. अगर navigator ऑब्जेक्ट पर 'share' और 'canShare', दोनों मौजूद हैं, तो ही मैं आगे जाकर डाइनैमिक import() के ज़रिए share.mjs को लोड करता/करती हूं. मोबाइल Safari जैसे ब्राउज़र पर, जो दोनों शर्तों में से सिर्फ़ एक को पूरा करते हैं, उन पर फ़ंक्शन लोड नहीं होता.

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

Fugu Greetings में, अगर मैं Android पर Chrome जैसे किसी सहायक ब्राउज़र पर शेयर करें बटन पर टैप करूं, तो पहले से मौजूद शेयर शीट खुल जाएगी. उदाहरण के लिए, मैं Gmail चुन सकता/सकती हूं और ईमेल लिखने वाला विजेट, अटैच की गई इमेज के साथ पॉप-अप होता है.

ओएस-लेवल की शेयर शीट, जिसमें ऐसे कई ऐप्लिकेशन दिख रहे हैं जिनके साथ इमेज शेयर की जा सकती है.
फ़ाइल शेयर करने के लिए कोई ऐप्लिकेशन चुनना.
अटैच की गई इमेज के साथ, Gmail का ईमेल लिखें विजेट.
फ़ाइल, Gmail के कंपोज़र में नए ईमेल के साथ अटैच हो जाती है.

संपर्क पिकर एपीआई

इसके बाद, आपको संपर्कों, डिवाइस की एड्रेस बुक या कॉन्टैक्ट मैनेजर ऐप्लिकेशन के बारे में बात करनी है. ग्रीटिंग कार्ड लिखते समय, हो सकता है कि किसी व्यक्ति का नाम सही तरीके से लिखना आसान न हो. उदाहरण के लिए, मेरा एक दोस्त सर्गेई है और वह अपना नाम सिरिलिक अक्षरों में लिखना पसंद करता है. मैं जर्मन QWERTZ कीबोर्ड का इस्तेमाल कर रहा/रही हूं और मुझे उनका नाम लिखने का तरीका नहीं पता है. संपर्क पिकर एपीआई इस समस्या को हल कर सकता है. मेरे दोस्त ने मेरे फ़ोन के संपर्क ऐप्लिकेशन में सेव किया है, इसलिए संपर्क पिकर एपीआई की मदद से, मैं वेब से अपने संपर्क टैप कर सकता हूं.

सबसे पहले, मुझे उन प्रॉपर्टी की सूची बतानी होगी जिनका ऐक्सेस मुझे देना है. इस मामले में, मुझे सिर्फ़ नाम चाहिए, लेकिन इस्तेमाल के दूसरे मामलों में टेलीफ़ोन नंबर, ईमेल, अवतार आइकॉन या घर या ऑफ़िस के पतों में दिलचस्पी हो सकती है. इसके बाद, मुझे एक 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');
}

फ़ुगु ग्रीटिंग में, जब मैं संपर्क बटन पर टैप करके अपने दो सबसे अच्छे दोस्तों को चुनता हूं, तो šерррекज़न КакаKIлове बताओ सबटाइटल और 劳伦斯·爱德华”拉里" इसके बाद, उनके नाम मेरे ग्रीटिंग कार्ड पर आ जाते हैं.

संपर्क पिकर, जिसमें पता पुस्तिका में मौजूद दो संपर्कों के नाम दिखाए जा रहे हैं.
अड्रेस बुक से संपर्क पिकर की मदद से दो नाम चुनना.
ग्रीटिंग कार्ड पर पहले से चुने गए दो संपर्कों के नाम.
इसके बाद, दोनों के नाम ग्रीटिंग कार्ड पर आ जाते हैं.

एसिंक्रोनस क्लिपबोर्ड API

अगला है कॉपी करना और चिपकाना. सॉफ़्टवेयर डेवलपर के तौर पर, हमारे पसंदीदा कामों में से एक है कॉपी करके चिपकाना. ग्रीटिंग कार्ड लेखक के तौर पर, मैं कभी-कभी ऐसा ही कर सकता हूँ. मैं किसी ग्रीटिंग कार्ड में इमेज चिपका सकता हूँ या अपना ग्रीटिंग कार्ड कॉपी कर सकता हूँ, ताकि मैं किसी और जगह से इसमें बदलाव करना जारी रख सकूँ. 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 Preview ऐप्लिकेशन में एक इमेज खोली है और उसे क्लिपबोर्ड पर कॉपी किया है. जब मैं चिपकाएं पर क्लिक करता/करती हूं, तो Fugu Greetings ऐप्लिकेशन मुझसे पूछता है कि क्या मुझे क्लिपबोर्ड पर ऐप्लिकेशन को टेक्स्ट और इमेज देखने की अनुमति देनी है.

Fugu Greetings ऐप्लिकेशन में क्लिपबोर्ड की अनुमति का अनुरोध दिख रहा है.
क्लिपबोर्ड की अनुमति का अनुरोध.

अनुमति स्वीकार करने के बाद, इमेज को ऐप्लिकेशन में चिपकाया जाता है. इसका दूसरा तरीका भी काम करता है. मुझे ग्रीटिंग कार्ड को क्लिपबोर्ड पर कॉपी करने दें. 'झलक देखें' को खोलने के बाद, फ़ाइल और फिर क्लिपबोर्ड से नया पर क्लिक करने पर, ग्रीटिंग कार्ड एक नई बिना टाइटल वाली इमेज में चिपका दिया जाता है.

macOS Preview ऐप्लिकेशन, जिसमें बिना टाइटल वाली इमेज चिपकाई गई है.
macOS Preview ऐप्लिकेशन में चिपकाई गई इमेज.

बैजिंग एपीआई

Badging API, एक और काम का एपीआई है. इंस्टॉल किए जा सकने वाले PWA के तौर पर, Fugu Greetings में एक ऐप्लिकेशन आइकॉन होता है. इसे उपयोगकर्ता ऐप्लिकेशन डॉक या होम स्क्रीन पर लगा सकते हैं. एपीआई को दिखाने का एक मज़ेदार और आसान तरीका यह है कि (ab) Fugu Greetings में इसका इस्तेमाल पेन स्ट्रोक काउंटर की तरह करें. मैंने एक इवेंट लिसनर जोड़ा है, जो pointerdown इवेंट होने पर पेन स्ट्रोक काउंटर को बढ़ाता है. इसके बाद, अपडेट किया गया आइकॉन बैज सेट करता है. जब भी कैनवस साफ़ हो जाता है, तब काउंटर रीसेट हो जाता है और बैज हटा दिया जाता है.

let strokes = 0;

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

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

इस सुविधा में लगातार सुधार किए जाते हैं, इसलिए कॉन्टेंट लोड होने का लॉजिक हमेशा की तरह ही है.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

इस उदाहरण में, मैंने प्रति संख्या एक पेन स्ट्रोक का इस्तेमाल करते हुए, एक से सात तक की संख्याएं बनाई हैं. आइकॉन पर मौजूद बैज काउंटर अब सात पर है.

एक से सात तक की संख्याएं ग्रीटिंग कार्ड पर लिखी गई हैं, हर एक में बस एक पेन स्ट्रोक है.
सात पेन स्ट्रोक का इस्तेमाल करके, 1 से 7 तक की संख्याएं बनाना.
Fugu Greetings ऐप्लिकेशन के बैज का आइकॉन, जिसमें सात अंक दिख रहे हैं.
ऐप्लिकेशन आइकॉन बैज के रूप में पेन स्ट्रोक काउंटर.

समय-समय पर होने वाला बैकग्राउंड सिंक एपीआई

क्या आपको अपने हर दिन की नए सिरे से शुरुआत करनी है? Fugu Greetings ऐप्लिकेशन की एक खास सुविधा यह है कि यह आपको अपना ग्रीटिंग कार्ड शुरू करने के लिए हर सुबह एक नई बैकग्राउंड इमेज के ज़रिए प्रेरित कर सकता है. ऐप्लिकेशन ऐसा करने के लिए, Periodic बैकग्राउंड सिंक एपीआई का इस्तेमाल करता है.

पहला चरण, सर्विस वर्कर रजिस्ट्रेशन में, समय-समय पर होने वाले किसी सिंक इवेंट को register है. यह 'image-of-the-day' नाम के सिंक टैग को सुनता है. इसका कम से कम एक दिन का अंतराल होता है. इससे उपयोगकर्ता को हर 24 घंटे में एक नई बैकग्राउंड इमेज मिल सकती है.

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

दूसरा चरण, सर्विस वर्कर में periodicsync इवेंट को सुनना है. अगर इवेंट टैग 'image-of-the-day' है यानी पहले रजिस्टर किया गया इवेंट टैग, तो दिन की इमेज getImageOfTheDay() फ़ंक्शन के ज़रिए वापस लाई जाती है. नतीजे में मिलने वाले नतीजे सभी क्लाइंट को दिखाए जाते हैं, ताकि वे अपने कैनवस और कैश मेमोरी को अपडेट कर सकें.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

यह वाकई एक प्रोग्रेसिव एनवायरमेंट है. इसलिए, कोड सिर्फ़ तब लोड होता है, जब ब्राउज़र पर एपीआई काम करता है. यह क्लाइंट कोड और सर्विस वर्कर कोड, दोनों पर लागू होता है. साथ काम न करने वाले ब्राउज़र में से कोई भी लोड नहीं होता. ध्यान दें कि डाइनैमिक import() के बजाय, सर्विस वर्कर में (जो सर्विस वर्कर के कॉन्टेक्स्ट में काम नहीं करता अब तक), मैं क्लासिक importScripts() का इस्तेमाल करता/करती हूं.

// 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 ऐप्लिकेशन में, दिन की नई ग्रीटिंग कार्ड इमेज होती है.
वॉलपेपर बटन दबाने से, उस दिन की इमेज दिखती है.

सूचना ट्रिगर करने वाला एपीआई

कभी-कभी बहुत प्रेरणा मिलने के बाद भी, आपको शुरू किया गया ग्रीटिंग कार्ड पूरा करने के लिए, रिमाइंडर की ज़रूरत होती है. यह सुविधा Notifications 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 ऐप्लिकेशन में एक प्रॉम्प्ट होता है. इसमें उपयोगकर्ता से पूछा जाता है कि उसे अपना ग्रीटिंग कार्ड पूरा करने के लिए कब रिमाइंडर चाहिए.
ग्रीटिंग कार्ड पूरा करने के लिए, रिमाइंडर के लिए स्थानीय सूचना शेड्यूल की जा रही है.

जब किसी फुगु ग्रीटिंग्स में कोई शेड्यूल की गई सूचना ट्रिगर होती है, तो उसे किसी भी दूसरी सूचना की तरह ही दिखाया जाता है. हालांकि, जैसा कि मैंने पहले लिखा था, उसे इंटरनेट की ज़रूरत नहीं थी.

macOS का सूचना केंद्र, Fugu Greetings से ट्रिगर की गई सूचना दिखा रहा है.
ट्रिगर की गई सूचना, macOS के सूचना केंद्र में दिखती है.

वेक लॉक एपीआई

मैं वेक लॉक एपीआई को भी शामिल करना चाहता हूं. कभी-कभी आपको स्क्रीन को तब तक देखना होता है, जब तक प्रेरणा आपको चूम न ले. उस समय सबसे ज़्यादा बुरा हो सकता है कि स्क्रीन बंद हो जाए. वेक लॉक एपीआई इसे होने से रोक सकता है.

सबसे पहले, 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');
}

फ़ुगु ग्रीटिंग्स में, इंसोम्निया चेकबॉक्स पर सही का निशान लगाने पर, स्क्रीन हमेशा चालू रहती है.

नींद न आने के चेकबॉक्स पर सही का निशान लगाने से स्क्रीन चालू रहती है.
इंडोम्निया चेकबॉक्स की वजह से, ऐप्लिकेशन चालू रहता है.

डिवाइस कुछ समय से इस्तेमाल में न होने का पता लगाने वाला एपीआई

कभी-कभी, भले ही आप स्क्रीन को कई घंटों तक घूरते रहें, लेकिन यह किसी भी समय फालतू नहीं होता और आपको यह अंदाज़ा भी नहीं लगता कि ग्रीटिंग कार्ड को किस तरह से पेश किया जाए. डिवाइस कुछ समय से इस्तेमाल में न होने का पता लगाने वाला एपीआई, ऐप्लिकेशन को उपयोगकर्ता के कुछ समय से डिवाइस इस्तेमाल न करने पर, उसका पता लगाने की सुविधा देता है. अगर उपयोगकर्ता ज़्यादा समय तक कोई गतिविधि नहीं करता है, तो ऐप्लिकेशन शुरुआती स्थिति में रीसेट हो जाता है और कैनवस को हटा देता है. फ़िलहाल, इस एपीआई को सूचना की अनुमति से सुरक्षित किया गया है. ऐसा इसलिए है, क्योंकि डिवाइस कुछ समय से इस्तेमाल में न होने का पता लगाने के लिए, प्रोडक्शन में इस्तेमाल के कई मामले सूचनाओं से जुड़े होते हैं. उदाहरण के लिए, सिर्फ़ उस डिवाइस पर सूचना भेजना जिसका इस्तेमाल उपयोगकर्ता फ़िलहाल कर रहा है.

यह पक्का करने के बाद कि सूचनाओं की अनुमति दी गई है, फिर मैं आइडल डिटेक्टर को इंस्टैंशिएट करता/करती हूं. मैं एक इवेंट लिसनर रजिस्टर करता/करती हूं जो कुछ समय से इस्तेमाल में न होने पर होने वाले बदलावों का पता लगाता है. इनमें उपयोगकर्ता और स्क्रीन की स्थिति शामिल है. उपयोगकर्ता सक्रिय या निष्क्रिय हो सकता है और उसकी स्क्रीन अनलॉक या लॉक की जा सकती है. अगर उपयोगकर्ता कुछ समय से डिवाइस का इस्तेमाल नहीं कर रहा है, तो कैनवस हटा दिया जाता है. मैंने इस्तेमाल न होने वाले डिटेक्टर को 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 ऐप्लिकेशन में, Ephemeral चेकबॉक्स पर सही का निशान लगाने और उपयोगकर्ता के ज़्यादा देर तक इस्तेमाल न करने पर, कैनवस हट जाता है.

लंबे समय तक कोई उपयोगकर्ता गतिविधि न करने पर, खाली कैनवस के साथ Fugu Greetings ऐप्लिकेशन का कैनवस साफ़ हो जाता है.
अगर इफ़ेमरल चेकबॉक्स पर सही का निशान लगा होता है और उपयोगकर्ता ज़्यादा देर तक डिवाइस का इस्तेमाल नहीं करता है, तो कैनवस हटा दिया जाता है.

आखिरी हिस्सा

ओह, क्या राइड है. सिर्फ़ एक सैंपल ऐप्लिकेशन में कई एपीआई होते हैं. याद रखें, मैं उपयोगकर्ता को किसी ऐसी सुविधा के लिए डाउनलोड शुल्क नहीं देना चाहता जो उसके ब्राउज़र पर काम नहीं करता. प्रोग्रेसिव एन्हैंसमेंट का इस्तेमाल करके, मैं यह पक्का करता हूं कि सिर्फ़ काम के कोड लोड हों. एचटीटीपी/2 के साथ, अनुरोध सस्ते होते हैं. इसलिए, यह पैटर्न कई ऐप्लिकेशन पर अच्छी तरह काम करता है. हालांकि, बड़े ऐप्लिकेशन के लिए बंडलर का इस्तेमाल करना बेहतर होगा.

Chrome DevTools नेटवर्क पैनल, सिर्फ़ ऐसे कोड वाली फ़ाइलों के अनुरोध दिखा रहा है जो मौजूदा ब्राउज़र पर काम करता है.
Chrome DevTools नेटवर्क टैब सिर्फ़ उन फ़ाइलों के अनुरोध दिखाता है जिनमें कोड है, जो मौजूदा ब्राउज़र पर काम करता है.

हर ब्राउज़र पर ऐप्लिकेशन थोड़ा अलग दिख सकता है, क्योंकि सभी प्लैटफ़ॉर्म पर सभी सुविधाएं काम नहीं करतीं. हालांकि, मुख्य फ़ंक्शन हमेशा मौजूद रहता है—यानी ब्राउज़र की क्षमताओं के हिसाब से बेहतर होता जाता है. ध्यान रखें कि ये क्षमताएं एक ही ब्राउज़र में भी बदल सकती हैं, यह इस बात पर निर्भर करेगा कि ऐप्लिकेशन इंस्टॉल किए गए ऐप्लिकेशन के रूप में चल रहा है या ब्राउज़र टैब में.

Android Chrome पर चल रहा है फ़ुगु ग्रीटिंग मैसेज, जिसमें कई उपलब्ध सुविधाएं दिख रही हैं.
Android Chrome पर चल रहा फ़ुगू ग्रीटिंग.
डेस्कटॉप Safari पर चल रही फ़ुगु ग्रीटिंग, कम उपलब्ध सुविधाएं दिखा रही हैं.
डेस्कटॉप Safari पर फ़ुगू ग्रीटिंग चल रही है.
डेस्कटॉप Chrome पर चल रहा फ़ुगु ग्रीटिंग मैसेज, कई उपलब्ध सुविधाएं दिखा रहा है.
डेस्कटॉप Chrome पर फ़ुगू ग्रीटिंग चल रहा है.

अगर आपकी दिलचस्पी Fugu Greetings ऐप्लिकेशन में है, तो ढूंढें और इसे GitHub पर फ़ोर्क करें.

GitHub पर Fugu Greetings रेपो.
GitHub पर Fugu Greetings ऐप्लिकेशन.

बेहतर Fugu API की बात करें, तो Chromium की टीम घास को हरी-भरी बनाने के लिए कड़ी मेहनत कर रही है. अपने ऐप्लिकेशन के डेवलपमेंट में प्रोग्रेसिव एनवायरमेंट लागू करके, मैं यह पक्का करता/करती हूं कि सभी को अच्छा और अच्छा बेसलाइन अनुभव मिले, लेकिन जो ब्राउज़र ज़्यादा वेब प्लैटफ़ॉर्म एपीआई के साथ काम करते हैं उन्हें और भी बेहतर अनुभव मिले. मुझे यह देखने का इंतज़ार रहेगा कि आप अपने ऐप्लिकेशन को बेहतर बनाने के लिए क्या-क्या करते हैं.

स्वीकार हैं

मैं क्रिश्चन लीबेल और हेमंथ एचएम का शुक्रगुज़ार हूं जिन्होंने फ़ुगु ग्रीटिंग्स में योगदान दिया. इस लेख की समीक्षा जो मेडली और केएस बास्क ने की है. जेक आर्किबाल्ड ने सर्विस वर्कर के संदर्भ में डाइनैमिक import() के साथ स्थिति को समझने में मेरी मदद की.