1. 程式人生 > >從壹開始前後端分離【 .NET Core2.0 +Vue2.0 】框架之二 || 後端專案搭建

從壹開始前後端分離【 .NET Core2.0 +Vue2.0 】框架之二 || 後端專案搭建

WHY

    至於為什麼要搭建.Net Core 平臺,這個網上的解釋以及鋪天蓋地,想了想,還是感覺重要的一點,跨平臺,嗯!沒錯,而且比.Net 更容易搭建,速度也更快,所有的包均有Nuget提供,不再像以前的單純引入元件,比如是這樣的:


已經沒有了之前的Assemblies和COM的引入,初次使用感覺會很彆扭,不過使用多了,發現還是很方便的,所以你一定要會使用Nuget,真的很強大,這點兒設計思路感覺更像Linux了。

2018-11-20更新:

.net core 框架效能測試

.net core 執行過程

HOW

    說了從零開始,就得從零開始,老生常談,開始。

當然,前提是你得安裝.Net Core,VS 2015也是可以,只不過需要單獨安裝.Net Core,首先你得裝個vs2015 並且保證已經升級至 update3及以上。

我的VS是2017,我這裡只說2017,有不會的網友可以留言,只要在Visual Studio Installer 中安裝下圖中的Core 平臺即可。


1、File --> Project (記得檔名不要是中文,不然,你懂的)


2、然後選擇.Net Core 版本和專案型別,我選擇相對穩定的ASP.NET Core 2.0,然後選擇API的專案型別

至於其他的,大家可以自己玩一玩,還有就是是否Docker支援,這兩年Docker著實很火,我也會在以後的時間裡,補上這塊兒的使用。。。


Duang ,然後就出現了,特別簡單的一個.Net Core API就這麼誕生了,嗯不錯,基本的分成這幾個部分,是不是特別像一個控制檯程式?而且真是簡潔了不少

點開Controllers --> ValuesController 檔案,你會發現四個方法,並且每個方法也有各自的特性,分別是HttpGet,HttpPost,HttpPut,HttpDelete,這四個就是傳說中的RESTful風格的程式設計。

為什麼會有這種風格呢:

RESTful 風格介面實際情況是,我們在前後端在約定介面的時候,可以約定各種風格的介面,但是,RESTful 介面是目前來說比較流行的,並且在運用中比較方便和常見的介面。

雖然它有一些缺陷,目前 github 也在主推 GraphQL 這種新的介面風格,但目前國內來說還是 RESTful 介面風格比較普遍。並且,在掌握了 RESTful 介面風格之後,會深入的理解這種介面的優缺點,到時候,你自然會去想解決方案,並且在專案中實行新的更好的理念,所以,我這系列的博文,依然採用 http://cnodejs.org/ 網站提供的 RESTful 介面來實戰。

瞭解程式開發的都應該知道,我們所做的大多數操作都是對資料庫的四格操作 “增刪改查” 對應到我們的介面操作分別是:post 插入新資料delete 刪除資料put 修改資料get 查詢資料

注意,這裡是我們約定,並非這些動作只能幹這件事情。從表層來說,除get外的其他方法,沒有什麼區別,都是一樣的。從深層來說包括 get在內的所有方法都是一模一樣的,沒有任何區別。但是,我們約定,每種動作對應不同的操作,這樣方便我們統一規範我們的所有操作。

假設,我們的介面是 /api/v1/love 這樣的介面,採用 RESTful 介面風格對應操作是如下的:get 操作 /api/v1/love獲取 /api/v1/love 的分頁列表資料,得到的主體,將是一個數組,我們可以用資料來遍歷迴圈列表post 操作 /api/v1/love我們會往 /api/v1/love 插入一條新的資料,我們插入的資料,將是JOSN利用物件傳輸的。get 操作 /api/v1/love/1我們獲取到一個 ID 為 1 的的資料,資料一般為一個物件,裡面包含了 1 的各項欄位資訊。put 操作 /api/v1/love/1我們向介面提交了一個新的資訊,來修改 ID 為 1 的這條資訊delete 操作 /api/v1/love/1我們向介面請求,刪除 ID 為 1 的這一條資料

由上述例子可知,我們實現了5種操作,但只用了兩個介面地址, /api/v1/love 和 /api/v1/love/1 。所以,採用這種介面風格,可以大幅的簡化我們的介面設計。


.Net Core 2.1 新不同

然後 F5 執行,就會看到介面地址,以及對應的內容,你可以根據自己的需要進行各種配置合適的路由,

這裡要注意下,如果出現特性相同,方法同名,引數一樣的,編譯會報錯,起名字很重要。

還有,這裡會自動跳轉到預設地址 api/values,當然是可以配置的,就在 Properties --> launchSettings.json 中


