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

मॉडर्न ब्राउज़र के लिए बनाना और 2003 की तरह लगातार बेहतर बनाना

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

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

नया JavaScript

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

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

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

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

यहां तक कि हाल ही में 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
Windows XP की हरे घास वाली बैकग्राउंड इमेज.
JavaScript की मुख्य सुविधाओं के मामले में, यह एक बेहतर विकल्प है. (Microsoft प्रॉडक्ट का स्क्रीनशॉट, जिसका इस्तेमाल अनुमति के साथ किया गया है.)

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

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

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

Fugu Greetings PWA, जिसमें पीडब्ल्यूए कम्यूनिटी के लोगो जैसी ड्रॉइंग है.
Fugu Greetings का सैंपल ऐप्लिकेशन.

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

अब प्रोग्रेसिव एन्हैंसमेंट के बारे में बात करने का समय आ गया है. MDN वेब दस्तावेज़ों की शब्दावली में इस कॉन्सेप्ट को इस तरह परिभाषित किया गया है:

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

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

[…]

प्रोग्रेसिव बेहतर बनाने की सुविधा एक काम की तकनीक है. इसकी मदद से, वेब डेवलपर सबसे बेहतर वेबसाइटें बनाने पर ध्यान दे सकते हैं. साथ ही, वे अपनी वेबसाइटों को कई ऐसे उपयोगकर्ता एजेंट पर काम करने लायक बना सकते हैं जिनके बारे में उन्हें जानकारी नहीं है. धीरे-धीरे घटने की सुविधा का इस्तेमाल, परतदार वृद्धि की सुविधा के साथ किया जाता है. हालांकि, दोनों एक जैसी नहीं हैं. अक्सर, धीरे-धीरे घटने की सुविधा को परतदार वृद्धि की सुविधा के उलट माना जाता है. असल में, दोनों तरीके मान्य हैं और अक्सर एक-दूसरे के साथ काम कर सकते हैं.

MDN के योगदान देने वाले

हर ग्रीटिंग कार्ड को शुरू से बनाना बहुत मुश्किल हो सकता है. इसलिए, क्यों न ऐसी सुविधा उपलब्ध कराई जाए जिससे उपयोगकर्ता किसी इमेज को इंपोर्ट करके, उससे काम शुरू कर सकें? पारंपरिक तरीके से, ऐसा करने के लिए आपको <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();
  });
};

अगर इंपोर्ट की सुविधा है, तो एक्सपोर्ट की सुविधा भी होनी चाहिए, ताकि उपयोगकर्ता अपने ग्रीटिंग कार्ड को स्थानीय स्टोरेज में सेव कर सकें. फ़ाइलों को सेव करने का पारंपरिक तरीका यह है कि 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();
};

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

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

तो मैं किसी एपीआई की सुविधा का पता कैसे लगाऊं? 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 Web Inspector का नेटवर्क टैब.
Firefox डेवलपर टूल में, लोड होने वाली लेगसी फ़ाइलें दिख रही हैं.
Firefox Developer Tools का नेटवर्क टैब.

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

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

File System Access API

अब इस बारे में बताने के बाद, फ़ाइल सिस्टम ऐक्सेस एपीआई के आधार पर, इसे लागू करने के बारे में बताने का समय आ गया है. किसी इमेज को इंपोर्ट करने के लिए, मैं window.chooseFileSystemEntries() को कॉल करता हूं और उसे accepts प्रॉपर्टी पास करता हूं. इसमें मैं यह बताता हूं कि मुझे इमेज फ़ाइलें चाहिए. फ़ाइल एक्सटेंशन और एमआईएम टाइप, दोनों काम करते हैं. इससे एक फ़ाइल हैंडल बनता है, जिससे 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 और Web Share Target API की मदद से, ऐसा किया जा सकता है. मोबाइल और हाल ही में डेस्कटॉप ऑपरेटिंग सिस्टम में, शेयर करने के लिए पहले से मौजूद तरीकों का इस्तेमाल किया जा सकता है. उदाहरण के लिए, यहां macOS पर डेस्कटॉप Safari की शेयर शीट दिखाई गई है. यह शीट, मेरे ब्लॉग पर मौजूद किसी लेख से ट्रिगर हुई है. लेख शेयर करें बटन पर क्लिक करके, किसी दोस्त के साथ लेख का लिंक शेयर किया जा सकता है. उदाहरण के लिए, macOS Messages ऐप्लिकेशन का इस्तेमाल करके.

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

ऐसा करने के लिए, कोड बहुत आसान है. मैं navigator.share() को कॉल करता/करती हूं और उसे ऑब्जेक्ट में title, text, और url को वैकल्पिक तौर पर पास करता/करती हूं. अगर मुझे कोई इमेज अटैच करनी है, तो क्या होगा? Web Share API का लेवल 1, फ़िलहाल इस सुविधा के साथ काम नहीं करता. अच्छी बात यह है कि वेब शेयर लेवल 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 के कंपोजर में एक नए ईमेल से जुड़ जाती है.

Contact Picker API

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

सबसे पहले, मुझे उन प्रॉपर्टी की सूची बतानी होगी जिन्हें मुझे ऐक्सेस करना है. इस मामले में, मुझे सिर्फ़ नाम चाहिए, लेकिन इस्तेमाल के दूसरे उदाहरणों के लिए, मुझे टेलीफ़ोन नंबर, ईमेल, अवतार आइकॉन या घर/ऑफ़िस के पते की जानकारी चाहिए. इसके बाद, मैंने options ऑब्जेक्ट को कॉन्फ़िगर किया और multiple को true पर सेट किया, ताकि एक से ज़्यादा एंट्री चुनी जा सकें. आखिर में, navigator.contacts.select() को कॉल किया जा सकता है, जो उपयोगकर्ता के चुने गए संपर्कों के लिए, मनमुताबिक प्रॉपर्टी दिखाता है.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

