前言
照理來說本節也應該講Web API原理,目前已經探討完了比較底層的Web API消息處理管道以及Web Host寄宿管道,接下來應該要觸及控制器、Action方法,以及過濾器、模型綁定等等,想想也是心痛不已,水太深了,摸索原理關鍵是太枯燥和乏味了,但是呢,從情感上還是挺樂意去摸索原理,而情緒上不太樂意去探究原理,於是乎,本文就由此誕生了,借此文緩解下枯燥的心情和壓抑的情緒。后續繼續摸索原理。
接下來我們要講的就是利用JSONP和利用Cors這兩種方式來實現跨域,請看下文。。。。。
JSONP實現跨域
Web API並沒有提供JSONP Formatter,但是這並不能影響我們前進的腳步,我們可以自定義Formatter來實現JSONP功能。既然是利用JSONP跨域,那么就得簡單介紹下JSONP。
為什么需要JSONP?
瀏覽器都是基於同源策略,使其腳本不能跨站點來獲得服務器端數據,但是辦法總是人想出來的,這個時候就需要JSONP了,當然也可以用別的辦法實現,JSONP是一種能實現讓基於JavaScript的客戶端程序繞過跨站點腳本的限制從而從非當前的服務器上來獲得數據的方式。默認情況下,應用程序利用Ajax是不允許訪問遠程跨域,但是我們可以利用<script>標簽加載JSONP來實現這種跨站點限制。這也不失為一種好的解決方案。JSONP的工作原理是當JSON數據返回時通過組合JSON數據,並將其包裹到一個函數中進行調用,利用JQuery更能很好的去實現這點。
假如有這樣如下的一個URL:
http://www.cnblogs.com/CreateMyself/WebAPI/xpy0928
但我們利用Ajax發出GET請求來獲取服務器端數據時那將是輕而易舉,但是,但是,但是,重要的前提說三遍,前提是在相同域下,若是不同的域下,利用Ajax來訪問數據估計不是這么輕松了吧。但是,但是,但是,重要的話再說三遍,此時我們就利用JSONP來實現跨域,此時將會變成如下請求模式:
http://www.cnblogs.com/CreateMyself/WebAPI/xpy0928?callback=?
發出如下URL請求通過一個callback回調,這樣得到的結果是和同一站點的結果是一致的,JQuery會反序列會這些數據並將其推入到函數中。
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實現跨域將是不可替代的方案。
