आसानी से काम करने वाले और रिस्पॉन्सिव HTML5 ऐप्लिकेशन बनाने के लिए, ऐप्लिकेशन के सभी अलग-अलग हिस्सों के बीच सिंक करना सबसे ज़रूरी है. जैसे, डेटा फ़ेच करना, प्रोसेस करना, ऐनिमेशन, और यूज़र इंटरफ़ेस एलिमेंट.
डेस्कटॉप या नेटिव एनवायरमेंट के मुकाबले, ब्राउज़र में मुख्य अंतर यह है कि ब्राउज़र, थ्रेडिंग मॉडल का ऐक्सेस नहीं देते. साथ ही, वे उपयोगकर्ता इंटरफ़ेस (यानी डीओएम) को ऐक्सेस करने वाली हर चीज़ के लिए एक थ्रेड उपलब्ध कराते हैं. इसका मतलब है कि यूज़र इंटरफ़ेस एलिमेंट को ऐक्सेस करने और उसमें बदलाव करने वाले सभी ऐप्लिकेशन लॉजिक, हमेशा एक ही थ्रेड में होते हैं. इसलिए, ऐप्लिकेशन की सभी वर्क यूनिट को जितना हो सके उतना छोटा और बेहतर तरीके से काम करने की ज़रूरत होती है. साथ ही, ब्राउज़र से मिलने वाली एसिंक्रोनस सुविधाओं का ज़्यादा से ज़्यादा फ़ायदा लेना भी अहम होता है.
ब्राउज़र एसिंक्रोनस एपीआई
अच्छी बात यह है कि ब्राउज़र कई एसिंक्रोनस एपीआई उपलब्ध कराते हैं. जैसे कि आम तौर पर इस्तेमाल किए जाने वाले XHR (XMLHttpRequest या 'AJAX') एपीआई, IndexedDB, SQLite, HTML5 Web {/1}, और HTML5 GeoLocation API. यहां तक कि कुछ डीओएम से जुड़ी कार्रवाइयां भी एसिंक्रोनस रूप से दिखाई जाती हैं, जैसे कि ट्रांज़िशनएंड इवेंट के ज़रिए CSS3 ऐनिमेशन.
ब्राउज़र, इवेंट या कॉलबैक की मदद से, ऐप्लिकेशन लॉजिक में एसिंक्रोनस प्रोग्रामिंग को एक्सपोज़ करते हैं.
इवेंट-आधारित एसिंक्रोनस एपीआई में, डेवलपर दिए गए ऑब्जेक्ट के लिए कोई इवेंट हैंडलर रजिस्टर करते हैं
(जैसे कि एचटीएमएल एलिमेंट या अन्य डीओएम ऑब्जेक्ट) और फिर कार्रवाई को कॉल करते हैं. आम तौर पर, ब्राउज़र किसी दूसरी थ्रेड में ऐक्शन करेगा और ज़रूरत पड़ने पर मुख्य थ्रेड में इवेंट को ट्रिगर करेगा.
उदाहरण के लिए, इवेंट पर आधारित असाइनोक्रोनस एपीआई, XHR API का इस्तेमाल करने वाला कोड ऐसा दिखेगा:
// Create the XHR object to do GET to /data resource
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
alert("We got data: " + xhr.response);
}
},false)
// perform the work
xhr.send();
CSS3 transitionEnd इवेंट, इवेंट पर आधारित असाइनोक्रोनस एपीआई का एक और उदाहरण है.
// get the html element with id 'flyingCar'
var flyingCarElem = document.getElementById("flyingCar");
// register an event handler
// ('transitionEnd' for FireFox, 'webkitTransitionEnd' for webkit)
flyingCarElem.addEventListener("transitionEnd",function(){
// will be called when the transition has finished.
alert("The car arrived");
});
// add the CSS3 class that will trigger the animation
// Note: some browers delegate some transitions to the GPU , but
// developer does not and should not have to care about it.
flyingCarElemen.classList.add('makeItFly')
SQLite और HTML5 जियोलोकेशन जैसे अन्य ब्राउज़र एपीआई, कॉलबैक पर आधारित होते हैं. इसका मतलब है कि डेवलपर, आर्ग्युमेंट के तौर पर एक फ़ंक्शन पास करता है. इसे, उससे जुड़े रिज़ॉल्यूशन के साथ, एपीआई के तहत लागू होने वाले फ़ंक्शन से कॉल किया जाएगा.
उदाहरण के लिए, HTML5 जियोलोकेशन के लिए कोड ऐसा दिखता है:
// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){
alert('Lat: ' + position.coords.latitude + ' ' +
'Lon: ' + position.coords.longitude);
});
इस मामले में, हम सिर्फ़ एक तरीका कॉल करते हैं और एक फ़ंक्शन पास करते हैं, जिसे अनुरोध किए गए नतीजे के साथ वापस कॉल किया जाएगा. इससे ब्राउज़र, इस सुविधा को सिंक्रोनस या असिंक्रोनस तरीके से लागू कर सकता है. साथ ही, डेवलपर को एक ही एपीआई दे सकता है, भले ही लागू करने की जानकारी कुछ भी हो.
ऐप्लिकेशन को एसिंक्रोनस के लिए तैयार करना
ब्राउज़र के बिल्ट-इन एसिंक्रोनस एपीआई के अलावा, अच्छी तरह से आर्किटेक्ट किए गए ऐप्लिकेशन को अपने लो लेवल एपीआई को एसिंक्रोनस तरीके से भी दिखाना चाहिए, खासकर तब, जब वे किसी तरह का I/O या कंप्यूटेशनल हेवी प्रोसेसिंग करते हैं. उदाहरण के लिए, डेटा पाने के लिए एपीआई, एक साथ काम नहीं करने चाहिए. साथ ही, इनका क्रम ऐसा नहीं होना चाहिए:
// WRONG: this will make the UI freeze when getting the data
var data = getData();
alert("We got data: " + data);
इस एपीआई डिज़ाइन के लिए, getData() को ब्लॉक करना ज़रूरी है. इससे डेटा फ़ेच होने तक, यूज़र इंटरफ़ेस फ़्रीज़ हो जाएगा. अगर डेटा, JavaScript कॉन्टेक्स्ट में लोकल है, तो हो सकता है कि यह कोई समस्या न हो. हालांकि, अगर डेटा को नेटवर्क से या SQLite या इंडेक्स स्टोर में लोकल तौर पर फ़ेच करना पड़ता है, तो इससे उपयोगकर्ता अनुभव पर काफ़ी असर पड़ सकता है.
सही डिज़ाइन यह है कि जिन ऐप्लिकेशन एपीआई को प्रोसेस करने में कुछ समय लग सकता है उन्हें शुरू से ही एसिंक्रोनस बनाएं. ऐसा इसलिए, क्योंकि सिंक्रोनस ऐप्लिकेशन कोड को एसिंक्रोनस बनाने के लिए, उसे फिर से डिज़ाइन करना मुश्किल हो सकता है.
उदाहरण के लिए, आसान getData() एपीआई कुछ ऐसा दिखेगा:
getData(function(data){
alert("We got data: " + data);
});
इस तरीके की सबसे अच्छी बात यह है कि इससे ऐप्लिकेशन के यूज़र इंटरफ़ेस (यूआई) कोड को शुरू से ही असाइनोक्रोनस (एक साथ कई काम करने की सुविधा) के हिसाब से बनाने के लिए मजबूर किया जाता है. साथ ही, इससे एपीआई को यह तय करने में मदद मिलती है कि उन्हें बाद में असाइनोक्रोनस मोड में काम करना है या नहीं.
ध्यान दें कि सभी ऐप्लिकेशन एपीआई को असाइनोक्रोनस होने की ज़रूरत नहीं है या ऐसा होना चाहिए. आम तौर पर, ऐसा कोई भी एपीआई जो किसी भी तरह का I/O या ज़्यादा प्रोसेसिंग (ऐसी कोई भी चीज़ जिसमें 15 मिलीसेकंड से ज़्यादा समय लग सकता है) करता है उसे शुरू से ही असिंक्रोनस तरीके से एक्सपोज़ किया जाना चाहिए. भले ही, पहली बार लागू करने का तरीका सिंक्रोनस हो.
हैंडलिंग से जुड़ी गड़बड़ियां
एसिंक्रोनस प्रोग्रामिंग की एक समस्या यह है कि गड़बड़ियों को मैनेज करने के लिए, try/catch का पारंपरिक तरीका अब काम नहीं करता. ऐसा इसलिए, क्योंकि आम तौर पर गड़बड़ियां किसी दूसरी थ्रेड में होती हैं. इसलिए, कॉल पाने वाले व्यक्ति के पास कॉलर को सूचना देने का एक तरीका होना चाहिए, ताकि प्रोसेस के दौरान कोई गड़बड़ी होने पर कॉलर को इसकी जानकारी दी जा सके.
इवेंट-आधारित असाइनोक्रोनस एपीआई में, इवेंट मिलने पर ऐप्लिकेशन कोड, इवेंट या ऑब्जेक्ट से क्वेरी करके अक्सर ऐसा किया जाता है. कॉलबैक आधारित एसिंक्रोनस एपीआई के लिए, सबसे सही तरीका यह है कि एक दूसरा आर्ग्युमेंट इस्तेमाल किया जाए. यह आर्ग्युमेंट के तौर पर, गड़बड़ी की सही जानकारी होने पर ही इसे कॉल किया जाता.
हमारा getData कॉल कुछ ऐसा दिखेगा:
// getData(successFunc,failFunc);
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});
इसे $.स्थगित के साथ एक साथ रखना
ऊपर बताए गए कॉलबैक तरीके की एक सीमा यह है कि सिंक करने के लिए, थोड़ा भी बेहतर लॉजिक लिखना मुश्किल हो सकता है.
उदाहरण के लिए, अगर आपको तीसरा एपीआई इस्तेमाल करने से पहले, दो एसिंक्रोनस एपीआई के पूरे होने का इंतज़ार करना है, तो कोड मुश्किल हो सकता है.
// first do the get data.
getData(function(data){
// then get the location
getLocation(function(location){
alert("we got data: " + data + " and location: " + location);
},function(ex){
alert("getLocation failed: " + ex);
});
},function(ex){
alert("getData failed: " + ex);
});
अगर ऐप्लिकेशन को एक ही कॉल को ऐप्लिकेशन के कई हिस्सों से करने की ज़रूरत पड़ती है, तो चीज़ें और भी मुश्किल हो सकती हैं. ऐसा इसलिए, क्योंकि हर कॉल को कई चरणों में ये कॉल करने होंगे या ऐप्लिकेशन को अपना कैश मेमोरी सिस्टम लागू करना होगा.
हालांकि, एक पुराना पैटर्न है, जिसे प्रॉमिस कहा जाता है. यह Java में फ़्यूचर से मिलता-जुलता है. साथ ही, jQuery कोर में $.Deferred नाम का एक बेहतर और आधुनिक तरीका है. यह एसिंक्रोनस प्रोग्रामिंग के लिए आसान और बेहतरीन समाधान उपलब्ध कराता है.
इसे आसान बनाने के लिए, Promises पैटर्न से पता चलता है कि असाइनोक्रोनस एपीआई, एक Promise ऑब्जेक्ट दिखाता है. यह एक तरह का “Promise है कि नतीजा, उससे जुड़े डेटा के साथ दिखाया जाएगा.” नतीजा पाने के लिए, कॉलर को Promise ऑब्जेक्ट मिलता है और वह done(successFunc(data)) को कॉल करता है. इससे Promise ऑब्जेक्ट को पता चलता है कि “डेटा” रिज़ॉल्व होने पर, इस successFunc को कॉल किया जाए.
इसलिए, ऊपर दिया गया getData कॉल का उदाहरण ऐसा बन जाता है:
// get the promise object for this API
var dataPromise = getData();
// register a function to get called when the data is resolved
dataPromise.done(function(data){
alert("We got data: " + data);
});
// register the failure function
dataPromise.fail(function(ex){
alert("oops, some problem occured: " + ex);
});
// Note: we can have as many dataPromise.done(...) as we want.
dataPromise.done(function(data){
alert("We asked it twice, we get it twice: " + data);
});
यहां, हमें पहले dataPromise ऑब्जेक्ट मिलता है. इसके बाद, .done मेथड को कॉल करके, एक फ़ंक्शन रजिस्टर किया जाता है. हमें डेटा रिज़ॉल्व होने पर, इस फ़ंक्शन को कॉल करना होता है. हम गलती से होने वाली गड़बड़ी को ठीक करने के लिए, .fail तरीके का इस्तेमाल भी कर सकते हैं. ध्यान दें कि ज़रूरत के हिसाब से .done या .fail कॉल किए जा सकते हैं. ऐसा इसलिए, क्योंकि Promise को लागू करने वाला (jQuery कोड) रजिस्टरेशन और कॉलबैक को मैनेज करेगा.
इस पैटर्न के साथ, ज़्यादा बेहतर सिंक्रोनाइज़ेशन कोड लागू करना ज़्यादा आसान होता है. साथ ही, jQuery पहले से ही $.when सबसे सामान्य कोड देता है.
उदाहरण के लिए, ऊपर नेस्ट किया गया getData/getLocation कॉलबैक कैसा होगा, यह कुछ ऐसा होगा:
// assuming both getData and getLocation return their respective Promise
var combinedPromise = $.when(getData(), getLocation())
// function will be called when both getData and getLocation resolve
combinePromise.done(function(data,location){
alert("We got data: " + dataResult + " and location: " + location);
});
सबसे अच्छी बात यह है कि jQuery.Deferred की मदद से, डेवलपर के लिए एसिंक्रोनस फ़ंक्शन को लागू करना बहुत आसान हो जाता है. उदाहरण के लिए, getData कुछ ऐसा दिख सकता है:
function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();
// ---- AJAX Call ---- //
XMLHttpRequest xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
// 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)
deferred.resolve(xhr.response);
}else{
// 3.2) REJECT the DEFERRED (this will trigger all the fail()...)
deferred.reject("HTTP error: " + xhr.status);
}
},false)
// perform the work
xhr.send();
// Note: could and should have used jQuery.ajax.
// Note: jQuery.ajax return Promise, but it is always a good idea to wrap it
// with application semantic in another Deferred/Promise
// ---- /AJAX Call ---- //
// 2) return the promise of this deferred
return deferred.promise();
}
इसलिए, जब getData() को कॉल किया जाता है, तो यह पहले एक नया jQuery.Deferred ऑब्जेक्ट (1) बनाता है और फिर अपना Promise (2) दिखाता है, ताकि कॉल करने वाला व्यक्ति इसके done और fail फ़ंक्शन रजिस्टर कर सके. इसके बाद, जब XHR कॉल वापस आता है, तो वह डिफ़र्ड (3.1) को हल करता है या उसे अस्वीकार कर देता है (3.2). deaffed.resolve करने से सभी हो गए(...) फ़ंक्शन और दूसरे प्रॉमिस फ़ंक्शन (जैसे कि, उसके बाद और पाइप) ट्रिगर हो जाएंगे और adस्थगित.reject को कॉल करने से सभी failed() फ़ंक्शन कॉल हो जाएंगे.
उपयोग के उदाहरण
यहां कुछ ऐसे उदाहरण दिए गए हैं जिनमें डिफ़र्ड ट्रांज़िशन का इस्तेमाल फ़ायदेमंद हो सकता है:
डेटा ऐक्सेस: डेटा ऐक्सेस एपीआई को $.Deferred के तौर पर एक्सपोज़ करना, अक्सर सही डिज़ाइन होता है. यह रिमोट डेटा के लिए ज़रूरी है, क्योंकि सिंक किए गए रिमोट कॉल से उपयोगकर्ता अनुभव पूरी तरह खराब हो जाएगा. हालांकि, यह लोकल डेटा के लिए भी ज़रूरी है, क्योंकि अक्सर निचले लेवल के एपीआई (उदाहरण के लिए, SQLite और IndexedDB) एसिंक्रोनस होते हैं. स्थगित एपीआई की $.when और .खोज क्वेरी को सिंक करने और एसिंक्रोनस सब-क्वेरी को चेन करने में बहुत मदद मिलती है.
यूज़र इंटरफ़ेस (यूआई) के ऐनिमेशन: ट्रांज़िशन एंड इवेंट के साथ एक या एक से ज़्यादा ऐनिमेशन को व्यवस्थित करना मुश्किल हो सकता है. खास तौर पर तब, जब ऐनिमेशन CSS3 ऐनिमेशन और JavaScript का मिला-जुला रूप होता है (जैसा कि अक्सर होता है). ऐनिमेशन फ़ंक्शन को 'स्थगित' के तौर पर रैप करने से, कोड की जटिलता कम हो सकती है और उसे बेहतर तरीके से लागू किया जा सकता है. cssAnimation(className) जैसा कोई सामान्य जेनरिक रैपर फ़ंक्शन भी बहुत मददगार हो सकता है. यह फ़ंक्शन, transitionEnd पर रिज़ॉल्व होने वाला Promise ऑब्जेक्ट दिखाता है.
यूज़र इंटरफ़ेस (यूआई) कॉम्पोनेंट डिसप्ले: यह थोड़ा बेहतर है, लेकिन बेहतर एचटीएमएल कॉम्पोनेंट फ़्रेमवर्क में भी 'स्थगित' का इस्तेमाल किया जाना चाहिए. जानकारी पर बहुत ज़्यादा ध्यान दिए बिना (यह किसी दूसरी पोस्ट का विषय होगा), जब किसी ऐप्लिकेशन को यूज़र इंटरफ़ेस के अलग-अलग हिस्से दिखाने की ज़रूरत होती है, तो उन कॉम्पोनेंट के लाइफ़साइकल को 'स्थगित' में पूरा करने से, समय को बेहतर तरीके से कंट्रोल किया जा सकता है.
ब्राउज़र का कोई भी असाइनोक्रोनस एपीआई: सामान्य बनाने के मकसद से, ब्राउज़र के एपीआई कॉल को डिफ़र्ड के तौर पर रैप करना अक्सर एक अच्छा आइडिया होता है. इसके लिए, हर बार चार से पांच लाइन कोड की ज़रूरत होती है. हालांकि, इससे किसी भी ऐप्लिकेशन कोड को आसानी से समझा जा सकता है. ऊपर दिए गए getData/getLocation के स्यूडो कोड में दिखाया गया है कि इससे ऐप्लिकेशन कोड में, सभी तरह के एपीआई (ब्राउज़र, ऐप्लिकेशन की खास जानकारी, और कंपाउंड) के लिए एक असाइनोक्रोनस मॉडल हो सकता है.
कैश मेमोरी में सेव करना: यह एक तरह का फ़ायदा है, लेकिन कुछ मामलों में यह काफ़ी मददगार हो सकता है. Promise API (उदाहरण के लिए, .done(…) और .fail(…)) को एसिंक्रोनस कॉल करने से पहले या बाद में कॉल किया जा सकता है. साथ ही, Deferred ऑब्जेक्ट का इस्तेमाल, एसिंक्रोनस कॉल के लिए कैश मेमोरी के हैंडल के तौर पर किया जा सकता है. उदाहरण के लिए, CacheManager, दिए गए अनुरोधों के लिए सिर्फ़ 'देर से पूरा होने वाला' टास्क को ट्रैक कर सकता है. साथ ही, अगर 'देर से पूरा होने वाला' टास्क अमान्य नहीं हुआ है, तो उससे मैच होने वाले टास्क का प्रॉमिस दिखा सकता है. इसकी सबसे अच्छी बात यह है कि कॉल करने वाले को यह जानने की ज़रूरत नहीं होती कि कॉल पहले ही हल हो गया है या हल किया जा रहा है. कॉलबैक फ़ंक्शन को ठीक उसी तरह से कॉल किया जाएगा.
नतीजा
$.Deferred का कॉन्सेप्ट आसान है, लेकिन इसे अच्छी तरह से समझने में समय लग सकता है. हालांकि, ब्राउज़र एनवायरमेंट की प्रकृति को देखते हुए, किसी भी गंभीर एचटीएमएल5 ऐप्लिकेशन डेवलपर के लिए, JavaScript में एसिंक्रोनस प्रोग्रामिंग में महारत हासिल करना ज़रूरी है. साथ ही, एसिंक्रोनस प्रोग्रामिंग को भरोसेमंद और बेहतर बनाने के लिए, Promise पैटर्न (और jQuery लागू करना) बेहतरीन टूल हैं.