Web APi之手動實現JSONP或安裝配置Cors跨域(七)

前言

照理來講本節也應該講Web API原理,目前已經探討完了比較底層的Web API消息處理管道以及Web Host寄宿管道,接下來應該要觸及控制器、Action方法,以及過濾器、模型綁定等等,想一想也是心痛不已,水太深了,摸索原理關鍵是太枯燥和乏味了,可是呢,從情感上仍是挺樂意去摸索原理,而情緒上不太樂意去探究原理,因而乎,本文就由此誕生了,藉此文緩解下枯燥的心情和壓抑的情緒。後續繼續摸索原理。javascript

接下來咱們要講的就是利用JSONP和利用Cors這兩種方式來實現跨域,請看下文。。。。。html

JSONP實現跨域

Web API並無提供JSONP  Formatter,可是這並不能影響咱們前進的腳步,咱們能夠自定義Formatter來實現JSONP功能。既然是利用JSONP跨域,那麼就得簡單介紹下JSONP。java

爲何須要JSONP?

瀏覽器都是基於同源策略,使其腳本不能跨站點來得到服務器端數據,可是辦法老是人想出來的,這個時候就須要JSONP了,固然也能夠用別的辦法實現,JSONP是一種能實現讓基於JavaScript的客戶端程序繞過跨站點腳本的限制從而從非當前的服務器上來得到數據的方式。默認狀況下,應用程序利用Ajax是不容許訪問遠程跨域,可是咱們能夠利用<script>標籤加載JSONP來實現這種跨站點限制。這也不失爲一種好的解決方案。JSONP的工做原理是當JSON數據返回時經過組合JSON數據,並將其包裹到一個函數中進行調用,利用JQuery更能很好的去實現這點。web

假若有這樣以下的一個URL:ajax

http://www.cnblogs.com/CreateMyself/WebAPI/xpy0928

但咱們利用Ajax發出GET請求來獲取服務器端數據時那將是垂手可得,可是,可是,可是,重要的前提說三遍,前提是在相同域下,如果不一樣的域下,利用Ajax來訪問數據估計不是這麼輕鬆了吧。可是,可是,可是,重要的話再說三遍,此時咱們就利用JSONP來實現跨域,此時將會變成以下請求模式:json

http://www.cnblogs.com/CreateMyself/WebAPI/xpy0928?callback=?

發出以下URL請求經過一個callback回調,這樣獲得的結果是和同一站點的結果是一致的,JQuery會反序列會這些數據並將其推入到函數中。api

JSONP數據是怎樣的?

它主要就是經過調用函數將返回的JSON數據進行包裹,相似於以下形式:跨域

Query7d59824917124eeb85e5872d0a4e7e5d([{"Id":"4836628","Name":"xpy0928"},{......}])

JSONP的工做原理是怎樣的呢?

在JavaScript客戶端發出請求後,當響應數據時,將其數據做爲執行要調用函數的參數,並在其內部將JSON數據進行反序列化瀏覽器

下面咱們就原理來進行演示,請看以下代碼:服務器

    function JSONP(url, callback) {
        var id = "_" + "Query" + (new Date()).getTime();  //建立一個幾乎惟一的id
        
        window[id] = function (result) {  //建立一個全局回調處理函數

            if (callback)
                callback(result);

            var getId = document.getElementById(id);  //移除Script標籤和id
            getId.parentNode.removeChild(getId);
            window[getId] = null;  //調用函數後進行銷燬
        }

        url = url.replace("callback=?", "callback=" + id);

        var script = document.createElement("script");  //建立Script標籤並執行window[id]函數
        script.setAttribute("id", id);
        script.setAttribute("src", url);
        script.setAttribute("type", "text/javascript");
        document.body.appendChild(script);
    }

簡單進行調用則以下:

function JSONPFunction() {
        
        JSONP("http://localhost:23133/api/default?callback=?",
        
        function(jsonData){          //將返回的數據jsonData做爲調用函數的參數

        }
};

以上是利用原生的JS實現,可是在JQuery中卻對此進行了封裝,以下:

$.getJSON("http://www.cnblogs.com/CreateMyself/WebAPI/xpy0928?callback=?",function(jsonData){

})

上述callback=?,對於callback中的?而言,JQuery會自動生成咱們上述手動建立的全局處理函數,並在調用完函數以後自動銷燬,毫無疑問該回調函數就相似於JS中的代理對象,也就是所謂的臨時代理函數,同時JQuery也會自動去檢測該請求是不是跨域請求,若不是,則以普通Ajax進行請求,不然則以異步加載JS文件的形式來執行JSONP中的回調函數。

JSONP在Web API中如何實現呢? 

上述講了JSONP原理和實現,那麼結合Web API是如何實現的呢?咱們只能自定義Formatter來手動實現這個功能,既然是有關於JSON,那麼天然是繼承於 JsonMediaypeFormatter 了,代碼以下:

第一步

自定義JsonpFormatter並繼承於JsonMediaTypeFormatter:

    public class JsonpFormatter : JsonMediaTypeFormatter
    {
     //當請求過來是帶有text/javascript時處理JSONP請求
        public JsonpFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));

            JsonpParameterName = "callback";
        }

        //查找函數名  
        public string JsonpParameterName { get; set; }

        private string JsonpCallbackFunction;

        public override bool CanWriteType(Type type)
        {
            return true;
        }

        //重寫此方法來捕獲請求對象
        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, System.Net.Http.HttpRequestMessage request, MediaTypeHeaderValue mediaType)
        {
            var formatter = new JsonpFormatter()
            {
                JsonpCallbackFunction = GetJsonCallbackFunction(request)
            };
            //運用JSON.NET來序列化自定義         
            formatter.SerializerSettings.Converters.Add(new StringEnumConverter());
            formatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

            return formatter;
        }

       //重寫此方法寫入到流並返回
        public override Task WriteToStreamAsync(Type type, object value,
                                        Stream stream,
                                        HttpContent content,
                                        TransportContext transportContext)
        {
            if (string.IsNullOrEmpty(JsonpCallbackFunction))
                return base.WriteToStreamAsync(type, value, stream, content, transportContext);

            StreamWriter writer = null;

            try
            {
                writer = new StreamWriter(stream);
                writer.Write(JsonpCallbackFunction + "(");
                writer.Flush();
            }
            catch (Exception ex)
            {
                try
                {
                    if (writer != null)
                        writer.Dispose();
                }
                catch { }

                var tcs = new TaskCompletionSource<object>();
                tcs.SetException(ex);
                return tcs.Task;
            }

            return base.WriteToStreamAsync(type, value, stream, content, transportContext)
                       .ContinueWith(innerTask =>
                       {
                           if (innerTask.Status == TaskStatus.RanToCompletion)
                           {
                               writer.Write(")");
                               writer.Flush();
                           }

                       }, TaskContinuationOptions.ExecuteSynchronously)
                        .ContinueWith(innerTask =>
                        {
                            writer.Dispose();
                            return innerTask;

                        }, TaskContinuationOptions.ExecuteSynchronously)
                        .Unwrap();
        }

        //從查詢字符串中得到JSONP Callback回調函數
        private string GetJsonCallbackFunction(HttpRequestMessage request)
        {
            if (request.Method != HttpMethod.Get)
                return null;

            var query = HttpUtility.ParseQueryString(request.RequestUri.Query);
            var queryVal = query[this.JsonpParameterName];

            if (string.IsNullOrEmpty(queryVal))
                return null;

            return queryVal;
        }
    }

第二步

此時只需將此自定義類在Web API配置文件中進行註冊便可:

            GlobalConfiguration
                .Configuration
                .Formatters
                .Insert(0, new JsonpFormatter());

第三步

給出後臺測試數據:

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Gender { get; set; }
    }


        public IEnumerable<Person> GetAllPerson()
        {

            Person[] Person = new Person[]
            {
                new Person{ Name="xpy0928", Age =11, Gender=""},
                new Person{ Name="xpy0929", Age =12, Gender=""},
                new Person{ Name="xpy0930", Age =13, Gender=""},
           };
            return Person;
        }