अब तक आपको पैटर्न समझ आ गया होगा: मैं फ़ाइल सिर्फ़ तब लोड करता/करती हूं, जब एपीआई काम करता हो.

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

Fugu Greeting में, संपर्क बटन पर टैप करके, अपने दो सबसे अच्छे दोस्त, Сергей Михайлович Брин और 劳伦斯·爱德华·"拉里"·佩奇 को चुनने पर, आपको पता चलता है कि संपर्क चुनने वाला टूल सिर्फ़ उनके नाम दिखाता है, न कि उनके ईमेल पते या फ़ोन नंबर जैसी अन्य जानकारी. इसके बाद, उनके नाम मेरे ग्रीटिंग कार्ड पर लिखे जाते हैं.

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

एसिंक्रोनस क्लिपबोर्ड एपीआई

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

सिस्टम के क्लिपबोर्ड पर कुछ कॉपी करने के लिए, मुझे उस पर लिखना होगा. navigator.clipboard.write() वाला तरीका, क्लिपबोर्ड के आइटम के कलेक्शन को पैरामीटर के तौर पर लेता है. हर क्लिपबोर्ड आइटम, असल में एक ऑब्जेक्ट होता है. इसमें वैल्यू के तौर पर ब्लॉब और कुंजी के तौर पर ब्लॉब का टाइप होता है.

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

चिपकाने के लिए, मुझे क्लिपबोर्ड के उन आइटम पर लूप करना होगा जिन्हें navigator.clipboard.read() को कॉल करके पाया जाता है. इसकी वजह यह है कि क्लिपबोर्ड पर एक से ज़्यादा आइटम, अलग-अलग तरह से मौजूद हो सकते हैं. क्लिपबोर्ड के हर आइटम में एक types फ़ील्ड होता है. इससे मुझे उपलब्ध संसाधनों के MIME टाइप के बारे में पता चलता है. मैं क्लिपबोर्ड आइटम के getType() तरीके को कॉल करता/करती हूं. साथ ही, पहले से मिला MIME टाइप पास करता/करती हूं.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

अब यह बताना ज़रूरी नहीं है. हम सिर्फ़ उन ब्राउज़र पर ऐसा करते हैं जिन पर यह सुविधा काम करती है.

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

तो यह सुविधा कैसे काम करती है? मेरे पास macOS के Preview ऐप्लिकेशन में एक इमेज है और मैंने उसे क्लिपबोर्ड पर कॉपी किया है. चिपकाएं पर क्लिक करने पर, Fugu Greetings ऐप्लिकेशन मुझसे पूछता है कि क्या मुझे ऐप्लिकेशन को क्लिपबोर्ड पर मौजूद टेक्स्ट और इमेज देखने की अनुमति देनी है.

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

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

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

Badging API

Badging API एक और काम का एपीआई है. इंस्टॉल किए जा सकने वाले 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');
}

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

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

Periodic Background Sync API

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

पहला चरण, सेवा वर्कर रजिस्ट्रेशन में समय-समय पर सिंक होने वाले इवेंट को 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 में, वॉलपेपर बटन दबाने पर, आपको दिन का ग्रीटिंग कार्ड दिखता है. यह कार्ड, Periodic Background Sync API की मदद से हर दिन अपडेट किया जाता है.

Fugu Greetings ऐप्लिकेशन, जिसमें दिन के हिसाब से ग्रीटिंग कार्ड की नई इमेज दिख रही है.
वॉलपेपर बटन दबाने पर, उस दिन की इमेज दिखती है.

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

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

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

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 में, अनिद्रा चेकबॉक्स होता है. इस पर सही का निशान लगाने पर, स्क्रीन चालू रहती है.

अगर &#39;अनिद्रा&#39; चेकबॉक्स पर सही का निशान लगा है, तो स्क्रीन चालू रहती है.
Insomnia चेकबॉक्स, ऐप्लिकेशन को चालू रखता है.

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

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

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

आखिरी हिस्सा

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

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

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

Android Chrome पर चलने वाला Fugu Greetings, जिसमें कई सुविधाएं दिख रही हैं.
Fugu Greetings, Android Chrome पर चल रहा है.
डेस्कटॉप Safari पर चलने वाला Fugu Greetings, जिसमें कम सुविधाएं दिख रही हैं.
Fugu Greetings, जो डेस्कटॉप Safari पर चल रहा है.
डेस्कटॉप पर Chrome में चल रहे Fugu Greetings की कई सुविधाएं दिख रही हैं.
Fugu Greetings, जो डेस्कटॉप Chrome पर चल रहा है.

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

GitHub पर Fugu Greetings का डेटा स्टोर करने की जगह.
GitHub पर Fugu Greetings ऐप्लिकेशन.

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

आभार

मैं क्रिश्चियन लीबल और हेमंत एचएम का धन्यवाद करता/करती हूं. दोनों ने Fugu Greetings में योगदान दिया है. इस लेख की समीक्षा जो मेडली और केस बेस्केस ने की है. जैक अर्किबाल्ड ने मुझे सेवा वर्कर कॉन्टेक्स्ट में डाइनैमिक import() की स्थिति का पता लगाने में मदद की.