使用 jQuery 和 JSONP 调用跨域 WCF REST 服务






4.89/5 (24投票s)
一个简单的非ASP.NET餐厅菜单和订单表格示例,该表格使用JSONP和jQuery与跨域RESTful WCF服务进行交互

引言
在上一篇文章《使用jQuery实现客户端交互式表单功能》中,我演示了如何使用HTML、JavaScript、jQuery和jQuery的AJAX API创建一个简单的餐厅菜单/订单表单。尽管上一篇文章有效地展示了这些客户端技术的用法,但餐厅菜单项的来源——一个静态XML文件——并非旨在代表真正的“生产级”数据源。如今,要访问企业内部或互联网上的数据和业务逻辑,开发人员更倾向于构建面向服务的应用程序,这些应用程序公开RESTful Web服务,并由客户端应用程序消耗这些服务。RESTful服务是指符合REST(Representational State Transfer)架构模式的服务。有关REST的更多信息,可以阅读REST作者Roy Fielding的博士论文的第5章和第6章。大多数现代Web技术都与RESTful Web服务进行通信,包括Microsoft的Silverlight、Web Forms和MVC、JavaFX、Adobe Flash、PHP、Python和Ruby on Rails。
本文将基于上一篇文章的餐厅菜单/订单表单示例进行扩展,用WCF服务替换静态XML文件。本文将演示以下内容:
- 使用jQuery的AJAX API与WCF服务进行双向通信
- 使用JSONP与WCF服务进行跨域通信
- 将复杂的、嵌套的.NET对象序列化为JSONP格式的HTTP响应消息
- 将JSONP格式的HTTP请求消息反序列化为复杂的、嵌套的.NET对象
- 优化JavaScript和使用缓存以最大限度地提高内容交付到客户端的速度
背景
WCF
对于.NET开发人员来说,Windows Communication Foundation (WCF),Microsoft面向服务架构(SOA)的平台,是目前构建面向服务应用程序的首选。根据Microsoft的说法,WCF是.NET Framework的一部分,它提供了一个统一的编程模型,用于快速构建跨Web和企业进行通信的面向服务应用程序。
在WCF之前,Microsoft提供了ASP.NET XML Web Service,或简称ASP.NET Web Services。ASP.NET Web Services使用SOAP(Simple Object Access Protocol)通过HTTP发送和接收消息。数据从.NET对象实例序列化为XML格式的SOAP消息(或者,如它们也被称为,“SOAP信封中的XML”),反之亦然。关于ASP.NET Web Services的元数据包含在WSDL(Web Services Description Language)中。根据Microsoft的说法,尽管仍然普遍存在,但随着WCF的出现,ASP.NET Web Services现在被认为是一种“遗留”技术。SOAP,一种访问Web服务的协议,不符合REST架构指南。
WCF托管在Microsoft的IIS(Internet Information Services)Web服务器上,是一个复杂但健壮且灵活的面向服务框架。通过正确配置WCF服务,开发人员可以以多种方式精确地向客户端公开业务逻辑和数据源。WCF服务可以发送和接收消息,格式包括XML(SOAP信封)、RESTful格式(包括POX(plain old XML)、ATOM(用于Web feed的XML语言)以及JSON(JavaScript Object Notation))。
JSON/JSONP
本文中的示例使用JSON,更具体地说是JSONP(带填充的JSON),一种专门的JSON类型,来与WCF服务交换信息。JSON是一种开放的、基于文本的数据交换格式,它提供了一种标准化的数据交换格式,更适合AJAX风格的Web应用程序。与XML相比,JSON格式的消息体积更小。例如,本文中使用的餐厅菜单,XML格式为927字节。同样的JSONP格式消息仅为311字节,大约是其大小的三分之一。在慢速连接、移动设备或可能数百万个并发Web浏览器上传输JSON格式消息时,节省的量非常可观。
由于WCF服务将托管在与餐厅菜单和订单表格网站不同的域(示例中是不同的端口)中,我们必须使用JSONP。JSONP基于JSON,它允许页面从通常被禁止的、属于不同域的服务器请求数据,这是因为“同源策略”。同源策略是浏览器端编程语言(如JavaScript)重要的安全概念。根据Wikipedia的说法,同源策略允许来自同一网站页面的脚本在没有特定限制的情况下访问彼此的方法和属性,但阻止访问不同网站页面的大多数方法和属性。JSONP利用了HTML <script>
元素的开放策略。
下面是本文餐厅菜单的JSONP格式示例,它作为HTTP响应的一部分由WCF服务返回给客户端的HTTP请求的GET
方法。
RestaurantMenu([{"Description":"Cheeseburger","Id":1,"Price":3.99},
{"Description":"Chicken Sandwich","Id":4,"Price":4.99},
{"Description":"Coffee","Id":7,"Price":0.99},{"Description":"French Fries",
"Id":5,"Price":1.29},{"Description":"Hamburger","Id":2,"Price":2.99},
{"Description":"Hot Dog","Id":3,"Price":2.49},
{"Description":"Ice Cream Cone","Id":9,"Price":1.99},
{"Description":"Soft Drink","Id":6,"Price":1.19},{"Description":"Water",
"Id":8,"Price":0}]);
AJAX(嗯,其实不是...)
AJAX(Asynchronous JavaScript and XML)通过XMLHttpRequest
对象,在浏览器和Web服务器之间异步交换数据,避免页面重新加载。尽管名称如此,AJAX除了XML消息格式外,还可以处理JSON。其他格式包括JSONP、JavaScript、HTML和文本。使用jQuery的AJAX API,我们将使用GET
方法向服务器发出HTTP请求。其他HTTP方法包括POST
、PUT
和DELETE
。为了访问跨域资源(在本例中是WCF服务),客户端将使用GET
方法发出HTTP请求。
撰写本文时,我发现使用JSONP技术上来说不是AJAX,因为它不使用XMLHttpRequest
对象,而这是AJAX的一个主要要求。JSONP格式的HTTP请求是通过动态地将HTML <script>
标签插入DOM来完成的。与常规JSON的application/json
不同,通过Firebug可以看到,WCF服务的HTTP响应的Content-Type
是application/x-javascript
。我只希望一切正常工作,无论是否是AJAX。
Using the Code
本文使用的Visual Studio 2010解决方案包含3个项目,如下所示:
Restaurant
– C# 类库RestaurantWcfService
– C# WCF REST服务应用程序RestaurantDemoSite
– 现有网站
餐厅类库
C# 类库项目Restaurant包含主要的业务对象和业务逻辑。用于保存餐厅菜单和餐厅订单的类的实例化包括RestaurantMenu
、MenuItem
、RestaurantOrder
和OrderItem
。RestaurantMenu
和RestaurantOrder
都继承自System.Collections.ObjectModel.Collection<T>
。RestaurantMenu
包含MenuItem
实例,而RestaurantOrder
包含OrderItem
实例。
处理包含餐厅订单的JSON格式HTTP请求反序列化的业务逻辑由ProcessOrder
类处理。我曾使用标准的.NET System.Web.Script.Serialization.JavaScriptSerializer
类在将JSONP格式的HTTP请求反序列化为RestaurantOrder
实例时遇到困难。我通过使用Json.NET
解决了反序列化问题。这个.NET Framework,被描述为一个灵活的JSON序列化器,用于将.NET对象转换为JSON并反之,它是由James Newton-King创建的。它真是个救星。Json.NET在Codeplex上可用。在将RAW JSONP格式的HTTP请求传递给Json.NET之前,我仍然需要使用我编写的NormalizeJsonString
方法对其进行清理。
最后,ProcessOrder
类包含WriteOrderToFile
方法,该方法将餐厅订单写入文本文件。这是为了演示订单如何从客户端发送到服务器,存储,然后根据需要稍后重新加载和反序列化。为了成功使用此方法,您需要创建“c:\RestaurantOrders”文件夹路径,并为IUSR
用户帐户添加读取和写入RestaurantOrders文件夹的权限。
ProcessOrder
类(请注意对Json.NET的引用:Newtonsoft.Json
)。
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Restaurant
{
public class ProcessOrder
{
public const string STR_JsonFilePath = @"c:\RestaurantOrders\";
public string ProcessOrderJSON(string restaurantOrder)
{
if (restaurantOrder.Length < 1)
{
return "Error: Empty message string...";
}
try
{
var orderId = Guid.NewGuid();
NormalizeJsonString(ref restaurantOrder);
//Json.NET: http://james.newtonking.com/projects/json-net.aspx
var order =
JsonConvert.DeserializeObject
<restaurantorder>(restaurantOrder);
WriteOrderToFile(restaurantOrder, orderId);
return String.Format(
"ORDER DETAILS{3}Time: {0}{3}Order Id: {1}{3}Items: {2}",
DateTime.Now.ToLocalTime(), Guid.NewGuid(),
order.Count(), Environment.NewLine);
}
catch (Exception ex)
{
return "Error: " + ex.Message;
}
}
private void NormalizeJsonString(ref string restaurantOrder)
{
restaurantOrder = Uri.UnescapeDataString(restaurantOrder);
int start = restaurantOrder.IndexOf("[");
int end = restaurantOrder.IndexOf("]") + 1;
int length = end - start;
restaurantOrder = restaurantOrder.Substring(start, length);
}
private void WriteOrderToFile(string restaurantOrder, Guid OrderId)
{
//Make sure to add permissions for IUSR to folder path
var fileName =
String.Format("{0}{1}.txt", STR_JsonFilePath, OrderId);
using (TextWriter writer = new StreamWriter(fileName))
{
writer.Write(restaurantOrder);
}
}
}
}
餐厅WCF服务
如果您之前构建过WCF服务,您会对这个项目的目录结构感到熟悉。RestaurantService.svc,WCF服务文件,不包含任何实际代码,只包含一个指向其代码隐藏文件RestaurantService.cs的指针。此文件包含将通过WCF服务暴露给客户端的每个方法。IRestaurantService.cs接口文件定义了RestaurantService
类和WCF服务之间的服务契约。IRestaurantService
接口还定义了类方法的每个操作契约。操作契约包括操作契约属性,这些属性定义了服务操作(带有操作契约的方法)将如何作为WCF服务的一部分运行。此示例中的操作契约属性包括必需的调用(HTTP方法-GET
)、HTTP请求和响应的格式(JSON)以及缓存(用于餐厅菜单)。WFC服务引用(依赖于)餐厅类库。
WCF Web服务项目RestaurantWcfService
包含两个暴露给客户端的方法。第一个是GetCurrentMenu
,它序列化一个RestaurantMenu
实例,其中包含嵌套的MenuItem
实例。它将JSONP格式的HTTP响应返回给客户端。HTTP请求没有传递任何参数给该方法。
第二个方法SendOrder
,通过客户端HTTP请求的string
数据类型的输入参数,接受JSONP格式的订单。SendOrder
然后将订单传递给Restaurant.ProcessOrder
类中的ProcessOrderJSON
方法。ProcessOrderJSON
返回一个string
给SendOrder
,其中包含一些订单信息(订单ID、日期/时间、订单项数量)。这些信息被序列化并以JSONP格式的HTTP响应返回给客户端。响应验证了订单已被接收和理解。
最后,web.config文件包含WCF的绑定、行为、端点和缓存配置。由于WCF配置选项几乎无限,我总是发现正确配置此文件是一个挑战。网上有许多关于配置WCF的参考资料,但请注意,许多资料是在.NET Framework 4之前编写的。随着.NET Framework 4的出现,配置WCF以支持REST和JSONP变得更加容易。请确保参考MSDN关于.NET Framework 4的WCF的最新资料。
IRestaurantService.cs接口
using Restaurant;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Web;
namespace RestaurantWcfService
{
[ServiceContract]
public interface IRestaurantService
{
[OperationContract]
[Description("Returns a copy of the restaurant menu.")]
[WebGet(BodyStyle = WebMessageBodyStyle.Bare,
RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json)]
[AspNetCacheProfile("CacheFor10Seconds")]
RestaurantMenu GetCurrentMenu();
[OperationContract]
[Description("Accepts a menu order and return an order confirmation.")]
[WebGet(BodyStyle = WebMessageBodyStyle.Bare,
RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json,
UriTemplate = "SendOrder?restaurantOrder={restaurantOrder}")]
string SendOrder(string restaurantOrder);
}
}
RestaurantService.cs类(继承自IRestaurantService.cs)
using Restaurant;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.ServiceModel.Activation;
namespace RestaurantWcfService
{
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class RestaurantService : IRestaurantService
{
public RestaurantMenu GetCurrentMenu()
{
//Instantiates new RestaurantMenu object and
//sorts MeuItem objects by byDescription using LINQ
var menuToReturn = new RestaurantMenu();
var menuToReturnOrdered = (
from items in menuToReturn
orderby items.Description
select items).ToList();
menuToReturn = new RestaurantMenu(menuToReturnOrdered);
return menuToReturn;
}
public string SendOrder(string restaurantOrder)
{
//Instantiates new ProcessOrder object and
//passes JSON-format order string to ProcessOrderJSON method
var orderProcessor = new ProcessOrder();
var orderResponse =
orderProcessor.ProcessOrderJSON(restaurantOrder);
return orderResponse;
}
}
}
WCF服务的web.config文件
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="false" targetFramework="4.0" />
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="CacheFor10Seconds" duration="10"
varyByParam="none" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
<system.serviceModel>
<bindings>
<webHttpBinding>
<binding name="webHttpBindingWithJsonP"
crossDomainScriptAccessEnabled="true" />
</webHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="webHttpBehavior">
<webHttp helpEnabled="true"/>
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
<services>
<service name="RestaurantWcfService.RestaurantService">
<endpoint address="" behaviorConfiguration="webHttpBehavior"
binding="webHttpBinding"
bindingConfiguration="webHttpBindingWithJsonP"
contract="RestaurantWcfService.IRestaurantService" />
</service>
</services>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
WCF Web HTTP服务帮助
安装并运行本文的代码后,您可以使用新的.NET Framework 4 WCF Web HTTP服务帮助页面功能,查看有关WCF服务操作(方法)的更多详细信息。根据您的IIS配置,本地地址应类似于:https:///MenuWcfRestService/RestaurantService.svc/Help。

