Om een webapplicatie te ontwerpen die goed werkt en waarvan de code makkelijk is aan te passen en te onderhouden moet de onderliggende structuur eenduidig zijn, vandaar dat design patterns steeds meer gebruikt worden in javascript/PHP setups. Een codebase is als een fundering : als de basis niet deugt, wordt het ook schipperen met alles wat je erop bouwt, en echt solide zal het nooit worden. We willen clientside en serverside objecten ontwerpen die op de juiste plek doen wat ze moeten doen :
Er is ook een statusobject nodig dat de acties van de user en de status van de applicatie vastlegt. Dit object houdt op de client en op de server de stand van zaken bij en gebruikt als dataopslag :
Het statusobject verwerkt en registreert acties en synchroniseert de actie-opslag op server en client. Useracties kunnen ook tijdens een sessie in een stack worden opgeslagen. Op die manier is het ook mogelijk useracties te manipuleren (undo/redo functies). Dit object past in het commander design pattern.
Een verder uitgewerkt diagram hieronder laat de samenhang zien tussen de verschillende lagen en interfaces in een uitgebreide codebase. Wat betreft het document kun je vier lagen benoemen :
Deze opbouw past ook in het populaire Model View Presenter design pattern. De interfaces passen in het facade-pattern, en de abstraction layer past in het adapter pattern. Meer informatie Design patterns zijn natuurlijk geen wondermiddel, het gaat erom ze goed toe te passen. Vaak zijn ze de resultante van een refactoringsproces en ze kunnen behulpzaam zijn bij het in kaart brengen van data- en actiestromen. Er kunnen meerdere application layers zijn, elke layer of module communniceert via custom events. Dit zorgt voor een "loose coupling" waardoor code beter te onderhouden en uit te breiden is, zie ook Message Broker
DOM interface Als we de DOM interface nader beschouwen zijn er verschillende delen te onderscheiden :
De codevoorbeelden zijn niet compleet, ze dienen ter illustratie.
De modules (of singleton classes)zijn elk in een aparte .js-file ondergebracht. De class "index.js" initieert ONLOAD de DOM abstraction layer en de custom event interface. Er is ook een globale class die onderdeel uitmaakt van de ONLOAD inline script interface, en een globale helperclass waarin algemene assisterende functies een plaats krijgen
//INIT ON LOAD=========================================================== /*CLASS : INDEX.JS*/ $(document).ready(function(){ $.extend(Util);//tools object $.extend(RequestManager);//xhr object $.logInit();//logger application.init(); //CUSTOM EVENT INTERFACE application.addListener('application_submit', DataProcessor.handler); application.addListener('application_submit', Dummy.handler); dummy.addListener('applicationfield_save', Updater.handler); //CONNECTOR : APPLICATION <==> SYNC HTTPREQUEST SERVER VIA INLINE SCRIPT if(!GL_firstload){ application.startPolling(4000); } }); //================================================================ //GLOBAL ONLOAD INTERFACE var GL = (function(){ this.load = true; this.status=''; })(); //HELPERCLASS var HL = (function(){ var customAlert = function(event){ var msg = ''; msg+= 'receiver : '+event.receiver +', \ntype: ' + event.type; msg+= ', \nresults: ' + event.results.data + ', \nsender : '; msg+= event.sender + ', \nidentifier : '+ event.target.identifier; alert(msg ); }; })();
Singleton classes deel 1
/*APPLICATION.JS*/ var application = (function(){ //BEGIN CLASS APPLICATION //PRIVATE METHODS EN VARS //private constants var CNT_DISPLAY = 'melding', CNT_ALERT = 'alert', MSG_ERROR = 'Dit is een custom foutmelding', MSG_SUCCESS = 'Dit is een custom succes-melding', var bRequestActive=false,//true als er nog XHR-request gaande is oForm,//referentie naar het formulier, zonder declaratie wordt oForm global oConnect = new objEventTarget();//event-interface object var handler = {//EVENT RECEIVER process : function(event) { var t=event.target; switch(event.type) { case 'click' : //ACTIONS break; } } }; var output = { //XHR-adapter to VIEW setResult : function(oResponse, sStatus) { //INTERFACE TO VIEW }, setNotify : function(oResponse, sStatus) { //INTERFACE TO VIEW } }; //XHR INTERFACE APPLICATION <==> SERVER var XHR = { //req 01 ------------------------------------------ doSave:function(t) { var elemId=t.getAttribute('rel'); bRequestActive=true; $.getForm(oForm,elemId);//sla element op voor submit $.submit({ type : "post", url : 'xhr/element_save.php', onsuccess : this.saveSuccessResult, onfailure : this.saveFailureResult, scope : o //callbackscope }); }, saveSuccessResult : function(oResponse) { this.trigger( {//CUSTOM EVENT SENDER type : 'applicationfield_save', sender : o.identifier, results : oResponse }); output.setResult(oResponse, MSG_SUCCESS); //logger $.echo(oResponse, true,'form.js saveSuccessResult regel 105',10); }, saveFailureResult : function(oResponse) { //doError(); //acties onerror output.setResult(oResponse, MSG_ERROR); }, //req 02 ---------------------------------------------- addPollUpdater:function() { $.getForm(oForm);//sla element op voor submit $.addPoll({ type : "post", url : 'xhr/getpolldata.php', onsuccess : this.pollSuccessResult, onfailure : this.pollFailureResult, scope : o //callbackscope }); }, pollSuccessResult : function(oResponse) { this.trigger( {//CUSTOM EVENT SENDER type : 'poll_update', sender : o.identifier, results : oResponse, success : true }); }, pollFailureResult : function(oResponse) { this.trigger( {//CUSTOM EVENT SENDER type : 'poll_update', sender : o.identifier, results : oResponse, success : false }); } }; //PUBLIC INTERFACE----------------------------------- var o={ identifier : 'application', init : function(){ oForm=$.getRefForm(0);//haal referentie naar formulier op //NAMESPACING CLICK EVENT $(oForm).bind('click.application', handler.process); }, submit:function(obj){ //ACTIONS }, startPolling:function(interval){ //ACTIONS } }; $.extend(o,oConnect);//custom events connector return o; //END CLASS APPLICATION-------------------------------------------------- })();
/*CLASS DATAPROCESSOR.JS*/ var DataProcessor = (function(){ var o = { receiver : 'DATAPROCESSOR', handler : function(event){ //pickup event event.receiver=o.receiver; HL.customAlert(event);//test } }; return o; })(); /*CLASS UPDATER.JS*/ var Updater = (function(){ var o = { receiver : 'UPDATER', handler : function(event){ event.receiver=o.receiver; HL.customAlert(event);//test } }; return o; })(); /*CLASS DUMMY.JS*/ var Dummy = (function(){ //PRIVATE CONSTANTS //PRIVATE VARS var oConnect = new objEventTarget();//event-interface object //PRIVATE METHODS var _handler = { process : function(event){ //HANDLE CUSTOM OR NAMESPACES EVENTS } }; var _showMSG = function(elem){ //INTERFACE MET VIEW VIA DOM ABSTRACTION LAYER JQUERY $('#test').css(oCss).slideDown('slow').click(function(){ $(this).fadeOut('slow',function(){ $(this).detach(); }); }); }; //XHR INTERFACE DUMMY <==> SERVER ASYNC HTTP REQUEST (AJAX) var _XHR = { init : function(){ var CONTAINER='#output_result', LOADING = '#loaderbar'; //VIEW ELEMENTS uitvoer=$(CONTAINER); loading=$(LOADING); }, send:function(t) { //XHR INTERFACE WITH CALLBACK TO VIEW }; //STATUSINTERFACE DMV URI-HASH , COOKIE EN XHR-REQUEST : DUMMY <==> BROWSER DATASTORAGE var _hash = { load : function(forceload,sender){ //CONNECTOR SYNC HTTP REQUEST SERVER ONLOAD DUMMY <==> INLINE SCRIPT if( RELOAD[GL.status] ){ //page is allowed for caching //on reload haal hash op uit cookie if(GL.load && PERSIST){ if(Util.getCookie('hash_'+GL.status)){ window.location.hash=Util.getCookie('hash_'+GL.status); GL.load=false; } } //get sender if(sender){ _source.set(sender); } else if(_source.get()){ sender=_source.get(); } else { sender='load'; } //load page if active hash changes if(uitvoer){ var uri,value; //PERMANENT CLIENT MEMORY value=window.location.hash; if(value != '' && typeof(value)!='undefined'){ if(Util.getCookie('hash_'+GL.status)){ Util.unsetCookie('hash_'+GL.status); } Util.setCookie('hash_'+GL.status,value);//cache } //STATUSINTERFACE DUMMY <==> SERVER AJAX _XHR.send("&sender="+sender+uri.value); oHash=uri.active;//cache alleen deel met cut=... } setTimeout(function(){ XHRhistory.loadHash(); }, INTERVAL); } }, init : function(){ //INIT //ONLOAD init interface if(typeof(GL.status)==='undefined'){ GL.status='init'; } }, //MORE PUBLIC METHODS HERE }; //PUBLIC INTERFACE-------------------------------- var o = { identifier : 'dummy', receiver : 'dummy', //CUSTOM EVENT RECEIVER handler : function(event){ //pickup event event.receiver=o.receiver; _handler.process(event); }, //CUSTOM EVENT SENDER shoot : function(elem){ this.trigger({//trigger event met custom object type : 'applicationfield_save', sender : 'shoot '+elem.value, results : {data : 'aaldert'}, success : true }); //ACTION TO VIEW _showMSG(elem); } }; $.extend(o,oConnect);//CUSTOM EVENT CONNECTOR OBJECT KOPPELEN return o; })();
De status persitence module Hieronder een verdere uitwerking van de status persistence module. De module heeft een client gedeelte en een server gedeelte, die met elkaar communiceren via XHR, URI-component en cookies. Tijdens de sessie wordt de status opgeslagen in de URI component, cookies en PHP $_SESSION. Statuspersistentie op de langere termijn kan via cookies en de database. Op die manier kan eenvoudig usergericht statusbeheer geimplementeerd worden. >> Zie ook : AJAX Status Persistentie
Event-driven programmeren op de server heeft voordelen : loose coupling van objecten is er één van. De data en actions van de client moeten gevalideerd worden door het Secure Transfer object (facade pattern), daarna moeten data en actie gerout worden naar de juiste receiver. Dit past in het observer pattern. De event handler verwerkt de events (observer pattern) en zorgt ervoor dat de juiste handelingen uitgevoerd worden, (factory pattern, lazy initialization). >> Zie ook : safeURI -> PHP eventhandler >> Zie ook : Native PHP Template
Een singleton class heeft als kenmerk dat er maar één instantie van bestaat in de global namespace. Door gebruik te maken van een closure die zichzelf aanroept wordt er een omgeving gecreëerd die verborgen blijft vanuit de global scope. Door het returnen van een interface-object kunnen andere scripts met de singletonclass (of module) communiceren. Door de opbouw zoals hieronder geschetst wordt kan er eenvoudig een codebase gebouwd worden bestaande uit verschillende modules die via public interfaces en custom event interfaces loosly coupled zijn. De kans op namespace conflicten wordt aanzienlijk kleiner. Binnen de modules kunnen andere applicatie-objecten op een veilige controleerbare manier worden geïnstantieërd en worden gereturned. Door het statische karakter van een singleton class kan de status van de verschillende applicatie-objecten eenvoudig worden bijgehouden.
var SingletonModule = (function(){ //constants var INIT_TAB = 1; ANOTHER_CONSTANT = 'value'; //private vars var doc, root = $.getLocation().root, $form = $('#form'); //DEFAULT OPTIONS INTERFACE FOR CONFIGURATION AND CALLBACK FUNCTIONS DECLARATION var oOptions = { option01 : {}, onComplete : function(){},//CUSTOM EVENT INTERFACE:RECEIVER option03 : true, option04 : false }; //APPLICATIE STATUS var rank = 0; aApplicaties = []; //dynamic dom interface constructor : MODEL->VIEW function Nodes(tab){ this.tab=parseInt(tab); this.input=$('#txt'+this.tab+'Input'); this.output=$('#txt'+this.tab+'Output'); this.clear=$('#cmd'+this.tab+'Clear'); this.fill=$('#cmd'+this.tab+'Fill'); this.copy=$('#cmd'+this.tab+'Copy'); }; //APPLICATION FACTORY function ApplicatieObject(obj){ this.init=obj; this.method=function(){//more code here}; //more methods an properties here aApplicaties.push(this); rank++; } //XHR-INTERFACE CLIENT <--> SERVER (MODEL SERVER <--> MODEL CLIENT) var XHR = { submit : function(obj){ obj= typeof(obj)==='undefined' ? oData.obj:obj; $.submit({ type : obj.type, url : obj.url, data : 'd='+obj.data, onsuccess : this.submitSuccessResult,//200 onfailure : this.submitFailureResult,//404 ea scope : self }); }, submitSuccessResult : function(oResponse){ //xhr-interface server->client //CUSTOM EVENT INTERFACE:SENDER this.trigger('onComplete', oResponse); return true; }, submitFailureResult : function(oResponse){ //xhr-interface server->client return false; } }; //private internal helper methods var _hlp ={ base64encode : function(safe){ //more code here doc.output.val(result); }, base64decode : function(safe){ var value=doc.output.val(); //more code here }, clear : function(){ //more code here } }; var _unloadDomElements = function(){ //actions here }; var _setValue = function(){ //actions here }; var _getValue = function(){ //actions here }; //INTERFACE CONTROLLER <--> MODEL $form.bind('tabsselect', function(event, ui) { doc= new Nodes(tab);//load dom-interface for this tab switch(tab){ case 1 : //MODEL ACTIONS break; case 2 : //MODEL ACTIONS break; }//end switch //clear button doc.clear.click(function(event){ _hlp.clear(); }); //load testsample button doc.fill.click(function(event){ doc.input.val(doc.input.sample); }); //copy button doc.copy.click(function(event){ doc.input.val( doc.output.val() ); }); });//end tabselect event handler //INTERFACE OBJECT, COMMUNICATIE MET ANDERE MODULES //public methods var self = { setValue : function(value){ if(_check(value)){ _setValue(); } }, getValue : function(){ _getValue(); }, setOptions : function(options){ $.extend(oOptions, options); }, initApplication : function(obj){ return new Application(obj); }, destroy :function(){ _unloadDomElements(); } }; return self; //ONLOAD INTERFACE MODEL -> VIEW doc= new Nodes(INIT_TAB);//load dom-interface for INIT_TAB $form.trigger('tabsselect',INIT_TAB); })();
De WidgetFactory class is een voorbeeld van een factory class met een private constructor, geschikt voor het initiëren van objecten in een secure production environment. De omgeving bepaalt welk object geïnstantieë wordt, maar kan de methods en properties van de factory niet overschrijven. De status van elke widget is intern binnen de factory eenvoudig te monitoren. Onderstaande code is alleen bedoeld ter illustratie.
var WidgetFactory = (function(){ //PRIVATE CONSTANTS //DOM controller/view constants var BTN = '#create', SHOW = '#show', DELETE = '#delete', MESSAGE = '#message'; //widget constants var WIDGETNAME = 'widget_', WRAP = 'wrap_', BTN01PREFIX = 'btnSetEngine_', BTN02PREFIX = 'btnOptions_', BTN03PREFIX = 'btnDestroy_', BTN01CAPTION = 'Set SearchEngine', BTN02CAPTION = 'Show options', BTN03CAPTION = 'Destroy this Widget'; //PRIVATE PROPERTIES var _rank = 0, _doc = {}, _widgets = {}; //PRIVATE DOM INTERFACE CONSTRUCTOR function Nodes(){ this.btn_create = $(BTN); this.btn_show = $(SHOW); this.btn_delete = $(DELETE); this.cnt_message = $(MESSAGE); } //PRIVATE WIDGET CONSTRUCTOR function Widget(options){ //private properties var _options = { test : true, searchengine : 'google', query : 'factory pattern', onCreate : function(event){}, onDestroy : function(event){}, btn01Prefix : BTN01PREFIX, btn02Prefix : BTN02PREFIX, btn03Prefix : BTN03PREFIX, btn01Caption : BTN01CAPTION, btn02Caption : BTN02CAPTION, btn03Caption : BTN03CAPTION }; //public properties this.constructor = 'WidgetFactory'; this.identifier = _rank; $.extend(_options, options); //widget methods this.getID = function(){ return this.identifier; }; this.getOptions = function(){ return _options; }; this.setOptions = function(options){ $.extend(_options, options); }; //create controller----------------------------- var $controller, $btn01, $btn02, $btn03, $input; //elements $controller=$( '<div class="app" style="margin-bottom:10px;" id='+WRAP+this.identifier+ '><span>widget_'+this.identifier+' : </span></div>' ); $btn01=$('<a href="" class="button m10" id="'+_options.btn01Prefix+this.identifier+ '">'+_options.btn01Caption+'</a>'); $btn02=$('<a href="" class="button m10" id="'+_options.btn02Prefix+this.identifier+ '">'+_options.btn02Caption+'</a>'); $btn03=$('<a href="" class="button m10" id="'+_options.btn03Prefix+this.identifier+ '">'+_options.btn03Caption+'</a>'); $input=$('<input type="text" name="searchengine_'+this.identifier+'" value="'+ _options.searchengine+'"/>'); //events $btn01.click(function(event){ event.preventDefault(); var thisWidget = _factory.getWidget(this.id), value = thisWidget.controller.input.val(); thisWidget.setOptions({ searchengine : value }); }); $btn02.click(function(event){ event.preventDefault(); var thisWidget = _factory.getWidget(this.id); $.echo(thisWidget.getOptions(),false,'widget_'+thisWidget.identifier+' options'); }); $btn03.click(function(event){ event.preventDefault(); _factory.destroyWidget(this.id); }); //attach $controller .append($input) .append($btn01) .append($btn02) .append($btn03); $controller.selector='id_'+this.identifier //store controller reference to widget $controller.input=$input; this.controller=$controller; //store reference to widget this.self=$(this); //events this.self.bind('onCreate', _options.onCreate) .bind('onDestroy', _options.onDestroy); //end controller-------------------------------- //store widget for reference and tracking _widgets[WIDGETNAME+this.identifier] = this; _rank++; //actions after widget is created this.self.trigger('onCreate'); } //PRIVATE METHODS var _factory = { getID : function(id){ if(typeof(id)==='string' && $.in_str('_', id)){ id=id.split('_')[1]; } return id; }, getWidget : function(id){ if( typeof(id)!=='undefined' ){ id=this.getID(id); return _widgets[WIDGETNAME+id]; } else{ return _widgets; } }, countWidgets : function(){ return this.length; }, createWidget : function(options){ var thisWidget = new Widget(options); //connect widgetcontroller to view _doc.container.append(thisWidget.controller); return thisWidget; }, destroyWidget : function(id){ id=this.getID(id); var thisWidget=_widgets[WIDGETNAME+id]; thisWidget.self.trigger('onDestroy'); //remove obj from widgets-object and from DOM thisWidget.controller.detach(); delete _widgets[WIDGETNAME + thisWidget.identifier]; return true; } }; //PUBLIC METHODS var self = { init : function(container){ //dom interface _doc = new Nodes(); _doc.container=$(container); //factory controller _doc.btn_create.click(function(event){ event.preventDefault(); _factory.createWidget({ searchengine : 'yahoo', query : 'design patterns', onCreate : function(event){ _doc.cnt_message.html('widget_'+this.identifier+' is created') $.doShow(_doc.cnt_message[0],2000); }, onDestroy : function(event){ alert('Widget_'+this.identifier+ ' famous last words : I\'m about to be destroyed');//test } }); }); _doc.btn_show.click(function(event){ event.preventDefault(); $.echo(_factory.getWidget(),false,'show all widgets line 192',3, ['self','controller','input']);//test }); _doc.btn_delete.click(function(event){ event.preventDefault(); try{ _factory.destroyWidget(_widgets.widget_2.identifier); _doc.btn_delete.html('Widget_2 deleted');//test } catch(e){} }); }, createWidget : function(options){ return _factory.createWidget(options); }, destroyWidget : function(id){ return _factory.destroyWidget(id); }, getCount : function(){ return _factory.countWidgets(); } }; return self; })();