天天看點

某電商平台開發記要

本文是部落客在開發某電商平台項目的一些雜項記錄,友善自己和團隊同僚查閱,偏向于具體技術或應用的細節和個人了解,但也未必非常具體。文中未提的更多内容可能會另起篇章。

  • 導航屬性——EF實體關系fluent配置
  • AutoMapper
  • Autofac
  • Repository模式
  • Model & DTO
  • 開源&商用.NET電商平台——NopCommerce(3.9版) & Himall(2.4版)
  • 伺服器搭建-VMware vSphere Hypervisor(esxi)
  • 自動化部署-Jenkins

實體關系——一對一[或零],一對多,多對多——對應到資料庫就是外鍵限制。為了性能及資料遷移考慮,在事務性要求不高的情形中,我們一般都禁用外鍵,但是EF中仍可保留實體關系以友善程式設計。

本文基于EF6.1.3版本。

EF中有兩類關系:Independent association 和 Foreign Key association。在實體定義時可以看出它們的不同。

1 //這是Independent association
 2 public class Order
 3 {
 4     public int ID { get; set; }
 5     public Customer Customer { get; set; } // <-- Customer object
 6     ...
 7 }
 8 
 9 //這是Foreign key association
10 public class Order
11 {
12     public int ID { get; set; }
13     public int CustomerId { get; set; }  // <-- Customer ID
14     public Customer Customer { get; set; } // <-- Customer object
15     ...
16 }      

很明顯看到兩者的差别就在于是否存在外鍵屬性,EF會按照預設規則建構或尋找到正确的外鍵字段。我們也可以顯式配置外鍵,兩種方法:

1 Map(m=>m.MapKey("CustomerId"));
2 HasForeignKey(m=>m.CustomerId);      

Map适用于Independent association,而HasForeignKey用于Foreign Key association。如果在Foreign Key association時使用Map,将會抛出:“CustomerId:Name:類型中的每個屬性名必須唯一,已定義屬性名CustomerId”的錯誤。

需要注意的是,一對一的實體關系,EF并未提供HasForeignKey指定外鍵。why?EF團隊認為,既然兩個實體是一一對應的關系,那麼可以由一個主鍵辨別兩個實體,so,will use its primary key as the foreign key。。。也是醉了。如果硬要指定一個外鍵的話,對于Independent association還好,我們可以用Map,但是Foreign Key association就悲劇了。可以使用WithMany()這個hack,但比較别扭,個人是不推薦這種方法。詳情可參看One to zero-or-one with HasForeignKey。嘗試使用[ForeignKey]特性,也會報錯[比如]:系“CategoryCashDepositInfo_CategoriesInfo”中 Role“CategoryCashDepositInfo_CategoriesInfo_Source”的多重性無效。因為 Dependent Role 屬性不是鍵屬性,Dependent Role 多重性的上限必須為“*”。so,一對一實體的外鍵也必須是它的主鍵,尼瑪。不幸遇到這種問題,在項目初期(一般來說踩坑都是比較早的),最好的方式還是改變資料結構以适應EF要求,畢竟它這麼要求确實有道理。

另:若一實體沒有導航屬性,但是另一實體包含該實體集合的屬性,那麼在生成資料庫時,EF也會自動為它們生成外鍵限制。

在增删改實體時,若有上下文跟蹤,則連帶着實體的導航屬性對應的資料也一并會受影響,比如在更新父子表時,不需要自己寫單獨更細兩張表的代碼,EF都幫我們處理好了。舉個典型例子:

某電商平台開發記要
某電商平台開發記要
public class Journal
{
    public int ID { get; set; }
    public decimal Amount { get; set; }
    public int OrderID { get; set; }
    public BillOrder Order { get; set; }

}

public class BillOrder
{
    public int ID { get; set; }
    public string Title { get; set; }
}

using (var context = new Entities())
{
    var order = new BillOrder { Title="test order" };
    //OrderID =order.ID 有無都一樣,最後資料表裡字段會賦予實際值
    var j = new Journal { Amount=10, Order= order,OrderID =order.ID };
    context.Journals.Add(j);//隻要add主類即可
    context.SaveChanges();
}      

View Code

