随着移动互联网和Web开发技术的发展,在项目中需要为越来越多的跨平台应用提供统一化的API接口。那么作为一个后端开发者,如何设计并开发出更规范、更清晰、更好用的WebAPI呢?
1.如何理解API?
宽泛的讲,API(Application Programming Interface)指的是应用程序编程接口,用来提供预先定义好的方法和函数,使用者无需去关心其内部细节就能实现相应的功能。而现在我们提到API,脑海中首先想到的往往是基于HTTP协议传输的WebService技术。
从概念上讲,API与开发语言无关,理论上具备网络编程功能的编程语言,如Java、C#、PHP、Node等,都能够通过相应HTTP请求并构造HTTP包来实现Web API,虽然它们内部的实现原理不同,但确确实实的提供了API服务。
对于.NET平台的开发者来讲,使用C#编程语言有WCF、WebService、ASP.NET WebAPI等多种技术方案。由于ASP.NET WebAPI简单轻量的特点,使其受到越来越多人的欢迎。
2.什么是好的WebAPI?
从我个人的角度理解,暂且不论是什么编程语言,WebAPI最终是要提供给第三方使用的,可能是你的同事或者合作伙伴,所以后端开发人员提供的WebAPI可以看作是个人或者团队的产品输出。从产品的特质来看,WebAPI首先要能满足使用要求,其次要具备标准化、稳定性等特征。如果能在这些特征基础上为使用者提供更多额外的功能,那才是好的WebAPI。
举个通俗的例子,我们在生活中都会用到插线板,不管哪个品牌的插线板都遵循同样的标准。如果在中国大陆提供不了220V标准电压,废品;如果电器插座插不进插孔,废品;如果使用一小时寿命就终结,废品…只有在提供安全、稳定、标准供电的基础上再去考虑彩色设计、卡通造型、防水保护、耗能统计这些附加功能,才能说是一个好的插线板。
同样现在很多WebAPI只是解决了能用的问题,但是否好用还要细细思量。来看一个我们之前的WebAPI示例。
Method:GET
URL:api/TestResults/GetView_ResultList?pagesize={pagesize}¤tpage={currentpage}&where={where}&orderby={orderby}
Request:
Response:{ “sample string 1”: “sample string 2”,“sample string 3”: “sample string 4”}
很多前端开发人员看到这个API接口,心里肯定会想WTF?但遗憾的是,当我们刚开始接触WebAPI时,大量的接口都是这种形式的,现在回头来看真的是很糟糕。没有文档、URL复杂、没有返回状态,除了开发者本人没人知道这个接口要干嘛。幸运的是随着对WebAPI的深入了解,我们开始认识到设计一套清晰易用的WebAPI是多么重用,并开始将WebAPI的设计向一些成熟的规范和约定靠拢,其中之一就RESTful风格的API设计。
3.RESTful风格的API设计
REST是“REpresentational State Transfer”的缩写,可以翻译成“表现状态转换”,理解起来比较抽象。REST是一种架构风格,并不是一个技术框架也非强制规范,而是通过对资源的定位、操作藉由HTTP协议提供服务,其中资源可以是数据、图片、流程等具体的或抽象的对象。简单来说就是通过定义URI和HTTP状态码,让API更加简洁清晰、易懂易记、富有层次。通过RESTful风格的API可以解决我们在WebAPI开发过程中遇到的一些问题。
3.1.如何让 URI 清晰易懂?
好的代码无需注释,代码即注释。对于API也一样,好的URI设计无需文档就能理解用途。在API接口的URL中应仅使用名词而不是动词。此外所有名词小写,同时注意复数与单数的使用差异。
/orders 所有订单
/orders/12 具有指定标识的订单
/orders?sort=asc 按照升序排列订单
3.2.如何最大程度地利用 HTTP 协议?
合理利用HTTP协议本身的方法GET、POST、PUT、DELETE。如果说上面的URI对资源进行定位,那么HTTP方法就是对资源进行操作,几个典型的用法有:
HTTP方法 | 用途 | 示例 |
---|---|---|
GET | 获取数据集合 | /orders |
GET | 获取指定数据 | /orders/{id} |
POST | 新建数据 | /orders |
PUT | 完整更新数据 | /orders/{id} |
PATCH | 部分更新数据 | /orders/{id} |
DELETE | 删除数据 | /orders/{id} |
3.3.如何选择数据格式?
使用JSON或XML传输数据,其中JSON阅读性更高,且更易扩展,能够使用多种环境和编程语言。
{
status:200,
data:[
{"id":1,"name":"订单1"},
{"id":2,"name":"订单2"},
{"id":3,"name":"订单3"}
],
msg:""
}
3.4.如何通过状态码表示出错信息?
充分使用HTTP状态码。我们知道HTTP协议是Request-Response模式的,针对各种情况的HTTP请求,服务器应该反馈恰当的状态码。如果我们把错误信息放在状态码为200的反馈中,势必会造成混乱。所以完全可以利用状态码将HTTP自身的错误和服务器内部的错误区分开。
状态码 | 含义 | 用途 |
---|---|---|
200 | OK | 表示请求成功,例如GET请求或更新、删除资源成功。 |
201 | Created | 表示POST创建资源成功。 |
204 | No Content | 表示请求已处理成功,但未返回任何内容。 |
400 | Bad Request | 表示服务器无法理解客户端的请求内容。 |
401 | Unauthorized | 表示不允许客户端访问资源,需要客户端自查传入的参数是否满足条件。 |
403 | Forbidden | 表示请求有效且客户端通过身份认证,但不允许客户端访问资源。 |
404 | Not Found | 表示请求的资源不存在。 |
415 | Unsupported Media Type | 表示服务器媒体类型Content-Type或Accept出现错误。 |
500 | Internal Server Error | 表示服务器内部错误 |
503 | Service Unavailable | 表示服务器已关闭或无法接受、处理请求。 |
3.5.如何规划 API 的版本?
在URI中加入版本信息,可以通过仅仅修改版本号就实现API服务的迁移,而无需修改所有API接口的地址。
/v1/orders
/v2/orders
3.6.如何通过查询参数实现搜索分页?
/v1/orders?sort=date 按照条件排序
/v1/orders?page=4 按照页码分页
/v1/orders?filter=xxx 按照条件过滤
3.7.如何定义返回数据结构?
用结构化的JSON数据填充HTTP Response的Body。参考JSON API规范如何传递数据的格式。在顶级结点使用data、errors、meta来描述数据、错误信息和元信息,其中data和errors不能在同一个返回数据中出现,meta一般情况下可以不用。
示例
{
"meta": {
"version":"2.1",
"copyright": "Copyright 2015 Example Corp.",
"authors": [
"Yehuda Katz",
"Steve Klabnik"
]
},
"data": {
// 数据主体
}
}
data
{
"data": {
"type": "orders",
"id": "1",
"attributes": {
// 资源的具体数据
},
"relationships": {
// 资源的可选属性,如关联数据、资源地址等
}
}
}
其中type描述数据的类型,可以对应为数据模型的类名;id为资源的唯一标识。
errors
{
"errors":[
{
"code":10001,
"title":"orderNo is null"
},
{
"code":10002,
"title":"the result can't be null",
"detail":""
}
]
}
其中code为具体的错误码,title为错误信息,在开发测试环境下可以添加detail字段用来放置更详细的错误信息。
4.如何在ASP.NET WebAPI实现RESTful风格?
4.1.基本配置
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 配置和服务
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
4.2.API控制器代码
public class ClientController : ApiController
{
private DBEntities db = new DBEntities();
/// <summary>
/// 获取全部资源
/// </summary>
/// <returns></returns>
public IQueryable<SYS_Client> GetClients()
{
return db.SYS_Client;
}
/// <summary>
/// 获取指定标识的资源
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[ResponseType(typeof(SYS_Client))]
public async Task<IHttpActionResult> GetClientById(string id)
{
SYS_Client client = await db.SYS_Client.FindAsync(id);
if (client == null)
{
return NotFound();
}
return Ok(client);
}
/// <summary>
/// 更新资源
/// </summary>
/// <param name="id"></param>
/// <param name="client"></param>
/// <returns></returns>
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutSYS_Client(string id, SYS_Client client)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != client.RowGuid)
{
return BadRequest();
}
db.Entry(client).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!IsExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
/// <summary>
/// 新增资源
/// </summary>
/// <param name="client"></param>
/// <returns></returns>
[ResponseType(typeof(SYS_Client))]
public async Task<IHttpActionResult> PostSYS_Client(SYS_Client client)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.SYS_Client.Add(client);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (IsExists(client.RowGuid))
{
return Conflict();
}
else
{
throw;
}
}
return CreatedAtRoute("DefaultApi", new { id = client.RowGuid }, client);
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[ResponseType(typeof(SYS_Client))]
public async Task<IHttpActionResult> DeleteSYS_Client(string id)
{
SYS_Client client = await db.SYS_Client.FindAsync(id);
if (client == null)
{
return NotFound();
}
db.SYS_Client.Remove(client);
await db.SaveChangesAsync();
return Ok(client);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool IsExists(string id)
{
return db.SYS_Client.Count(e => e.RowGuid == id) > 0;
}
}
4.3.API清单
以上内容只是抛砖引玉,如果想要设计和开发更好用的WebAPI,无疑要经过更多的改进和优化。只有不断进步,才能输出更好的产品。