餐厅演示网站
RestaurantDemoSite
是一个非ASP.NET网站,仅包含HTML和JavaScript。为了本文的需要,我选择将RestaurantDemoSite
网站托管在与WCF服务(默认端口80)不同的端口(2929)上。我这样做是为了演示JSONP在跨域脚本中的必要性。将它们托管在两个不同的端口被认为是托管在两个不同的域上。端口2929是我个人开发机器上一个随机选择的开放端口。WCF服务和网站都配置为IIS中的虚拟目录,然后被添加到Visual Studio 2010解决方案中,以及餐厅类库。
按照第一篇文章的格式,该网站包含两个相同的页面,每个页面都有相同的餐厅菜单/订单表单。“开发”版本针对调试和演示进行了优化。“生产”版本则对JavaScript和CSS文件进行了最小化和打包,以优化生产环境下的使用。演示使用了最新可用的jQuery JavaScript库(jquery-1.6.3.js)和jQuery插件Format Currency(jquery.formatCurrency-1.4.0.js)。
该页面包含新的HTML5 <!DOCTYPE>
声明。我使用了HTML5的新数字输入类型来输入订购数量。我定义了最小值和最大值,这也是HTML5的新功能。您可以在最新版本的Google Chrome中看到这些HTML功能的运行。
所有客户端业务逻辑都包含在restaurant.js JavaScript文件中。该文件调用jQuery和Format Currency。我选择了有时备受争议的静态代码分析工具JSLint来帮助调试和重构我的JavaScript代码。即使您不同意JSLint的所有警告,理解警告的原因也将极大地提高您对JavaScript的整体理解。JSLint的一个很好的替代品,我也尝试过,是JSHint,它是JSLint项目的一个分支。JSHint声称是JSLint的一个更易于配置的版本。
restaurant.js JavaScript文件
var addMenuItemToOrder, calculateSubtotal, clearForm, clickRemove,
formatRowColor, formatRowCurrency, getRestaurantMenu, handleOrder,
orderTotal, populateDropdown, tableToJson, sendOrder, wcfServiceUrl;
// Populate drop-down box with JSON data (menu)
populateDropdown = function () {
var id, price, description;
id = this.Id;
price = this.Price;
description = this.Description;
$("#select_item")
.append($("<option></option>")
.val(id)
.html(description)
.attr("title", price));
};
// Use strict for all other functions
// Based on post at:
// http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
(function () {
"use strict";
wcfServiceUrl =
"https:///MenuWcfRestService/RestaurantService.svc/";
// Execute when the DOM is fully loaded
$(document).ready(function () {
getRestaurantMenu();
});
// Add selected item to order
$(function () {
$("#add_btn").click(addMenuItemToOrder);
});
// Place order if it contains items
$(function () {
$("#order_btn").click(handleOrder);
});
// Retrieve JSON data (menu) and loop for each menu item
getRestaurantMenu = function () {
$.ajax({
cache: true,
url: wcfServiceUrl + "GetCurrentMenu",
data: "{}",
type: "GET",
jsonpCallback: "RestaurantMenu",
contentType: "application/javascript",
dataType: "jsonp",
error: function () {
alert("Menu failed!");
},
success: function (menu) {
$.each(menu, populateDropdown); // must call function as var
}
});
};
// Add selected menu item to order table
addMenuItemToOrder = function () {
var order_item_selected_quantity, selected_item,
order_item_selected_id, order_item_selected_description,
order_item_selected_price, order_item_selected_subtotal;
// Limit order quantity to between 1-99
order_item_selected_quantity =
parseInt($("#select_quantity").val(), 10);
if (order_item_selected_quantity < 1 ||
order_item_selected_quantity > 99 ||
isNaN(order_item_selected_quantity)) {
return;
}
// Can't add 'Select an Item...' to order
if ($("#select_item").get(0).selectedIndex === 0) {
return;
}
// Get values
selected_item = $("#select_item option:selected");
order_item_selected_id = parseInt(selected_item.val(), 10);
order_item_selected_description = selected_item.text();
order_item_selected_price = parseFloat(selected_item.attr("title"));
// Calculate subtotal
order_item_selected_subtotal =
calculateSubtotal(order_item_selected_price,
order_item_selected_quantity);
// Write out menu selection to table row
$("<tr class='order_row'></tr>").html("<td>" +
order_item_selected_quantity +
"</td><td class='order_item_id'>" +
order_item_selected_id +
"</td><td class='order_item_name'>" +
order_item_selected_description +
"</td><td class='order_item_price'>" +
order_item_selected_price +
"</td><td class='order_item_subtotal'>" +
order_item_selected_subtotal +
"</td><td><input type='button' value='remove' /></td>")
.appendTo("#order_cart").hide();
// Display grand total of order_item_selected_id
$("#order_cart tr.order_row:last").fadeIn("medium", function () {
// Callback once animation is complete
orderTotal();
});
formatRowCurrency();
formatRowColor();
clickRemove();
clearForm();
};
// Calculate subtotal
calculateSubtotal = function (price, quantity) {
return price * quantity;
};
// Create alternating colored rows in order table
formatRowColor = function () {
$("#order_cart tr.order_row:odd").css("background-color", "#FAF9F9");
$("#order_cart tr.order_row:even").css("background-color", "#FFF");
};
// Format new order item values to currency
formatRowCurrency = function () {
$("#order_cart td.order_item_price:last").formatCurrency();
$("#order_cart td.order_item_subtotal:last").formatCurrency();
};
// Bind a click event to the correct remove button
clickRemove = function () {
$("#order_cart tr.order_row:last input").click(function () {
$(this).parent().parent().children().fadeOut("fast", function () {
$(this).parent().slideUp("slow", function () { // the row (tr)
$(this).remove(); // the row (tr)
orderTotal();
});
});
});
};
// Clear order input form and re-focus cursor
clearForm = function () {
$("#select_quantity").val("");
$("#select_item option:first-child").attr("selected", "selected");
$("#select_quantity").focus();
};
// Calculate new order total
orderTotal = function () {
var order_total = 0;
$("#order_cart td.order_item_subtotal").each(function () {
var amount = ($(this).html()).replace("$", "");
order_total += parseFloat(amount);
});
$("#order_total").text(order_total).formatCurrency();
};
// Call functions to prepare order and send to WCF Service
handleOrder = function () {
if ($("#order_cart tr.order_row:last").length === 0) {
alert("No items selected...");
} else {
var data = tableToJson();
sendOrder(data);
}
};
// Convert HTML table data into an array
// Based on code from:
// http://johndyer.name/post/table-tag-to-json-data.aspx
tableToJson = function () {
var data, headers, orderCartTable, myTableRow, rowData, i, j;
headers = ["Quantity", "Id"];
data = [];
orderCartTable = document.getElementById("order_cart");
// Go through cells
for (i = 1; i < orderCartTable.rows.length - 1; i++) {
myTableRow = orderCartTable.rows[i];
rowData = {};
for (j = 0; j < 2; j++) {
rowData[headers[j]] = myTableRow.cells[j].innerHTML;
}
data.push(rowData);
}
return data;
};
// Convert array to JSON and send to WCF Service
sendOrder = function (data) {
var jsonString = JSON.stringify({ restaurantOrder: data });
$.ajax({
url: wcfServiceUrl + "SendOrder?restaurantOrder=" + jsonString,
type: "GET",
contentType: "application/javascript",
dataType: "jsonp",
jsonpCallback: "OrderResponse",
error: function () {
alert("Order failed!");
},
success: function (confirmation) {
alert(confirmation.toString());
}
});
};
} ());
使用Firebug查看后台
在现实生活中,餐厅菜单更改的频率不高。因此,为了加快页面交付速度,我选择了在客户端缓存餐厅菜单。缓存配置在IRestaurantService
的操作契约中,以及在restaurant.js中对GetCurrentMenu
的jQuery AJAX调用中。在此示例中,我将缓存设置为10秒,您可以通过Firebug查看调用GetCurrentMenu
的HTTP响应头中的Cache-Control
属性来确认。
下面是在Firefox中运行Firebug加载餐厅菜单/订单表单页面的初始截图。请注意,AJAX调用的“Domain”与页面和相关文件的域不同。此外,“Status”和“Remote IP”都表明对GetCurrentMenu
(餐厅菜单)的HTTP响应与页面和相关文件一样被缓存了。Firebug是开发和调试JavaScript,尤其是在处理AJAX时,一个不可或缺的工具。