更多可參看 MVC3+EF4.1學習系列(六)-----導航屬性資料更新的處理

待驗證:一對一時,導航屬性有沒有延遲加載一說?另導航屬性鍊查詢細節,比如Comment.OrderItem.OrderInfo.PayDate,其中OrderItem是Comment的導航熟悉,OrderInfo是OrderItem的導航屬性,這個時候SQL查詢步驟是怎樣的呢?——一對一時,不會自動加載,即擷取父對象後,導航屬性對應的子對象一直為null(不管後續有沒有用到,用到的話會抛出NullReferenceException),但是在擷取父對象時使用Include顯式加載子對象,是可以的。同其它導航屬性一樣,之前測試出現無法加載是因為忘記給導航屬性前面添加virtual關鍵字了。。。

導航屬性的删除更新操作需要特别注意,如果直接将導航屬性指派為新對象,儲存後,資料表中将新增記錄,而原記錄仍然存在,原因顯而易見,這裡不說了。

1 var order = context.BillOrders.First();
2 context.Set<BillOrderSub>().RemoveRange(order.Items);//這步不能少
3 var items = new int[] { 1, 1, 1 }.Select(i => new BillOrderSub()).ToArray();
4 order.Items = items;
5 context.SaveChanges();      

注意要使用DbSet定義的RemoveRange之類的方法,否則會報下面的錯誤

某電商平台開發記要

另:給父對象設定EntityState,并不會自動給導肮實體賦予相同EntityState。删除父對象時,一對一的導航實體不會自動跟着删除;若是一對多的情況,那麼隻要删除父對象,導航實體會自動被删除;多對多的情況未驗證,因為涉及到映射表,推測會自動删除映射關系,即删除映射表裡的相關記錄。經測試,多對多情況,也無法自動删除。

AutoMapper提供的自定義映射——Custom value resolvers 和 Projection,乍看之下似乎差不多,側重解決點是不一樣的,但是它們似乎又通用。。。在使用上,後者MapFrom方法的其中一個重載接收表達式樹(Expression)類型的參數,是以涉及到稍微複雜的語句,可能出現如下圖所示的情況:

某電商平台開發記要

這個時候隻能采用前者的ResolveUsing方法了,如下:

某電商平台開發記要

還有個IValueResolver接口,與IMemberValueResolver的差別在于IValueResolver不指定source類的哪個屬性需要轉換,這就導緻了轉換時自定義邏輯可能要引用source類,如果其它類也有類似轉換,那麼就不能複用了。

6.0.1.0版本,如下寫法,則隻有最後一個Resolver起作用。

某電商平台開發記要

改成下面寫法,則無問題。

某電商平台開發記要

注意到上面opt => opt.ResolveUsing<ShopGradeResolver>(),每次Mapper.Map的時候都會new一個ShopGradeResolver對象,其實是沒必要的,因為隻執行邏輯而狀态無關。是以可改為單例模式:opt => opt.ResolveUsing(Singleton<ShopGradeResolver>.Instance)。

另,當source類有導航屬性時,會在Mapper.Map時去資料庫裡查,是以若用不到該導航屬性則應該設定映射規則時ignore之。

Mapper.Initialize調用多次,最後一次會覆寫前面的,是以如果映射是由各個項目自己處理,那麼應該考慮使用Profile,然後在主項目中 Mapper.Initialize(cfg => cfg.AddProfiles(typeFinder.GetAssemblies())); AutoMapper will scan the designated assemblies for public classes inheriting from Profile and add them to the configuration. 更多請看 Configuration。

Lifetime Scope 和 Instance Scope,我們擷取執行個體,都要先BeginLifetimeScope(),而後根據元件注冊時的InstanceScope政策,擷取元件執行個體。InstanceScope中,InstancePerRequest在Asp.net MVC等站點開發時比較常用,即對每一請求傳回同一執行個體,though, it’s still just instance per matching lifetime scope——MatchingScopeLifetimeTags.RequestLifetimeScopeTag,MVC中為“AutofacWebRequest”,在。注意,ASP.NET Core uses Instance Per Lifetime Scope rather than Instance Per Request. 如何在MVC中使用,請參看文檔:http://docs.autofac.org/en/latest/faq/per-request-scope.html?highlight=RequestLifetimeScope#implementing-custom-per-request-semantics

