הקמת מנוע AJAX ותאימות לדפדפנים

‏ • 15 בדצמבר, 2005

אפליקציות AJAX זקוקות לשכבה בין הלקוח לשרת שתשמש ליצירת התקשורת ביניהם – מנוע ה-AJAX. במאמר זה נסביר על הקמתו ואיך "שיטת" ה-AJAX עובדת.


מה העקרונות?


במאמר הקודם "הקדמה ל-AJAX" אפליקציית AJAX הוגדרה כאפליקציה שממלאת אחר 5 דרישות:



  1. התצוגה תהיה תקינה – שימוש ב-XHTML+CSS,

  2. התצוגה תהיה דינמית ואינטראקטיבית עם המשתמש בעזרת DOM,

  3. העברה וניתוח של מידע ע"י שימוש ב-XML ו-XSLT,

  4. החזרת מידע באופן אסינכרוני ע"י שימוש באובייקט XMLHttpRequest,

  5. שימוש ב-JS כדי לאחד הכל.

שבה האפליקצייה עובדת כמו תוכנה ומביאה למשתמש את המידע שהוא מבקש/בוחר.


כלומר, מנוע ה-AJAX מהווה רכיב התקשורת בין הלקוח לשרת – כאשר הלקוח לוחץ על "כתובית" (לדוגמא) הדפדפן יפנה למנוע ה-AJAX ומנוע ה-AJAX יפנה לשרת. השרת יחזיר תשובה שתנותח ע"י פונקציה שמנוע ה-AJAX יפנה אליה, ותוצג למשתמש.


איך בונים את המנוע?


המנוע, הרכיב הכי חשוב באפליקציית AJAX, הוא "שכבה" שתפקידה ליצור את התקשורת בין השרת ללקוח. את התקשורת הזו אפשר להשיג במס' דרכים:



  • שימוש ברכיב XMLHttpRequest, או

  • שימוש ב-IFrame נסתר, או

  • שימוש ברכיב פלאש או JAVA Applet.

דפדפנים מודרנים מכירים את כל השיטות, אך דפדפנים ישנים מכירים רק את הדרך השנייה (של IFrame נסתר) או שלא מכירים בכלל (ולרוב השימוש בדפדפנים כאלו כבר לא קיים). הדרך המועדפת היא הדרך הראשונה, שימוש ברכיב XMLHttpRequest, כיוון שאפשר לשלוט ביתר קלות על הודעת ה-HTTP הנשלחת. הבעייה הקיימת בשיטה זו זה שאין תקן בנושא ודפדפן הישר בעיניו יעשה. לכן עלינו לספק דרך שבה נוכל לספק פונקציוליות דומה בין כמה דפדפנים, זאת נעשה ע"י יצירת פונקציה עוטפת שתספק לנו את רכיב ה-XMLHttpRequest בדרך שהדפדפן תומך בו.


הדפדפן אינטרנט אקספלורר (IE) תומך ב-XMLHttpRequest בעזרת שימוש ברכיב ActiveX בגרסאות 5.5 ו-6 שלו, ואילו פיירפוקס, אופרה וספארי תומכים ב-XMLHttpRequest בעזרת אובייקט פנימי בדפדפן שנקרא XMLHttpRequest. המתודות והמאפיינים של האובייקט XMLHttpRequest בדפדפנים אלו הם אותם מאפיינים ומתודות שקיימים ברכיב ה-ActiveX של IE, ולכן עלינו רק ליצור פונקציה שתיצור את האובייקט בדרך הנכונה לכל דפדפן.

var getTransferObject = function(){    return null;    };
if(window.ActiveXObject){
    // Check if the browser has support for ActiveXObject (IE Usually) 
    try{ 
        // Check for the new version of XMLHttp compoment 
        var x= new ActiveXObject("MSXML2.XMLHTTP"); 
        getTransferObject = function(){ return new ActiveXObject("MSXML2.XMLHTTP"); }
        delete x;
    }catch(_ex){ 
        try{ // Check for late version of XMLHTTP compoment 
            var x = new ActiveXObject("Microsoft.XMLHTTP"); 
            getTransferObject = function(){ return new ActiveXObject("Microsoft.XMLHTTP"); }
            delete x;
        }catch(ex){
            // Otherwise the version of IE is too old 
        } 
    } 
}else if(window.XMLHttpRequest){
    // XMLHttpRequest object supported by Opera, Firefox and Safari - may too in IE 7. 
    getTransferObject = function(){ return new XMLHttpRequest(); }
} 

