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

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

पब्लिश होने की तारीख: 29 जून, 2020

मार्च 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

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

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

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

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

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

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

सुविधा का पता लगाने की सुविधा का इस्तेमाल आम तौर पर यह पता लगाने के लिए किया जाता है कि ब्राउज़र, नई सुविधाओं को हैंडल कर सकते हैं या नहीं. वहीं, पॉलीफ़िल का इस्तेमाल अक्सर 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 के तौर पर BLOB यूआरएल का इस्तेमाल किया जाए. डाउनलोड शुरू करने के लिए, आपको प्रोग्राम के हिसाब से इस पर "क्लिक" करना होगा. साथ ही, मेमोरी लीक को रोकने के लिए, आपको शायद blob ऑब्जेक्ट के यूआरएल को रद्द करना न भूलना पड़े.

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 काम नहीं करता उन पर मैं लेगसी स्क्रिप्ट लोड करता हूं.

Safari Web Inspector में, लेगसी फ़ाइलों को लोड होते हुए दिखाया गया है.
Firefox Developer Tools में, लेगसी फ़ाइलों को लोड होते हुए दिखाया गया है.

हालांकि, 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() को कॉल करके, लिखने के लिए स्ट्रीम बनाता हूँ. इसके बाद, मैं फ़ाइल में ग्रीटिंग कार्ड की इमेज के तौर पर इस्तेमाल होने वाला BLOB लिखता हूं. आखिर में, मैं लिखने के लिए स्ट्रीम बंद कर देता हूं.

कोई भी चीज़ कभी भी काम नहीं कर सकती: हो सकता है कि डिस्क में जगह न हो, लिखने या पढ़ने में कोई गड़बड़ी हो या हो सकता है कि उपयोगकर्ता ने फ़ाइल डायलॉग को रद्द कर दिया हो. इसलिए, मैं हमेशा कॉल को 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 और Web Share Target API

attempt-right

कार्ड को हमेशा के लिए सेव करने के अलावा, हो सकता है कि मुझे अपना ग्रीटिंग कार्ड शेयर करना हो. Web Share API और Web Share Target API की मदद से ऐसा किया जा सकता है. मोबाइल और हाल ही में डेस्कटॉप ऑपरेटिंग सिस्टम में, शेयर करने के लिए पहले से मौजूद सुविधाएं जोड़ी गई हैं.

उदाहरण के लिए, macOS पर डेस्कटॉप Safari की शेयर शीट तब ट्रिगर होती है, जब कोई उपयोगकर्ता मेरे ब्लॉग पर लेख शेयर करें पर क्लिक करता है. macOS Messages ऐप्लिकेशन का इस्तेमाल करके, किसी दोस्त के साथ लेख का लिंक शेयर किया जा सकता है.

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

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

Contact Picker API

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

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

Asynchronous Clipboard API

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

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

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 ऐप्लिकेशन में, क्लिपबोर्ड की अनुमति का अनुरोध दिखाने वाला प्रॉम्प्ट.
क्लिपबोर्ड की अनुमति के लिए प्रॉम्प्ट.

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

macOS Preview ऐप्लिकेशन में, बिना टाइटल वाली और अभी-अभी चिपकाई गई इमेज.
macOS के Preview ऐप्लिकेशन में चिपकाई गई इमेज.

Badging API

Badging API भी एक उपयोगी एपीआई है. Fugu Greetings को इंस्टॉल किया जा सकता है. इसलिए, इसमें एक ऐप्लिकेशन आइकॉन होता है. उपयोगकर्ता इसे ऐप्लिकेशन डॉक या होम स्क्रीन पर रख सकते हैं. एपीआई को मज़ेदार तरीके से दिखाने के लिए, इसका इस्तेमाल 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 का इस्तेमाल करता है.

पहला चरण, सर्विस वर्कर के रजिस्ट्रेशन में समय-समय पर सिंक होने वाले इवेंट को रजिस्टर करना है. यह '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() का इस्तेमाल किया गया है. ऐसा इसलिए, क्योंकि सर्विस वर्कर के कॉन्टेक्स्ट में डाइनैमिक import() का इस्तेमाल अभी नहीं किया जा सकता.

// 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 में, Wallpaper बटन दबाने पर, दिन के हिसाब से ग्रीटिंग कार्ड की इमेज दिखती है. यह इमेज, Periodic Background Sync 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 के सूचना केंद्र में दिखती है.

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

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

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

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

GitHub पर Fugu को फ़ोर्क किया जा सकता है.

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

Acknowledgements

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