共享社区

首页 » 互联网资源共享社区 » 网页资料共享 » 利用script标签实现的跨域名AJAX请求(ExtJS)
ajax - 2008-6-2 12:01:00
在AJAX应用环境中,由于安全的原因,浏览器不允许XMLHttpRequest组件请求跨域资源。在很多情况下,这个限制给我来带来的诸多不便。很多同行,研究了各种各样的解决方案:

1. 通过修改document.domain和隐藏的IFrame来实现跨域请求。这种方案可能是最简单的一种跨域请求的方案,但是它同样是一种限制最大的方案。首先,它只能实现在同一个顶级域名下的跨域请求;另外,当在一个页面中还包含有其它的IFrame时,可能还会产生安全性异常,拒绝访问。

2.通过请求当前域的代理,由服务器代理去访问另一个域的资源。XMLHttpRequest通过请求本域内的一个服务器资源,将要访问的目标资源提供给服务器,交由服务器去代理访问目标资源。这种方案,可以实现完全的跨域访问,但是开发,请求过程的消费会比较大。

3. 通过HTML中可以请求跨域资源的标签引用来达到目的,比如Image,Script,LINK这些标签。在这些标签中,Script无疑是最合适的。在请求每一个脚本资源时,浏览器都会去解析并运行脚本文件内定义的函数,或需要马上执行的JavaScript代码,我们可以通过服务器返回一段脚本或 JSON对象,在浏览器解析执行,从而达到跨域请求的目的。使用script标签来实现跨域请求,只能使用get方法请求服务器资源。并且传参的长度也受到地址栏长度的限制。

这里,我们来介绍第三种方案。先来看一段在网络上找到的例子(具体来源已经不得而知了,如果有人知道请提供原文链接,谢谢):

JavaScript:

  1: var scriptBlock = document.createElement("script");

  2: 

  3: function StartGet()

  4: {

  5:    scriptBlock.src = "";

  6:    scriptBlock.src = "http://Domain2/GetData.aspx";

  7:    scriptBlock.type = "text/javascript";

  8:    scriptBlock.language = "javascript";

  9:    document.getElementsByTagName("head")[0].appendChild(scriptBlock);

  10:    scriptBlock.onreadystatechange = ReturnData;

  11: }

  12: 

  13: function ReturnData()

  14: {

  15:    //alert(scriptBlock.readyState);

  16: 

  17:    //uninitialized        Object is not initialized with data.

  18:    //loading            Object is loading its data.

  19:    //loaded            Object has finished loading its data.

  20:    //interactive      User can interact with the object even though it is not fully loaded.

  21:    //complete          Object is completely initialized.

  22: 

  23:    if("loaded" == scriptBlock.readyState)

  24:    {

  25:        var div = document.getElementById("htmldiv");

  26:        div.innerHTML = a.project[0].name; //a是返回的json里面的变量

  27:    }   

  28: }

通过调用StartGet方法,发送一个跨域请求。用onreadystatechange事件监听请求结束事件,并且将返回的结果显示到页面上。

Thumbsup在这个事件函数中,a的对象是从何而来的呢?它就是通过http://Domain2/GetData.aspx输出的一段JSON对象,并被浏览器解析过。看看下面的服务器端的代码就应该明白了。

ASP.NET:

protected void Page_Load(object sender, EventArgs e)

{

  Response.Write("var a = {'project':[{'name':'a1'},{'name':'a2'}]};");

}

服务器通过这段代码输出一段JSON对象的脚本内容。

上面的例子就可以完整的描述通过Script来进跨域请求资源。但是,这里还有一个问题script标签的onreadystatechange事件并不是W3C标准的事件定义,只在IE中有效。下面的例子,它是ExtJS团队给出的对Ext.data.Connection类的扩展,以支持跨域的请求。通过它的扩展我们可以方便的使用Ext.Ajax来请求跨域资源,并且保证的资源回收的安全。下面先看看它的代码:

Ext.lib.Ajax.isCrossDomain = function(u) {

    var match = /(?:(\w*:)\/\/)?([\w\.]*(?::\d*)?)/.exec(u);

    if (!match[1]) return false; // No protocol, not cross-domain

    return (match[1] != location.protocol) || (match[2] != location.host);

};