השלב הבא הוא יצירת פונקציה שתבצע עבורנו את השליחה. אנו נשלח לפונקציה פרמטרים שקריאים עבורנו, המתכנתים, והפונקציה תשתמש בהם עבור יצירת בקשת ה-HTTP.

/*
    * make an "Ajax Request".
    * @params: 
        - url : contains the target of the request.
        - params: Hash Table that contains a data that send with the request.
        - aOptions: Associative array that contains an optional property: method of sendinge, 
            2 optional function: 
                - onSuccess(param: RequestObject): will dispatch when the request get a "true" response (status-code=200)
                - onFailure(param: RequestObject): will dispatch when the request get a "false" response (status=code isn't 200)
            and an Hash Table of http headers named: httpHeaders
*/
function doAjaxRequest(url, params, aOptions){
    // get transfer object
    var oReq = getTransferObject();
    if(!oReq){
        // Check for XMLHttpRequest
        throw "Download the newer Firefox or upgrade your browser please.";
        return false;
    }

    // Add HTTP headers to the message
    for(var header in aOptions.httpHeaders){
        oReq.setRequestHeader(header,aOptions.httpHeaders[header]);
    }

    oReq.onreadystatechange = function(){
        if(oReq.readyState==4){
            if(oReq.status==200){
                if(aOptions.onSuccess) aOptions.onSuccess(oReq);
            }else{
                if(aOptions.onFailure) aOptions.onFailure(oReq);
            }
        }
    }

    // make parameters string
    var _params = [];
    for(var p in params){
        _params.push(p+"="+encodeURIComponent(params[p]));
    }
    _params = _params.join("");

    // Default send method: GET
    var method = aOptions.method?aOptions.method:"GET",
        bodyPost= null;


    if(method=="POST"){
        oReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        oReq.open(method, url, true);
        bodyPost = _params; // POST data send into the request
    }else{
        oReq.open(method, url+"?"+_params, true); // GET data send as part of the url
    }
    //send request
    oReq.send(bodyPost);
} 

מנגנון האירועים


