RSS Reader in jQuery .vs. JavaScript (AJAX)

Parsing RSS is a task that many developers have been faced with. jQuery makes this significantly easier on the client-side, but the good ol’ AJAX is not that bad either if you set things up functionally to minimize on-page code. Two versions of the same RSS Reader with parsing handled with and without the “write less, do more” JavaScript library follow.
DISCLAIMER:
I know I haven’t really captured the spirit of jQuery by using identical functions to straight JavaScript, however, I mainly wanted to show the similarities and differences between using one parser .vs. another.
FOREWARNING:
Thus, I fully accept that a jQuery whiz, which I am not yet, could clearly use the library to write a few lines of a jQuery RSS parser that would easily blow this one away in terms of efficiency and robustness, thus highlighting the advantages over using old-fashioned JavaScript.
JUSTIFICATION:
That’s just not the point of this article, which is merely a quick look at making an AJAX request and parsing XML in jQuery .vs. JavaScript.
Here’s a very basic RSS Reader in jQuery:
/** * parseAtom * Parse Feeds in Atompub (ATOM) feed format * @param feed XML Object */ function parseAtom(feed) { var feed = $(feed).find("feed"); //<feed> var feed_title = $(feed).find("title").first().text(); //<title> var feed_subtitle = $(feed).find("subtitle").first().text(); //<subtitle> var feed_link = $(feed).find("link").first().text(); //<link> var feed_description = $(feed).find("description").first().text(); //<description> var feed_updated = $(feed).find("updated").first().text(); //<updated> var metadata = '<a href="'+feed_link+'">'+feed_title+'</a><br/>' + feed_description + ' | ' + feed_subtitle + '<br/>' + feed_updated + '<div style="clear:both;"> </div>'; //METADATA var news = ''; var entry = 0; $(feed).find("entry").each( function() { //<entry> title = $(this).find("title").text().replace(feed_title,''); //<title> guid = $(this).find("guid").text(); //<guid> id = !empty(guid) ? guid : title.substring(0,12)+'-'+item; //unique ID image = $(this).find("link[type='image/jpeg']").attr("href"); //<image> link = $(this).find("link").text(); //<link> desc = $(this).find("description").text(); //<description> description = desc.replace(/</gi,'<').replace(/>/gi,'>'); if (typeof image==='undefined'||image===null||image==='') { image = 'http://www.canada.com/images/topic/logo_canada.com.gif'; } else { image = 'http://www.canada.com/'+image; } news += '<article><a href="#'+id+'" title="Click once for summary, twice to visit source..."><img class="icon" src="'+image+'" alt="'+title+'" /> '+title+'</a><div id="'+id+'" class="lb"><a href="'+link+'"><img src="'+image+'" alt="'+title+'" /></a><a href="#top" class="close"><img src="close.gif" alt="Close" /></a><div class="story">'+description+'</div></div></article>'; entry++; }); return news; // or metadata+news to show header above news list } /** * parseRSS * Parse Feeds in Really Simple Syndication (or RDF Site Summary) format * @param rss XML Object */ function parseRSS(rss) { var rss = $(rss).find("rss"); //<rss> var rss_channel = $(rss).find("channel"); //<channel> var rss_title = $(rss).find("title").first().text(); //<title> var rss_link = $(rss).find("link").first().text(); //<link> var rss_image = $(rss).find("image").first(); //<image> var rss_image_url = rss_image.find("url").text(); //<url> var rss_image_link = rss_image.find("link").text(); //<link> var rss_image_title = rss_image.find("title").text(); //<title> var rss_description = $(rss).find("description").first().text(); //<description> var rss_language = $(rss).find("language").text(); //<language> var rss_lastBuildDate = $(rss).find("updated").first().text(); //<updated> var rss_copyright = $(rss).find("copyright").text(); //<copyright> var rss_docs = $(rss).find("docs").text(); //<docs> var rss_ttl = $(rss).find("ttl").text(); //<ttl> var metadata = '<a href="'+rss_link+'"><img src="'+rss_image_url+'" title="'+rss_image_title+'": "'+rss_description+'" style="float:left; vertical-align:middle;"/></a><br/>' + rss_lastBuildDate + ' | ' + rss_copyright + '<br/>Following spec at: ' + rss_docs + ' | Cache for: ' + rss_ttl + ' | ' + rss_language + '<div style="clear:both;"> </div>'; //METADATA var news = ''; var item = 0; $(rss).find("item").each( function() { //<item> title = $(this).find("title").text().replace(rss_title,''); //<title> guid = $(this).find("guid").text(); //<guid> id = !empty(guid) ? guid : title.substring(0,12)+'-'+item; //unique ID thumb = $(this).find("media:thumbnail").attr("url"); //<image> image = !empty(thumb) ? thumb : 'rss.png'; link = $(this).find("link").text(); //<link> desc = $(this).find("description").text(); //<description> description = desc.replace(/</gi,'<').replace(/>/gi,'>'); content = $(this).find("content").text(); //<content> news += '<article><a href="#'+id+'" title="Click once for summary, twice to visit source..."><img class="icon" src="'+image+'" alt="'+title+'" /> '+title+'</a><div id="'+id+'" class="lb"><a href="'+link+'"><img src="'+image+'" alt="'+title+'" /></a><a href="#top" class="close"><img src="close.gif" alt="Close" /></a><div class="story">'+description+'</div></div></article>'; }); return news; } function parseFeed(url, type) { contentType = type.toLowerCase(); $.ajax({ url: 'proxy.php?url='+encodeURI(url)+'&path='+getPath(url)+'&f='+contentType+'&e=utf-8', success: function(data) { var news = ''; if (contentType==='atom'||contentType==='feed') { news = parseAtom(data); //use ATOM parser if feed type was set } else { news = parseRSS(data); //use RSS 2.0 parser if feed was not set, or it was a value other than ATOM or FEED (i.e. RSS, mRSS, RSS1.0, RSS2.0, RDF, XML, etc...) } $('#content').html(news); }, error: function(ex) { alert('Error loading feed: '+ex); } }); }
-or-
Here’s the same reader in JavaScript with the AJAX call handled by hand (and thus marginally less cross-browser than the jQuery one, but should work in IE down to 5.5):
/************************************************************/ /* AJAX cross-browser helper utilities */ function loadXMLDoc(docURL) { if (window.XMLHttpRequest) { // FireFox, Opera, Chrome, Safari xhttp = new XMLHttpRequest(); } else { // Internet Explorer xhttp = new ActiveXObject("Microsoft.XMLHTTP"); } xhttp.open("GET",docURL,false); xhttp.send(); return xhttp.responseXML; } function loadXMLString(txt) { if (window.DOMParser) { // FireFox, Opera, Chrome, Safari parser=new DOMParser(); xmlDoc=parser.parseFromString(txt,"text/xml"); } else { // Internet Explorer xmlDoc=new ActiveXObject("Microsoft.XMLDOM"); xmlDoc.async = "false"; xmlDoc.loadXML(txt); } return xmlDoc; } /************************************************************/ function parseRSS(data) { var rss = data.getElementsByTagName("rss"); //<rss> //Parse RSS Feed channel properties channel = data.getElementsByTagName("channel"); //<channel> title = data.getElementsByTagName("title")[0].childNodes[0].nodeValue; //<title> link = data.getElementsByTagName("link")[0].childNodes[0].nodeValue; //<link> description = data.getElementsByTagName("description")[0].childNodes[0].nodeValue; //<description> image_url = 'rss.png', image_link = '#', image_html=title+' (RSS Feed)'; image = data.getElementsByTagName("image")[0]; //<image> (optional RSS element) if (!empty(image)) { image_url = image.getElementsByTagName("url")[0].childNodes[0].nodeValue; //<url> image_link = image.getElementsByTagName("link")[0].childNodes[0].nodeValue; //<link> image_html = !empty(image_url) ? image_url : '<img src="'+image_url+'" title="'+title+'": "'+description+'" style="float:left; vertical-align:middle;"/>'; } //language = data.getElementsByTagName("language")[0].childNodes[0].nodeValue;//<language> //copyright = data.getElementsByTagName("copyright")[0].childNodes[0].nodeValue; //<copyright> //lastBuildDate = data.getElementsByTagName("lastBuildDate")[0].childNodes[0].nodeValue; //<lastBuildDate> //docs = data.getElementsByTagName("docs")[0].childNodes[0].nodeValue; //<docs> //ttl = data.getElementsByTagName("ttl")[0].childNodes[0].nodeValue; //<ttl> metadata = "<a href=""+link+"">"+image_html+"</a>";//"<br/>" + lastBuildDate + " | " + copyright + "<br/>Following spec at: " + docs + " | Cache for: " + ttl + " | " + language + "<div style="clear:both;"> </div>"; //Parse RSS Feed items items = data.getElementsByTagName("item"); //<item> news = "<ol>"; for (i = 0; i < items.length; i++) { title = items[i].getElementsByTagName("title")[0].childNodes[0].nodeValue; //<title> guid = items[i].getElementsByTagName("guid")[0].childNodes[0].nodeValue; //<guid> id = !empty(guid) ? guid : title.substring(0,12)+'-'+item; //unique ID image = 'RSS.png'; try { //pic = items[i].getElementsByTagName("media:thumbnail")[0].getAttribute("url"); //<media:thumbnail> (image) mRSS2.0 only // image = !empty(pic) ? pic : 'RSS.png'; } catch (ex) { } link = items[i].getElementsByTagName("link")[0].childNodes[0].nodeValue; //<link> pubDate = items[i].getElementsByTagName("pubDate")[0].childNodes[0].nodeValue; //<pubDate> desc = items[i].getElementsByTagName("description")[0].childNodes[0].nodeValue; //<description> description = desc.replace(/</gi,'<').replace(/>/gi,'>'); news += '<div class="article"><a href="#lb'+i+'" title="Click once for summary, twice to visit source..."><img class="icon" src="'+image+'" alt="'+title+'" /> '+title+'</a><div id="lb'+i+'" class="lb"><a href="'+link+'"><img src="'+image+'" alt="'+title+'" /></a><a href="#top" class="close"><img src="close.gif" alt="Close" /></a><div class="story">'+description+'</div></div></div>'; } news += "</ol>"; return metadata+news; } function parseAtom(data) { var feed = data.getElementsByTagName("feed"); //<feed> var title = data.getElementsByTagName("title")[0].childNodes[0].nodeValue; //<title> var subtitle = data.getElementsByTagName("subtitle")[0].childNodes[0].nodeValue; //<subtitle> //var link = data.getElementsByTagName("link")[0].childNodes[0].nodeValue; //<link> //var description = data.getElementsByTagName("description")[0].childNodes[0].nodeValue;//<description> var updated = data.getElementsByTagName("updated")[0].childNodes[0].nodeValue; //<updated> var news = '';//METADATA: '<a href="'+link+'">'+title+'</a><br/>' + description + ' | ' + subtitle + '<br/>' + updated + '<div style="clear:both;"> </div>'; var entry = data.getElementsByTagName("entry"); for (var i=0; i<entry.length; i++) { //<entry> title = entry[i].getElementsByTagName("title")[0].childNodes[0].nodeValue; //<title> guid = items[i].getElementsByTagName("guid")[0].childNodes[0].nodeValue; //<guid> id = !empty(guid) ? guid : title.substring(0,12)+'-'+item; //unique ID image = 'RSS.png'; try { pic = entry[i].getElementsByTagName("link")[1].getAttribute("href"); //<image> link[type='image/jpeg'] image = !empty(pic) ? pic : 'RSS.png'; } catch (ex) { } link = entry[i].getElementsByTagName("link")[0].getAttribute("href"); //<link> link[type='text/html'] desc = entry[i].getElementsByTagName("description")[0].childNodes[0].nodeValue; //<description> description = desc.replace(/</gi,'<').replace(/>/gi,'>'); news += '<div class="article"><a href="#lb'+i+'" title="Click once for summary, twice to visit source..."><img class="icon" src="'+image+'" alt="'+title+'" /> '+title+'</a><div id="lb'+i+'" class="lb"><a href="'+link+'"><img src="'+image+'" alt="'+title+'" /></a><a href="#top" class="close"><img src="close.gif" alt="Close" /></a><div class="story">'+description+'</div></div></div>'; } return news; } var FEED_URL = 'http://feeds.wired.com/wired/index'; var type = 'RSS'; var contentType = type.toLowerCase(); var url = 'proxy.php?url='+encodeURI(FEED_URL)+'&f='+contentType+'&e=utf-8'; // xmlDoc = loadXMLString(xml); var xmlDoc = loadXMLDoc(url); var news = ''; if (contentType==='atom'||contentType==='feed') { news = parseAtom(xmlDoc); } else { news = parseRSS(xmlDoc); } document.write(news);
-or-
Conclusion
OK, admittedly both are equally ugly code but as they say there’s more than one way to skin a cat.This was at least a useful exercise for me to get my head around some of the differences between jQuery and vanilla JavaScript DOM parsing with hand-coded AJAX, for such a task as trivial as displaying an RSS feed’s XML (which clearly got out of hand here).
If I had to do it over again, I would probably see the value of jQuery and design the code with use of the library in mind from the start, doing a better job at using the jQuery style of coding instead of old-school JS function style. Each of the standard JavaScript AJAX RSS Reader and jQuery RSS Reader are at least slightly dynamic in that they let you select from multiple feeds as well as URL-based submission (by passing in a parameter value for the parameter name url). You could even easily put in a text box and allow user-powered URL submission, but that’s an entirely different topic. Lastly, something that’s also important to note though, is that when working with external feeds (not co-located on the same server) either solution requires a Server-Side Proxy script like the one I described in a post last month to run your request on the backend which will fetch and pass on the data, in order to overcome browser security sandboxes (which exist for good reason).
PETA:
Though I’m quite allergic, I still don’t really approve of skinning cats!
Related articles
- Parsing XML using jQuery (devcurry.com)
- 13 Helpful Mobile Web Design Tools & Resources (thenextweb.com)
- Your choice of cross-browser javascript GUI – Stack Overflow (stackoverflow.com)
- JavaScript & jQuery Modal Dialogs Roundup :MS-Joe (Joe Stagner) (msjoe.com)
- The jQuery Divide:Understand where jQuery ends and JavaScript begins (slideshare.net)
