跟同事合作前後端分離項目,自己對 WebApi 的很多知識不夠全,雖說不必要學全棧,可是也要瞭解基礎知識,才能合理設計介面、API,方便與前端交接。 晚上回到宿舍後,對 WebApi 的知識查漏補缺,主要補充了 WebAPi 的一些方法、特性等如何與前端契合,如何利用工具測試 API 、Axios ...
跟同事合作前後端分離項目,自己對 WebApi 的很多知識不夠全,雖說不必要學全棧,可是也要瞭解基礎知識,才能合理設計介面、API,方便與前端交接。
晚上回到宿舍後,對 WebApi 的知識查漏補缺,主要補充了 WebAPi 的一些方法、特性等如何與前端契合,如何利用工具測試 API 、Axios 請求介面。
本文主要寫 WebApi 前端請求數據到 API 、後端返回處理結果,不涉及登錄、跨域請求、前端 UI 等。(難一點我不會了。。。看張隊的公眾號,篇篇都看不懂。。。)
前提:會一點點 VUE、會一點 Axios、會一點點 Asp.net Core。
工具:Visual Studio 2019(或者其它版本) + Visual Studio Code + Swagger +Postman
由於 Visual Studio 2019 寫 ASP.NET Core 頁面時,沒有 Vue 的智能提示,所以需要使用 VSCode 來寫前端頁面。
本文 代碼 已發佈到 GitHub https://github.com/whuanle/CZGL.IKonwWebApi
目錄
4, [HttpGet]、[HttpPost]、[HttpDelete]、[HttpPut]
一. 微軟WebApi
特性 | 綁定源 |
---|---|
[FromBody] | 請求正文 |
[FromForm] | 請求正文中的表單數據 |
[FromHeader] | 請求標頭 |
[FromQuery] | 請求查詢字元串參數 |
[FromRoute] | 當前請求中的路由數據 |
[FromServices] | 作為操作參數插入的請求服務 |
來一張 Postman 的圖片:
HTTP 請求中,會攜帶很多參數,這些參數可以在前端設置,例如表單、Header、文件、Cookie、Session、Token等。
那麼,上面的表格正是用來從 HTTP 請求中獲取數據的 “方法”
或者說 “手段”
。HttpContext 等對象不在本文討論範圍。
Microsoft.AspNetCore.Mvc
命名空間提供很多用於配置Web API 控制器的行為和操作方法的屬性:
特性 | 說明 |
---|---|
[Route] | 指定控制器或操作的 URL 模式。 |
[Bind] | 指定要包含的首碼和屬性,以進行模型綁定。 |
[Consumes] | 指定某個操作接受的數據類型。 |
[Produces] | 指定某個操作返回的數據類型。 |
[HttpGet] | 標識支持 HTTP GET 方法的操作。 |
[HttpPost] | 標識支持 HTTP POST 方法的操作。 |
... ... ... | ... ... ... |
WebApi 應用
首先創建一個 Asp.Net Core MVC 應用,然後在 Controllers 目錄添加一個 API 控制器 DefaultController.cs
。(這裡不創建 WebApi 而是 創建 MVC,通過 MVC 創建 API 控制器)。
創建後預設代碼:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
}
1. 安裝 Swagger
在 Nuget 中搜索 Swashbuckle.AspNetCore
,或打開 程式包管理器控制台 -> 程式包管理器控制台
,輸入以下命令進行安裝
Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc2
打開 Startup
文件,添加引用
using Microsoft.OpenApi.Models;
在 ConfigureServices
中添加服務,雙引號文字內容隨便改。
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
添加中間件
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
// 添加下麵的內容
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
訪問 /swagger
可以訪問到 Swagger 的 UI 界面。
為了便於查看輸出和固定埠,打開 Progarm,cs
,修改內容
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("https://*:5123")
.UseStartup<Startup>();
不要使用 IIS 托管運行。
註意:本文全部使用 [HttpPost] ;全局使用 JsonResult 作為返回類型。
二. 數據綁定與獲取
1,預設不加
直接寫 action
,不使用特性
[HttpPost("aaa")]
public async Task<JsonResult> AAA(int? a, int? b)
{
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = a + "|" + b });
}
打開 https://localhost:5123/swagger/index.html 查看 UI 界面
也就是說,創建一個 action
,什麼都不加,預設是 query
。
通過 Postman 提交數據、測試介面
對於 Query 的 action 來說, axios 的寫法
postaaa: function () {
axios.post('/api/default/aaa?a=111&b=222'
)
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
在網上查找資料時,發現有人說通過 params 添加數據也可以,不過筆者測試,貌似不行。
講道理,別人可以,為啥我不行。。。
axios 代碼:
postaaa: function () {
axios.post('/api/default/aaa', {
params: {
a: 123,
b: 234
}
}
)
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
包括下麵的,都試過了,不行。
axios.post('/api/default/aaa', {
a:1234,
b:1122
}
axios.post('/api/default/aaa', {
data:{
a:1234,
b:1122
}
}
把 [HttpPost]
改成 [HttpGet]
,則可以使用
axios.post('/api/default/aaa', {
params: {
a: 123,
b: 234
}
}
... ...
提示:
... ...
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
.then
當請求成功時觸發,請求失敗時觸發 catch
。res
是請求成功後返回的信息,res.data
是請求成功後伺服器返回的信息。即是 action
處理數據後返回的信息。
在瀏覽器,按下 F12 打開控制台,點擊 Console ,每次請求後,這裡會列印請求結果和數據。
2, [FromBody]
官方文檔解釋:請求正文。[FromBody] 針對複雜類型參數進行推斷。 [FromBody] 不適用於具有特殊含義的任何複雜的內置類型,如 IFormCollection 和 CancellationToken。 綁定源推理代碼將忽略這些特殊類型。
算了,看得一頭霧水,手動實際試試。
剛剛開始的時候,我這樣使用:
public async Task<JsonResult> BBB([FromBody]int? a, [FromBody]int? b)
結果編譯時就報錯,提示只能使用一個 [FromBody],於是改成
[HttpPost("bbb")]
public async Task<JsonResult> BBB([FromBody]int? a, int? b)
{
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = a + "|" + b });
}
打開 Swagger UI 界面,刷新一下
從圖片中發現,只有 b,沒有 a,而且右上角有下拉框,說明瞭加 [FromBody] 是 json 上傳。
那麼說明 [FromBody] 修飾得應當是對象,而不是 欄位。
修改程式如下:
// 增加一個類型
public class AppJson
{
public int? a { get; set; }
public int? b { get; set; }
}
[HttpPost("bbb")]
public async Task<JsonResult> BBB([FromBody]AppJson ss)
{
if (ss.a == null || ss.b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 2000, result = ss.a + "|" + ss.b });
}
再看看微軟的文檔:[FromBody] 針對複雜類型參數進行推斷。
,這下可理解了。。。
即是不應該對 int、string 等類型使用 [FromBody] ,而應該使用一個 複雜類型
。
而且,一個 action 中,應該只能使用一個 [FromBody] 。
打開 Swagger 界面(有修改需要刷新下界面,下麵不再贅述)。
這樣才是我們要的結果嘛,前端提交的是 Json 對象。
用 Postman 測試下
證實了猜想,嘿嘿,嘿嘿嘿。
前端提交的是 Json 對象,遵循 Json 的格式規範,那麼 [FromBody] 把它轉為 Object 對象。
前端 axios 寫法:
methods: {
postaaa: function () {
axios.post('/api/default/bbb', {
"a": 4444,
"b": 5555
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
}
3, [FromForm]
[HttpPost("ccc")]
public async Task<JsonResult> CCC([FromForm]int? a, [FromForm]int? b)
{
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
當然,這樣寫也行,多個欄位或者對象都可以
[HttpPost("ccc")]
public async Task<JsonResult> CCC([FromForm]AppJson ss)
{
if (ss.a == null || ss.b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = ss.a + "|" + ss.b });
}
根據提示,使用 Postman 進行測試
事實上,這樣也行 ↓
form-data 和 x-www.form-urlencoded 都是鍵值形式,文件 form-data 可以用來上傳文件。具體的區別請自行查詢。
axios 寫法(把 Content-Type 欄位修改成 form-data 或 x-www.form-urlencoded )
postccc: function () {
let fromData = new FormData()
fromData.append('a', 111)
fromData.append('b', 222)
axios.post('/api/default/ccc', fromData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
4, [FromHeader]
[FromHeader] 不以表單形式上傳,而是跟隨 Header 傳遞參數。
[HttpPost("ddd")]
public async Task<JsonResult> DDD([FromHeader]int? a, [FromHeader]int? b)
{
if (a == null || b == null)
return new JsonResult(new { code = 0, result = "aaaaaaaa" });
return new JsonResult(new { code = 200, result = a + "|" + b });
}
axios 寫法
postddd: function () {
axios.post('/api/default/ddd', {}, {
headers: {
a: 123,
b: 133
}
})
.then(res => {
console.log(res.data)
console.log(res.data.code)
console.log(res.data.result)
})
.catch(err => {
console.error(err);
})
}
需要註意的是,headers 的參數,必須放在第三位。沒有要提交的表單數據,第二位就使用 {} 代替。
params 跟隨 url 一起在第一位,json 或表單數據等參數放在第二位,headers 放在第三位。
由於筆者對前端不太熟,這裡有說錯,麻煩大神評論指出啦。
5, [FromQuery]
前面已經說了,Action 參數不加修飾,預設就是 [FromQuery] ,參考第一小節。
有個地方需要記住, Action 參數不加修飾。預設就是 [FromQuery] ,有時幾種參數併在一起放到 Action 里,會忽略掉,調試時忘記了,造成麻煩。
6, [FromRoute]
獲取路由規則,這個跟前端上傳的參數無關;跟 URL 可以說有關,又可以說無關。
[HttpPost("fff")]
public async Task<JsonResult> FFFxxx(int a,int b,
[FromRoute]string controller,
[FromRoute]string action)
{
// 這裡就不處理 a和 b了
return new JsonResult(new { code = 200, result = controller+"|"+action });
}
[FromRoute] 是根據路由模板獲取的,上面 API 的兩個參數和路由模板的名稱是對應的:
[FromRoute]string controller, [FromRoute]string action
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
當然,還可以加個 [FromRoute]int? id
[FromRoute] 和 [FromQuery] 區別
以此 URL 為例
https://localhost:5123/api/Default/fff?a=111&b=22
Route 會查到 controller = Default
,action = FFFxxx
。查詢到的是代碼里的真實名稱。
Query 會查詢到 a = 111
和 b = 22
那麼,如果路由規則里,不在 URL 里出現呢?
[HttpPost("/ooo")]
public async Task<JsonResult> FFFooo(int a, int b,
[FromRoute]string controller,
[FromRoute]string action)
{
// 這裡就不處理 a和 b了
return new JsonResult(new { code = 200, result = controller + "|" + action });
}
那麼,訪問地址變成 https://localhost:5123/ooo
通過 Postman ,測試
說明瞭 [FromRoute] 獲取的是代碼里的 Controller 和 Action 名稱,跟 URL 無關,根據測試結果推斷跟路由表規則也無關。
7, [FromService]
這個是與依賴註入容器有關,跟 URL 、路由等無關。
新建一個介面、一個類
public interface ITest
{
string GGG { get; }
}
public class Test : ITest
{
public string GGG { get { return DateTime.Now.ToLongDateString(); } }
}
在 ConfigureServices
中 註入
services.AddSingleton<ITest, Test>();
在 DefaultController
中,創建構造函數,然後
private readonly ITest ggg;
public DefaultController(ITest ttt)
{
ggg = ttt;
}
添加一個 API
[HttpPost("ggg")]
public async Task<JsonResult> GGG([FromServices]ITest t)
{
return new JsonResult(new { code = 200, result = t.GGG });
}
訪問時,什麼參數都不需要加,直接訪問此 API 即可。
[FromService] 跟後端的代碼有關,跟 Controller 、Action 、URL、表單數據等無關。
小結:
特性可以幾種放在一起用,不過儘量每個 API 的參數只使用一種特性。
優先取值 Form > Route > Query
。
IFromFile 由於文件的上傳,本文就不談這個了。
關於數據綁定,更詳細的內容請參考:
https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2
三. action 特性方法
Microsoft.AspNetCore.Mvc
命名空間提供可用於配置 Web API 控制器的行為和操作方法的屬性。
下表是針對於 Controller 或 Action 的特性.
特性 | 說明 |
---|---|
[Route] | 指定控制器或操作的 URL 模式。 |
[Bind] | 指定要包含的首碼和屬性,以進行模型綁定。 |
[Consumes] | 指定某個操作接受的數據類型。 |
[Produces] | 指定某個操作返回的數據類型。 |
[HttpGet] | 標識支持 HTTP GET 方法的操作。 |
... | ... |
下麵使用這些屬性來指定 Controller 或 Action 接受的 HTTP 方法、返回的數據類型或狀態代碼。
1, [Route]
在微軟文檔中,把這個特性稱為 屬性路由
,定義:屬性路由使用一組屬性將操作直接映射到路由模板。
請教了大神,大神解釋說,ASP.NET Core 有路由規則表,路由表是全局性、唯一性的,在程式運行時,會把所有路由規則收集起來。
MVC 應用中設置路由的方法有多種,例如
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
[Route("Home/Index")]
public IActionResult Index()
{
return View();
}
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
}
路由是全局唯一的,可以通過不同形式使用,但是規則不能發生衝突,程式會在編譯時把路由表收集起來。
根據筆者經驗,發生衝突,應該就是在編譯階段直接報錯了。(註:筆者不敢確定)
關於路由,請參考 :