Ext.override(Ext.data.Connection, {



    request : function(o){

        if(this.fireEvent("beforerequest", this, o) !== false){

            var p = o.params;



            if(typeof p == "function"){

                p = p.call(o.scope||window, o);

            }

            if(typeof p == "object"){

                p = Ext.urlEncode(p);

            }

            if(this.extraParams){

                var extras = Ext.urlEncode(this.extraParams);

                p = p ? (p + '&' + extras) : extras;

            }



            var url = o.url || this.url;

            if(typeof url == 'function'){

                url = url.call(o.scope||window, o);

            }



            if(o.form){

                var form = Ext.getDom(o.form);

                url = url || form.action;



                var enctype = form.getAttribute("enctype");

                if(o.isUpload || (enctype && enctype.toLowerCase() == 'multipart/form-data')){

                    return this.doFormUpload(o, p, url);

                }

                var f = Ext.lib.Ajax.serializeForm(form);

                p = p ? (p + '&' + f) : f;

            }



            var hs = o.headers;

            if(this.defaultHeaders){

                hs = Ext.apply(hs || {}, this.defaultHeaders);

                if(!o.headers){

                    o.headers = hs;

                }

            }



            var cb = {

                success: this.handleResponse,

                failure: this.handleFailure,

                scope: this,

                argument: {options: o},

                timeout : this.timeout

            };



            var method = o.method||this.method||(p ? "POST" : "GET");



            if(method == 'GET' && (this.disableCaching && o.disableCaching !== false) || o.disableCaching === true){

                url += (url.indexOf('?') != -1 ? '&' : '?') + '_dc=' + (new Date().getTime());

            }



            if(typeof o.autoAbort == 'boolean'){ // options gets top priority

                if(o.autoAbort){

                    this.abort();

                }

            }else if(this.autoAbort !== false){

                this.abort();

            }

            if((method == 'GET' && p) || o.xmlData || o.jsonData){

                url += (url.indexOf('?') != -1 ? '&' : '?') + p;

                p = '';

            }

            if (o.scriptTag || this.scriptTag || Ext.lib.Ajax.isCrossDomain(url)) {

              this.transId = this.scriptRequest(method, url, cb, p, o);

            } else {

              this.transId = Ext.lib.Ajax.request(method, url, cb, p, o);

            }

            return this.transId;

        }else{

            Ext.callback(o.callback, o.scope, [o, null, null]);

            return null;

        }

    },

   

    scriptRequest : function(method, url, cb, data, options) {

        var transId = ++Ext.data.ScriptTagProxy.TRANS_ID;

        var trans = {

            id : transId,

            cb : options.callbackName || "stcCallback"+transId,

            scriptId : "stcScript"+transId,

            options : options

        };



        url += (url.indexOf("?") != -1 ? "&" : "?") + data + String.format("&{0}={1}", options.callbackParam || this.callbackParam || 'callback', trans.cb);



        var conn = this;

        window[trans.cb] = function(o){

            conn.handleScriptResponse(o, trans);

        };



//      Set up the timeout handler

        trans.timeoutId = this.handleScriptFailure.defer(cb.timeout, this, [trans]);



        var script = document.createElement("script");

        script.setAttribute("src", url);

        script.setAttribute("type", "text/javascript");

        script.setAttribute("id", trans.scriptId);

        document.getElementsByTagName("head")[0].appendChild(script);



        return trans;

    },



    handleScriptResponse : function(o, trans){

        this.transId = false;

        this.destroyScriptTrans(trans, true);

        var options = trans.options;

       

//      Attempt to parse a string parameter as XML.

        var doc;

        if (typeof o == 'string') {

            if (window.ActiveXObject) {

                doc = new ActiveXObject("Microsoft.XMLDOM");

                doc.async = "false";

                doc.loadXML(o);

            } else {

                doc = new DOMParser().parseFromString(o,"text/xml");

            }

        }



//      Create the bogus XHR

        response = {

            responseObject: o,

            responseText: (typeof o == "object") ? Ext.util.JSON.encode(o) : String(o),

            responseXML: doc,

            argument: options.argument

        }

        this.fireEvent("requestcomplete", this, response, options);

        Ext.callback(options.success, options.scope, [response, options]);

        Ext.callback(options.callback, options.scope, [options, true, response]);

    },

   

    handleScriptFailure: function(trans) {

        this.transId = false;

        this.destroyScriptTrans(trans, false);

        var options = trans.options;

        response = {

            argument:  options.argument,

            status: 500,

            statusText: 'Server failed to respond',

            responseText: ''

        };

        this.fireEvent("requestexception", this, response, options, {

            status: -1,

            statusText: 'communication failure'

        });

        Ext.callback(options.failure, options.scope, [response, options]);

        Ext.callback(options.callback, options.scope, [options, false, response]);

    },

   

    // private

    destroyScriptTrans : function(trans, isLoaded){

        document.getElementsByTagName("head")[0].removeChild(document.getElementById(trans.scriptId));

        clearTimeout(trans.timeoutId);

        if(isLoaded){

            window[trans.cb] = undefined;

            try{

                delete window[trans.cb];

            }catch(e){}

        }else{

            // if hasn't been loaded, wait for load to remove it to prevent script error

            window[trans.cb] = function(){

                window[trans.cb] = undefined;

                try{

                    delete window[trans.cb];

                }catch(e){}

            };

        }

    }

});

在reqeust方法中,做好参数的处理工作后(这些都是原先的实现)。在发送请求时,判断是否有scriptTag 属性(值为true),如果scriptTag有定义并且为true,那么调用scriptRequest 来通过script标签发送跨域请求。在请求中,它会将所有的参数拼接成地址传参的方式,并且还有一个callback参数(或自己指定的参数名),用于标识客户端在接收返回的回调方法(在服务器端生成的javascript代码中调用),这个回调函数会根据不同的请求动态生成,在同一个上下文环境中每次请求的回调函数名都不一样。通过指定参数就可以解决在script标签中没有onreadystatechange事件定义带来的麻烦。在出错处理上,它使用的是超时的出错处理,因为没有事件,所以它会有一个请求超时时间延迟函数调用来进行资源的回收工作。

经过上面的扩展,我们在使用Ext.Ajax.request方法时,只需要新增一个标志标识它是一个跨域请求:scriptTag: true 。如下的调用:

Ext.Ajax.request({

    url: 'http://localhost:8080/aspicio/getxml.do',

    params: {

        listId: 'CountryListManager141Grid509',

        format: 'xml'

    },

    scriptTag: true, // Use script tag transport

    success: function(r) {

        console.log(r);

    }

});

下面是一段服务器端的示例代码:

  1: //取得客户端回调函数名

  2: string callBack = Reqeust.QueryString("callback");

  3: //其它参数均可以通过Reqeust.QueryString得到。

  4: //向客户端输出javascript调用。

  5: Response.Write(callBack + "('[value:0]')";);

通过服务器发起对回调函数的调用,从而完成整个跨域请求过程。
1
查看完整版本: 利用script标签实现的跨域名AJAX请求(ExtJS)