It is important to always resolve services from a lifetime scope and not the root container. Due to the disposal tracking nature of lifetime scopes, if you resolve a lot of disposable components from the container (the “root lifetime scope”), you may inadvertently cause yourself a memory leak. The root container will hold references to those disposable components for as long as it lives (usually the lifetime of the application)。

Autofac主張LifetimeScope不要線程共享,否則,You can get into a bad situation where components can’t be resolved if you spawn the thread and then dispose the parent scope.即共享scope被其它線程釋放導緻元件無法正常擷取。鑒于此,Autofac并未為多線程共享LifetimeScope提供便捷方法,若定要如此,那麼隻能人為處理(比如将LifetimeScope作為參數傳入線程或設為全局靜态變量)。

以上為4.x版本參照。

先來看一篇博文——部落格園的大牛們,被你們害慘了,Entity Framework從來都不需要去寫Repository設計模式。對于這位博友的觀點,在其應用場景下我表示贊同。大部分架構和模式,都是為了達到解耦的目的,EF本身就是Repository模式實作,它讓業務層與具體資料庫解耦,即可較友善地切換不同資料庫。那麼假如說業務層需要同ORM解耦,去應對可能的ORM切換,那麼我們也可以在業務層和ORM層再套一層Repository。以下為簡單的實作代碼:

public partial interface IRepository<T> where T : BaseEntity
    {
        T GetById(object id);
        void Insert(T entity);
        void Insert(IEnumerable<T> entities);
        void Update(T entity);
        void Update(IEnumerable<T> entities);
        void Delete(T entity);
        void Delete(IEnumerable<T> entities);
        IQueryable<T> Table { get; }
        IQueryable<T> TableNoTracking { get; }
    }      

各路ORM隻要實作該接口即可,比如EF:

某電商平台開發記要
某電商平台開發記要
public partial class EfRepository<T> : IRepository<T> where T : BaseEntity
    {
        #region Fields

        private readonly IDbContext _context;
        private IDbSet<T> _entities;

        #endregion

        #region Ctor

        public EfRepository(IDbContext context)
        {
            this._context = context;
        }

        #endregion

        #region Utilities

        protected string GetFullErrorText(DbEntityValidationException exc)
        {
            var msg = string.Empty;
            foreach (var validationErrors in exc.EntityValidationErrors)
                foreach (var error in validationErrors.ValidationErrors)
                    msg += string.Format("Property: {0} Error: {1}", error.PropertyName, error.ErrorMessage) + Environment.NewLine;
            return msg;
        }

        #endregion

        #region Methods

        public virtual T GetById(object id)
        {
            //see some suggested performance optimization (not tested)
            //http://stackoverflow.com/questions/11686225/dbset-find-method-ridiculously-slow-compared-to-singleordefault-on-id/11688189#comment34876113_11688189
            return this.Entities.Find(id);
        }

        public virtual void Insert(T entity)
        {
            try
            {
                if (entity == null)
                    throw new ArgumentNullException("entity");

                this.Entities.Add(entity);

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                throw new Exception(GetFullErrorText(dbEx), dbEx);
            }
        }

        public virtual void Insert(IEnumerable<T> entities)
        {
            try
            {
                if (entities == null)
                    throw new ArgumentNullException("entities");

                foreach (var entity in entities)
                    this.Entities.Add(entity);

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                throw new Exception(GetFullErrorText(dbEx), dbEx);
            }
        }

        public virtual void Update(T entity)
        {
            try
            {
                if (entity == null)
                    throw new ArgumentNullException("entity");

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                throw new Exception(GetFullErrorText(dbEx), dbEx);
            }
        }

        public virtual void Update(IEnumerable<T> entities)
        {
            try
            {
                if (entities == null)
                    throw new ArgumentNullException("entities");

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                throw new Exception(GetFullErrorText(dbEx), dbEx);
            }
        }

        public virtual void Delete(T entity)
        {
            try
            {
                if (entity == null)
                    throw new ArgumentNullException("entity");

                this.Entities.Remove(entity);

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                throw new Exception(GetFullErrorText(dbEx), dbEx);
            }
        }

        public virtual void Delete(IEnumerable<T> entities)
        {
            try
            {
                if (entities == null)
                    throw new ArgumentNullException("entities");

                foreach (var entity in entities)
                    this.Entities.Remove(entity);

                this._context.SaveChanges();
            }
            catch (DbEntityValidationException dbEx)
            {
                throw new Exception(GetFullErrorText(dbEx), dbEx);
            }
        }
        
        #endregion

        #region Properties

        public virtual IQueryable<T> Table
        {
            get
            {
                return this.Entities;
            }
        }

        public virtual IQueryable<T> TableNoTracking
        {
            get
            {
                return this.Entities.AsNoTracking();
            }
        }

        protected virtual IDbSet<T> Entities
        {
            get
            {
                if (_entities == null)
                    _entities = _context.Set<T>();
                return _entities;
            }
        }

        #endregion
    }      

