寫(xiě)在前面
鑒于很多使用 ng-alain 都以 .net 為后端,以下我將以一個(gè)示例來(lái)描述 ng-alain 如何同 .net core 一起開(kāi)發(fā)蛔翅。示例以單個(gè)中后臺(tái)項(xiàng)目為基準(zhǔn)瓶殃,對(duì)于多項(xiàng)目的應(yīng)用大體相同充包,但整體目錄結(jié)構(gòu)當(dāng)然不能以單個(gè)項(xiàng)目了,更多應(yīng)該以多人開(kāi)發(fā)為準(zhǔn)遥椿。
所有源碼基矮,從 Github 中獲取。
一冠场、構(gòu)建項(xiàng)目
1家浇、構(gòu)建
分為 ng-alain 和 .net core 兩個(gè)部分,當(dāng)然我推薦二者分開(kāi)創(chuàng)建在一個(gè)根目錄下碴裙,例如以一個(gè) asdf
為項(xiàng)目名钢悲,創(chuàng)建一個(gè) asdf
目錄,然后分別使用以下方式構(gòu)建一個(gè)完整的前后端:
md asdf
cd asdf
ng-alain
ng new asdf --style less
cd asdf
ng add ng-alain
此時(shí)舔株,我們將 asdf/asdf
目錄重命名另一個(gè)名稱(chēng)以便區(qū)分前后端:fe
莺琳。
.net core
dotnet new webapi -o asdf
# 這里采用命令行,依然可以使用 vs 創(chuàng)建項(xiàng)目
同樣载慈,我們將 asdf/asdf
目錄重命名另一個(gè)名稱(chēng)以便區(qū)分前后端:be
惭等。
示例的命名方式你可以使用你喜歡的風(fēng)格。
最終我們的 asdf
項(xiàng)目的目錄結(jié)構(gòu)如下:
asdf
- be (后端 .net core)
- fe (前端 Angular)
2办铡、運(yùn)行
前后端分開(kāi)自然辞做,分別開(kāi)啟兩個(gè)命令行琳要,而 vscode 著實(shí)很方便,分別在 be
和 fe
目錄下運(yùn)行:
be: dotnet watch run
fe: npm run hmr
默認(rèn)情況下分開(kāi)可以通過(guò) https://localhost:5001/api/values 和 http://localhost:4200/ 訪問(wèn)前后端秤茅。
IIS
其實(shí)若是使用 vs 創(chuàng)建項(xiàng)目稚补,可以更友好的將后端綁定至某個(gè)域名下,配合修改 host 可以使開(kāi)發(fā)環(huán)境更接近生產(chǎn)環(huán)境嫂伞。當(dāng)然啦孔厉,這一部分百度或博客園可以得到信息支持,這里不再贅述帖努。
二撰豺、編寫(xiě)后端
我在淺談Angular網(wǎng)絡(luò)請(qǐng)求 描述過(guò)網(wǎng)絡(luò)請(qǐng)求與用戶認(rèn)證相關(guān)的,以此為擴(kuò)展拼余,我們來(lái)嘗試怎么實(shí)現(xiàn)這些細(xì)節(jié)污桦。
.net core 也有中間件的概念,如同 Angular 攔截器匙监。依然以一個(gè)網(wǎng)絡(luò)請(qǐng)求流程式來(lái)描述這一過(guò)程凡橱,其大概如下:
- 使用攔截器,從 Header 獲取用戶 Token亭姥,并檢查 Token 有效性
- 使用過(guò)濾器稼钩,檢查請(qǐng)求體參數(shù)有效性
- 執(zhí)行方法,并響應(yīng)結(jié)果
- 使用攔截器處理統(tǒng)一異常處理
當(dāng)然达罗,這只是一個(gè)大概性坝撑,細(xì)節(jié)上可能需要處理得更多。
1粮揉、校驗(yàn) Token
在 Startup.cs
增加一個(gè)簡(jiǎn)單通過(guò) header
來(lái)獲取 token
屬性值巡李,并進(jìn)行校驗(yàn)有效性,最后再結(jié)果寫(xiě)入至 context.Items
里扶认,這樣整個(gè)請(qǐng)求只需要一次用戶信息侨拦,后續(xù)直接使用 context.Items
來(lái)獲取用戶信息。
app.Use(async (context, next) => {
if (!context.Request.Path.ToString().StartsWith("/api/passport")) {
var _token = "";
if (context.Request.Headers.TryGetValue("token", out var tokens) && tokens.Count > 0) {
_token = tokens[0];
}
if (_token != "asdf") {
context.Response.StatusCode = 401;
return ;
}
var user = new User();
user.Id = 1;
user.Name = "cipchk";
context.Items.Add("token", _token);
context.Items.Add("user", user);
}
await next();
});
整段代碼實(shí)現(xiàn)幾個(gè)細(xì)節(jié):
- 忽略所有
/api/passport
開(kāi)頭用戶 Token 校驗(yàn) - 從 headers 獲取
token
值辐宾,并校驗(yàn)值- 若錯(cuò)誤則直接返回
401
不再執(zhí)行后續(xù)動(dòng)作
- 若錯(cuò)誤則直接返回
- 獲取
User
信息并寫(xiě)入context.Items
中狱从,以后后續(xù)請(qǐng)求直接讀取
此時(shí),若再一次訪問(wèn) https://localhost:5001/api/values 后端接收到的是一個(gè) 401
狀態(tài)碼叠纹。
2矫夯、統(tǒng)一異常處理
依然可以使用中間件的寫(xiě)法,但為了區(qū)分不同吊洼,這里采用過(guò)濾器的寫(xiě)法。
繼承 ExceptionFilterAttribute
并重寫(xiě) OnException
就可以簡(jiǎn)單的完成制肮,ExceptionFilterAttribute
異常過(guò)濾器會(huì)在執(zhí)行過(guò)程中若遇到 throw
時(shí)會(huì)被觸發(fā)冒窍。
public class ExceptionAttribute : ExceptionFilterAttribute {
public override void OnException(ExceptionContext context) {
var res = context.HttpContext.Response;
res.StatusCode = 200;
res.ContentType = "application/json; charset=utf-8";
context.Result = new JsonResult(new {
msg = context.Exception.Message,
code = 503
});
}
}
最后递沪,需要注冊(cè)到整個(gè)應(yīng)用里。
services.AddMvc(options => {
options.Filters.Add(new ExceptionAttribute());
})
3综液、PassportController
新建一個(gè) PassportController.cs
文件款慨,內(nèi)容如下:
[Route("api/[controller]")]
[ApiController]
public class PassportController : ControllerBase
{
[HttpGet("{id}")]
public JsonResult Get(int id)
{
if (id != 1) throw new Exception("無(wú)效用戶");
return new JsonResult(new { msg = "ok", data = "asdf" });
}
}
前面我們已經(jīng)忽略對(duì)所有 /api/passport
開(kāi)頭的 URL 的用戶 Token 校驗(yàn),它是一個(gè)授權(quán)頁(yè)理當(dāng)如此谬莹。這里體現(xiàn)了兩個(gè)細(xì)節(jié):
- 若無(wú)效用戶拋出一個(gè)錯(cuò)誤
-
ExceptionAttribute
會(huì)捕獲到這個(gè)錯(cuò)誤檩奠,并重新指定變更響應(yīng)體內(nèi)容 - 當(dāng)然你依然可以使用
return new JsonResult(new { msg = "無(wú)效用戶" });
這種方式
-
- 若有效用戶返回用戶 Token 值
這里都是手工創(chuàng)建統(tǒng)一響應(yīng)體,你依然可以利用過(guò)濾器或中間件來(lái)統(tǒng)一處理響應(yīng)體為統(tǒng)一風(fēng)格附帽,而對(duì)于方法內(nèi)永遠(yuǎn)都只返回一個(gè) data
對(duì)應(yīng)值埠戳。上述單純只是一個(gè)示例,需要自行更進(jìn)一步封裝蕉扮。
4整胃、跨域問(wèn)題
本文描述是前后端分開(kāi)開(kāi)發(fā),因此開(kāi)發(fā)過(guò)程中勢(shì)必存在跨域請(qǐng)求的問(wèn)題喳钟,一個(gè)簡(jiǎn)單的辦法在 Startup.cs
里增加跨域代碼:
#if DEBUG
app.UseCors(builder =>
{
builder.AllowAnyMethod()
.AllowAnyHeader()
.AllowAnyOrigin();
});
#endif
條件編譯可以很好的解決生產(chǎn)和開(kāi)發(fā)環(huán)境的不同屁使,因?yàn)椴渴饡r(shí)我們將不存在跨域問(wèn)題,后續(xù)會(huì)描述奔则。
5蛮寂、小結(jié)
到此,我們已經(jīng)實(shí)現(xiàn)大部分 淺談Angular網(wǎng)絡(luò)請(qǐng)求 描述的功能易茬,哪怕都是一個(gè)簡(jiǎn)化酬蹋,但我們可以訪問(wèn):
- https://localhost:5001/api/values 返回 401
- https://localhost:5001/api/passport/1 返回用戶 Token
- https://localhost:5001/api/passport/2 返回?zé)o效用戶異常
三、編寫(xiě)前端
ng-alain 默認(rèn)是以盡可能接收生產(chǎn)環(huán)境項(xiàng)目的腳手架疾呻,誠(chéng)如我在 淺談Angular網(wǎng)絡(luò)請(qǐng)求 描述的那樣除嘹,不應(yīng)該編寫(xiě)一個(gè)簡(jiǎn)單的 Hello World 請(qǐng)求來(lái)校驗(yàn)是否可用。
在開(kāi)始之前岸蜗,需要先了解 Angular 環(huán)境變更配置尉咕,它位于 fe/src/environments
目錄中,每一個(gè)文件表示一種環(huán)境璃岳,他們有者相同參數(shù)年缎,但其值可能各不同。
若是你跟著本文來(lái)做的話铃慷,那么相對(duì)應(yīng)的是 environment.hmr.ts
单芜,我們修改這里的 SERVER_URL
值為 https://localhost:5001/api/
。這是表示所有請(qǐng)求都會(huì)在請(qǐng)求URL前自動(dòng)加上該地址犁柜。
1洲鸠、APP_INITIALIZER
Angular 啟用前我們能做的事只有這里,腳手架默認(rèn)實(shí)現(xiàn)了 StartupService
(位于:fe/src/app/core/startup/
下),當(dāng)然默認(rèn)代碼并不可用扒腕,我們將其修改為:
load(): Promise<any> {
// only works with promises
// https://github.com/angular/angular/issues/15088
return new Promise((resolve, reject) => {
this.httpClient.get('values').subscribe(
(res: any) => {
this.injector.get(NzMessageService).success(JSON.stringify(res));
},
() => {},
() => {
resolve(null);
},
);
});
}
這里請(qǐng)求是 api/values
绢淀,但由于我們給 Angular 環(huán)境變量統(tǒng)一配置 URL 前綴,因此只需要一個(gè)簡(jiǎn)單的 values
為請(qǐng)求 URL瘾腰。(注:若請(qǐng)求URL地址不是期望結(jié)果皆的,需要重新運(yùn)行 npm run hmr
)
此時(shí),你訪問(wèn)前端時(shí)會(huì)自動(dòng)跳轉(zhuǎn)至 /passport/login
登錄頁(yè)蹋盆,這一切都是由于 @delon/auth
用戶認(rèn)證模塊在管理的费薄,我們沒(méi)有寫(xiě)任何一行關(guān)于前端校驗(yàn)的代碼。
2栖雾、登錄頁(yè)
登錄示例頁(yè)的大部分代碼是可用的楞抡,但本文并不關(guān)心這一些,我們修改其中發(fā)送請(qǐng)求部分如下:
// mock http
this.loading = true;
this.http.get('passport/1').subscribe((res: any) => {
this.loading = false;
// 清空路由復(fù)用信息
this.reuseTabService.clear();
// 設(shè)置Token信息
this.tokenService.set({
token: res.data,
});
// 重新獲取 StartupService 內(nèi)容岩灭,若其包括 User 有關(guān)的信息的話
this.startupSrv.load().then(() => this.router.navigate(['/']));
// 否則直接跳轉(zhuǎn)
// this.router.navigate(['/']);
});
請(qǐng)求 passport/1
返回用戶 Token拌倍,并把 Token 值寫(xiě)入 TokenService
中,最后跳轉(zhuǎn)至儀表盤(pán)頁(yè)噪径。
登錄成功后柱恤,你還會(huì)接收到一個(gè)條 [ "value1", "value2" ]
的消息,這是來(lái)自 APP_INITIALIZER
產(chǎn)生的找爱,至少表明我們能正常訪問(wèn)到 values
梗顺,因?yàn)榇藭r(shí)你會(huì)發(fā)現(xiàn)該主體的 Header
已經(jīng)包含了 token: asdf
字樣。
3车摄、小結(jié)
關(guān)于前端部分我簡(jiǎn)略的描述寺谤,這里還有更多細(xì)節(jié),例如:響應(yīng)體直接返回 data
的內(nèi)容吮播,更多細(xì)節(jié)自行挖掘变屁。
四、部署
示例中的 .net core 部分是單純的 Web API 項(xiàng)目意狠,由此不存在任何頁(yè)面之說(shuō)粟关。而如何讓 .net core 項(xiàng)目直接以 Angular 項(xiàng)目來(lái)訪問(wèn)呢?這需要解決兩個(gè)問(wèn)題环戈。
- 前端打包至
wwwroot
目錄里 - .net core 默認(rèn)以
index.html
默認(rèn)訪問(wèn)頁(yè)
1闷板、打包前端
這里一個(gè)簡(jiǎn)單的辦法是修改 angular.json
的輸出路徑(當(dāng)然也可以直接命令行里直接指定):
"outputPath": "../be/wwwroot",
最后,執(zhí)行:
npm run bash
打包后的文件會(huì)直接存放至 be/wwwroot
目錄下院塞。
2遮晚、打包后端
默認(rèn)情況下需要開(kāi)啟靜態(tài)資源訪問(wèn)能力并且指定默認(rèn)頁(yè)為 index.html
,在 Startup.cs
增加:
var options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("index.html");
app.UseDefaultFiles(options);
app.UseStaticFiles();
此時(shí)拦止,若你再訪問(wèn) https://localhost:5001/ 會(huì)發(fā)現(xiàn)我們啟動(dòng)的是一個(gè) ng-alain 項(xiàng)目县遣。
而對(duì)于后端的打包,也非常簡(jiǎn)單:
dotnet publish -o ../dist
最終,直接將根目錄下的 dist
部署至 Web 服務(wù)器上艺玲。
五括蝠、總結(jié)
本文算是對(duì) 淺談Angular網(wǎng)絡(luò)請(qǐng)求 進(jìn)一步實(shí)踐,雖然一切都采用簡(jiǎn)化的代碼來(lái)解釋?zhuān)傮w的流程是等同的饭聚。
基于 .net core 為后端是出于群里反應(yīng)很多項(xiàng)目都是使用 .net,可能是 .net framework 版本搁拙,但本質(zhì)上是相同秒梳。示例我是以 vscode 編寫(xiě),可能后端的格式會(huì)有些同 vs 不同箕速。
(完)