jQuery源碼分析系列(35) : Ajax - jsonp的實現與原理


ajax的核心是通過XmlHttpRequest獲取非本頁內容,而jsonp的核心則是動態添加<script>標簽來調用服務器提供的js腳本

json核心就是:允許用戶傳遞一個callback參數給服務端,然后服務端返回數據時會將這個callback參數作為函數名來包裹住JSON數據,這樣客戶端就可以隨意定制自己的函數來自動處理返回數據了。

jquery ext dojo這類庫的實現手段其實大同小異

在同源策略下,在某個服務器下的頁面是無法獲取到該服務器以外的數據的,但img、iframe、script等標簽是個例外,這些標簽可以通過src屬性請求到其他服務器上的數據。

利用script標簽的開放策略,我們可以實現跨域請求數據,當然,也需要服務端的配合。

先看一段jQuery處理jsonp的情況

通過發送php請求

客戶端

$.ajax({
    async: false, // 同步加載數據,即等到ajax執行完畢再接着執行下面的語句
    url: 'http://192.168.1.114/yii/demos/test.php', //不同的域
    type: 'GET', // jsonp模式只有GET是合法的
    data: {
        'action': 'aaron'
    }, // 預傳參的數組
    dataType: 'jsonp', // 數據類型
    jsonp: 'backfunc', // 指定回調函數名,與服務器端接收的一致,並回傳回來
    success: function(json) {
        console.log(json);
    }
})

php服務端

<?php
$act = trim($_GET['action']);

if($act == 'aaron' ){
    echo trim($_GET['backfunc']).'('. json_encode(array('status'=>1,'info'=>'OK')) .')';  
}
?>

一般的ajax是不能跨域請求的,因此需要使用一種特別的方式來實現跨域,其中的原理是利用 <script> 元素的這個開放策略

這里有2個重要的參數

  • jsonpCallback

為jsonp請求指定一個回調函數名。這個值將用來取代jQuery自動生成的隨機函數名。這主要用來讓jQuery生成一個獨特的函數名,這樣管理請求更容易,也能方便地提供回調函數和錯誤處理。你也可以在想讓瀏覽器緩存GET請求的時候,指定這個回調函數名。從jQuery 1.5開始,你也可以使用一個函數作為該參數設置,在這種情況下,該函數的返回值就是jsonpCallback的結果。

  • jsonp

在一個jsonp請求中重寫回調函數的名字。這個值用來替代在"callback=?"這種GET或POST請求中URL參數里的"callback"部分,比如{jsonp:'onJsonPLoad'}會導致將"onJsonPLoad=?"傳給服務器。在jQuery 1.5,,設置jsonp選項為false,阻止了jQuery從加入"?callback"字符串的URL或試圖使用"=?"轉換。在這種情況下,你也應該明確設置jsonpCallback設置。例如, { jsonp: false, jsonpCallback: "callbackName" }、

 


當我們正常地請求一個JSON數據的時候,服務端返回的是一串JSON類型的數據,而我們使用JSONP模式來請求數據的時候

服務端返回的是一段可執行的JavaScript代碼

所以我們可見服務器代碼最后一行

$_GET['backfunc']).'('. json_encode(array('status'=>1,'info'=>'OK')) .')

就是執行的 backfunc方法,然后把數據通過回調的方式傳遞過去

OK,就是整個流程就是:

客戶端發送一個請求,規定一個可執行的函數名(這里就是jQuery做了封裝的處理,自動幫你生成回調函數並把數據取出來供success屬性方法來調用,不是傳遞的一個回調句柄),服務端接受了這個backfunc函數名,然后把數據通過實參的形式發送出去

 


jQuery的實現:

通過ajax請求不同域的實現,底層不是靠XmlHttpRequest而是script,所以不要被這個方法給迷惑了

在ajax請求中類型如果是type是get post,其實內部都只會用get,因為其跨域的原理就是用的動態加載script的src,所以我們只能把參數通過url的方式傳遞

比如

$.ajax({
    url: 'http://192.168.1.114/yii/demos/test.php', //不同的域
    type: 'GET', // jsonp模式只有GET是合法的
    data: {
        'action': 'aaron'
    }, // 預傳參的數組
    dataType: 'jsonp', // 數據類型
    jsonp: 'backfunc', // 指定回調函數名,與服務器端接收的一致,並回傳回來
})

其實jquery內部會轉化成

http://192.168.1.114/yii/demos/test.php?backfunc=jQuery2030038573939353227615_1402643146875&action=aaron

然后動態加載

<script type="text/javascript" src="http://192.168.1.114/yii/demos/test.php?backfunc=jQuery2030038573939353227615_1402643146875&action=aaron"></script>

然后php方就會執行backfunc(傳遞參數);

 

所以流程就會分二步:

1:針對jsonp的預處理,主要是轉化拼接這些參數,然后處理緩存,因為jsonp的方式也是靠加載script所以要關閉瀏覽器緩存

inspectPrefiltersOrTransports中,當作了jsonp的預處理后,還要在執行inspect(dataTypeOrTransport);的遞歸,就是為了關閉這個緩存機制

