使用 jQuery AJAX 调用消耗 WCF REST 服务
本文介绍了一种使用 jQuery AJAX 调用消费跨域 WCF REST 服务的方法,并附带示例。
引言
本文介绍了一种使用 jQuery AJAX 调用消费跨域 WCF REST 服务的方法,并附带示例。
背景
Representational State Transfer (REST) 是一种用于分布式超媒体系统(如万维网)的软件架构风格。它最早由 Roy Fielding 在其博士论文《Architectural Styles and the Design of Network-based Software Architectures》中提出。您可以从 这里、这里 和 这里 找到关于创建 WCF REST 服务的教程。在这些教程中,我发现 Steve Michelotti 提出的《RESTful WCF Services with No svc file and No config》尤其有趣。这是创建 WCF REST 服务最简单的方法,也可能是我发现的任何类型服务的创建方法中最简单的。本文不侧重于 WCF REST 服务的创建,而是介绍一种使用 jQuery AJAX 调用在 Web 浏览器端消费 WCF REST 服务的方法,并附带示例。
附带的 Visual Studio 解决方案包含两个项目
- “StudentService”项目是一个 WCF REST 服务
- “StudentServiceTestWeb”是一个简单的 “ASP.NET MVC” 项目,我将在其中演示如何使用 Web 浏览器中的 “jQuery” “AJAX” 调用来消费 “StudentService” 项目公开的服务。
此 Visual Studio 解决方案使用 Visual Studio 2010 开发,jQuery 版本为 “1.4.4”。在本文中,我将首先介绍 WCF REST 服务项目 “StudentService”,然后介绍 Web 项目 “StudentServiceTestWeb”。之后,我将讨论使用 Web 浏览器中的 “jQuery” “AJAX” 调用消费 WCF REST 服务时的一些 “跨域” 问题。
WCF REST 服务项目 “StudentService”
WCF REST 服务实现在一个简单的 “ASP.NET” Web 应用程序中。用于开发示例 WCF REST 服务的方法与 Steve Michelotti 介绍的方法完全相同。如果您查看 他的博客,将有助于您更轻松地理解本示例。
在深入研究 WCF REST 服务之前,让我们先看看服务使用的数据模型,该模型实现在 “Models\ModelClasses.cs” 文件中
using System;
using System.Collections.Generic;
namespace StudentService.Models
{
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public Nullable<int> Score { get; set; }
public string State { get; set; }
}
public class Department
{
public string SchoolName { get; set; }
public string DepartmentName { get; set; }
public List<Student> Students { get; set; }
}
}
实现了两个相关的类 “Student
” 和 “Department
”,其中 “Department
” 类持有一个 “Student
” 对象 “List
” 的引用。以这两个类作为数据模型,WCF REST 服务实现在 “Service.cs” 文件中
using System.Web;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using StudentService.Models;
using System.ServiceModel;
using System.Collections.Generic;
using System;
namespace StudentService
{
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode
= AspNetCompatibilityRequirementsMode.Allowed)]
public class Service
{
[OperationContract]
[WebGet(UriTemplate = "Service/GetDepartment")]
public Department GetDepartment()
{
Department department = new Department
{
SchoolName = "School of Some Engineering",
DepartmentName = "Department of Cool Rest WCF",
Students = new List<Student>()
};
return department;
}
[OperationContract]
[WebGet(UriTemplate = "Service/GetAStudent({id})")]
public Student GetAStudent(string id)
{
Random rd = new Random();
Student aStudent = new Student
{
ID = System.Convert.ToInt16(id),
Name = "Name No. " + id,
Score = Convert.ToInt16(60 + rd.NextDouble() * 40),
State = "GA"
};
return aStudent;
}
[OperationContract]
[WebInvoke(Method ="POST",
UriTemplate = "Service/AddStudentToDepartment",
BodyStyle = WebMessageBodyStyle.WrappedRequest)]
public Department
AddStudentToDepartment(Department department, Student student)
{
List<Student> Students = department.Students;
Students.Add(student);
return department;
}
}
}
在此服务中实现了三个 “OperationContract
”
- “
GetDepartment
” 接受一个 “GET” 方法。它不接受任何输入参数,并返回一个 “Department
” 对象。此 “Department
” 对象具有一个空的 “Students
” “List
”。 - “
GetAStudent
” 接受一个 “GET” 方法。它接受一个输入参数 “id
”。它创建一个具有相同 “id
” 的 “Student
” 对象,并将此对象返回给调用者。 - “
AddStudentToDepartment
” 接受一个 “POST” 方法;它接受两个输入参数:“Department
” 对象和 “Student
” 对象。然后,它将 “Student
” 添加到 “Department
” 的 “Students
” “List
” 中,并将 “Department
” 对象返回给调用者。
为了使此 WCF REST 服务正常工作,我们需要修改 “Global.asax.cs” 文件和 “Web.config” 文件。我们需要将以下代码添加到 “Global.asax.cs” 中的 “Application_Start
” 事件中
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new ServiceRoute("",
new WebServiceHostFactory(),
typeof(Service)));
}
我们还需要将以下配置添加到 “Web.config” 文件中
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
<standardEndpoints>
<webHttpEndpoint>
<standardEndpoint name="" helpEnabled="true"
automaticFormatSelectionEnabled="true" />
</webHttpEndpoint>
</standardEndpoints>
</system.serviceModel>
对于熟悉 WCF 服务开发的各位来说,可能会对开发和配置 WCF REST 服务的简洁性感到惊讶。是的,没有 “svc” 文件,也没有 “Endpoint” 配置,WCF REST 服务就已完成并正常工作。下图显示了此 WCF REST 服务的“帮助页面”
“Description
” 字段包含访问开发环境中每个 OperationContract
方法的 “URL”,该 URL 在调试模式下有效。
MVC Web 项目 “StudentServiceTestWeb”
为了演示如何使用 “jQuery” “AJAX” 调用消费 WCF REST 服务,我们开发了一个 “ASP.NET MVC” 应用程序。
此 Web 应用程序是最简单的 “MVC” 应用程序。它在 “Controllers\HomeController.cs” 文件中只有一个 “controller”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace StudentServiceTestWeb.Controllers
{
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
“HomeController
” 的唯一目的是加载 Web 应用程序的唯一 “View” 页面 “Index.aspx”
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Consume WCF Rest Service with jQuery</title>
<link rel="stylesheet"
href='<%= Url.Content("~/Content/Styles/Site.css") %>'
type="text/css" />
</head>
<body>
<div id="divContainer">
<div id="divDepartmentHeader"></div>
<div id="divStudentList"></div>
<div id="divNewStudent">
<span>Pending new student: - Click "Add student to department" button</span>
<br />
<span id="spanStudentList"></span>
</div>
<div id="divButtonContainer" style="float: right; margin: 2px">
<button id="btnAquireNewStudent">Aquire a new student</button>
<button id="btnAddStudentToDepartment">
Add student to department</button>
</div>
</div>
</body>
</html>
<script src='<%= Url.Content("~/Content/Scripts/jquery-1.4.4.min.js") %>'
type="text/javascript">
</script>
<script type="text/javascript">
// Urls to access the WCF Rest service methods
var GetDepartmentURl = "https://:4305/Service/GetDepartment";
var GetAStudent = "https://:4305/Service/GetAStudent";
var AddStudentToDepartment = "https://:4305/Service/AddStudentToDepartment";
var nextStudentID = 1;
var jDepartment = null;
var jPendingStudent = null;
// Utility function - Create HTML table on Json data
function makeTable(jObject) {
var jArrayObject = jObject
if (jObject.constructor != Array) {
jArrayObject = new Array();
jArrayObject[0] = jObject;
}
var table = document.createElement("table");
table.setAttribute('cellpadding', '4px');
table.setAttribute('rules', 'all');
var tboby = document.createElement("tbody");
var trh = document.createElement('tr');
for (var key in jArrayObject[0]) {
var th = document.createElement('th');
th.appendChild(document.createTextNode(key));
trh.appendChild(th);
}
tboby.appendChild(trh);
$.each(jArrayObject, function (i, v) {
var tr = document.createElement('tr');
for (var key in v) {
var td = document.createElement('td');
td.appendChild(document.createTextNode(v[key]));
tr.appendChild(td);
}
tboby.appendChild(tr);
});
table.appendChild(tboby)
return table;
}
$(document).ready(function () {
$.ajax({
cache: false,
type: "GET",
async: false,
dataType: "json",
url: GetDepartmentURl,
success: function (department) {
jDepartment = department;
var div = document.createElement("div");
var span = document.createElement("span");
span.setAttribute('id', 'DepartmentHeader');
span.appendChild(document.createTextNode(department.SchoolName));
span.appendChild(document.createTextNode(" - "));
span.appendChild(document.createTextNode(department.DepartmentName));
div.appendChild(span);
span = document.createElement("span");
div.appendChild(document.createElement("br"));
span.appendChild(document.createTextNode("List of students:"));
div.appendChild(span);
$("#divDepartmentHeader").html("").append(div);
},
error: function (xhr) {
alert(xhr.responseText);
}
});
$("#btnAquireNewStudent").click(function () {
if (jPendingStudent != null) {
alert("Please add the pending student to the department first");
return;
}
$.ajax({
cache: false,
type: "GET",
async: false,
url: GetAStudent + "(" + nextStudentID + ")",
dataType: "json",
success: function (student) {
jPendingStudent = student;
nextStudentID++;
var table = makeTable(student);
$("#spanStudentList").html("").append(table);
$("#divNewStudent").css("visibility", "visible");
$("#divNewStudent").show();
},
error: function (xhr) {
alert(xhr.responseText);
}
});
});
$("#btnAddStudentToDepartment").click(function () {
if (jDepartment == null) {
alert("Please wait until the department loads");
return;
}
if (jPendingStudent == null) {
alert("Please first get a new student to add to the department");
return;
}
var jData = {};
jData.department = jDepartment;
jData.student = jPendingStudent;
$.ajax({
cache: false,
type: "POST",
async: false,
url: AddStudentToDepartment,
data: JSON.stringify(jData),
contentType: "application/json",
dataType: "json",
success: function (department) {
jPendingStudent = null;
jDepartment = department;
var table = makeTable(department.Students);
$("#divStudentList").html("").append(table);
$("#divNewStudent").hide();
},
error: function (xhr) {
alert(xhr.responseText);
}
});
});
});
</script>
访问 WCF REST 服务所需的所有客户端 “JavaScript” 代码,使用 “jQuery” “AJAX” 调用,都实现在 “Index.aspx” 页面中。在进入 “JavaScript” 部分之前,让我们先看看 HTML 组件
- 在 “
<body>
” 标签中有几个 “<div>
” 标签。这些 “<div>
” 标签将用作 “JavaScript” 代码的占位符,用于构建应用程序的用户界面组件。 - 一个 “button” 控件 “
btnAquireNewStudent
”。点击此按钮将向OperationContract
的 “GetAStudent
” 发出 “AJAX” 调用。 - 一个 “button” 控件 “
btnAddStudentToDepartment
”。点击此按钮将向OperationContract
的 “AddStudentToDepartment
” 发出 “AJAX” 调用。
“Index.aspx” 页面中的 “JavaScript” 代码非常简单
- 在 “
$(document).ready()
” 事件中,会向 WCF REST 服务中的 “GetDepartment
” 方法发出一个 “AJAX” 调用。收到服务器响应后,UI 将通过 “Department
” 对象 “JSON” 表示形式的信息进行更新。此 “JSON” 对象然后被分配给全局变量 “jDepartment
”。 - 在 “
btnAquireNewStudent
” 的点击事件中,会向 “GetAStudent
” 方法发出一个 “AJAX” 调用。全局变量 “nextStudentID
” 被用作输入参数。如果调用成功,则使用 “makeTable
” 函数使用接收到的 “JSON” 对象创建一个 HTML “table”,UI 会更新以显示新接收的学生。此 “JSON” 对象然后被分配给全局变量 “jPendingStudent
”,并且 “nextStudentID
” 全局变量会被调整,以便当用户再次点击此 “button” 时发送一个不同的 “id”。 - 在 “
btnAddStudentToDepartment
” 的点击事件中,会向 “AddStudentToDepartment
” 方法发出一个 “AJAX” 调用。此调用使用 “POST” 方法。发送到 “AddStudentToDepartment
” 的数据是一个 “JSON” 字符串,该字符串是通过将 “jDepartment
” 和 “jPendingStudent
” 的 “JSON” 对象包装而生成的。如果调用成功,将调用 “makeTable
” 函数为 “Student
” 对象 “List
” 生成一个 HTML “table”,并相应地更新 UI。
现在我们已经完成了这个“简单”的“StudentServiceTestWeb”项目。但在开发环境中进行测试运行之前,我们还有另一个问题需要讨论。
“跨域”问题
“Cross-domain” 或 “Cross-site” HTTP 请求是针对与发起请求资源域不同的域的资源的 HTTP 请求。总的来说,Web 浏览器的默认行为是阻止 “cross-domain” 访问,如果它们认为这些访问类型是 “不安全的”。从 Web 浏览器的角度来看,域由 “URL” 识别。例如,当浏览器从 “http://domainb.foo:8080/image.jpg” 加载图像时,图像文件的域由 “http://domainb.foo:8080” 标识,其中包括 “port number”。
在本例中,Visual Studio 为 WCF REST 服务分配了端口号 “4305”,为 Web 应用程序的调试服务器分配了端口号 “5187” 来运行应用程序。当在调试模式下从 Web 应用程序向 WCF REST 服务发出 “AJAX” 调用时,Web 浏览器会将其视为 “cross-domain”,因为 Web 应用程序和该服务具有不同的 “port numbers”。为了解决这个问题,我们需要 WCF REST 服务明确告知 Web 浏览器允许 “cross-domain” 访问。这可以在 “StudentService” 项目的 “Global.asax.cs” 文件中完成
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using System.ServiceModel.Activation;
using System.Web.Routing;
namespace StudentService
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.Add(new ServiceRoute("",
new WebServiceHostFactory(), typeof(Service)));
}
protected void Application_BeginRequest(object sender, EventArgs e)
{
HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
HttpContext.Current.Response.Cache.SetNoStore();
EnableCrossDmainAjaxCall();
}
private void EnableCrossDmainAjaxCall()
{
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin",
"https://:5187");
if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
{
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods",
"GET, POST");
HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers",
"Content-Type, Accept");
HttpContext.Current.Response.AddHeader("Access-Control-Max-Age",
"1728000");
HttpContext.Current.Response.End();
}
}
}
}
在 “Application_BeginRequest
” 事件中,会调用 “EnableCrossDmainAjaxCall
” 函数。此函数将所需的 “HTTP access control” 标头发送给浏览器。在流行的浏览器如 “Firefox”、“Chrome”、“Safari” 和 “IE” 中,每种浏览器可能行为略有不同,但总的来说,都遵循类似的模式。
- 为了使 “jQuery” “AJAX” 调用成功,浏览器必须收到响应标头 “Access-Control-Allow-Origin”,并且此标头的值必须标识调用 Web 应用程序的域。如果您希望所有域中的所有 Web 页面都能访问您的服务,可以将值设置为 “*”。
- 如果浏览器认为您的 “AJAX” 调用是 “不安全的”,它们会在实际发出 “AJAX” 调用之前向服务器发送一个 “preflighted” 请求。在本例中,对 “
OperationContract
” “AddStudentToDepartment
” 的调用使用了 “POST” 方法,并且 “content type” 为 “application/json”,这被大多数浏览器视为 “不安全”。“Preflighted” 请求使用 “OPTIONS
” 方法。如果服务器收到 “OPTIONS
” 请求,它需要使用 “Access-Control-Allow-Methods” 来告知浏览器允许的 HTTP 方法用于 “AJAX” 调用。它还需要使用 “Access-Control-Allow-Headers” 来告知浏览器允许在实际 “AJAX” 调用中使用的 HTTP 标头。在本例中,WCF REST 服务允许浏览器使用 “POST” 和 “GET” 方法,并允许在 “AJAX” 调用中使用 “Content-Type” 和 “Accept” 标头。 - “Access-Control-Max-Age” 的值告诉浏览器 “preflighted” 结果的有效期,以便当浏览器再次发出相同的 “AJAX” 调用时,它们不需要在 “preflighted” 结果过期前发送 “
OPTIONS
” 请求。
不同浏览器在 “HTTP access control” 方面的行为可能略有不同。但我已经用 “Firefox”、“Chrome”、“Safari” 和 “IE” 测试了 “EnableCrossDmainAjaxCall
” 函数。它们都对 “EnableCrossDmainAjaxCall
” 函数发送的标头响应良好。
运行应用程序
要运行此应用程序,您需要确保 WCF REST 服务和 Web 应用程序都已启动。如果您将 “StudentServiceTestWeb” 项目设置为 “启动” 项目,当您调试运行时,Visual Studio 应该会同时启动服务和 Web 应用程序。当 Web 页面首次加载时,会调用 “OperationContract
” “GetDepartment
”,学校名称和部门名称会从服务加载,如 Web 浏览器所示
点击 “Acquire a new student” 按钮;会调用 “OperationContract
” “GetAStudent
”,并且新 “Student” 的信息会显示在 Web 浏览器中
点击 “Add student to department” 按钮;会调用 “OperationContract
” “AddStudentToDepartment
”,并且 UI 会更新以显示 “Department” 中的 “Student
” 对象 “List
”。下图显示了向 “Department
” 添加多个 “Student
” 对象后的结果
关注点
- 本文介绍了一种使用 jQuery AJAX 调用消费跨域 WCF REST 服务的方法,并附带示例。
- 利用 Steve Michelotti 博客中的方法,可以轻松实现 WCF REST 服务。
- 要调用位于与您的 Web 应用程序不同域的 WCF REST 服务,您需要注意 “HTTP access control” 问题。我已经测试了本文介绍的方法,它在所有流行的浏览器上都能正常工作。
- 在我开发本文使用的 “
EnableCrossDmainAjaxCall
” 函数时,我花了很大精力来“允许” “cross-domain” “AJAX” 调用。通过研究 “HTTP access control” 链接中的资料,创建一种机制来“允许”所需的 “cross-domain” 访问并“阻止”所有其他访问,以提高安全性,应该不难。 - 除了使用本文介绍的方法外,还有一些关于使用 “JsonP” 解决 “cross-domain” 问题的讨论。如果您感兴趣,可以参考一下。
- 我在自己测试时注意到一个有趣的现象是,当 WCF REST 服务托管在 Visual Studio 调试服务器中时,Web 应用程序在 IE 中运行速度非常快,而在其他浏览器中则明显较慢。然后我将 Web 应用程序和服务都部署到真正的 IIS 服务器上。当它们由真正的 IIS 服务器托管时,浏览器之间的速度差异就消失了,至少是人眼无法察觉的速度差异。
- 希望您喜欢我的帖子,并希望它们能以某种方式帮助到您。
历史
这是本文的第一个修订版。