通過IOC(比如上文介紹的Autofac),動态注入業務層,業務層隻引用接口(基礎的實體和集合類),不需引用特定ORM程式集。

然而,有多少項目有切換ORM的風險呢,如果真的到了需要切換ORM的地步了,未必沒有更好的方法可以嘗試。有人說便于模拟資料mock,用于開發和測試,這倒是有點道理——連接配接到開發/測試資料庫,顯得有點“重”,也不靈活,領域模型和資料庫更改需要同步。

後來發現有RhinoMocks這個東東,它可以針對任意接口建立出mock執行個體。有了IRepository,我們就可以MockRepository.GenerateMock<IRepository<XXX>>();就可以出來一個TestRepository。從面向接口程式設計的角度來說,由于各種ORM并沒有統一接口,是以我們自定義了IRepository,其實可以看作是代理/适配接口,并非真正意義上的Repository模式,just提取了個接口而已。。。

說回來,大部分程式員要麼不懂設計,要麼過度設計,要麼隻會套用模式,從來不想想這是否解決了[或帶來了]什麼問題,而他們是有存在的必要的——去填補那80%。拿以前引用過的一句話與各位共勉:設計,是一種美。就像蓋大樓,如果每座房屋都是千篇一律,那麼也就不存在架構師了。

POCO:Plain Old Class Object,也就是最基本的CLR Class,在原先的EF中,實體類通常是從一個基類繼承下來的,而且帶有大量的屬性描述。而POCO則是指最原始的Class,換句話說這個實體的Class僅僅需要從Object繼承即可,不需要從某一個特定的基類繼承。在EF中,主要是配合Code First使用。Cost Frist則是指我們先定義POCO這樣的實體class,然後生成資料庫。實際上現在也可以使用Entity Framework Power tools将已經存在的資料庫反向生成POCO的class(不通過edmx檔案)。——該段來自某博問回答。

Model:領域模型。可以包含行為(方法/邏輯)

DTO:資料傳輸對象。The canonical definition of a DTO is the data shape of an object without any behavior( 不包含行為)。

ViewModel:是在MVVM模式中,在展示層頻繁使用的Model

很多人糾結Model和DTO的關系,怎麼用,哪個在下哪個在上,搭建項目時就照貓畫虎用上了,然後再想要分析出個這麼用的原因來。網上也不乏誤人子弟的觀點,似乎隻要是個項目,都要“DTO”一把。其實從它們出現的目的去了解就很清楚了,DTO可以看作一種模式,避免了多次調用資料的問題,比如原本取目前使用者的姓名和性别,要分兩次,現下我們隻要定義一個包含這兩個屬性的User類,用戶端擷取目前使用者,服務端一次取出兩個屬性值并構造出User對象傳回,隻要請求一次就可以了。我們現在面向對象程式設計,基本上很自然地就使用了這種方式。是以領域模型和DTO并非前後/平級關系,或者說并非相同概念,POCO/Model都是DTO的一種實作方式,我們可以繼續封裝,多個類再組合成為更大的類,目的就是減少服務請求次數,這便是DTO。

筆者大緻看了下兩者的代碼,總的來說,各有優缺點。優點就不說了,畢竟這麼長時間的優化(前者是代碼層面,後者更多的是功能業務上)。下面說說初步看到的缺點。