接下來點開 appsettings.json 檔案,這裡就是整個系統app的配置地址,更類似以前的web.config,以後大家會用到

繼續往下,開啟Startup.cs 檔案這裡是整個專案的啟動檔案,所有的啟動相關的都會在這裡配置,比如 依賴注入,跨域請求,Redis快取等,更多詳情在以後的文章中都會有所提起


2018-09-23 更新

Program.cs

namespace CoreBackend.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}

這個Program是程式的入口, 看起來很眼熟, 是因為asp.net core application實際就是控制檯程式(console application).

它是一個呼叫asp.net core 相關庫的console application. 

Main方法裡面的內容主要是用來配置和執行程式的.

因為我們的web程式需要一個宿主, 所以 BuildWebHost這個方法就建立了一個WebHostBuilder. 而且我們還需要Web Server.

asp.net core 自帶了兩種http servers, 一個是WebListener, 它只能用於windows系統, 另一個是kestrel, 它是跨平臺的.

kestrel是預設的web server, 就是通過UseKestrel()這個方法來啟用的.

但是我們開發的時候使用的是IIS Express, 呼叫UseIISIntegration()這個方法是啟用IIS Express, 它作為Kestrel的Reverse Proxy server來用.

如果在windows伺服器上部署的話, 就應該使用IIS作為Kestrel的反向代理伺服器來管理和代理請求.

如果在linux上的話, 可以使用apache, nginx等等的作為kestrel的proxy server.

當然也可以單獨使用kestrel作為web 伺服器, 但是使用iis作為reverse proxy還是由很多有點的: 例如,IIS可以過濾請求, 管理證書, 程式崩潰時自動重啟等.

UseStartup<Startup>(), 這句話表示在程式啟動的時候, 我們會呼叫Startup這個類.

Build()完之後返回一個實現了IWebHost介面的例項(WebHostBuilder), 然後呼叫Run()就會執行Web程式, 並且阻止這個呼叫的執行緒, 直到程式關閉.

2018-11-20更新

如果想要對AspNetCore原始碼進行研究,可以檢視原始碼,這裡提供兩個方法:

1、F12,當然這個不能看到詳細的,你需要安裝一個元件,VS2017 Resharper

Startup.cs

namespace CoreBackend.Api
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
//判斷是否是環境變數
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //這個就是一個簡單的中介軟體寫法 app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } } }

其實Startup算是程式真正的切入點.

ConfigureServices方法是用來把services(各種服務, 例如identity, ef, mvc等等包括第三方的, 或者自己寫的)加入(register)到container(asp.net core的容器)中去, 並配置這些services. 這個container是用來進行dependency injection的(依賴注入). 所有注入的services(此外還包括一些框架已經註冊好的services) 在以後寫程式碼的時候, 都可以將它們注入(inject)進去. 例如上面的Configure方法的引數, app, env, loggerFactory都是注入進去的services.

Configure方法是asp.net core程式用來具體指定如何處理每個http請求的, 例如我們可以讓這個程式知道我使用mvc來處理http請求, 那就呼叫app.UseMvc()這個方法就行. 但是目前, 所有的http請求都會導致返回"Hello World!".

更新:

.net core 除錯的兩種方法

1、通過IIS除錯

2、專案自帶的Kestrel web應用調式

註冊並使用MVC

因為asp.net core 2.0使用了一個大而全的metapackage, 所以這些基本的services和middleware是不需要另外安裝的.

首先, 在ConfigureServices裡面向Container註冊MVC: services.AddMvc();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(); // 註冊MVC到Container
        }

然後再Configure裡面告訴程式使用mvc中介軟體:

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler();
            }

            app.UseMvc();

注意順序, 應該在處理異常的middleware後邊呼叫app.UseMvc(), 所以處理異常的middleware可以在把request交給mvc之間就處理異常, 更總要的是它還可以捕獲並處理返回MVC相關程式碼執行中的異常.

然後別忘了把app.Run那部分程式碼去掉. 然後改回到Develpment環境, 跑一下, 試試效果:

Chrome顯示了一個空白頁, 按F12, 顯示了404 Not Found錯誤.

這是因為我只添加了MVC middleware, 但是它啥也沒做, 也沒有找到任何可用於處理請求的程式碼, 所以我們要新增Controller來返回資料/資源等等

Routing 路由

路由有兩種方式: Convention-based (按約定), attribute-based(基於路由屬性配置的). 

其中convention-based (基於約定的) 主要用於MVC (返回View或者Razor Page那種的).

Web api 推薦使用attribute-based.

這種基於屬性配置的路由可以配置Controller或者Action級別, uri會根據Http method然後被匹配到一個controller裡具體的action上.