接下來就是進行驗證了。調用上述前臺所寫的JSONP方法:

     function getPerson() {
        JSONP("http://localhost:23133/api/default?callback=?",
              function (persons) {
                  $.each(persons, function (index, person) {
                      var html = "<ul>";
                      html += "<li>Name: " + person.Name + "</li>";
                      html += "<li>Age:" + person.Age + "</li>";
                      html += "<li>Gender: " + person.Gender + "</li>";
                      html += "</ul>";
                      $("#person").append($(html));
                  });
              });
    };
    $(function () {
        $("#btn").click(function () {
            getPerson();
        });
    });

上述也可自行利用Ajax來請求,如下幾項必不可少:

        $.ajax({
                type: "Get",
                url: "http://localhost:23133/api/default/?callback=?",
                dataType: "json",
                contentType: "application/json; charset=utf-8",
.......
})

點擊加載數據:

<input type="button" value="獲取數據" id="btn" />
<ul id="person"></ul>

既然是跨站點就開兩個應用程序就得了唄,服務器端:localhost:23133,客戶端:localhost:29199,走你,完事:

總結

一切圓滿結束,彷佛利用JSONP實現跨域是個不錯的解決方案,可是有的人就問了,JSONP也有侷限性啊,只能針對於Get請求不能用於POST請求啊,而且還須要手動去寫這麼操蛋的代碼,有點使人髮指,恩,是的,確實是個問題,你想到的同時我也替你想到了,請看下文!

Cors實現跨域

使用Cors跨域配置是極其的簡單,可是前提是你得經過NuGet下載程序包,搜索程序包【Microsoft.AspNet.WebApi.Cors】便可,如圖:

下載完成後,有兩種配置跨域的方式

第一

在Web API配置文件中進行全局配置:

            var cors = new EnableCorsAttribute("*", "*", "*");
            config.EnableCors(cors);

第二

若你僅僅只是想某個控制器應用跨域也就是說實現局部控制器跨域,固然你也能夠經過添加特性來實現這點:

   [EnableCors(origins: "*", headers: "*", methods: "*")]
    public class HomeController : Controller
    {

    }

嘗試(一)

在被請求的服務器端的Web API配置文件中,進行全文配置,接下來發出POST請求以下:

        $("#btn").click(function () {
            $.ajax({
                type: "POST",
                url: "http://localhost:23133/api/Default/PostAllPerson",
                dataType: "json",
                contentType: "application/json; charset=utf-8",
                cache: false,
                success: function (persons) {
                    $.each(persons, function (index, person) {
                        var html = "<ul>";
                        html += "<li>Name: " + person.Name + "</li>";
                        html += "<li>Age:" + person.Age + "</li>";
                        html += "<li>Gender: " + person.Gender + "</li>";
                        html += "</ul>";
                        $("#person").append($(html));
                    });
                }
            });
        });

如咱們所指望的同樣,測試經過:

嘗試(二)

在控制器上進行局部配置,併發出Get請求,修改以下:

    [EnableCors(origins: "*", headers: "*", methods: "*")]
    public class DefaultController : ApiController
    {
        public IEnumerable<Person> GetAllPerson()
        {}
    }

發出請求以下:

     $.ajax({
                type: "Get",
                url: "http://localhost:23133/api/Default",
                dataType: "json",
                ........
           })

咱們查看其請求報文頭信息以及返回狀態碼便知是否成功,以下(如預期同樣):

經測試利用Cors實現對於Get和POST請求都是來者不拒,都能很友好的返回響應的數據而且配置簡單。固然Cors的功能遠不止如此簡單,更多詳細信息,請參看【Cors-Origin For WebAPI】 

總結 

利用JSONP能較好的實如今Web API上的跨域,可是有兩個嚴重的缺陷,第一:只能是Get請求。第二:你得自定義實現JsonMediaTypeFormatter。在Cors未出世以前你沒有更好的解決方案,你只能忍受,自從Cors出世,咱們再也不受請求的限制,再也不手動去實現,只需稍微配置便可達到咱們所需,因此利用Cors實現跨域將是不可替代的方案。

相關文章
相關標籤/搜索