[乾貨]Chloe官網及基於NFine的後臺源碼毫無保留開放

扯淡

通過很多日夜的趕工,Chloe 的官網於上週正式上線。上篇博客中LZ說過要將官網以及後臺源碼都會開放出來,爲了儘快兌現我說過的話,趁週末,我稍微整理了一下項目的源碼,就今兒毫無保留的開放給你們,但願能對你們有幫助,獻醜了。css

項目不大,官網就是幾個展現頁面。後臺借鑑了 NFine 的設計,直接套用了 NFine 的前端模版以及他的一些權限設計。因爲我的開發習慣和所用技術與 NFine 的有些不一樣,好比,NFine 前端數據展現用 jqgrid,而我比較傾向於使用 knockoutjs,同時,後端我也根據本身的一些思想搭建了一個與 NFine 徹底不一樣的架構,因此,基本重寫了 NFine 的前端開發模式和後端架構。目前能夠在4個數據庫間無縫切換(SqlServer、MySql、Oracle和SQLite),主要得益於強大的 Chloe^_^。html

後臺:前端

houtai

在線體驗地址
官網: http://www.52chloe.com/  
後臺:http://www.52chloe.com:82/vue

項目介紹

前端主要技術
jQuery:聞名中外的前端類庫
knockout.js:MVVM 框架,相似 AngularJS 和 vue.JS
bootstrap:家喻戶曉的css
editormd:一個免費開源的 markdown 編輯器,主要用於文檔編輯,很好用,推薦
layer:一款近年來備受青睞的web彈層組件。這個仍是 NFine 原本就有的,不錯的一個組件,我也直接用上了
jquery-plugin-validation:前端數據庫驗證插件,也是 NFine 原本就有的,很不錯,我也保留了下來jquery

後端是用asp.net mvc 5,Newtonsoft.Json,Validator,數據庫訪問毫無疑問是用 Chloe.ORM。git

開發人員都喜歡給本身的框架安個名字,我也不例外,我這個將就叫作Ace吧(海賊王裏的艾斯)。咱們先看下整個項目架構: 程序員

chloesite

很常規的分層,簡單介紹下各層:
Ace:項目架構基礎層,有點相似abp裏面的abp.dll,裏面包含了一些基礎接口的定義,如應用服務接口,以及不少重用性高的代碼。同時,我在這個文件夾下建了Ace.Web和Ace.Web.Mvc兩個dll,分別是對asp.net和asp.net mvc的一些公共擴展和通用的方法。這一層裏的東西,基本都是重用度極高的代碼,並且是比較方便移植的。
Application:應(業)用(務)服(邏)務(輯)層。
Data:數據層。包含實體類和ORM操做有關的基礎類。
Web:所謂的展現層。github

整個框架並無使用repository,緣由有三:
* 沒理解錯的話repository是DDD領域驅動設計裏的概念,我這個並非DDD,我不會爲了封裝而封裝。 web

* 不是DDD也能夠用repository啊!沒錯,不是DDD也能夠有repository。repository設計能夠隱藏數據的來源細節,但若是用repository包裝了一遍數據庫訪問組件(ORM),極可能由於一些所謂的設計「規範」會限制ORM框架的發揮,這不是我想要的結果。好比強大的EF,自己對鏈接查詢以及各類功能支持是很好,但通過倉儲包裝後,可能變得不夠靈活了,不知道您是否疑惑包裝後該怎麼多表鏈接查詢,怎麼調用存儲過程,我想實現xxx怎麼寫好?我我的以爲,若是一個設計給開發帶來了必定的困擾,就應該考慮設計是否應該存在或合理不合理了。引入一個框架,咱們就應該要把它應有的功能發揮的淋漓盡致,否則不如不用。 ajax

* repository能夠更加方便的切換數據庫訪問框架啊!這個確實有必定道理,想得很前瞻。然而,我不予考慮。由於得不償失的行爲。爲何這樣說?若是項目真的須要換ORM,無非兩個緣由:1.所用的ORM數據庫類型支持的少,而項目要換數據庫類型,ORM卻不支持新數據庫了;2.所用ORM技術太老,就是想換個其餘的ORM;3.所用ORM出現了性能瓶頸,拖程序後腿了。若是由於第一個緣由而換數據庫,那麼更多的應該檢討項目之初技術選型是否正確,由於Chloe已經友好支持支持了多數據庫,因此這個緣由在目前咱們的項目裏不會存在了。第2、三個緣由分析:一個項目若是真的須要更換技術,確定得對代碼重構,試想一下,一個項目從開始到完成整個開發週期長仍是重構的時間長?前面也說過,對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);
        }
    }
}
View Code

 

程序啓動的時候,在啓動事件(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")的方式引用,以下:

view-js

image

這樣一個頁面就能和它對應的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

相關文章
相關標籤/搜索