常用的Http Method有:

  • Get, 查詢, Attribute: HttpGet, 例如: '/api/product', '/api/product/1'
  • POST, 建立, HttpPost, '/api/product'
  • PUT 整體修改更新 HttpPut, '/api/product/1'
  • PATCH 部分更新, HttpPatch, '/api/product/1'
  • DELETE 刪除, HttpDelete, '/api/product/1

還有一個Route屬性(attribute)也可以用於Controller層, 它可以控制action級的URI字首.

以下不是本系列,就看思路即可,不用敲程式碼

//以下不是本系列教程,就看思路即可,不用敲程式碼
namespace CoreBackend.Api.Controllers { //[Route("api/product")] [Route("api/[controller]")] public class ProductController: Controller { [HttpGet] public JsonResult GetProducts() { return new JsonResult(new List<Product> { new Product { Id = 1, Name = "牛奶", Price = 2.5f }, new Product { Id = 2, Name = "麵包", Price = 4.5f } }); } } }

使用[Route("api/[controller]")], 它使得整個Controller下面所有action的uri字首變成了"/api/product", 其中[controller]表示XxxController.cs中的Xxx(其實是小寫).

也可以具體指定, [Route("api/product")], 這樣做的好處是, 如果ProductController重構以後改名了, 只要不改Route裡面的內容, 那麼請求的地址不會發生變化.

然後在GetProducts方法上面, 寫上HttpGet, 也可以寫HttpGet(). 它裡面還可以加引數,例如: HttpGet("all"), 那麼這個Action的請求的地址就變成了 "/api/product/All".

內容協商 Content Negotiation

如果 web api提供了多種內容格式, 那麼可以通過Accept Header來選擇最好的內容返回格式: 例如:

application/json, application/xml等等

如果設定的格式在web api裡面沒有, 那麼web api就會使用預設的格式.

asp.net core 預設提供的是json格式, 也可以配置xml等格式.

目前只考慮 Output formatter, 就是返回的內容格式.

建立Post Action

以下不是本系列,就看思路即可,不用敲程式碼

//以下不是本系列教程,就看思路即可,不用敲程式碼     
[Route("{id}", Name = "GetProduct")] public IActionResult GetProduct(int id) { var product = ProductService...(x => x.Id == id); if (product == null) { return NotFound(); } return Ok(product); } [HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } var maxId = ProductService.Max(x => x.Id); var newProduct = new Product { Id = ++maxId, Name = product.Name, Price = product.Price }; ProductService.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); }

[HttpPost] 表示請求的謂詞是Post. 加上Controller的Route字首, 那麼訪問這個Action的地址就應該是: 'api/product'

後邊也可以跟著自定義的路由地址, 例如 [HttpPost("create")], 那麼這個Action的路由地址就應該是: 'api/product/create'.

[FromBody] , 請求的body裡面包含著方法需要的實體資料, 方法需要把這個資料Deserialize成ProductCreation, [FromBody]就是幹這些活的.

客戶端程式可能會發起一個Bad的Request, 導致資料不能被Deserialize, 這時候引數product就會變成null. 所以這是一個客戶端發生的錯誤, 程式為讓客戶端知道是它引起了錯誤, 就應該返回一個Bad Request 400 (Bad Request表示客戶端引起的錯誤)的 Status Code.

傳遞進來的model型別是 ProductCreation, 而我們最終操作的型別是Product, 所以需要進行一個Map操作, 目前還是挨個屬性寫程式碼進行Map吧, 以後會改成Automapper.

返回 CreatedAtRoute: 對於POST, 建議的返回Status Code 是 201 (Created), 可以使用CreatedAtRoute這個內建的Helper Method. 它可以返回一個帶有地址Header的Response, 這個Location Header將會包含一個URI, 通過這個URI可以找到我們新建立的實體資料. 這裡就是指之前寫的GetProduct(int id)這個方法. 但是這個Action必須有一個路由的名字才可以引用它, 所以在GetProduct方法上的Route這個attribute裡面加上Name="GetProduct", 然後在CreatedAtRoute方法第一個引數寫上這個名字就可以了, 儘管進行了引用, 但是Post方法走完的時候並不會呼叫GetProduct方法. CreatedAtRoute第二個引數就是對應著GetProduct的引數列表, 使用匿名類即可, 最後一個引數是我們剛剛建立的資料實體

執行程式試驗一下, 注意需要在Headers裡面設定Content-Type: application/json.

Validation 驗證

針對上面的Post方法,  如果請求沒有Body, 引數product就會是null, 這個我們已經判斷了; 如果body裡面的資料所包含的屬性在product中不存在, 那麼這個屬性就會被忽略.

但是如果body資料的屬性有問題, 比如說name沒有填寫, 或者name太長, 那麼在執行action方法的時候就會報錯, 這時候框架會自動丟擲500異常, 表示是伺服器的錯誤, 這是不對的. 這種錯誤是由客戶端引起的, 所以需要返回400 Bad Request錯誤.

