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

मॉडर्न ब्राउज़र के लिए बनाना और 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 में मौजूद सुविधाओं को जोड़ने के लिए किया जाता है.

[…]

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

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

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

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

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

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

macOS पर डेस्कटॉप Safari की शेयर शीट, लेख के &#39;शेयर करें&#39; बटन से ट्रिगर हुई
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 Greeting कार्ड ऐप्लिकेशन की मदद से, यह काम कैसे करना है. सबसे पहले, मुझे एक ब्लॉब वाले 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 कीबोर्ड है और मुझे नहीं पता कि उसका नाम कैसे टाइप किया जाए. इस समस्या को Contact Picker API की मदद से हल किया जा सकता है. चूंकि मेरे दोस्त ने संपर्क पिकर 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 फ़ील्ड होता है. इससे मुझे उपलब्ध संसाधनों के एमआईएमई टाइप की जानकारी मिलती है. मैं क्लिपबोर्ड आइटम के 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

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

पहला कदम, 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, जिसमें कई उपलब्ध सुविधाएं दिख रही हैं.
Android Chrome पर चल रहा Fugu Greetings.
डेस्कटॉप Safari पर चलने वाला Fugu Greetings, जिसमें कम सुविधाएं दिख रही हैं.
Fugu Greetings, जो डेस्कटॉप Safari पर चल रहा है.
डेस्कटॉप पर Chrome में चल रहे Fugu Greetings की कई सुविधाएं दिख रही हैं.
Fugu Greetings, जो डेस्कटॉप Chrome पर चल रहा है.

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

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

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

स्वीकार की गई

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