2.2.2. fejezet, Kliens JavaScript receptek
A böngésző oldalak előállítását XSL transzformációk és JQuery utófeldolgozók végzik. AJAX objektum gyártása az alábbi módon történik:
var XMLHTTPREQUEST_TIMEOUT = 10000; //AJAX objektum gyártása function createXMLHttpRequest() { if ($.browser.msie){ try { return new XMLHttpRequest(); } catch(e) {} try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {} try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {} } else { try { return new XMLHttpRequest(); } catch(e) {} } alert("XMLHttpRequest not supported"); return null; }
Fontos megjegyezni, hogy a JQuery forms modulja szintén alkalmas AJAX kommunikációra, ám a két irányú XML kommunikáció nem az erőssége. Ezért ha a programounk oda-vissza XML formában kommunikál a szerverrel, keressünk más megoldást.
A Client osztály transzformálja az XML dokumentumot HTML kóddá:
function Client(owner){ this.init(owner); $.extend(this, new ActionDrivenObject(this, owner, 'client')); } Client.prototype = { init: function(owner){ this.className = 'Client'; this.jComponent = owner; this.httpReqObj = createXMLHttpRequest(); this.xStyleSheets = new Object(); this.staticXMLRefs = new Object(); this.session = this.jComponent.xmlServer.sessionHandlerFactory.sessionHandler; this.containers = new Object(); }, addXSLT: function(styleName, path){ this.xStyleSheets[styleName] = new Object(); if ($.browser.mozilla || $.browser.opera) { this.httpReqObj.open("GET", path, false); //syncronous call with last param false this.httpReqObj.send(null); this.xStyleSheets[styleName].xslRef = this.httpReqObj.responseXML; } else if ($.browser.msie) { var xsltProcessor = new ActiveXObject("MSXML2.FreeThreadedDomDocument"); xsltProcessor.async = false; xsltProcessor.load(path); this.xStyleSheets[styleName].xsltProcessor = xsltProcessor; } }, addStaticXMLRef: function(refName, xmlRef){ this.staticXMLRefs[refName] = xmlRef; }, loadTemplate: function(targetId, path){ var targetObj = $(targetId); if (targetObj != null) { this.httpReqObj.open("GET", path, false); //syncronous call with last param false this.httpReqObj.send(null); var tpl = this.httpReqObj.responseText; if (tpl != null) { $(targetObj).html(tpl); } } }, transform: function(targetId, styleName, xmlRef, params){ var containerDoc = this.createDocument(); containerDoc.appendChild(xmlRef); var targetObj = $(targetId); var ready = true; if ((targetObj.length > 0) && (this.xStyleSheets[styleName] != undefined)) { if (($.browser.mozilla || $.browser.opera) && (this.xStyleSheets[styleName].xslRef != null)) { var xsltProcessor = new XSLTProcessor(); xsltProcessor.reset(); xsltProcessor.importStylesheet(this.xStyleSheets[styleName].xslRef); if (params != null) for (paramName in params) { xsltProcessor.setParameter(null, paramName, params[paramName]); } try { fragment = xsltProcessor.transformToFragment(containerDoc, document); $(targetObj).empty(); $(targetObj).html(fragment); } catch (ex) { ready = false; alert("Exception in transformation (" + ex.name + ":" + ex.message + ")"); } } else if (($.browser.msie) && (this.xStyleSheets[styleName].xsltProcessor != null)) { var objCompiled = new ActiveXObject("MSXML2.XSLTemplate"); objCompiled.stylesheet = this.xStyleSheets[styleName].xsltProcessor.documentElement; var objXSLProc = objCompiled.createProcessor(); objXSLProc.input = containerDoc; if (params != null) for (paramName in params) { objXSLProc.addParameter(paramName, params[paramName]); } try { objXSLProc.transform(); var fragmentText = objXSLProc.output; $(targetObj).html(fragmentText); } catch (ex) { ready = false; alert("Exception in transformation: (" + ex.name + ":" + ex.message + ")"); } } } return ready; }, transform2Table: function(params){ var pageContent = params.container.actions.getcontent.params.content; if ((pageContent != undefined) && (pageContent.value != undefined) && (pageContent.value != null)) { var xslParams = new Object(); xslParams.editable = params.editable; xslParams.fielddefs = params.container.getTableDefDoc(); xslParams.target = params.target; var items = $('> *', pageContent.value); if (items.length > 0) if (JComponent.client.transform(params.target, 'table', items[0], xslParams)) if (params.onTransformReady != undefined) params.onTransformReady(params.containerName, params.target); } }, transform2Editor: function(params){ var pageContent = params.container.actions.getcontent.params.content; if ((pageContent != undefined) && (pageContent.value != undefined) && (pageContent.value != null)) { var xslParams = new Object(); xslParams.fielddefs = params.container.getTableDefDoc(); xslParams.iframesrc = params.iframesrc; var items = $('> *', pageContent.value); if (items.length > 0) if (JComponent.client.transform(params.target, 'editor', items[0], xslParams)) if (params.onTransformReady != undefined) params.onTransformReady(params.containerName, params.target); } } }
Látható, hogy a kliens hierarchiája követi a szerver felépítését. Így például a Session objektumig a this.jComponent.xmlServer.sessionHandlerFactory útvonal vezet. Minden objektum hivatkozik a szülőjére, és a tartalmazottjaira. Ezen felül minden esemény vezérelt osztály az ActionDrivenObject osztályból származik. Ez végzi a válasz XML-ek kezelését, és van egy targets tulajdonsága, ahol a tartalmazott objektumok általános formában elérhetők.
function ActionDrivenObject(superClass, owner, name){ this._init(superClass, owner, name); } ActionDrivenObject.prototype = { _init: function(superClass, owner, name){ this.owner = owner; this.name = name; this.targets = new Object(); this.actions = new Object(); this.actions.getdescription = new GetDescriptionAction(superClass); }, createDocument: function(){ var xmlDoc = null; if (document.implementation && document.implementation.createDocument) { xmlDoc = document.implementation.createDocument("", "", null); } else if (window.ActiveXObject) { xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); } return xmlDoc; }, getCallGroup: function(){ return new CallObj(this.getTargetPath(), null, null, false); }, getTargetPath: function(){ var result = this.name; if ((this.owner != null) && (this.owner.getTargetPath != undefined)) result = this.owner.getTargetPath() + pathSeparator + result; return result; }, handleResponse: function(request){ if (request.pathArray.length > 0) { var target = request.pathArray.shift(); if (this.targets[target] != null) { this.targets[target].handleResponse(request); } else if (request.action == 'getdescription') { this.buildTarget(target); if (this.targets[target] != null) this.targets[target].handleResponse(request); } } else { var action = this.actions[request.action]; if (action != null) action.processResponse(request); if ((request.params != undefined) && (typeof request.params == 'object')) { if (request.params.subrequests != undefined) { var selfObj = this; $(request.params.subrequests).find('> request').each(function(index){ var subrequest = new Request(this); selfObj.handleResponse(subrequest); }); }; }; }; }, importAction: function(actionName, params){ if (this.actions[actionName] == undefined) this.actions[actionName] = new Action(this, actionName, params); else this.actions[actionName].importParams(params); } }
Fontos megjegyezni itt a superClass változó használatát. A JavaScript objektum orientáltsága látszólag a C++ és Delphi osztályokhoz hasonló, ám itt egy ősosztályt a leszármaztatottban egy az abból létrehozott objektum reprezentál. Így a leszármaztatott objektum referenciájára hivatkozás nagyon fontos egy leszármaztatott osztály inicializálásakor felfűzött eseménynél. Ha az owner értéke a this objektum lenne a GetDescriptorAction konstruktoránál, az ActionDrivenObject leszármaztatottjaiban nem működne helyesen ez az esemény, mert nem a leszármazottba importálná az eseményeket, hanem az ősosztályba - jelen esetben az ActionDrivenObject-be -, ahonnan nem látszanak a leszármaztatott tulajdonságai és metódusai.
A GetDescriptorAction végzi az XML-ben leírt metódusok importálását
function GetDescriptionAction(owner){ this.init(owner); $.extend(this,new GenericAction(owner,'getdescriptoraction',new Object())); this.params.descriptor = new Object(); this.params.descriptor.direction="out"; this.params.fulldescriptor = new Object(); this.params.fulldescriptor.direction="in"; } GetDescriptionAction.prototype = { init: function(owner){ this.className='GetDescriptorAction'; }, generateCallObj: function(fulldescriptor){ this.params.fulldescriptor.value=fulldescriptor; var callObj = new CallObj(this.owner.getTargetPath(),'getdescription',this.params,false); return callObj; }, call: function(){ var callObj = this.generateCallObj(); var transactObj = new TransactObj(); transactObj.addCall(callObj); this.owner.getXMLServer().commObj.addTransactObj(transactObj); this.owner.getXMLServer().commObj.sendAllTransactObj(); }, processResponse: function(request){ if ((request.params!=null)){ var selfObj = this; if (request.params.descriptor!=null){ this.params.descriptor.value = request.params.descriptor; if (this.owner.actions == undefined) this.owner.actions = new Object(); var actions = this.owner.actions; if (selfObj.owner.className != undefined) $(this.params.descriptor.value).find(selfObj.owner.className.toLowerCase()+'descriptor > actions > actiondescriptor').each(function(index){ var actionName = $(this).attr('name'); var tmpParams = new Object(); $(this).find('> params > param').each(function(index){ var paramName = $(this).attr('name'); tmpParams[paramName]=new Object(); tmpParams[paramName].direction = $(this).attr('direction'); tmpParams[paramName].optional = $(this).attr('optional'); tmpParams[paramName].type = $(this).attr('type'); tmpParams[paramName].order = parseInt($(this).attr('order')); if (isNaN(tmpParams[paramName].order)) tmpParams[paramName].order = -1; }); selfObj.owner.importAction(actionName,tmpParams); if (actions[actionName].generateCallObj==undefined){ var tmpArray = new Array(); for(paramName in tmpParams){ if ((tmpParams[paramName].direction=="in")&&(tmpParams[paramName].order>-1)){ tmpArray[tmpParams[paramName].order]=paramName; }; }; var inParams = new Array(); var cnt=0; for (var i=0;i<tmpArray.length;i++) if ((tmpArray[i]!=null)&&(tmpArray[i]!=undefined)) inParams[cnt++]=tmpArray[i]; var fnStr = new String(); for (var j=0;j<inParams.length;j++) fnStr += "this.params."+inParams[j]+".value="+inParams[j]+";"; fnStr+="return new CallObj(this.owner.getTargetPath(),'"+actionName+"',this.params,false);"; actions[actionName].importMethod('generateCallObj',inParams,fnStr); }; }); }; }; } }
Az Action ősosztálya egy általános eseménykezelő, ami a válasz XML feldolgozása közben a metódusok paramétereit importálja. A válasz XML kimenő paramétereit írja be az esemény paramétereibe. Az XSLT erőforrás igényes művelet, ha kiemeljük az XML-ből a lényeges részeket, gyorsíthatja a feldolgozást.
function Action(owner,name,params){ this.init(); $.extend(this,new GenericAction(owner,name,params)); } Action.prototype = { init: function(){ this.className='Action'; }, processResponse: function(request){ if ((request.params!=null)){ for(var paramName in request.params){ if ((this.params != undefined)&&(this.params[paramName] != undefined)){ if (this.params[paramName].type != undefined){ var param = request.params[paramName]; switch(this.params[paramName].type){ case 'java.lang.String': this.params[paramName].value = $(param).text();break; case 'java.lang.Boolean': this.params[paramName].value = ($(param).text()=='true');break; case 'java.lang.Integer': this.params[paramName].value = parseInt($(param).text());break; case 'org.w3c.dom.Element': this.params[paramName].value = param.cloneNode(true);break; } } else this.params[paramName].value = request.params[paramName]; } else if (paramName!='subrequests'){ alert(paramName+" of "+this.name+"("+this.className+") of "+this.owner.name+"("+this.owner.className+") is undefined"); this.params[paramName]=new Object(); this.params[paramName].value = request.params[paramName]; } } } } }
A GenericAction mint az Action ősosztálya, metódusok importálására képes. Így jön létre a generateCallObject metódus, aminek a paramétereit az eseményt leíró XML-ből emeli be. Az esemény így más kliens nyelvekre - mint mondjuk a Delphi pascal-ja - egyszerűen portolható, a SOAP-hoz és a WSDL-hez hasonlóan.
function GenericAction(owner,name,params){ this._init(owner,name,params); } GenericAction.prototype = { _init: function(owner,name,params){ this.owner = owner; this.name = name; this.params = params; }, importMethod: function(methodName,inParams,fnStr){ if ($.browser.mozilla){ var tmpStr = "function("+(inParams!=null?inParams.join(','):"")+"){"; tmpStr+=fnStr; tmpStr+="}"; this[methodName]=eval(tmpStr,this); } else if ($.browser.msie){ if (inParams==null) this[methodName]=new Function(fnStr); else this[methodName]=new Function(inParams,fnStr); }; }, importParams: function(allParams){ for(paramName in allParams){ if ((this.params[paramName]==undefined)||(this.params[paramName]==null)){ this.params[paramName]=new Object(); $.extend(true,this.params[paramName],allParams[paramName]); } else { for(paramPropName in allParams[paramName]) this.params[paramName][paramPropName]=allParams[paramName][paramPropName]; } } } }
JavaScript-ben vagy egy új Function objektum létrehozásánál kerüljük az alábbi függvénytörzs tartalmat:
actions.delete.generateCallObj()
A delete helyette használjunk más metódusnevet. Például:
actions.rmrecord.generateCallObj()
A Firefox és Mozilla helyesen értelmezi, ám az Internet Explorer a delete-et mint kulcsszót fordítja. Ezért meglepődhetünk, mikor a "várt elem azonosító" hibaüzenetet kapjuk fordításkor.
A tartalom elküldése előtt azt általánosan feldolgozhatóvá kell tenni, mivel az AJAX komponensek böngészőnként eltérő kódolást használnak. Erre alkalmas a JavaScript encodeURI függvénye.
function CallObj(targetPath,actionName,params,checkPrior){ this.init(targetPath,actionName,params,checkPrior); } CallObj.prototype = { init: function(targetPath,actionName,paramsObj,checkPrior){ this.className='CallObj'; this.targetPath = targetPath; this.actionName = actionName; this.paramsObj = paramsObj; this.checkPrior = checkPrior; this.subCalls = new Array(); }, xmlNorm: function(input){ input = input.replace('&','&'); input = input.replace('>','>'); input = input.replace('<','<'); input = input.replace('©','©'); input = input.replace('®','®'); input = input.replace('™','™'); input = encodeURI(input); return input; }, toString: function(){ return this.getString(0); }, addSubCall: function(callObj){ if (callObj.targetPath.indexOf(this.targetPath)==0) callObj.targetPath = callObj.targetPath.substr(this.targetPath.length+1); this.subCalls.push(callObj); }, getString: function(index){ var result = "<call target='"+this.targetPath+"'"+(this.actionName!=null ? " action='"+this.actionName+"'" : "")+" order='"+index+"'"+(this.checkPrior?" checkprior='true'":"")+">"; var params = new String() if (this.paramsObj!=null){ for(var paramName in this.paramsObj) if(this.paramsObj[paramName].direction=="in"){ var value = this.paramsObj[paramName].value; params += "<param name='"+paramName+"'>"+(value!=null?this.xmlNorm(value.toString()):"")+"</param>"; } if (params!="") result += "<params>"+params+"</params>"; } for(var i = 0; i<this.subCalls.length; i++){ result += this.subCalls[i].getString(i); } result += "</call>"; return result; } }
Érdemes néhány speciális karaktert még bekódolni, mert a szerver XML felépítője érzékeny lehet ezekre. Így került be az & karakter átalakítása az xmlNorm függvényben. Szerver oldalon a visszaalakítást szintén meg kell tennünk. Erre használható az URLDecoder osztály.
String result = URLDecoder.decode(param.toString(), "utf-8");
JavaScript esetén is előfordulhat memória szivárgás (memory leak). Ezek megértésére mindenképp szenteljünk időt, mert sok memóriát elfogyaszthat egy figyelmetlenül megtervezett alkalmazás. Mi esetünkben ez a beérkezett XML feldolgozásánál jelentkezett. Ezért az XML feldolgozását, átalakítását JavaScript helyett XSLT-vel végezzük.
- A hozzászóláshoz be kell jelentkezni