兩者的代碼架構都有問題。如NopCommerce的Core項目,引用了Web相關的dll,不過Nop可以認為就是專為Web搭建的,是以這麼做也無可厚非。但是實際開發時還是得将底層項目純粹化,畢竟其它類型的項目(如windows服務)也要建構其上。Himall甚至有循環引用的問題,為了避免編譯出錯,使用了運作時動态加載的方式,然而我沒找到非得互相引用的原因。

Himall中,所謂的快遞插件是快遞模闆(用于列印),插件的配置資料儲存在插件目錄下的config.xml,NopCommerce中,插件可以在安裝時初始化配置[和其儲存地方比如資料庫]。和NopCommerce不同,Himall的插件并不能自呈現(不能自定義view)。另插件尋找方式兩者也不同,himall是先到目錄下找dll(根據名稱規則),再找相關配置,而nopcommerce是先找配置(Description.txt),再找相關dll,兩種方式并無優劣,但從代碼實作上來講後者比前者好。

Himall可能經手了太多人,許多邏輯或思考有重複的嫌疑,其實完全可以合為一處,很多影響性能的地方也未作處理,如AutoMapper在每次執行個體轉換時都要去建立一遍映射規則,将其置于應用程式啟動時執行一次即可,舉手之勞不知為何不做。

NopCommerce似乎沒有用事務。。。

NopCommerce都是通過構造函數注入執行個體,如下

private readonly IRepository<ShoppingCartItemInfo> _shoppingcartItemRep;
        private readonly IRepository<ProductInfo> _productRep;
        private readonly IRepository<UserMemberInfo> _userRep;

        public CartService(IRepository<ShoppingCartItemInfo> shoppingcartItemRep,IRepository<ProductInfo> productRep,IRepository<UserMemberInfo> userRep)
        {
            this._shoppingcartItemRep = shoppingcartItemRep;
            this._productRep = productRep;
            this._userRep = userRep;
        }      

但是并非每次都會用到這些執行個體,是以我覺得還是應該按需擷取,比如以屬性的方式

private IRepository<ShoppingCartItemInfo> ShoppingcartItemRep
        {
            get { return EngineContext.Resolve<IRepository<ShoppingCartItemInfo>>(); }
        }      

另外,這兩套架構有很多值得借鑒的地方,有興趣的同學可自行研究,本人對它們接觸時間不長,就不展開講了。。。

開局一台塔式伺服器(Dell T430)一套鼠鍵,裝備全靠撿。。。windows server肯定是必須的,考慮到後續要安裝如redis、git啥的,雖然大部分有windows版本,但網站最好還是要部署到單獨系統,是以另外再安裝Linux比較好。伺服器隻有一台,隻能搞多個虛拟機,筆者知道的選擇有兩種:VMware Workstation 和 VMware vSphere Hypervisor(esxi),前者一定是裝在OS(Window或Linux)上的,基于OS做虛拟資源處理,而後者本身就可看作是個OS,直接操作硬體資源[配置設定到各個虛拟機],是以可以認為後者更有效率,性能更佳。vmware中文官網(https://www.vmware.com/cn.html)

從官網上下載下傳vSphere Hypervisor,目前是6.5版,使用ultraiso做一個U盤安裝盤,可參看【親測】UltraISO 制作ESXi 的 USB 安裝盤,這裡有一個uefi的概念,可以自行了解 UEFI是什麼?與BIOS的差別在哪裡?UEFI詳解!,直接感覺就是在啟動的到時候少了自檢(記憶體、硬碟等硬體資訊列印)這一步 。安裝和配置步驟可看 HOW TO: Install and Configure VMware vSphere Hypervisor 6.5 (ESXi 6.5)。官方中文文檔 VMware vSphere 6.5 文檔中心,感覺這文檔也不完整,很多連接配接不能點,英文文檔的一下沒找到,很多東西還是得靠搜尋引擎和自己摸索。

遇到評估許可證已過期的提示,去下載下傳個注冊機即可:) 其實也不用,esxi是免費的,許可過期直接去官網申請一個就能永久使用了(要新增賬號)。