var dataTypeOrTransport = prefilterOrFactory(options, originalOptions, jqXHR);
            /**
             * 針對jonsp處理
             */
            if (typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[dataTypeOrTransport]) {
                //增加cache設置標記
                //不需要緩存
                //dataTypes: Array[2]
                // 0: "script"
                // 1: "json"
                options.dataTypes.unshift(dataTypeOrTransport);
                inspect(dataTypeOrTransport);
                return false;
            } else if (seekingTransport) {
                return !(selected = dataTypeOrTransport);
            }

具體的預處理的代碼

// Detect, normalize options and install callbacks for jsonp requests
// 向前置過濾器對象中添加特定類型的過濾器
// 添加的過濾器將格式化參數,並且為jsonp請求增加callbacks
jQuery.ajaxPrefilter("json jsonp", function(s, originalSettings, jqXHR) {

    var callbackName,
        overwritten,
        responseContainer,
        // 如果是表單提交,則需要檢查數據
        jsonProp = s.jsonp !== false && (rjsonp.test(s.url) ?
            "url" :
            typeof s.data === "string" 
            && !(s.contentType || "").indexOf("application/x-www-form-urlencoded") 
            && rjsonp.test(s.data) && "data"
        );

    // Handle iff the expected data type is "jsonp" or we have a parameter to set
    // 這個方法只處理jsonp,如果json的url或data有jsonp的特征,會被當成jsonp處理
    if (jsonProp || s.dataTypes[0] === "jsonp") {

        // Get callback name, remembering preexisting value associated with it
        // s.jsonpCallback時函數,則執行函數用返回值做為回調函數名
        callbackName = s.jsonpCallback = jQuery.isFunction(s.jsonpCallback) ?
            s.jsonpCallback() :
            s.jsonpCallback;

        // Insert callback into url or form data
        // 插入回調url或表單數據
        // "test.php?symbol=IBM&callback=jQuery20309245402452070266_1402451299022"
        if (jsonProp) {
            s[jsonProp] = s[jsonProp].replace(rjsonp, "$1" + callbackName);
        } else if (s.jsonp !== false) {
            s.url += (ajax_rquery.test(s.url) ? "&" : "?") + s.jsonp + "=" + callbackName;
        }

        // Use data converter to retrieve json after script execution
        s.converters["script json"] = function() {
            if (!responseContainer) {
                jQuery.error(callbackName + " was not called");
            }
            return responseContainer[0];
        };

        // force json dataType
        // 強制跟換類型
        s.dataTypes[0] = "json";

        // Install callback
        // 增加一個全局的臨時函數
        overwritten = window[callbackName];
        window[callbackName] = function() {
            responseContainer = arguments;
        };

        // Clean-up function (fires after converters)
        // 在代碼執行完畢后清理這個全部函數
        jqXHR.always(function() {
            // Restore preexisting value
            window[callbackName] = overwritten;

            // Save back as free
            if (s[callbackName]) {
                // make sure that re-using the options doesn't screw things around
                s.jsonpCallback = originalSettings.jsonpCallback;

                // save the callback name for future use
                oldCallbacks.push(callbackName);
            }

            // Call if it was a function and we have a response
            if (responseContainer && jQuery.isFunction(overwritten)) {
                overwritten(responseContainer[0]);
            }

            responseContainer = overwritten = undefined;
        });

        // Delegate to script
        return "script";
    }
});

 jquery會在window對象中加載一個全局的函數,當代碼插入時函數執行,執行完畢后就會被移除。同時jquery還對非跨域的請求進行了優化,如果這個請求是在同一個域名下那么他就會像正常的Ajax請求一樣工作。

 

 

分發器執行代碼:

當我們所有的參數都轉化好了,此時會經過請求發送器用來處理發送的具體

為什么會叫做分發器,因為發送的請求目標

ajax因為參雜了jsonp的處理,所以實際上的請求不是通過 xhr.send(XmlHttpRequest)發送的

而是通過get方式的腳本加載的

所以

transports對象在初始化構件的時候,會生成2個處理器

  1. *: Array[1]        針對xhr方式
  2. script: Array[1]  針對script,jsonp方式

所以

transport = inspectPrefiltersOrTransports(transports, s, options, jqXHR);

那么得到的transport就會根據當前的處理的類型,來選擇采用哪種發送器(*、script)

 

針對script的請求器

jQuery.ajaxTransport("script", function(s) {
    // This transport only deals with cross domain requests
    if (s.crossDomain) {
        var script, callback;
        return {
            send: function(_, complete) {
                script = jQuery("<script>").prop({
                    async: true,
                    charset: s.scriptCharset,
                    //"http://192.168.1.114/yii/demos/test.php?backfunc=jQuery20308569577629677951_1402642881663&action=aaron&_=1402642881664"
                    src: s.url
                }).on(
                    "load error",
                    callback = function(evt) {
                        script.remove();
                        callback = null;
                        if (evt) {
                            complete(evt.type === "error" ? 404 : 200, evt.type);
                        }
                    }
                );
                document.head.appendChild(script[0]);
            },
            abort: function() {
                if (callback) {
                    callback();
                }
            }
        };
    }
});

此時就很明了吧

所以最終的實現就是通過動態加載腳本!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM