前言
自從寫完上次略長的《用ABP入門DDD》後,針對ABP框架的專案模板初始化,我寫了個命令列工具Abp-CLI,其中子命令abplus init
可以從github拉取專案模板以初始化專案。自然而然的,又去處理了aspnetboilerplate/module-zero-core-template這個專案模板庫當中的vue專案模板,解決以前發現的,又貌似一直沒人修復的幾個問題PR362,PR366,PR367。
在更新vue專案模板的示例程式碼時,感覺有必要講解下ABP中的AsyncCrudAppService<>
怎麼用。
跟我做
先自賣自誇一下,只要你本地有裝dotnet環境,就可以跟著我一步一步來做。
安裝dotnet core全域性工具:AbpTools
可以在任意目錄下,開啟powershell命令視窗(按住Shift鍵的同時滑鼠右鍵點選目錄空白處,可以看到右鍵選單在此處開啟Powershell視窗
),執行以下命令安裝AbpTools:
dotnet tool install -g AbpTools
如果安裝成功,會提示你已經可以使用abplus命令了。
可以通過以下命令檢視已安裝了哪些dotnet core全域性工具:
PS$>dotnet tool list -g
包 ID 版本 命令
-------------------------------
abptools 1.0.4 abplus
目前是1.0.4版。
初始化專案Personball.CrudDemo
在powershell命令視窗中選擇一個放程式碼的目錄(用cd命令),執行以下命令初始化專案:
abplus init Personball.CrudDemo -T personball/module-zero-core-template@v4.2.2
由於撰寫本文時預設的專案模板庫aspnetboilerplate/module-zero-core-template雖然合併了上述的PR362、PR366、PR367,但還沒Release下一版本,所以暫時通過-T
指定使用我自己修復過的vue專案模板。
執行起來看看
先執行 Api Host:
- 用VS2017開啟
Personball.CrudDemo/aspnet-core
目錄下的Personball.CrudDemo.sln
- 右鍵點選
Personball.CrudDemo.Web.Mvc
,移除 - 右鍵點選
Personball.CrudDemo.Web.Host
,設為啟動項 - 在VS2017的檢視選單中選擇SQL Server 物件資源管理器,展開
SQL Server>(localdb)\...(localdb特定版本的例項名)
,右鍵資料庫,選擇新增新資料庫,資料庫名稱輸入CrudDemoDb
,確認。 - 右鍵
CrudDemoDb
資料,點選屬性,找到連線字串,複製下來,貼上替換Personball.CrudDemo.Web.Host
專案的appsettings.json
配置檔案的ConnectionStrings
配置下的Default
值。 - 開啟程式包管理器控制檯,預設專案選
Personball.CrudDemo.EntityFrameworkCore
,輸入Update-Database
- 按F5執行
繼續執行前端vue專案(需要nodejs和npm):
- 用VSCode開啟
Personball.CrudDemo/vue
目錄 - 在VsCode的終端視窗中執行
yarn install
- Install完成後,執行
yarn serve
先進後臺,體驗下功能
- 開啟瀏覽器,輸入剛才
yarn serve
提示的訪問地址,預設是http://localhost:8080 - 輸入預設賬號admin,密碼123qwe,租戶空著不選
- 登陸成功後,展開左側選單,選擇使用者
這樣我們就到了後臺的使用者列表,可以先試試輸入查詢條件,試一下列表查詢功能。
加斷點,再試一下
切換到VS2017,我們加個斷點
- 展開Personball.CrudDemo.Application專案,展開Users目錄,找到
UserAppService
- 找到
CreateFilteredQuery
方法,在return語句的地方加個斷點,再到後臺裡試一下使用者列表查詢。
接下來看程式碼
以後端程式碼UserAppService
和前端vue模板中的src/views/setting/user/user.vue
為例:
後臺接收列表查詢引數使用的是PagedUserResultRequestDto
,繼承自PagedResultRequestDto
,加上了UI介面所需的一些自定義查詢條件屬性:
public class PagedUserResultRequestDto : PagedResultRequestDto
{
public string UserName { get; set; }
public string Name { get; set; }
public bool? IsActive { get; set; }
public DateTimeOffset? From { get; set; }//javascript date within timezone
public DateTimeOffset? To { get; set; }//javascript date within timezone
}
而前端在user.vue
檔案中,使用PageUserRequest
和後端的DTO對應:
class PageUserRequest extends PageRequest{
userName:string;
name:string;
isActive:boolean=null;//nullable
from:Date;
to:Date;
}
這裡可以直接用PageUserRequest
型別的前端變數做UI控制元件繫結:
pagerequest:PageUserRequest=new PageUserRequest();
creationTime:Date[]=[];//時間範圍控制元件的值繫結另外處理
<FormItem :label="L('UserName')+':'" style="width:100%">
<Input v-model="pagerequest.userName"></Input>
</FormItem>
對於複雜的,比如時間範圍控制元件(上面的creationTime),再另外處理:
async getpage(){
//set page parameters
this.pagerequest.maxResultCount=this.pageSize;
this.pagerequest.skipCount=(this.currentPage-1)*this.pageSize;
//filters
if (this.creationTime.length>0) {
this.pagerequest.from=this.creationTime[0];
}
if (this.creationTime.length>1) {
this.pagerequest.to=this.creationTime[1];
}
await this.$store.dispatch({
type:'user/getAll',
data:this.pagerequest
})
}
前端整合了typescript的vue程式碼的用法基本介紹到這,主要是前一版的vue專案模板中出現了在前端程式碼裡組裝where條件的情況。所以說明下,以免後端真的去處理where字串可能引起SQL隱碼攻擊問題。
我們繼續回到後端程式碼。
AsyncCrudAppService說明
ABP作為開發框架,非常優秀的一個地方,就是作者對DRY的追求。
對於CRUD這種通用功能,必須要有一個解決方案,這就有了泛型版的應用服務基類CrudAppService<>
。
我們先看下這個基類上有哪些成員:
namespace Abp.Application.Services
{
public abstract class CrudAppServiceBase<TEntity, TEntityDto, TPrimaryKey, TGetAllInput, TCreateInput, TUpdateInput> :
ApplicationService
where TEntity : class, IEntity<TPrimaryKey>
where TEntityDto : IEntityDto<TPrimaryKey>
where TUpdateInput : IEntityDto<TPrimaryKey>
{
protected readonly IRepository<TEntity, TPrimaryKey> Repository;
protected CrudAppServiceBase(IRepository<TEntity, TPrimaryKey> repository);
protected virtual string CreatePermissionName { get; set; }
protected virtual string GetAllPermissionName { get; set; }
protected virtual string GetPermissionName { get; set; }
protected virtual string UpdatePermissionName { get; set; }
protected virtual string DeletePermissionName { get; set; }
protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TGetAllInput input);
protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TGetAllInput input);
protected virtual void CheckCreatePermission();
protected virtual void CheckDeletePermission();
protected virtual void CheckGetAllPermission();
protected virtual void CheckGetPermission();
protected virtual void CheckPermission(string permissionName);
protected virtual void CheckUpdatePermission();
protected virtual IQueryable<TEntity> CreateFilteredQuery(TGetAllInput input);
protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity);
protected virtual TEntity MapToEntity(TCreateInput createInput);
protected virtual TEntityDto MapToEntityDto(TEntity entity);
}
}
其中的泛型引數,依次說明如下:
- TEntity:CRUD操作對應的實體類
- TEntityDto:GetAll方法返回的實體DTO
- TPrimaryKey:實體的主鍵
- TGetAllInput:GetAll方法接收的輸入引數
- TCreateInput:Create方法接收的輸入引數
- TUpdateInput:Update方法接收的輸入引數
從上面我們還可以看到有關於許可權(xxxPermissionName
屬性和CheckxxxPermission
方法),關於分頁(ApplyPaging
),關於排序(ApplySorting
),關於查詢條件(CreateFilteredQuery
),關於物件對映(MapToxxx
),所有CRUD涉及的環節都提供了擴充套件點(方法是virtual,可以override)。
所以對於單頁後臺來說,基於CrudAppServiceBase實現CRUD功能非常簡便,而且很容易擴充套件定製。
以前面說的UserAppService
為例,它繼承AsyncCrudAppService<>
(AsyncCrudAppService繼承了上面的CrudAppServiceBase,提供了非同步版本的CRUD介面實現)。除了IUserAppService
中額外定義的兩個方法:
Task<ListResultDto<RoleDto>> GetRoles();
Task ChangeLanguage(ChangeUserLanguageDto input);
其他方法都是基於AsyncCrudAppService<>
的可擴充套件點進行自定義以滿足需求。
如果只是一個非常簡單的純資料實體(User還是有不少邏輯的),這個AppService還可以更簡單:
[AbpAuthorize]
public class ArticleAppService : AsyncCrudAppService<Article, ArticleDto, int, PagedArticleResultRequestDto, CreateArticleDto, ArticleDto>, IArticleAppService
{
public ArticleAppService(IRepository<Article, int> repository) : base(repository)
{
LocalizationSourceName = JsxConsts.LocalizationSourceName;
}
protected override IQueryable<Article> CreateFilteredQuery(PagedArticleResultRequestDto input)
{
return Repository.GetAll()
.WhereIf(input.Category.HasValue, a => a.Category == input.Category)
.WhereIf(!input.Keyword.IsNullOrWhiteSpace(), a => a.Title.Contains(input.Keyword) || a.Content.Contains(input.Keyword))
.WhereIf(input.From.HasValue, b => b.CreationTime >= input.From.Value.LocalDateTime)
.WhereIf(input.To.HasValue, b => b.CreationTime <= input.To.Value.LocalDateTime);
}
}
類似這個ArticleAppService
,只要定製下CreateFilteredQuery
中的查詢過濾條件,其他功能程式碼都免了,而CRUD的介面都是完整可以用的。
不說程式碼生成器,只要自定義一個程式碼片段來快速產出這個ArticleAppService,就可以節省很多的敲鍵盤時間,效率是非常高的,關鍵是省事——DRY,Don't Repeat Yourself。
再回到UserAppService
中的PagedUserResultRequestDto
。
這個DTO就是泛型引數中的TGetAllInput
。通過繼承PagedResultRequestDto
,在AsyncCrudAppService基類中的各個涉及方法的簽名裡以OOP多型方式傳遞該引數完全沒有問題。
而TEntityDto
和TUpdateInput
有時候可以共用一個DTO,只要定製好對映關係,問題一般不大。例如Users/Dto/UserMapProfile
中:
CreateMap<UserDto, User>()//use UserDto as TUpdateInput
.ForMember(x => x.Roles, opt => opt.Ignore())
.ForMember(x => x.CreationTime, opt => opt.Ignore())
.ForMember(x => x.LastLoginTime, opt => opt.Ignore());
最後,關於vue專案模板,提兩個注意點
1.時區問題
不管什麼前端框架,可能都會遇到前端提交的JavaScript中的Date型別物件是帶時區的,或者預設是UTC時間。
這個問題,建議在介面接收引數的時候用DateTimeOffset
型別接收,再通過其屬性LocalDateTime
轉為伺服器本地時間使用,當然如果你資料庫直接存了帶時區的時間,那連轉換都免了。
2.iview框架版本問題
原先打算在PR366中降iview的主版本號到^2.13.1
來修復yarn serve
編譯錯誤的問題,後來發現改成~3.0.0
也行得通。
但是本地demo跑起來時發現頁籤的選中樣式還是有點問題,懶得改css的話,可以直接降到^2.13.1
。