6.5版,我們可以在浏覽器(VM web client)裡管理ESXi,甚至可以直接關閉實體機(在維護模式下)。在虛拟機裡安裝完作業系統,為了友善管理,還可以安裝VMare Tools,安裝了VMare Tools之後,可以通過浏覽器直接啟動(要退出維護模式)、重新開機、關閉作業系統(否則要進入到作業系統界面去做這些操作)等(據說還有系統間複制粘貼之類的功能)。

某電商平台開發記要

當然了,我們安裝好系統以後,直接遠端登入操作更友善。

從官網上下了windows server 2016标準版安裝後,顯示已激活,但水印提示180天到期,以管理者權限運作cmd,輸入  DISM /online /Get-CurrentEdition,發現是評估版。然後DISM /online /Set-Edition:ServerStandard /ProductKey:XXXXX-XXXXX-XXXXX-XXXXX-XXXXX /AcceptEula(産品密鑰是網上找的),執行完成後重新開機,水印提示沒了(已非評估版),但是卻顯示未激活。。。提示如下:

某電商平台開發記要

并沒有說未激活就不能用的意思,先用着吧,等哪天網上能找到靠譜的密鑰。。。(用于測試環境,so,問題不會太大)

另外建立了一個虛拟機用于安裝centos,過程不贅述。之前通過windows系統去遠端登入linux需要安裝ssh用戶端,由于筆者的PC系統是win10,可以安裝Ubuntu子系統,然後通過Ubuntu去連接配接遠端centos(Ubuntu預設安裝了ssh),如下:

某電商平台開發記要

KVM切換器:用于多台主機一台顯示器,切換顯示

iDRAC:Integrated Dell Remote Access Controller,也就是內建戴爾遠端控制卡,使用它,可以遠端進行安裝系統,重新開機等等原本需要進入機房才能進行的操作。

Server Core:windows server 2008開始,最小化的伺服器核心,去掉了幾乎所有的應用界面,并且将支援的伺服器角色降到最小,隻能進行活動目錄、DHCP、DNS、檔案/列印、媒體、Web等幾種伺服器角色的安裝,還可以安裝Sqlserver和PHP,能否和怎麼安裝其它東西筆者并未深入了解。我們可以通過指令行安裝和配置IIS,然後通過IIS用戶端,遠端釋出站點。網上資料較少,不好玩。

Docker:Docker和虛拟機都是虛拟技術,我們從它們産生的曆史背景可以更好地了解它們之間的差別。虛拟機使得使用者能在單台實體主機部署多個作業系統(與實體機安裝多系統不一樣,不同虛拟機可以安裝不同核心的作業系統),便于使用者學習或者最大限度的使用實體機資源;實體機首先要安裝主作業系統,虛拟機再在此之上安裝和運作——或者說“虛拟”出——它們各自的系統;說白了,虛拟機展現給使用者的角色,是一個個互相隔離的作業系統。我們知道,在作業系統裡安裝應用[以及該應用需要的運作環境],有時是一個挺折騰的過程,特别是涉及到同應用不同版本共存、潛在軟體沖突等情況;而當我們終于在測試機上把所有環境都配置好,并運作地妥妥貼貼,釋出到線上,相同的過程還得重新來一遍,還未必能保證不出現其它問題;由于有這些痛點,Docker就出現了,它隔離的是作業系統中的各個應用,或者說應用環境(也可以是一個作業系統,比如我們可以在centos系統裡運作一個ubuntu鏡像,這就搭建了一個基于ubuntu的應用環境)。可參看 docker容器與虛拟機有什麼差別?而在centos中啟動一個ubuntu的docker,都是兩個系統,為啥效率會比虛拟機高的多?因為ubuntu共享centos的kernel。由于docker的前提是kernel共用,是以我們看不到在linux下啟動一個windows鏡像,反之亦然。可參看 一篇不一樣的docker原了解析。另基于一個鏡像啟動多個容器,多個容器之間共享鏡像,每個容器在啟動的時候并不需要單獨複制一份鏡像檔案,減少了鏡像對磁盤空間的占用和容器啟動時間。參看 Docker鏡像進階:了解其背後的技術原理。