关注点
撰写本文使我注意到几件事情,包括:
- WCF - 无论我使用WCF服务多少次,正确配置它们似乎是90%的技术知识和10%的运气。好吧,也许是20%的运气!说真的,网上有很多关于WCF配置问题的绝佳资源。如果您遇到了特定的WCF问题,很可能别人也遇到过并且已经发布了解决方案。请确保信息与您正在使用的.NET Framework是最新版本。
- 第三方库、插件和框架 - 不要局限于使用.NET Framework、JavaScript或jQuery的开箱即用功能来解决所有编码挑战。有各种各样的框架、JavaScript库和jQuery插件可供选择。成为一名优秀的开发人员在于为问题提供最佳解决方案,而不是必须自己编写每一行代码。几分钟的研究可能相当于数小时的编码!
- 重构 - 重构代码至关重要。仅仅让它工作是不够的。额外的好处?我通过重构在软件开发方面获得了相当多的知识。强迫自己回去优化代码可能是一个巨大的学习机会。使用JSLint/JSHint、FxCop、RefactorPro!、CodeRush、ReSharper等第三方重构工具是提高重构和编码技能的好方法。我尽可能多地使用所有这些工具。
- 跨域与JSONP - 使用JSONP是绕过同源策略限制的一种技术。JSONP有利有弊。花些时间研究可能更符合您项目需求的替代方法。
历史
- 2011-09-25:提交原始文章和代码示例