扯淡
經過不少日夜的趕工,Chloe 的官網於上周正式上線。上篇博客中LZ說過要將官網以及后台源碼都會開放出來,為了盡快兌現我說過的話,趁周末,我稍微整理了一下項目的源碼,就今兒毫無保留的開放給大家,希望能對大家有幫助,獻丑了。
項目不大,官網就是幾個展示頁面。后台借鑒了 NFine 的設計,直接套用了 NFine 的前端模版以及他的一些權限設計。由於個人開發習慣和所用技術與 NFine 的有些不同,比如,NFine 前端數據展示用 jqgrid,而我比較傾向於使用 knockoutjs,同時,后端我也根據自己的一些思想搭建了一個與 NFine 完全不同的架構,所以,基本重寫了 NFine 的前端開發模式和后端架構。目前可以在4個數據庫間無縫切換(SqlServer、MySql、Oracle和SQLite),主要得益於強大的 Chloe^_^。
后台:
在線體驗地址
官網: http://www.52chloe.com/
后台:http://www.52chloe.com:82/
項目介紹
前端主要技術
jQuery:舉世聞名的前端類庫
knockout.js:MVVM 框架,類似 AngularJS 和 vue.JS
bootstrap:家喻戶曉的css
editormd:一個免費開源的 markdown 編輯器,主要用於文檔編輯,很好用,推薦
layer:一款近年來備受青睞的web彈層組件。這個還是 NFine 本來就有的,不錯的一個組件,我也直接用上了
jquery-plugin-validation:前端數據庫驗證插件,也是 NFine 本來就有的,很不錯,我也保留了下來
后端是用asp.net mvc 5,Newtonsoft.Json,Validator,數據庫訪問毫無疑問是用 Chloe.ORM。
開發人員都喜歡給自己的框架安個名字,我也不例外,我這個將就叫做Ace吧(海賊王里的艾斯)。我們先看下整個項目架構:
很常規的分層,簡單介紹下各層:
Ace:項目架構基礎層,有點類似abp里面的abp.dll,里面包含了一些基礎接口的定義,如應用服務接口,以及很多重用性高的代碼。同時,我在這個文件夾下建了Ace.Web和Ace.Web.Mvc兩個dll,分別是對asp.net和asp.net mvc的一些公共擴展和通用的方法。這一層里的東西,基本都是重用度極高的代碼,而且是比較方便移植的。
Application:應(業)用(務)服(邏)務(輯)層。
Data:數據層。包含實體類和ORM操作有關的基礎類。
Web:所謂的展示層。
整個框架並沒有使用repository,原因有三:
* 沒理解錯的話repository是DDD領域驅動設計里的概念,我這個並不是DDD,我不會為了封裝而封裝。
* 不是DDD也可以用repository啊!沒錯,不是DDD也可以有repository。repository設計可以隱藏數據的來源細節,但如果用repository包裝了一遍數據庫訪問組件(ORM),很可能因為一些所謂的設計“規范”會限制ORM框架的發揮,這不是我想要的結果。比如強大的EF,本身對連接查詢以及各種功能支持是很好,但經過倉儲包裝后,可能變得不夠靈活了,不知道您是否疑惑包裝后該怎么多表連接查詢,怎么調用存儲過程,我想實現xxx怎么寫好?我個人覺得,如果一個設計給開發帶來了一定的困擾,就應該考慮設計是否應該存在或合理不合理了。引入一個框架,我們就應該要把它應有的功能發揮的淋漓盡致,不然不如不用。
* repository可以更加方便的切換數據庫訪問框架啊!這個確實有一定道理,想得很前瞻。然而,我不予考慮。因為得不償失的行為。為什么這樣說?如果項目真的需要換ORM,無非兩個原因:1.所用的ORM數據庫類型支持的少,而項目要換數據庫類型,ORM卻不支持新數據庫了;2.所用ORM技術太老,就是想換個其他的ORM;3.所用ORM出現了性能瓶頸,拖程序后腿了。如果因為第一個原因而換數據庫,那么更多的應該反省項目之初技術選型是否正確,因為Chloe已經友好支持支持了多數據庫,所以這個原因在目前我們的項目里不會存在了。第二、三個原因分析:一個項目如果真的需要更換技術,肯定得對代碼重構,試想一下,一個項目從開始到完成整個開發周期長還是重構的時間長?前面也說過,對ORM封裝了一遍,多多少少會限制了ORM的發揮,開發的時候多多少少不順,如果為了短時間(相對開發周期)重構的舒適而讓整個開發過程飽受各種不爽、不順折磨,我真的不樂意。再一個,項目一跑起來到項目被淘汰,很可能壓根就不會更換ORM,所以,我覺得從方便切換ORM的角度考慮,使用倉儲是得不償失的行為。
以上只是個人對倉儲的一些理解,並不完全正確,如有不同看法,還望各位留言探討。
層層之間需要解耦,引用了什么IOC框架嗎?No!IOC很“高大上”,實質就是一個超級大工廠,特色功能就是依賴注入,說白了就是支持有參構造函數創建對象和屬性賦值。然而,我個人不大喜歡用注入,因此,我覺得沒必要引入IOC框架了。所以我自己建了個超級工廠,充當了IOC容器的角色。
大概實現如下:

public class AppServiceFactory : IAppServiceFactory { List<IAppService> _managedServices = new List<IAppService>(); static readonly Type[] InjectConstructorParamTypes = new Type[] { typeof(IAppServiceFactoryProvider) }; static readonly object[] EmptyObjects = new object[0]; static List<Type> _serviceTypes = new List<Type>(); public AppServiceFactory() : this(null) { } public AppServiceFactory(IAceSession session) { this.Session = session; } public IAceSession Session { get; set; } public void AttachService(IAppService service) { this._managedServices.Add(service); } public void DetachService(IAppService service) { this._managedServices.Remove(service); } public T CreateService<T>(bool managed = true) where T : IAppService { Type t = typeof(T); ConstructorInfo c = null; bool injectAppServiceFactoryProvider = false; /* List 的查找效率和反射創建對象的性能稍微差點,如有必要,需要優化 */ _serviceTypes.Find(a => { if (a.IsAbstract == true) return false; if (t.IsAssignableFrom(a) == false) return false; c = a.GetConstructor(Type.EmptyTypes); injectAppServiceFactoryProvider = false; if (c == null) { c = a.GetConstructor(InjectConstructorParamTypes); injectAppServiceFactoryProvider = true; } if (c == null) { return false; } return true; }); if (c == null) throw new Exception("Can not find the service implementation."); T service = default(T); if (injectAppServiceFactoryProvider == false) { service = (T)c.Invoke(EmptyObjects); IAppServiceFactoryProvider factoryProvider = service as IAppServiceFactoryProvider; if (factoryProvider != null) { factoryProvider.ServiceFactory = this; } } else { service = (T)c.Invoke(new Object[1] { this }); } IAceSessionAppService sessionService = service as IAceSessionAppService; if (sessionService != null) sessionService.AceSession = this.Session; if (managed == true) this.AttachService(service); return service; throw new NotImplementedException(); } public void Dispose() { foreach (var service in this._managedServices) { if (service != null) service.Dispose(); } } public static T CreateAppService<T>() where T : IAppService { using (AppServiceFactory factory = new AppServiceFactory()) { return factory.CreateService<T>(); } } public static void Register<T>() { Register(typeof(T)); } public static void Register(Type t) { if (t == null || t.IsAbstract || typeof(IAppService).IsAssignableFrom(t) == false) throw new ArgumentException(); lock (_serviceTypes) { _serviceTypes.Add(t); } } public static void RegisterServices() { Assembly assembly = Assembly.GetExecutingAssembly(); RegisterServicesFromAssembly(assembly); } public static void RegisterServicesFromAssembly(Assembly assembly) { var typesToRegister = assembly.GetTypes().Where(a => { var b = a.IsAbstract == false && a.IsClass && typeof(IAppService).IsAssignableFrom(a) && a.GetConstructor(Type.EmptyTypes) != null; return b; }); foreach (var type in typesToRegister) { Register(type); } } }
程序啟動的時候,在啟動事件(Application_Start)里會將所有應用服務的實現類給注冊進去。這個工廠實現了 IDisposable 方法,因為有些應用服務用完是需要銷毀的,所以,這個工廠創建出對象的同時,還承擔銷毀應用服務對象的職責。
對象到對象的映射框架呢?也No,DTO和實體間轉換目前我是傻呼呼的一個一個屬性賦值搞的,但以后有可能會引入。
當今流行的repository、IOC和OOM等技術都不用,大家會不會覺得LZ很奇葩或者out了?嘿嘿,我的理念是能減少依賴就盡量減少依賴。減少學習成本同時也可以更好的移植或掌控項目。
對於一個開發框架而言,最基本的就是規范和避免重復編碼。雖然我的這個框架簡單,但不乏實用技巧點。
框架的一些規范與技巧設計:
ajax請求:對於系統后台,ajax交互是很頻繁的事。對於ajax請求的返回數據我制定了基本的狀態碼和格式
public enum ResultStatus { OK = 100, Failed = 101, NotLogin = 102, Unauthorized = 103, }
public class Result { ResultStatus _status = ResultStatus.OK; public Result() { } public Result(ResultStatus status) { this._status = status; } public Result(ResultStatus status, string msg) { this.Status = status; this.Msg = msg; } public ResultStatus Status { get { return this._status; } set { this._status = value; } } public object Data { get; set; } public string Msg { get; set; } }
序列化成json大概是這樣子:{"Data":{},"Status":100,"Msg":null}
同時,為了避免頻繁new這個Result類,我在web層的 BaseController 中增加了很多有關方法:
protected ContentResult JsonContent(object obj) { string json = JsonHelper.Serialize(obj); return base.Content(json); } protected ContentResult SuccessData(object data = null) { Result<object> result = Result.CreateResult<object>(ResultStatus.OK, data); return this.JsonContent(result); } protected ContentResult SuccessMsg(string msg = null) { Result result = new Result(ResultStatus.OK, msg); return this.JsonContent(result); } protected ContentResult AddSuccessData(object data, string msg = "添加成功") { Result<object> result = Result.CreateResult<object>(ResultStatus.OK, data); result.Msg = msg; return this.JsonContent(result); } protected ContentResult AddSuccessMsg(string msg = "添加成功") { return this.SuccessMsg(msg); } protected ContentResult UpdateSuccessMsg(string msg = "更新成功") { return this.SuccessMsg(msg); } protected ContentResult DeleteSuccessMsg(string msg = "刪除成功") { return this.SuccessMsg(msg); } protected ContentResult FailedMsg(string msg = null) { Result retResult = new Result(ResultStatus.Failed, msg); return this.JsonContent(retResult); }
這樣,我們在Action中可以直接這樣用:
[HttpGet] public ActionResult GetModels(Pagination pagination, string keyword) { PagedData<Sys_User> pagedData = this.CreateService<IUserAppService>().GetPageData(pagination, keyword); return this.SuccessData(pagedData); } [HttpPost] public ActionResult Add(AddUserInput input) { this.CreateService<IUserAppService>().AddUser(input); return this.AddSuccessMsg(); } [HttpPost] public ActionResult Update(UpdateUserInput input) { this.CreateService<IUserAppService>().UpdateUser(input); return this.UpdateSuccessMsg(); } [HttpPost] public ActionResult Delete(string id) { this.CreateService<IUserAppService>().DeleteAccount(id); return this.DeleteSuccessMsg(); } [HttpPost] public ActionResult RevisePassword(string userId, string newPassword) { if (userId.IsNullOrEmpty()) return this.FailedMsg("userId 不能為空"); this.CreateService<IUserAppService>().RevisePassword(userId, newPassword); return this.SuccessMsg("重置密碼成功"); }
通過vs強大的智能提示,直接 this. 就可以出來,省了不少事。
盡量"少的使用using":
在.net的規范中,對於任何實現了 IDisposable 接口的類,用完我們都應將其銷毀掉。在項目開發中,如果充斥着太多的 using 寫法,我是非常煩的,我想大家也是同樣的感受,比如 DbContext。但如何避免使用 using,但又保證對象最終又被銷毀呢?如果大家用了一些IOC框架,估計不需要太多的關心對象的銷毀問題,因為IOC容器幫大家做了這些事。我不用IOC,那該咋辦呢?其實不難,就拿 DbContext 舉例。在我這個架構中,應用服務層是直接操作 DbContext 的,我建了個應用服務基類(AppServiceBase),DbContext 的創建和銷毀交給 AppServiceBase 就行了。因此,這又引申了一個規范,每個應用服務必須實現 IDisposable 接口(一個類內部如果有 IDisposable 的對象,我覺得該類也應該實現 IDisposable 接口)!
public abstract class AppServiceBase : IAppServiceFactoryProvider, IDisposable { IDbContext _dbContext; protected AppServiceBase() : this(null) { } protected AppServiceBase(IAppServiceFactory serviceFactory) { this.ServiceFactory = serviceFactory; } public IAppServiceFactory ServiceFactory { get; set; } public IDbContext DbContext { get { if (this._dbContext == null) this._dbContext = DbContextFactory.CreateContext(); return this._dbContext; } set { this._dbContext = value; } } public void Dispose() { if (this._dbContext != null) { this._dbContext.Dispose(); } this.Dispose(true); } protected virtual void Dispose(bool disposing) { } }
然后子類應用服務就可以直接 this.DbContext 這樣毫無顧忌的使用 DbContext 了,再也不用關心 DbContext 是如何創建和被銷毀了。技巧雖小,卻給我開發便利了許多。
所有應用服務是由前面提到的超級工廠創建的,所以,我的超級工廠也需要實現 IDisposable 接口,目的就是為了掌管其創建出的應用服務對象。那么問題來了,LZ你使用的超級工廠對象的時候還不是得要銷毀你的超級工廠,不用 using 怎么做?哈哈,這個問題用同樣的基類方式就可以解決。
不知道大家是否留意MVC的 Controller 這個類也實現了 IDisposable 接口,它提供了一個 Dispose(bool disposing) 方法的重載!對於這個重載方法,我們好好利用它就行,嘿嘿~
我們建的 Controller 都會繼承於我們自定義的 BaseController 中,BaseController 則重寫了 Dispose(bool disposing) 方法,用於銷毀一些我們自定義的 IDisposable 對象:
public abstract class BaseController : Controller { static readonly Type TypeOfCurrent = typeof(BaseController); static readonly Type TypeOfDisposableAttribute = typeof(DisposableAttribute); protected override void Dispose(bool disposing) { base.Dispose(disposing); this.DisposeMembers(); } [Disposable] AppServiceFactory _appServicesFactory; IAppServiceFactory AppServicesFactory { get { if (this._appServicesFactory == null) this._appServicesFactory = new AppServiceFactory(this.CurrentSession); return this._appServicesFactory; } } protected T CreateService<T>(bool managed = true) where T : IAppService { return this.AppServicesFactory.CreateService<T>(managed); } /// <summary> /// 掃描對象內所有帶有 DisposableAttribute 標記並實現了 IDisposable 接口的屬性和字段,執行其 Dispose() 方法 /// </summary> void DisposeMembers() { Type type = this.GetType(); List<PropertyInfo> properties = new List<PropertyInfo>(); List<FieldInfo> fields = new List<FieldInfo>(); Type searchType = type; while (true) { properties.AddRange(searchType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(a => { if (typeof(IDisposable).IsAssignableFrom(a.PropertyType)) { return a.CustomAttributes.Any(c => c.AttributeType == TypeOfDisposableAttribute); } return false; })); fields.AddRange(searchType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(a => { if (typeof(IDisposable).IsAssignableFrom(a.FieldType)) { return a.CustomAttributes.Any(c => c.AttributeType == TypeOfDisposableAttribute); } return false; })); if (searchType == TypeOfCurrent) break; else searchType = searchType.BaseType; } foreach (var pro in properties) { IDisposable val = pro.GetValue(this) as IDisposable; if (val != null) val.Dispose(); } foreach (var field in fields) { IDisposable val = field.GetValue(this) as IDisposable; if (val != null) val.Dispose(); } } }
注意看,上面的 BaseController 有一個 CreateService 方法,用於創建應用服務對象。然后呢,子類的操作方法中,我們就可以毫無顧忌的調用 this.CreateService 方法創建相關的應用服務了:
[HttpPost] public ActionResult Add(AddUserInput input) { this.CreateService<IUserAppService>().AddUser(input); return this.AddSuccessMsg(); }
如果您從github下載了源碼就會發現,源碼中using關鍵字少之又少~
管理每個View對應的js文件:
對於后台開發,基本上每個頁面都需要js支持,但我們又不想js代碼和html放在一個窗口里開發,所以大家都習慣建一個js文件,然后在頁面中引用就行了。在 webform 時代,我們都習慣將頁面和其對應的js文件放在同一個文件夾中,方便!!但在mvc默認設置中,views文件夾下是不允許放js文件的,這可咋辦呢??雖然有設置可以讓views文件夾里的js文件可以被訪問,但,我不想打破mvc的默認規則!因此,我用了個投機取巧的辦法,就是利用mvc的部分視圖功能。我把js代碼寫在部分視圖中,然后頁面中直接以@Html.Partial("Index-js")的方式引用,如下:
這樣一個頁面就能和它對應的js文件成雙對的在同一個文件夾中了,實現了js代碼和 html 代碼可以分開!但有一點要注意,最終的頁面輸出到瀏覽器的時候 html 和 js 代碼是混在一起的!那么問題來了,你這樣干會影響頁面加載速度,並且沒能充分利用瀏覽器對靜態資源的緩存啊~在一些技術群里,我跟他們說出我的這個設想和做法時,有不少人提出了這樣的質疑~其實,對於一個后台頁面而言,這點影響不足為懼,並沒有想象中那么大。這其實是個智仁問題,我也不多解釋了,利弊自己權衡就好。沒有絕對好的設計,只要實用和適合自己就夠了!~
結語
每個人都夢想有個屬於自己的開發框架,我也一樣。目前的Ace框架雖然簡單,在大牛面前不值一提,但還是希望能對一些人有幫助。
官網使用的是 SQLite 數據庫,GitHub 項目中已經有了相應的db文件,只要你本地裝了asp.net環境,下載即可運行。考慮到很多同學對 SQLite 不熟悉,以及大家裝的是 SqlServer、MySql 或 Oracle,我也給大家准備好了所有的相關dll以及相應的數據庫腳本,只要建個數據庫,然后運行腳本就可以創建相關的表了。框架已經支持4種數據庫之間隨意切換,所以,大家在web.config里設置下數據庫類型和數據庫連接字符串就可以運行了(web.config都有修改說明)。試問,世上這么熱心的程序員,除了博主還有誰?如果這都換不來您的一個推薦,博主我真的無語問蒼天,是時候考慮關閉博客了555~
技術教程或心得我倒不是很擅長寫,我只想把日常開發的一些干貨分享給大家,您的推薦是我分享的最大動力。同時,如果您對 Chloe 這個項目感興趣,敬請在 Github 關注或收藏(star)一下,以便能及時收到更新通知。也歡迎廣大C#同胞入群交流,暢談.NET復興大計。最后,感謝大家閱讀至此!同時也非常感謝 NFine 作者給咱們提供了一個那么好的一個開發框架!
Chloe 官網及后台源碼地址:https://github.com/shuxinqin/Ace