Πολλαπλή δυναμική εισαγωγή και εκτέλεση script tags

14 06 2008

Τις προάλλες αντιμετώπισα ένα ιδιαίτερα ενοχλητικό και εκνευριστικό πρόβλημα που με απασχόλησε για 2 μέρες μέχρι που μόλις πριν λίγο το έλυσα και αποφάσισα να μοιραστώ μαζί σας τη λύση στην οποία κατέληξα.

Ας πάρουμε τα πράγματα από την αρχή. Το πρόβλημα ήταν το εξής: Σε ένα div εντός μιας σελίδας, φόρτωνα με AJAX κάποιο κομμάτι HTML από άλλη σελίδα του site (το ποιό ακριβώς εξαρτείτο από την επιλογή του χρήστη). Το κομμάτι HTML αυτό, ενίοτε περιείχε και <script> tags, είτε προς εξωτερικό js αρχείο, είτε inline, είτε διάφορων ειδών, τα οποία έπρεπε να τρέξουν με τη σειρά που υπήρχαν στην HTML για να λειτουργεί σωστά. Αρχικά σκέφτηκα να τα παίρνω με τη σειρά και να τα εισάγω με DOM manipulation στο head της σελίδας, λύση που προτεινόταν σε αρκετά σχετικά άρθρα. Δεν είχα υπολογίσει όμως κάτι: Ο browser δεν θα καθόταν να περιμένει να φορτώσει το καθένα τους, όπως όταν φορτώνει μια σελίδα. Θα μπορούσα να περιγράψω τη συμπεριφορά του σαν τη συμπεριφορά του ΙΕ στην ιδιότητα defer ενός script, μόνο που το έκαναν όλοι οι browsers, χωρίς να υπάρχει καμία ιδιότητα defer στα εν λόγω!

Έπρεπε να βρω έναν τρόπο να εκτελούνται με τη σειρά, και κανένα να μην εκτελεστεί πριν φορτώσει το προηγούμενο. Προσπαθώντας να αποφύγω τους κινδύνους των synchronous requests ή το απόλυτο χάος της AJAX λύσης, έμαθα ψάχνοντας το εξής: Ο ΙΕ υποστηρίζει event onreadystatechange για τα <script> tags. Ο Firefox, ο Safari και πιθανόν και άλλοι browsers υποστηρίζουν event onload για δαύτα. Άρα συνδυάζοντας τα, θα είχαμε τη λύση! Όσα scripts ήταν inline δεν ετίθετο τέτοιο θέμα μιας και είχαν ήδη φορτώσει, θα έπρεπε απλά να γίνονται eval() όταν έρθει η σειρά τους.

Κατέληξα λοιπόν στην παρακάτω αναδρομική συνάρτηση:

		tab_element.innerHTML = ajax.responseText;
		var scriptTagsStack = YAHOO.util.Dom.getElementsBy(function() {return true;},'script',tab_element);

		var currentScript = scriptTagsStack[0];
		var scriptElem;
		function processScripts()
		{
			if(!scriptTagsStack.length || !scriptTagsStack[0] || currentScript==null) return;
			if(scriptElem && scriptElem.readyState && scriptElem.readyState!='complete' && scriptElem.readyState!='loaded') return;
			currentScript = scriptTagsStack[0];

			scriptTagsStack.splice(0,1);

			if(currentScript && !currentScript.getAttribute("src") || currentScript.getAttribute("src") == '')
			{
				var currentScriptCode = currentScript.innerHTML;
				try{
					currentScriptCode = currentScriptCode.replace("<!--",'').replace("//-->",'');
					eval(currentScriptCode);
					processScripts();
				}catch(e) { alert(e.message); }
			}
			else if(currentScript)
			{
				scriptElem = document.createElement('script');
				scriptElem.setAttribute('type','text/javascript');
				scriptElem.setAttribute('src',currentScript.getAttribute("src"));
				scriptElem.onload = processScripts;
				scriptElem.onreadystatechange = processScripts;
				document.getElementsByTagName('head')[0].appendChild(scriptElem);
				currentScript.parentNode.removeChild(currentScript);
			}

		}
		processScripts();

Διευκρινίσεις:

  • tab_element είναι το div στο οποίο εισάγεται το responseText από το AJAX request
  • Στη δεύτερη γραμμή χρησιμοποιείται η συνάρτηση getElementsBy() από το YUI javascript framework. Ο τρόπος που χρησιμοποιείται εδώ είναι ουσιαστικά όμοιος με την element.getElementsByTagName() με μόνη διαφορά ότι δεν επιστρέφει NodeList αλλά Array. Αν χρησιμοποιούσα την getElementsByTagName() θα πέταγε error στη γραμμή που κάνει splice, μιας και οι μέθοδοι των arrays δεν εφαρμόζονται στις NodeLists. Σίγουρα μπορεί εύκολα να μετατραπεί ώστε να μην χρησιμοποιεί συνάρτηση του YUI.
  • Η βασική φιλοσοφία της είναι η εξής: Χρησιμοποιεί ένα array από τα script elements που πρέπει να προστεθούν. Κάθε φορά που τρέχει είτε περνάει από eval() τα περιεχόμενα του πρώτου στοιχείου του πίνακα (αν πρόκειται για inline script), είτε προσθέτει το <script> tag στο head αν πρόκειται για linked script. Στην πρώτη περίπτωση καλεί αμέσως τον εαυτό της, ενώ στη δεύτερη αναθέτει στα onreadystatechange/onload event handlers του script element τον εαυτό της. Επίσης, αφαιρεί κάθε φορά το πρώτο στοιχείο του πίνακα (αυτό το οποίο επεξεργάζεται κάθε φορά δηλαδή). Όταν θα κληθεί με άδειο πίνακα, τερματίζει (συνθήκη διαφυγής από την αναδρομή).
  • Έχει δοκιμαστεί σε ΙΕ7, Firefox2, Safari 3 και Opera 9.5. Δεν έχω ιδέα για το πως δουλεύει σε άλλους browsers και εκδόσεις και θα εκτιμούσα να με ενημέρωνε όποιος μάθει.
  • Προφανώς είναι beta, μόλις πριν λίγο την κατάφερα να δουλέψει σωστά.

Ελπίζω να χρησιμεύσει σε κάποιον και να του γλιτώσει το χρόνο που έφαγα εγώ σπαζοκεφαλιάζοντας.


Ενέργειες

Πληροφορίες

2 απαντήσεις

15 06 2008
lexx

Thanks for sharing. Χαίρομαι που βρήκες άκρη :)

9 11 2008
thanos

Katarxhn poly kalh douleia,alla ston IE7 den douleve g kapoio teleiws diko tou logo(ok,t aneferes oti htan beta) k mporei na htan kapoia dikia m vlakeia.. :P
Parolayta t thema m htan n typwnw enan flash player…m AJAX kalw to link gia to kathe video kai meta t response,exw ftiaksei mia function ftiaxnei kai typwnei ton player..elpizw na voithisa opoion dn tou leitourgei epakrivws h lysh p dineis esy..

:)

Γράψτε ένα σχόλιο