在前面的文章JavaScript调用WebService请求 中,我们演示了如何使用JavaScript来调用ASP.NET的WebService服务,这次我们看一看使用使用JavaScript来调用WebAPI。至于WebAPI和WebService的优缺点以及区别,不是本文的重点,读者可以自行查询。
我这里使用的环境是Windows 7 sp1,开发环境是Visual Studio 2017。VS2017有一样好处就是,你项目使用的.Net Framework的版本如果高于你部署环境的版本,VS2017会把所有依赖的程序集打包在一起。如下图所示,在VS2017中创建一个ASP.NET Web应用程序。框架选4.5,因为随后要解决跨域问题的时候,需要引用4.5的程序包。
然后在弹出的“新建ASP.NET Web应用程序”窗口中,选择Web API 并确定,如下图所示:
稍等片刻,VS2017自动为你创建一系列文件夹和程序,如下图所示:
在解决方案中,右键“Models”文件夹,添加一个空的类,然后你就可以改造这个空类。这里重点提醒一下,如果你这个空类是跟你数据库中某个表一致的,那你这个空类的属性名最好跟数据库中的字段名保持一致,这样后面可以省去很多麻烦。如果名称一致,可以让系统自动把数据库中查询到的数据集转换成你的自定义类。下面贴一下,我的这个改造后的空类,也就是自定义类(Device.cs):
public class Device
{
private string _deviceCode;
///
/// 设备编码
///
public string DeviceCode
{
get { return _deviceCode; }
set { _deviceCode = value; }
}
private int _devicePower;
///
/// 设备是否低压
///
public int Power
{
get { return _devicePower; }
set { _devicePower = value; }
}
private int _deviceOnline;
///
/// 设备是否在线
///
public int Online
{
get { return _deviceOnline; }
set { _deviceOnline = value; }
}
private int _deviceOpen;
///
/// 设备是否打开
///
public int Open
{
get { return _deviceOpen; }
set { _deviceOpen = value; }
}
private float _deviceDegree;
///
/// 设备倾斜角度
///
public float Degree
{
get { return _deviceDegree; }
set { _deviceDegree = value; }
}
private double _lat;
///
/// 设备纬度
///
public double Latitude
{
get { return _lat; }
set { _lat = value; }
}
private double _log;
///
/// 设备经度
///
public double Longitude
{
get { return _log; }
set { _log = value; }
}
private DateTime _updateTime;
///
/// 设备数据更新时间
///
public DateTime UpdateTime
{
get { return _updateTime; }
set { _updateTime = value; }
}
public Device()
{
_deviceCode = string.Empty;
_deviceOpen = -1;
_devicePower = -1;
_deviceOnline = -1;
_deviceDegree = -1;
_lat = 0;
_log = 0;
_updateTime = DateTime.Parse("1990/1/1 00:00:00");
}
}
再提醒一下,自定义类的属性名,最好跟数据库中的字段名保持一致,顺序也一致!这样这个Model类就创建好了。
Controller类是用来操作Model类,并且向外提供访问接口的。我们依然在解决方案下面的“Controller”文件夹右键单击,选择添加,然后点击“控制器(Controllers)”,在弹出的“添加基架”的窗口中选择“Web API 2 控制器 - 空”,然后点击确定,如下图所示:
然后在随后弹出的命名窗口中,这里的名字不能随意,因为刚才创建的Model类的名字是“Device”,那么这里的Controller类的名字就应该是“DeviceController”,一定要跟Model类名称对应起来,不然系统映射不过去。创建好的“DeviceController.cs”也是一个空类,需要我们改造一下用来控制Model类。如下代码:
[RoutePrefix("apis/device")]
public class DeviceController : ApiController
{
[Route("getall")]
[HttpGet, HttpPost]
public IEnumerable GetAllDevice()
{
DataTable dt = MySqlHelper.getRows("SELECT * FROM smartcover.smc ");
List devices = JsonHelper.ToList(dt);
return devices;
}
[Route("getbycode/{code}")]
[HttpGet]
public Device GetDeviceByCode(string code)
{
string sql = "SELECT * FROM smartcover.smc where deviceCode = '" + code + "'";
DataTable dt = MySqlHelper.getRows(sql);
List results = JsonHelper.ToList(dt);
var product = results.FirstOrDefault((p) => p.DeviceCode == code);
if (product == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return product;
}
[Route("getbytime/{datetime}")]
[HttpGet, HttpPost]
public IEnumerable GetDeviceOneDay(string datetime)
{
if (datetime == null || datetime == "") return null;
DateTime result;
bool isRight = DateTime.TryParse(datetime, out result);
if (!isRight) return null;
string s = MySqlHelper.getDayStart(datetime);
string e = MySqlHelper.getDayEnd(datetime);
string sql = "SELECT * FROM smartcover.smc where updatetime between '" + s + "' and '" + e + "'";
DataTable dt = MySqlHelper.getRows(sql);
List results = JsonHelper.ToList(dt);
return results;
}
}
上面这段代码就是“DeviceController”,可以看到,在“DeviceController”类的上面有一行代码修饰:
[RoutePrefix("apis/device")]
这里的意思是,我自定义的路由,“DeviceController”里面的接口都是后面发布成功后的URL:“http://localhost/WebTest/apis/device/XXX”进行访问,而“XXX”则是每一个接口的名称。这里篇幅所限,我就说一下第一个接口“GetAllDevice()”,在这个接口上面有一个[Route("getall")]进行修饰,
[Route("getall")]
那么这个[Route("getall")]的意思是,我的WebAPI发布成功后,则可以通过URL:“http://localhost/WebTest/apis/device/getall”进行访问。接着第二个修饰“[HttpGet, HttpPost]”,这表示,我们这个“getall”接口允许“GET、POST”两种方式进行调用。大家注意我这个“GetAllDevice”的返回值,是一个自定义类的列表List
另外需要注意的是“GetAllDevice”这个接口里面有这么一行代码:
List devices = JsonHelper.ToList(dt);
这句代码就是把从数据库查询得到的DataTable转换为自定义类列表。重点是“ToList”这个函数,下面是这个函数的代码:
public static List ToList(this DataTable dt)
{
var columnNames = dt.Columns.Cast()
.Select(c => c.ColumnName.ToUpper())
.ToList();
var properties = typeof(T).GetProperties();
return dt.AsEnumerable().Select(row =>
{
var objT = Activator.CreateInstance();
string value = "";
foreach (var pro in properties)
{
if (columnNames.Contains(pro.Name.ToUpper()))
{
value = row[pro.Name].ToString();
if (!string.IsNullOrEmpty(value))
{
if (Nullable.GetUnderlyingType(pro.PropertyType) != null) //存在匹配的表头
{
value = row[pro.Name].ToString().Replace("$", "").Replace(",", ""); //从dataRow中提取数据
pro.SetValue(objT, Convert.ChangeType(value, Type.GetType(Nullable.GetUnderlyingType(pro.PropertyType).ToString())), null); //赋值操作
}
else
{
value = row[pro.Name].ToString().Replace("%", ""); //存在匹配的表头
pro.SetValue(objT, Convert.ChangeType(value, Type.GetType(pro.PropertyType.ToString())), null);//赋值操作
}
}
}
}
return objT;
}).ToList();
}
这个函数,首先把数据库查询到的Datatable的列名取到数组中,然后再把自定义类的属性名也放到一个数组中。遍历自定义类中的属性名,判断每一个自定义类的中的属性名是否跟当前DataTable的列名一致,一致则进行赋值。所以前面讲到,创建Model类的时候一定要注意跟数据库保持一致,并且顺序也要一致,最好是使用工具直接生成数据库对应的类。
通过上述这些步骤,我们的第一个WebAPI也就创建成功了。
在第一部分内容中,我们创建的是WebAPI应用程序,但是如果你选择的是“空”,VS2017不会为你生成浏览内容,所以我这里建议还是选择WebAPI应用程序,如下图
假设你上图中选择的是Web API 则你可以在VS2017中直接启动调试了(可以暂时不用部署在IIS上)。直接用VS2017默认的IIS Express进行调试:
启动后则会打开你的浏览器并弹出如下界面(注意,如果你选择的是空的应用程序,则不会下面的内容):
这个界面就是VS2017自动为你生成的,在上图红框中,你可以找到你自定义的接口:
上面的过程是发布一个自定义接口的方法,如此类推,你可以接着加入其它的Model和Controller类,来丰富你的工程项目。
在解决方案下面,右键你的应用程序,我这里是“WebTest”,不是右键解决方案,找到“发布”菜单,如下图:
点击“发布”菜单后,首先需要选择发布的目标,这里我们选择发布到IIS,然后需要配置一下发布的选项,如下图:
然后我们点击右下角的“创建配置文件”,然后在“连接”选项里面的“发布方法”选择“文件系统”,并在“目标位置”选择一个本地的文件夹(随便位置,其实就是发布包的位置),如下图:
再接着,我们在“设置”选项中,配置Release发布选项,并勾选前两项:
最后,保存启动之后会在“目标位置”生成发布文件,这个发布文件你可以部署到其他机器的IIS上,也可以部署到本机的IIS上。
部署到IIS这里就不多讲了,发布后的文件,你可以拷贝到电脑任何位置上,然后你可以使用默认的IIS应用程序池装载你发布的WebAPI,也开始重新创建一个应用程序池装载你发布的WebAPI。我一般喜欢创建一个新的应用程序池,然后把上面的发布文件,部署到这个新的应用程序池下面。端口记得跟默认的端口区别开来就行。
JavaScript调用WebAPI其实跟调用WebService差别不是很大,可以用dojo的xhr方式,也可以用jQuery的$.ajax方式,这里我就用dojo的xhr方式。jQuery的$.ajax方式读者可以自行测试。下面是代码:
require([
"dojo/_base/xhr"
function(xhr)
{
xhr.post({
//请求自建的WebAPI
url:"http://localhost:8088/WebTest/apis/device/getall",
headers: { "Content-Type": "application/json; charset=utf-8" },
crossdomin:true,
handleAs : "json",
load : function(data)
{
//dosomting
data.forEach(function (json,index)
{
console.log(json);
});
console.log("success");
},
error:function (error)
{
console.log("error"+error);
}
});
}
);
细心地读者可以看到,我在ASP.NET WebAPI的Model“DeviceController”中,GetAllDevice()这个函数返回的是一个自定义类列表,通过JavaScript调用,返回的也是一个列表,并且可以通过转换成一个JSON对象,相比WebService确实方便了很多。
想了想还是贴一下用jQuery的$.ajax方式,解读就不细说了,读者自行调试一下看看区别。
$.ajax({
type: "POST",
url: "http://localhost:8088/WebTest/apis/device/getall",
contentType: "application/json; charset=utf-8",
dataType: "json"
}).done(function(res)
{
//dosomting
res.forEach(function (json,index)
{
console.log(json);
});
console.log("success");
}).fail(function(a, b, c, d)
{
alert("Failure: "
+ JSON.stringify(a) + " "
+ JSON.stringify(b) + " "
+ JSON.stringify(c) + " "
+ JSON.stringify(d) );
});
这里的带参数调用,我说的是WebAPI中的参数是自定义参数,而不是常规的string、int、double等这一类的参数,下面是我自定义的一个接口,,这个接口的功能就是根据客户端传入的用户名和密码,服务器端验证用户名和密码是否正确。带的参数也是自定义的类:
[Route("checkout")]
[HttpGet, HttpPost]
public int UserCheck(User jsonUser)
{
if (jsonUser == null )
{
return -1;
}
else
{
try
{
string username = jsonUser.UserName;
string userpassword = jsonUser.UserPassword;
//查询用户
string userSql = "select * from smartcover.myusers where username='" + username + "'";
DataTable dt = MySqlHelper.getRows(userSql);
if (dt == null || dt.Rows.Count < 1)
{
return 2;//用户不存在
}
else
{
string encryptdPassword = dt.Rows[0]["userpassword"].ToString();
if (encryptdPassword == userpassword)
{
return 1;//用户验证成功
}
else if (userpassword == Decrypt(encryptdPassword))
{
return 0;//用户验证成功
}
else
{
return 3;//用户密码不正确
}
}
}
catch
{
return -1; //创建失败
}
}
}
上面代码是ASP.NET端的WebAPI代码,这个“UserCheck()”函数被命名为“checkout”接口,参数是一个自定义类User。下面我们看看JavaScript端怎么来调用这个接口的:
var userJson={
UserName:myusername,
UserPassword:myuserpassword
};
require(["dojo/_base/xhr"],
function(xhr)
{
xhr.post({
//请求自建的WebAPI
url:"http://localhost:8088/WebTest/apis/user/checkout",
//headers: { "Content-Type": "application/json" },
postData:userJson,
handleAs : "json",
load : function(data)
{
//dosomting
var jb=JSON.parse(data);
console.log(jb);
},
error:function (error)
{
console.log("error"+error);
}
});
}
);
上面的代码userJson,是根据用户输入的用户名和密码创建的一个JSON对象,注意,这里并没有把这个JSON对象转换成JSON字符串。这里多说几句,我这里用的请求全部是POST,读者也可以试试GET请求,对于没有参数的WebAPI两者没什么区别,而对于有参数的WebAPI,两者的区别就大了,就是get请求的数据会附在URL之后(就是把数据放置在HTTP协议头中),而post请求则是放在http协议包的包体中。如果是带的是自定义类参数,上面代码“headers: { "Content-Type": "application/json" }”这一行,就需要把它给注释掉,因为如果你加上这行代码,这个请求会把参数转换问JSON字符串,但是你的WebAPI中的参数是实体类,系统并不会自动帮你转换,就会出错。如果非要加上这行代码,第一需要把你的WebAPI参数改为String类型,在你的WebAPI中使用“JsonDeserialize”进行序列化,重新把你传过来的JSON字符串序列化成你的实体类。具体详情,请看JavaScript调用WebService请求 。这方面内容我参考了WebApi 接口参数:传参详解。
也就是说,如果你的WebAPI的参数是实体类,那么在JavaScript请求的时候,参数应该是对应的JSON对象。对于基础类型的参数读者也可以自行测试一下。
JavaScript调用WebService存在跨域问题,我在JavaScript调用WebService请求中介绍了一些解决办法,同样WebAPI中我也同样碰到了跨域问题,相对于WebService的跨域,WebAPI的跨域问题解决的更方便一些。不需要在IIS服务器的“HTTP响应标头”上添加一些跨域的属性。特别是使用VS2017后,使得问题解决的更方便。
通过NuGet插件,可以加载System.Web.Http.Cors文件,如下图所示:
如上图所示,在你的解决方案下面,右键“引用”,然后点击“管理NuGet程序包”,弹出如下窗体:
在搜索框中搜索“cors”,系统会搜索相关的程序包,找到“Microsoft.AspNet.WebApi.Cors”,然后下载安装,安装完毕后,会自动添加System.Web.Http.Cors引用,如下图所示:
添加好这个System.Web.Http.Cors引用后,就可以在解决方案的“App_Start”文件夹中找到“webApiConfig.cs”这个文件,然后双击进入这个文件的代码,在命名空间上加入“using System.Web.Http.Cors;”这句代码。如下图所示
然后在函数“Register()”里面加入如下代码:
//配置解决js跨域访问的问题
var cors = new EnableCorsAttribute(ConfigurationManager.AppSettings["origins"], "*", "*");
config.EnableCors(cors);
上面这两行代码把跨域属性注册的WebAPI中,而“ConfigurationManager.AppSettings["origins"]”这句代码,其实就是从“Web.congfig”文件中读取配置的值,这样我们部署的时候不用修改源码了。那么,在项目的“Web.congfig”文件中就要把跨域的属性值配置好,如下代码。这里假设你的WebAPI部署的是“localhost:8080”端口,而你的JavaScript代码是部署在“localhost:8088”这个端口上,虽然在同一台机器上,但是分属不同端口,这就跨域了。所以,在“Web.congfig”文件中我们要允许JavaScript项目部署的“localhost:8088”这个端口,能够跨域访问WebAPI部署的“localhost:8080”端口。
在上面“Web.congfig”文件中,你可以设置多个域值,中间用逗号分开。这时候,有细心的读者就问了,干嘛设置一个个的域值啊,我干脆设置成“*”多好啊。您还别说,我开始就是设置的“*”,可是一调试还是会报“blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.”这个错误。但是我单独设置成一个个URL,就可以解决了,你说这怪不怪。希望有人能帮我解答这个疑惑。
另外,CORS的浏览器支持情况,对IE8、9是部分支持的。网上说的解决方案都是Internet Explorer 8 、9使用 XDomainRequest 对象实现CORS。这样似乎有些复杂了,于是网上一顿搜索。最后发现在调用处指定 jQuery.support.cors = true; 这一句就能解决IE8、9的问题了。
调试运行,我这里是可以解决了,也希望能解决你的一部分问题,大家有什么疑惑的可以讨论讨论。