傳統的更新站點(測試環境)步驟:

  1. 從代碼伺服器上擷取最新代碼,如git
  2. 本地編譯
  3. 登入到遠端伺服器,将編譯生成的程式集、靜态資源等(不包括web.config)覆寫到站點檔案夾
  4. 可能還要修改伺服器上的web.config[和其它配置檔案]
  5. 通知相關人等站點已更新

若代碼送出頻繁,想要所有人第一時間看到效果,必須同樣頻繁的做這些操作,有沒有神器能幫我們自動做這些工作呢?當然是有的,本人用的是Jenkins,目前最新穩定版是2.46.2。下面以釋出Asp.net mvc站點為例,擇要點說明如何使用。

Jenkins的一些概念:https://jenkins.io/doc/book/glossary/

在windows系統上安裝好後,Jenkins以windows服務的形式運作,并以web方式供我們管理。打開浏覽器進入(預設http://localhost:8080/)後,需要安裝必要的插件,比如git和msbuild,然後在Global Tool Configuration下設定這兩個插件調用的執行檔案位址:

某電商平台開發記要

這裡需要注意兩點:

  • 我是用Chocolatey安裝的git,注意安裝好git之後可能需要重新開機伺服器,否則在後面設定git遠端倉庫時會提示找不到git.exe的錯誤
  • 安裝了.Net Framework的機子,可以在C:\Windows\Microsoft.NET\Framework64\v4.0.30319下面找到MSBuild.exe,但是它的版本是4.6.xxx,很早以前的,是以不能用。筆者用的是VS2017社群版開發,去微軟官網下載下傳Visual Studio 2017 生成工具,安裝後的版本為15.0;這裡我們還要安裝14.0版本的MSBuild,為什麼呢,後面會說到。

然後在項目配置裡面,設定源碼管理:

某電商平台開發記要

由于這裡是https協定,是以我們要提供使用者名密碼,Jenkins會據此從遠端倉庫取代碼。那麼什麼時候取呢,這就要在Poll SCM(Source Code Manage,這裡即git)裡設定了。比如 H H 1,15 1-11 * 表示once a day on the 1st and 15th of every month except December,H可以看作任務名稱的hash值對應的一個數,是以不指定确定值的話,用這個即可。間隔表示法,H/15 * * * * 表示每15分鐘取一次。具體規則在設定時點文本框右邊問号可看到。

現在可以執行一下,不出意外Jenkins會拉取代碼,并放入 安裝目錄\Jenkins\workspace\任務名\ 下。接下來設定編譯步驟:

某電商平台開發記要

如果項目中引用的dll有從nuget下載下傳擷取,這些并不會包含在SCM裡,是以我們要先執行nuget.exe restore下載下傳相關dll。nuget.exe這個應用程式可以到官網下載下傳,目前版本是3.5。當我們執行這步的時候(注意尚未開始編譯),提示建構失敗:

某電商平台開發記要

剛開始我以為是編譯時産生的問題,經過一番艱苦卓絕的查閱,就差把MSBuild重新研究一遍(MSBuild 保留屬性和已知屬性),終于發現原來是nuget導緻的。可以參看 nuget.exe does not work with msbuild 12 as of 3.5.0 & NuGet CLI does not work with MSBuild 15.0 on Windows。總之安裝了MSBuild14就哦了。

然後正式開始編譯,可以直接編譯web項目,但是有些項目沒有直接被web項目引用,是生成到bin目錄下,是以這裡編譯整個解決方案。筆者先用MSBuild15試之,報錯:

C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\Microsoft.Common.CurrentVersion.targets(1111,5): error MSB3644: 未找到架構“.NETFramework,Version=v4.6.1”的引用程式集。若要解決此問題,請安裝此架構版本的 SDK 或 Targeting Pack,或将應用程式的目标重新指向已裝有 SDK 或 Targeting Pack 的架構版本。請注意,将從全局程式集緩存(GAC)解析程式集,并将使用這些程式集替換引用程式集。是以,程式集的目标可能未正确指向您所預期的架構。

改用MSBuild14沒這個錯誤,但是在編譯Web項目時報錯:

error MSB4019: 未找到導入的項目“C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\WebApplications\Microsoft.WebApplication.targets”。請确認 <Import> 聲明中的路徑正确,且磁盤上存在該檔案。

網上說這是安裝Visual Studio生成的路徑,我不打算在伺服器(我将Jenkins安裝在伺服器上)安裝VS,從開發機上目錄C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\Microsoft\VisualStudio\v15.0\WebApplications找到這個檔案,然後在伺服器上建立報錯的那個路徑,将之copy後解決。

編譯單元測試項目時報錯:

error CS0246: The type or namespace name 'TestMethod' could not be found (are you missing a using directive or an assembly reference?)

看了下引用的dll在vs安裝目錄下,按照剛才的做法,将該dll拷貝到伺服器,并沒有用,不知為何。想到單元測試本身就不必釋出,是以建立了解決方案配置Test,在該配置下,取消單元測試項目的生成,然後将MSBuild的編譯參數/p:Configuration=Test。可參看 How to Exclude A Project When Building A Solution? 這樣做還有個好處,請看使用Web.Config Transformation配置靈活的配置檔案

繼續,報錯:error MSB6003: 指定的任務可執行檔案“tsc.exe”未能運作。未能找到路徑“C:\Program Files (x86)\Microsoft SDKs\TypeScript”的一部分。從開發機拷貝,解決。

若Jenkins和web伺服器不是同一個機子,我們需要用到釋出配置檔案,比如Web Deploy,然後增加幾個MSBuild參數,這裡不贅述了。

Web Deploy:先去http://www.microsoft.com/web/downloads/platform.aspx下載下傳Microsoft Web Platform Installer,給伺服器裝上,然後裝上Web Deploy3.5,大緻流程可參考Web Deploy 伺服器安裝設定與使用,還有一個博文【初碼幹貨】在Window Server 2016中使用Web Deploy方式釋出.NET Web應用的重新梳理稍顯複雜,沒試過。

建構完了可以設定通知,發送郵件,要即時的話,可以用釘釘。看到也有個微網誌插件,不過幾年沒更新了,不知是否還能用。

其它

動态加載程式集:在MVC中,頁面是[在請求時]使用BuildManager動态編譯的,BuildManager will search refrence assembies in the ‘bin’ folder and in the GAC。是以若頁面使用了我們要動态加載的程式集,而程式集檔案不在上述兩處,則會報錯。具體可參看Developing a plugin framework in ASP.NET MVC with medium trust,另外文中說的file lock不知道作者是怎麼解決的。

運作時貌似都會将bin目錄下的dll加載到臨時檔案夾下(比如c:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\vs\),是以運作時bin下的dll能删掉,而不會提示占用。

在構造函數中使用this會發生什麼?——并沒有神奇的事情發生。。。

我們可以用requirejs等元件子產品化js代碼,使用webpack打包多個js檔案合成為一個js檔案,webpack會自動分析子產品之間的依賴關系。當然webpack不單單這個功能,可參看 入門Webpack,看這篇就夠了。 當然在Http1.1的時代,有無必要打包(即減少請求次數)而喪失部分緩存優勢(針對單個檔案),本人持保留态度。

以下轉自知乎:CMD是國内玉伯大神在開發SeaJS的時候提出來的,屬于CommonJS的一種規範,此外還有AMD,其對于的架構是RequireJS

1、二者都是異步子產品定義(Asynchronuous Module Definition)的一個實作;

2、CMD和AMD都是CommonJS的一種規範的實作定義,RequireJS和SeaJS是對應的實踐;

3、CMD和AMD的差別:CMD相當于按需加載,定義一個子產品的時候不需要立即制定依賴子產品,在需要的時候require就可以了,比較友善;而AMD則相反,定義子產品的時候需要制定依賴子產品,并以形參的方式引入factory中。

.gitignore隻适用于尚未添加到git庫的檔案。如果已經添加了,則需用git rm移除後再重新commit。

參考資料:

MapKey vs HasForeignKey Difference - Fluent Api

Entity Framework Code First 學習日記(8)-一對一關系

EF 延遲加載和預先加載

Docker 核心技術與實作原理

轉載請注明本文出處:http://www.cnblogs.com/newton/p/6544563.html

繼續閱讀