בשיטת ה-AJAX כמו שתואר קודם לכן הדפדפן כתוצאה מאירוע שהמשתמש עשה (באם זה לחיצה, כתיבה, וכו') קורא למנוע ה-AJAX שכתבנו ומשתמש בו כדי לבצע את בקשת ה-HTTP.את הקריאה למנוע ה-AJAX אנחנו נבצע בעזרת מנגנון האירועים של תקן ה-DOM. רצוי לקרוא את מדריך האירועים של האתר וובמאסטר כדי להבין את הלוגיקה שעומדת מאחורי תכנות מכוון אירועים.


הבעייה שקיימת עבורנו בוני האתרים היא שוב תאימות דפדפנים, ישנם דפדפנים (ו-IE בראשם) שאינם עוקבים אחר תקני W3C בכלל, ותקן מנגנון האירועים בפרט ועושים לנו את החיים יותר קשים. לכן עלינו לכתוב גם עבורם קוד תאימות. במאמר זה אני אציג קוד תאימות עבור 3 פונקציות בסיסיות שבלעדיהן אפליקציית AJAX לא תוכל להתקיים: הוספת אירוע, הגרעת אירוע וקבלת אובייקט האירוע, שאותן אני אתחום בתוך מערך אסוציאטיבי. את שאר ההתאמות תוכלו לבצע ע"י שימוש בטבלת התאימות של האתר QuirksMode.

var Events = {
    whichBrowser:function(){
        // Check browsers support
        if(document.attachEvent) return -1; // IE
        else if(document.addEventListener) return 1; // Standards compliance browsers
        else return 0;
    },
    iBrowser:0,

    // function for add an event handler to specific element
    // will override for use a specific browser method
    // return false if no detection used (usually when the browser isn't supported the w3c dom or IE dom
    addHandler:function(){ return false; },

    // function for remove an event handler to specific element
    // will override for use a specific browser method
    // return false if no detection used (usually when the browser isn't supported the w3c dom or IE dom
    removeHandler:function(){ return false; },

    getEventObj:function(e){
        // return the Event Object that registered to the event
        return e?e:event;
    },

    // return the element that the handler work from.
    getTarget:null
}

// make a browser detectation
Events.iBrowser = Events.whichBrowser();

if(Events.iBrowser===1){
    // Override method for Standards compliance browsers
    Events.addHandler = function(sEventName, oElement, fHandler){
        oElement.addEventListener(sEventName, fHandler, false);
        return true;
    }
    Events.removeHandler=function(sEventName, oElement, fHadnler){
        oElement.removeEventListener(sEventName, fHandler, false);
        return true;
    }
    Events.getTarget = function(oEvent){ oEvent.target; }

}else if(Events.iBrowser===-1){
    // Override method for MSIE engine
    Events.addHandler = function(sEventName, oElement, fHandler){
        oElement.attachEvent("on"+sEventName, fHandler);
        return true;
    }
    Events.removeHandler=function(sEventName, oElement, fHadnler){
        oElement.detachEvent("on"+sEventName, fHandler);
        return true;
    }
    Events.getTarget = function(oEvent){     oEvent.srcElement;    }
} 

תוכנית לדוגמא: "הצעת מדינות"


מה התוכנית עושה?


התוכנית מאפשרת להציע שמות של מדינות לפי אותיות ראשונות שמקישים


איך התוכנית עובדת?


המשתמש מקיש אותיות של מדינה ותוך כדי שהוא מקיש מופיעים הצעות להשלמה של שם המדינה.


מתחילים לעבוד!


נתחיל מיצירת קוד ה-HTML, הוא פשוט ביותר, יצירת שדה טקסט ורשימה. לשדה טקסט ניתן id בשם country ולרשימה ניתן id בשם completion.

<input type="text" id="country" />
<select id="completion"></select> 

את הרשימה נסתיר בעזרת CSS, ע"י מתן display:none; וניתן לה אפשרות להתמקם איפה שצריך יחסית לדף

    #country{position:relative;}
    #completion{position:absolute;z-index:5;display:none;} 

עכשיו ניצור אובייקט ב-JS שיכיל את הפרטים על פעולת ההצעה: מאיפה נשאב את הנתונים, כל כמה זמן נבצע את ההצעה מרגע שלא נלחץ מקש, אובייקט תיבת הטקסט ,אובייקט הרשימה ואת החוק שיבדק על הקלט

var suggestion={ 
    file:"Countries.asp", // Where we get the suggest. 
    xTime:300, // when we show the suggestion. 
    xTimeOut:null, // will use us later. 
    txtInput:null, // Text input object. 
    slctOptions:null, // Combo box object. 
    rule:/^[à-ú]{2,}$/ // Rule for the input. 
};

כדי להציע מדינה בעת כתיבת שמה עלינו לתפוס את אירוע הכתיבה. יש לנו שתי אפשרויות: אירוע הלחיצה (keypress) או אירוע סיום הלחיצה (רגע עזיבת המקש, keyup) מבין שתי האפשרויות האפשרות השנייה היא הכי טובה כיוון שהמקש הנלחץ סיים את פעולתו (הוספה/מחיקה/החלפה של תו) על שדה הטקסט, והשינוי בטקסט נעשה. לכן עלינו לתפוס את אירוע עזיבת המקש. בפונקציה שתאזין לאירוע (הפונקציה שתפעל ברגע שהאירוע ייווצר) אנחנו נבדוק שתוכן התיבה מתאים לחוק (rule) שהוגדר באובייקט שהגדרנו קודם. במידה והחוק מתקיים, לאחר הזמן שנקבע באובייקט שהגדרנו נבצע קריאת HTTP לקובץ שהגדרנו באובייקט וננתח את התוצאות שלו כך שיופיעו בתוך רשימת ההצעות.


הערות: את המאזין לאירוע נשים בתוך פונקציית האזנה לאירוע onload, כי רק אז אובייקט תיבת הטקסט ורשימת ההצעות קיימים בודאות (שאותם גם נאתחל בפונקציית האזנה ל-onload)

function hOnLoad(){
    suggestion.txtInput = document.getElementById("country"); // init input
    suggestion.slctOptions = document.getElementById("completion"); // init suggest to input
    Events.addHandler("keyup", suggestion.txtInput, hKeyUp);
    Events.addHandler("blur", suggestion.slctOptions, hBlur);
    Events.addHandler("change", suggestion.slctOptions, slctOptions_hChange);
}

function hKeyUp(e){
    clearTimeout(suggestion.xTimeOut); // Important
    if(suggestion.rule.test(suggestion.txtInput.value)){
        suggestion.xTimeOut = setTimeout(function(){ // important
            // Do an Ajax Request after X ms
            doAjaxRequest(suggestion.file, {q:suggestion.txtInput.value},{
                method:"GET",
                onSuccess:ajaxSuccess,
                onFailure:ajaxFailure
            });
        }, suggestion.xTime);
    }
}

function hBlur(){
    suggestion.slctOptions.style.display="none";
}

function slctOptions_hChange(e){
    suggestion.txtInput.value = suggestion.slctOptions.value; // put the data of the selection in the input
    suggestion.slctOptions.style.display="none"; // hide the combo box
}

function ajaxSuccess(oReq){
    var xml = oReq.responseXML, // Get the response content as a XMLDOM object
    oDoc = xml.documentElement,
    allCountries = null;
    if(oDoc.hasChildNodes()){ // Check for search results
        // Using DOM for processing the response and show as HTML in the document
        allCountries = oDoc.getElementsByTagName("country"); // Get results

        // Remove last results
        while(suggestion.slctOptions.firstChild!=null){
            suggestion.slctOptions.removeChild(suggestion.slctOptions.firstChild);
        }
        suggestion.slctOptions.appendChild(document.createElement("option"));
        suggestion.slctOptions.firstChild.selected="true";
        // Add new results
        for(var i=0, c;i<allCountries.length;i++){
            c = allCountries[i].firstChild.data;
            var option = document.createElement("option");
            option.value = c;
            option.appendChild(document.createTextNode(c));
            suggestion.slctOptions.appendChild(option);
        }
        suggestion.slctOptions.size = (allCountries.length>5?5:allCountries.length)+1;
        var styleSlct = suggestion.slctOptions.style;
        styleSlct.top = (suggestion.txtInput.offsetTop+suggestion.txtInput.offsetHeight)+"px";
        styleSlct.left = suggestion.txtInput.offsetLeft+"px";
        styleSlct.width = suggestion.txtInput.offsetWidth+"px";
        // Show results
        styleSlct.display="block";
    }
}

function ajaxFailure(oReq){
    // Hide the combo box because there isn't suggests to suggest
    suggestion.slctOptions.style.display="none";
}


Events.addHandler("load", window, hOnLoad); 

למה ההצעה נעשית לאחר זמן מסוים?


יכול להיות שהמשתמש מקיש מהר יחסית למשתמש פשוט אחר, במקרה כזה ישלחו מס' הודעות HTTP בכל פעם. מצב זה יגרום למס' בעיות:



  • הוספת עומס על השרת.

  • הצעות שלא מתאימות לטקסט שנכתב

אנו רצים להימנע ממצבים כאלו ולכן אנחנו נשלח הודעת HTTP לאחר X שניות (בעזרת setTimeout), ובמידה ולא נשלחה עדיין הודעה והמשתמש שינה את התוכן של התיבה, אנחנו נחדש(נעשה clearTimeout ולאחר מכן שוב setTimeout) את ההמתנה לביצוע קריאת ה-HTTP.


הקובץ Countries.asp


הקובץ מכיל מערך של מדינות וסקריפט ששולף מהמערך אך ורק את המדינות שמתחילות במחרוזת ששלחנו כפרמטר בבקשת ה-HTTP. את אותו מערך ממירים ל-XML שנשלח בחזרה כחלק מהתשובה

<% 
var countries=[...];
var q = ""+Request("q")(1), sResults=[];
if(q){
    for(var i=0;i<countries.length;i++){
        if(countries[i].indexOf(q)==0){
            sResults.push(countries[i]);
        }
    }
}
Response.ContentType="text/xml";
%><?xml version="1.0" encoding="utf-8" ?>
<countries>
<country><% =sResults.join("</country><country>") %></country>
</countries>

לחץ כאן כדי לראות את התוכנית בפעולה.


לחץ כאן כדי להוריד את קבצי התוכנית. (הדוגמא משופרת באדיבות rjnhojbht)


קישורים עניניים


תגיות: , , ,

ניר טייב

בונה אתרים ומתכנת בשפות:HTML, CSS, JavaScript, PHP 5, JSP&Servlets ורובי.

תגובות בפייסבוק