驗證Model/實體, asp.net core 內建可以使用 Data Annotations進行: 

//以下不是本系列教程,就看思路即可,不用敲程式碼
using System; using System.ComponentModel.DataAnnotations; namespace CoreBackend.Api.Dtos { public class ProductCreation { [Display(Name = "產品名稱")] [Required(ErrorMessage = "{0}是必填項")] // [MinLength(2, ErrorMessage = "{0}的最小長度是{1}")] // [MaxLength(10, ErrorMessage = "{0}的長度不可以超過{1}")]
     [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")] public string Name { get; set; } [Display(Name = "價格")] [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")] public float Price { get; set; } } }

這些Data Annotation (理解為用於驗證的註解), 可以在System.ComponentModel.DataAnnotation找到, 例如[Required]表示必填, [MinLength]表示最小長度, [StringLength]可以同時驗證最小和最大長度, [Range]表示數值的範圍等等很多.

[Display(Name="xxx")]的用處是, 給屬性起一個比較友好的名字.

其他的驗證註解都有一個屬性叫做ErrorMessage (string), 表示如果驗證失敗, 就會把ErrorMessage的內容新增到錯誤結果裡面去. 這個ErrorMessage可以使用引數, {0}表示Display的Name屬性, {1}表示當前註解的第一個變數, {2}表示當前註解的第二個變數.

在Controller裡面新增驗證邏輯:

//以下不是本系列教程,就看思路即可,不用敲程式碼     
[HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var maxId = ProductService.Max(x => x.Id); var newProduct = new Product { Id = ++maxId, Name = product.Name, Price = product.Price }; ProductService.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); }

ModelState: 是一個Dictionary, 它裡面是請求提交到Action的Name和Value的對們, 一個name對應著model的一個屬性, 它也包含了一個針對每個提交的屬性的錯誤資訊的集合.

每次請求進到Action的時候, 我們在ProductCreationModel新增的那些註解的驗證, 就會被檢查. 只要其中有一個驗證沒通過, 那麼ModelState.IsValid屬性就是False. 可以設定斷點檢視ModelState裡面都有哪些東西.

如果有錯誤的話, 我們可以把ModelState當作Bad Request的引數一起返回到前臺.

PUT

put應該用於對model進行完整的更新. 

首先最好還是單獨為Put寫一個Dto Model, 儘管屬性可能都是一樣的, 但是也建議這樣寫, 實在不想寫也可以.

ProducModification.cs

    public class ProductModification
    {
        [Display(Name = "產品名稱")]
        [Required(ErrorMessage = "{0}是必填項")]
        [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")]
        public string Name { get; set; }

        [Display(Name = "價格")]
        [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")]
        public float Price { get; set; }
    }

然後編寫Controller的方法:

//以下不是本系列教程,就看思路即可,不用敲程式碼     
[HttpPut("{id}")]
        public IActionResult Put(int id, [FromBody] ProductModification product)
        {
            if (product == null)
            {
                return BadRequest();
            }

            if (product.Name == "產品")
            {
                ModelState.AddModelError("Name", "產品的名稱不可以是'產品'二字");
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var model = ProductService.SingleOrDefault(x => x.Id == id);
            if (model == null)
            {
                return NotFound();
            }
            model.Name = product.Name;
            model.Price = product.Price;

            // return Ok(model);
            return NoContent();
        }

按照Http Put的約定, 需要一個id這樣的引數, 用於查詢現有的model.

由於Put做的是完整的更新, 所以把ProducModification整個Model作為引數.

進來之後, 進行了一套和POST一摸一樣的驗證, 這地方肯定可以改進, 如果驗證邏輯比較複雜的話, 到處寫同樣驗證邏輯肯定是不好的, 所以建議使用FluentValidation.

然後, 把ProductModification的屬性都對映查詢找到給Product, 這個以後用AutoMapper來對映.

返回: PUT建議返回NoContent(), 因為更新是客戶端發起的, 客戶端已經有了最新的值, 無需伺服器再給它傳遞一次, 當然了, 如果有些值是在後臺更新的, 那麼也可以使用Ok(xxx)然後把更新後的model作為引數一起傳到前臺.

WHAT

    好啦,專案搭建就這麼愉快的解決了,而且你也應該簡單瞭解了.Net Core API是如何安裝,建立,各個檔案的意義以及如何運作,如何配置等,但是既然是介面,那一定是要前後端一起進行配置,使用,交流的平臺,從上文看出,每次都特別麻煩,而且不直觀,UI 不友好,怎麼辦呢?

NEXT

    下一節我們就使用一個神器 Swagger,一個快速,輕量級的專案RESTFUL介面的文件線上自動生成+功能測試功能軟體。

CODE

NOTE

如何不會使用Git,可以參考