某電商平臺(tái)開發(fā)記要
本文是博主在開發(fā)某電商平臺(tái)項(xiàng)目的一些雜項(xiàng)記錄,方便自己和團(tuán)隊(duì)同事查閱,偏向于具體技術(shù)或應(yīng)用的細(xì)節(jié)和個(gè)人理解,但也未必非常具體。文中未提的更多內(nèi)容可能會(huì)另起篇章。
- 導(dǎo)航屬性——EF實(shí)體關(guān)系fluent配置
- AutoMapper
- Autofac
- Repository模式
- Model & DTO
- 開源&商用.NET電商平臺(tái)——NopCommerce(3.9版) & Himall(2.4版)
- 服務(wù)器搭建-VMware vSphere Hypervisor(esxi)
- 自動(dòng)化部署-Jenkins
導(dǎo)航屬性——EF實(shí)體關(guān)系fluent配置
實(shí)體關(guān)系——一對(duì)一[或零],一對(duì)多,多對(duì)多——對(duì)應(yīng)到數(shù)據(jù)庫(kù)就是外鍵約束。為了性能及數(shù)據(jù)遷移考慮,在事務(wù)性要求不高的情形中,我們一般都禁用外鍵,但是EF中仍可保留實(shí)體關(guān)系以方便編程。
本文基于EF6.1.3版本。
EF中有兩類關(guān)系:Independent association 和 Foreign Key association。在實(shí)體定義時(shí)可以看出它們的不同。
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會(huì)按照默認(rèn)規(guī)則構(gòu)建或?qū)ふ业秸_的外鍵字段。我們也可以顯式配置外鍵,兩種方法:
1 Map(m=>m.MapKey("CustomerId")); 2 HasForeignKey(m=>m.CustomerId);
Map適用于Independent association,而HasForeignKey用于Foreign Key association。如果在Foreign Key association時(shí)使用Map,將會(huì)拋出:“CustomerId:Name:類型中的每個(gè)屬性名必須唯一,已定義屬性名CustomerId”的錯(cuò)誤。
需要注意的是,一對(duì)一的實(shí)體關(guān)系,EF并未提供HasForeignKey指定外鍵。why?EF團(tuán)隊(duì)認(rèn)為,既然兩個(gè)實(shí)體是一一對(duì)應(yīng)的關(guān)系,那么可以由一個(gè)主鍵標(biāo)識(shí)兩個(gè)實(shí)體,so,will use its primary key as the foreign key。。。也是醉了。如果硬要指定一個(gè)外鍵的話,對(duì)于Independent association還好,我們可以用Map,但是Foreign Key association就悲劇了。可以使用WithMany()這個(gè)hack,但比較別扭,個(gè)人是不推薦這種方法。詳情可參看One to zero-or-one with HasForeignKey。嘗試使用[ForeignKey]特性,也會(huì)報(bào)錯(cuò)[比如]:系“CategoryCashDepositInfo_CategoriesInfo”中 Role“CategoryCashDepositInfo_CategoriesInfo_Source”的多重性無(wú)效。因?yàn)?Dependent Role 屬性不是鍵屬性,Dependent Role 多重性的上限必須為“*”。so,一對(duì)一實(shí)體的外鍵也必須是它的主鍵,尼瑪。不幸遇到這種問(wèn)題,在項(xiàng)目初期(一般來(lái)說(shuō)踩坑都是比較早的),最好的方式還是改變數(shù)據(jù)結(jié)構(gòu)以適應(yīng)EF要求,畢竟它這么要求確實(shí)有道理。
另:若一實(shí)體沒(méi)有導(dǎo)航屬性,但是另一實(shí)體包含該實(shí)體集合的屬性,那么在生成數(shù)據(jù)庫(kù)時(shí),EF也會(huì)自動(dòng)為它們生成外鍵約束。
在增刪改實(shí)體時(shí),若有上下文跟蹤,則連帶著實(shí)體的導(dǎo)航屬性對(duì)應(yīng)的數(shù)據(jù)也一并會(huì)受影響,比如在更新父子表時(shí),不需要自己寫單獨(dú)更細(xì)兩張表的代碼,EF都幫我們處理好了。舉個(gè)典型例子:
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 有無(wú)都一樣,最后數(shù)據(jù)表里字段會(huì)賦予實(shí)際值 var j = new Journal { Amount=10, Order= order,OrderID =order.ID }; context.Journals.Add(j);//只要add主類即可 context.SaveChanges(); }
更多可參看 MVC3+EF4.1學(xué)習(xí)系列(六)-----導(dǎo)航屬性數(shù)據(jù)更新的處理
待驗(yàn)證:一對(duì)一時(shí),導(dǎo)航屬性有沒(méi)有延遲加載一說(shuō)?另導(dǎo)航屬性鏈查詢細(xì)節(jié),比如Comment.OrderItem.OrderInfo.PayDate,其中OrderItem是Comment的導(dǎo)航熟悉,OrderInfo是OrderItem的導(dǎo)航屬性,這個(gè)時(shí)候SQL查詢步驟是怎樣的呢?——一對(duì)一時(shí),不會(huì)自動(dòng)加載,即獲取父對(duì)象后,導(dǎo)航屬性對(duì)應(yīng)的子對(duì)象一直為null(不管后續(xù)有沒(méi)有用到,用到的話會(huì)拋出NullReferenceException),但是在獲取父對(duì)象時(shí)使用Include顯式加載子對(duì)象,是可以的。同其它導(dǎo)航屬性一樣,之前測(cè)試出現(xiàn)無(wú)法加載是因?yàn)橥浗o導(dǎo)航屬性前面添加virtual關(guān)鍵字了。。。
導(dǎo)航屬性的刪除更新操作需要特別注意,如果直接將導(dǎo)航屬性賦值為新對(duì)象,保存后,數(shù)據(jù)表中將新增記錄,而原記錄仍然存在,原因顯而易見,這里不說(shuō)了。
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之類的方法,否則會(huì)報(bào)下面的錯(cuò)誤

另:給父對(duì)象設(shè)置EntityState,并不會(huì)自動(dòng)給導(dǎo)骯實(shí)體賦予相同EntityState。刪除父對(duì)象時(shí),一對(duì)一的導(dǎo)航實(shí)體不會(huì)自動(dòng)跟著刪除;若是一對(duì)多的情況,那么只要?jiǎng)h除父對(duì)象,導(dǎo)航實(shí)體會(huì)自動(dòng)被刪除;多對(duì)多的情況未驗(yàn)證,因?yàn)樯婕暗接成浔恚茰y(cè)會(huì)自動(dòng)刪除映射關(guān)系,即刪除映射表里的相關(guān)記錄。經(jīng)測(cè)試,多對(duì)多情況,也無(wú)法自動(dòng)刪除。
AutoMapper提供的自定義映射——Custom value resolvers 和 Projection,乍看之下似乎差不多,側(cè)重解決點(diǎn)是不一樣的,但是它們似乎又通用。。。在使用上,后者M(jìn)apFrom方法的其中一個(gè)重載接收表達(dá)式樹(Expression)類型的參數(shù),因此涉及到稍微復(fù)雜的語(yǔ)句,可能出現(xiàn)如下圖所示的情況:

這個(gè)時(shí)候只能采用前者的ResolveUsing方法了,如下:

還有個(gè)IValueResolver接口,與IMemberValueResolver的區(qū)別在于IValueResolver不指定source類的哪個(gè)屬性需要轉(zhuǎn)換,這就導(dǎo)致了轉(zhuǎn)換時(shí)自定義邏輯可能要引用source類,如果其它類也有類似轉(zhuǎn)換,那么就不能復(fù)用了。
6.0.1.0版本,如下寫法,則只有最后一個(gè)Resolver起作用。

改成下面寫法,則無(wú)問(wèn)題。
注意到上面opt => opt.ResolveUsing<ShopGradeResolver>(),每次Mapper.Map的時(shí)候都會(huì)new一個(gè)ShopGradeResolver對(duì)象,其實(shí)是沒(méi)必要的,因?yàn)橹粓?zhí)行邏輯而狀態(tài)無(wú)關(guān)。所以可改為單例模式:opt => opt.ResolveUsing(Singleton<ShopGradeResolver>.Instance)。
另,當(dāng)source類有導(dǎo)航屬性時(shí),會(huì)在Mapper.Map時(shí)去數(shù)據(jù)庫(kù)里查,因此若用不到該導(dǎo)航屬性則應(yīng)該設(shè)置映射規(guī)則時(shí)ignore之。
Mapper.Initialize調(diào)用多次,最后一次會(huì)覆蓋前面的,因此如果映射是由各個(gè)項(xiàng)目自己處理,那么應(yīng)該考慮使用Profile,然后在主項(xiàng)目中 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. 更多請(qǐng)看 Configuration。
Lifetime Scope 和 Instance Scope,我們獲取實(shí)例,都要先BeginLifetimeScope(),而后根據(jù)組件注冊(cè)時(shí)的InstanceScope策略,獲取組件實(shí)例。InstanceScope中,InstancePerRequest在Asp.net MVC等站點(diǎn)開發(fā)時(shí)比較常用,即對(duì)每一請(qǐng)求返回同一實(shí)例,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中使用,請(qǐng)參看文檔: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被其它線程釋放導(dǎo)致組件無(wú)法正常獲取。鑒于此,Autofac并未為多線程共享LifetimeScope提供便捷方法,若定要如此,那么只能人為處理(比如將LifetimeScope作為參數(shù)傳入線程或設(shè)為全局靜態(tài)變量)。
以上為4.x版本參照。
先來(lái)看一篇博文——博客園的大牛們,被你們害慘了,Entity Framework從來(lái)都不需要去寫Repository設(shè)計(jì)模式。對(duì)于這位博友的觀點(diǎn),在其應(yīng)用場(chǎng)景下我表示贊同。大部分架構(gòu)和模式,都是為了達(dá)到解耦的目的,EF本身就是Repository模式實(shí)現(xiàn),它讓業(yè)務(wù)層與具體數(shù)據(jù)庫(kù)解耦,即可較方便地切換不同數(shù)據(jù)庫(kù)。那么假如說(shuō)業(yè)務(wù)層需要同ORM解耦,去應(yīng)對(duì)可能的ORM切換,那么我們也可以在業(yè)務(wù)層和ORM層再套一層Repository。以下為簡(jiǎn)單的實(shí)現(xiàn)代碼:
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只要實(shí)現(xiàn)該接口即可,比如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 }
通過(guò)IOC(比如上文介紹的Autofac),動(dòng)態(tài)注入業(yè)務(wù)層,業(yè)務(wù)層只引用接口(基礎(chǔ)的實(shí)體和集合類),不需引用特定ORM程序集。
然而,有多少項(xiàng)目有切換ORM的風(fēng)險(xiǎn)呢,如果真的到了需要切換ORM的地步了,未必沒(méi)有更好的方法可以嘗試。有人說(shuō)便于模擬數(shù)據(jù)mock,用于開發(fā)和測(cè)試,這倒是有點(diǎn)道理——連接到開發(fā)/測(cè)試數(shù)據(jù)庫(kù),顯得有點(diǎn)“重”,也不靈活,領(lǐng)域模型和數(shù)據(jù)庫(kù)更改需要同步。
后來(lái)發(fā)現(xiàn)有RhinoMocks這個(gè)東東,它可以針對(duì)任意接口創(chuàng)建出mock實(shí)例。有了IRepository,我們就可以MockRepository.GenerateMock<IRepository<XXX>>();就可以出來(lái)一個(gè)TestRepository。從面向接口編程的角度來(lái)說(shuō),由于各種ORM并沒(méi)有統(tǒng)一接口,所以我們自定義了IRepository,其實(shí)可以看作是代理/適配接口,并非真正意義上的Repository模式,just提取了個(gè)接口而已。。。
說(shuō)回來(lái),大部分程序員要么不懂設(shè)計(jì),要么過(guò)度設(shè)計(jì),要么只會(huì)套用模式,從來(lái)不想想這是否解決了[或帶來(lái)了]什么問(wèn)題,而他們是有存在的必要的——去填補(bǔ)那80%。拿以前引用過(guò)的一句話與各位共勉:設(shè)計(jì),是一種美。就像蓋大樓,如果每座房屋都是千篇一律,那么也就不存在架構(gòu)師了。
Model:領(lǐng)域模型。可以包含行為(方法/邏輯)
DTO:數(shù)據(jù)傳輸對(duì)象。The canonical definition of a DTO is the data shape of an object without any behavior( 不包含行為)。
ViewModel:是在MVVM模式中,在展示層頻繁使用的Model
很多人糾結(jié)Model和DTO的關(guān)系,怎么用,哪個(gè)在下哪個(gè)在上,搭建項(xiàng)目時(shí)就照貓畫虎用上了,然后再想要分析出個(gè)這么用的原因來(lái)。網(wǎng)上也不乏誤人子弟的觀點(diǎn),似乎只要是個(gè)項(xiàng)目,都要“DTO”一把。其實(shí)從它們出現(xiàn)的目的去理解就很清楚了,DTO可以看作一種模式,避免了多次調(diào)用數(shù)據(jù)的問(wèn)題,比如原本取當(dāng)前用戶的姓名和性別,要分兩次,現(xiàn)下我們只要定義一個(gè)包含這兩個(gè)屬性的User類,客戶端獲取當(dāng)前用戶,服務(wù)端一次取出兩個(gè)屬性值并構(gòu)造出User對(duì)象返回,只要請(qǐng)求一次就可以了。我們現(xiàn)在面向?qū)ο缶幊蹋旧虾茏匀坏鼐褪褂昧诉@種方式。所以領(lǐng)域模型和DTO并非前后/平級(jí)關(guān)系,或者說(shuō)并非相同概念,POCO/Model都是DTO的一種實(shí)現(xiàn)方式,我們可以繼續(xù)封裝,多個(gè)類再組合成為更大的類,目的就是減少服務(wù)請(qǐng)求次數(shù),這便是DTO。
開源&商用.NET電商平臺(tái)——NopCommerce(3.9版) & Himall(2.4版)
筆者大致看了下兩者的代碼,總的來(lái)說(shuō),各有優(yōu)缺點(diǎn)。優(yōu)點(diǎn)就不說(shuō)了,畢竟這么長(zhǎng)時(shí)間的優(yōu)化(前者是代碼層面,后者更多的是功能業(yè)務(wù)上)。下面說(shuō)說(shuō)初步看到的缺點(diǎn)。
兩者的代碼架構(gòu)都有問(wèn)題。如NopCommerce的Core項(xiàng)目,引用了Web相關(guān)的dll,不過(guò)Nop可以認(rèn)為就是專為Web搭建的,所以這么做也無(wú)可厚非。但是實(shí)際開發(fā)時(shí)還是得將底層項(xiàng)目純粹化,畢竟其它類型的項(xiàng)目(如windows服務(wù))也要構(gòu)建其上。Himall甚至有循環(huán)引用的問(wèn)題,為了避免編譯出錯(cuò),使用了運(yùn)行時(shí)動(dòng)態(tài)加載的方式,然而我沒(méi)找到非得相互引用的原因。
Himall中,所謂的快遞插件是快遞模板(用于打印),插件的配置數(shù)據(jù)保存在插件目錄下的config.xml,NopCommerce中,插件可以在安裝時(shí)初始化配置[和其保存地方比如數(shù)據(jù)庫(kù)]。和NopCommerce不同,Himall的插件并不能自呈現(xiàn)(不能自定義view)。另插件尋找方式兩者也不同,himall是先到目錄下找dll(根據(jù)名稱規(guī)則),再找相關(guān)配置,而nopcommerce是先找配置(Description.txt),再找相關(guān)dll,兩種方式并無(wú)優(yōu)劣,但從代碼實(shí)現(xiàn)上來(lái)講后者比前者好。
Himall可能經(jīng)手了太多人,許多邏輯或思考有重復(fù)的嫌疑,其實(shí)完全可以合為一處,很多影響性能的地方也未作處理,如AutoMapper在每次實(shí)例轉(zhuǎn)換時(shí)都要去建立一遍映射規(guī)則,將其置于應(yīng)用程序啟動(dòng)時(shí)執(zhí)行一次即可,舉手之勞不知為何不做。
NopCommerce似乎沒(méi)有用事務(wù)。。。
NopCommerce都是通過(guò)構(gòu)造函數(shù)注入實(shí)例,如下
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; }
但是并非每次都會(huì)用到這些實(shí)例,所以我覺得還是應(yīng)該按需獲取,比如以屬性的方式
private IRepository<ShoppingCartItemInfo> ShoppingcartItemRep { get { return EngineContext.Resolve<IRepository<ShoppingCartItemInfo>>(); } }
另外,這兩套框架有很多值得借鑒的地方,有興趣的同學(xué)可自行研究,本人對(duì)它們接觸時(shí)間不長(zhǎng),就不展開講了。。。
服務(wù)器搭建-VMware vSphere Hypervisor(esxi)
開局一臺(tái)塔式服務(wù)器(Dell T430)一套鼠鍵,裝備全靠撿。。。windows server肯定是必須的,考慮到后續(xù)要安裝如redis、git啥的,雖然大部分有windows版本,但網(wǎng)站最好還是要部署到單獨(dú)系統(tǒng),所以另外再安裝Linux比較好。服務(wù)器只有一臺(tái),只能搞多個(gè)虛擬機(jī),筆者知道的選擇有兩種:VMware Workstation 和 VMware vSphere Hypervisor(esxi),前者一定是裝在OS(Window或Linux)上的,基于OS做虛擬資源處理,而后者本身就可看作是個(gè)OS,直接操作硬件資源[分配到各個(gè)虛擬機(jī)],所以可以認(rèn)為后者更有效率,性能更佳。vmware中文官網(wǎng)(https://www.vmware.com/cn.html)
從官網(wǎng)上下載vSphere Hypervisor,目前是6.5版,使用ultraiso做一個(gè)U盤安裝盤,可參看【親測(cè)】UltraISO 制作ESXi 的 USB 安裝盤,這里有一個(gè)uefi的概念,可以自行了解 UEFI是什么?與BIOS的區(qū)別在哪里?UEFI詳解!,直接感覺就是在啟動(dòng)的到時(shí)候少了自檢(內(nèi)存、硬盤等硬件信息打印)這一步 。安裝和配置步驟可看 HOW TO: Install and Configure VMware vSphere Hypervisor 6.5 (ESXi 6.5)。官方中文文檔 VMware vSphere 6.5 文檔中心,感覺這文檔也不完整,很多連接不能點(diǎn),英文文檔的一下沒(méi)找到,很多東西還是得靠搜索引擎和自己摸索。
遇到評(píng)估許可證已過(guò)期的提示,去下載個(gè)注冊(cè)機(jī)即可:) 其實(shí)也不用,esxi是免費(fèi)的,許可過(guò)期直接去官網(wǎng)申請(qǐng)一個(gè)就能永久使用了(要注冊(cè)賬號(hào))。
6.5版,我們可以在瀏覽器(VM web client)里管理ESXi,甚至可以直接關(guān)閉物理機(jī)(在維護(hù)模式下)。在虛擬機(jī)里安裝完操作系統(tǒng),為了方便管理,還可以安裝VMare Tools,安裝了VMare Tools之后,可以通過(guò)瀏覽器直接啟動(dòng)(要退出維護(hù)模式)、重啟、關(guān)閉操作系統(tǒng)(否則要進(jìn)入到操作系統(tǒng)界面去做這些操作)等(據(jù)說(shuō)還有系統(tǒng)間復(fù)制粘貼之類的功能)。

當(dāng)然了,我們安裝好系統(tǒng)以后,直接遠(yuǎn)程登錄操作更方便。
從官網(wǎng)上下了windows server 2016標(biāo)準(zhǔn)版安裝后,顯示已激活,但水印提示180天到期,以管理員權(quán)限運(yùn)行cmd,輸入 DISM /online /Get-CurrentEdition,發(fā)現(xiàn)是評(píng)估版。然后DISM /online /Set-Edition:ServerStandard /ProductKey:XXXXX-XXXXX-XXXXX-XXXXX-XXXXX /AcceptEula(產(chǎn)品密鑰是網(wǎng)上找的),執(zhí)行完成后重啟,水印提示沒(méi)了(已非評(píng)估版),但是卻顯示未激活。。。提示如下:

并沒(méi)有說(shuō)未激活就不能用的意思,先用著吧,等哪天網(wǎng)上能找到靠譜的密鑰。。。(用于測(cè)試環(huán)境,so,問(wèn)題不會(huì)太大)
另外創(chuàng)建了一個(gè)虛擬機(jī)用于安裝centos,過(guò)程不贅述。之前通過(guò)windows系統(tǒng)去遠(yuǎn)程登錄linux需要安裝ssh客戶端,由于筆者的PC系統(tǒng)是win10,可以安裝Ubuntu子系統(tǒng),然后通過(guò)Ubuntu去連接遠(yuǎn)程centos(Ubuntu默認(rèn)安裝了ssh),如下:

KVM切換器:用于多臺(tái)主機(jī)一臺(tái)顯示器,切換顯示
iDRAC:Integrated Dell Remote Access Controller,也就是集成戴爾遠(yuǎn)程控制卡,使用它,可以遠(yuǎn)程進(jìn)行安裝系統(tǒng),重啟等等原本需要進(jìn)入機(jī)房才能進(jìn)行的操作。
Server Core:windows server 2008開始,最小化的服務(wù)器核心,去掉了幾乎所有的應(yīng)用界面,并且將支持的服務(wù)器角色降到最小,只能進(jìn)行活動(dòng)目錄、DHCP、DNS、文件/打印、媒體、Web等幾種服務(wù)器角色的安裝,還可以安裝Sqlserver和PHP,能否和怎么安裝其它東西筆者并未深入了解。我們可以通過(guò)命令行安裝和配置IIS,然后通過(guò)IIS客戶端,遠(yuǎn)程發(fā)布站點(diǎn)。網(wǎng)上資料較少,不好玩。
Docker:Docker和虛擬機(jī)都是虛擬技術(shù),我們從它們產(chǎn)生的歷史背景可以更好地理解它們之間的區(qū)別。虛擬機(jī)使得用戶能在單臺(tái)物理主機(jī)部署多個(gè)操作系統(tǒng)(與物理機(jī)安裝多系統(tǒng)不一樣,不同虛擬機(jī)可以安裝不同內(nèi)核的操作系統(tǒng)),便于用戶學(xué)習(xí)或者最大限度的使用物理機(jī)資源;物理機(jī)首先要安裝主操作系統(tǒng),虛擬機(jī)再在此之上安裝和運(yùn)行——或者說(shuō)“虛擬”出——它們各自的系統(tǒng);說(shuō)白了,虛擬機(jī)展現(xiàn)給用戶的角色,是一個(gè)個(gè)相互隔離的操作系統(tǒng)。我們知道,在操作系統(tǒng)里安裝應(yīng)用[以及該應(yīng)用需要的運(yùn)行環(huán)境],有時(shí)是一個(gè)挺折騰的過(guò)程,特別是涉及到同應(yīng)用不同版本共存、潛在軟件沖突等情況;而當(dāng)我們終于在測(cè)試機(jī)上把所有環(huán)境都配置好,并運(yùn)行地妥妥貼貼,發(fā)布到線上,相同的過(guò)程還得重新來(lái)一遍,還未必能保證不出現(xiàn)其它問(wèn)題;由于有這些痛點(diǎn),Docker就出現(xiàn)了,它隔離的是操作系統(tǒng)中的各個(gè)應(yīng)用,或者說(shuō)應(yīng)用環(huán)境(也可以是一個(gè)操作系統(tǒng),比如我們可以在centos系統(tǒng)里運(yùn)行一個(gè)ubuntu鏡像,這就搭建了一個(gè)基于ubuntu的應(yīng)用環(huán)境)。可參看 docker容器與虛擬機(jī)有什么區(qū)別?而在centos中啟動(dòng)一個(gè)ubuntu的docker,都是兩個(gè)系統(tǒng),為啥效率會(huì)比虛擬機(jī)高的多?因?yàn)閡buntu共享centos的kernel。由于docker的前提是kernel共用,所以我們看不到在linux下啟動(dòng)一個(gè)windows鏡像,反之亦然。可參看 一篇不一樣的docker原理解析。另基于一個(gè)鏡像啟動(dòng)多個(gè)容器,多個(gè)容器之間共享鏡像,每個(gè)容器在啟動(dòng)的時(shí)候并不需要單獨(dú)復(fù)制一份鏡像文件,減少了鏡像對(duì)磁盤空間的占用和容器啟動(dòng)時(shí)間。參看 Docker鏡像進(jìn)階:了解其背后的技術(shù)原理。
傳統(tǒng)的更新站點(diǎn)(測(cè)試環(huán)境)步驟:
- 從代碼服務(wù)器上獲取最新代碼,如git
- 本地編譯
- 登錄到遠(yuǎn)程服務(wù)器,將編譯生成的程序集、靜態(tài)資源等(不包括web.config)覆蓋到站點(diǎn)文件夾
- 可能還要修改服務(wù)器上的web.config[和其它配置文件]
- 通知相關(guān)人等站點(diǎn)已更新
若代碼提交頻繁,想要所有人第一時(shí)間看到效果,必須同樣頻繁的做這些操作,有沒(méi)有神器能幫我們自動(dòng)做這些工作呢?當(dāng)然是有的,本人用的是Jenkins,目前最新穩(wěn)定版是2.46.2。下面以發(fā)布Asp.net mvc站點(diǎn)為例,擇要點(diǎn)說(shuō)明如何使用。
Jenkins的一些概念:https://jenkins.io/doc/book/glossary/
在windows系統(tǒng)上安裝好后,Jenkins以windows服務(wù)的形式運(yùn)行,并以web方式供我們管理。打開瀏覽器進(jìn)入(默認(rèn)http://localhost:8080/)后,需要安裝必要的插件,比如git和msbuild,然后在Global Tool Configuration下設(shè)置這兩個(gè)插件調(diào)用的執(zhí)行文件地址:

這里需要注意兩點(diǎn):
- 我是用Chocolatey安裝的git,注意安裝好git之后可能需要重啟服務(wù)器,否則在后面設(shè)置git遠(yuǎn)程倉(cāng)庫(kù)時(shí)會(huì)提示找不到git.exe的錯(cuò)誤
- 安裝了.Net Framework的機(jī)子,可以在C:\Windows\Microsoft.NET\Framework64\v4.0.30319下面找到MSBuild.exe,但是它的版本是4.6.xxx,很早以前的,所以不能用。筆者用的是VS2017社區(qū)版開發(fā),去微軟官網(wǎng)下載Visual Studio 2017 生成工具,安裝后的版本為15.0;這里我們還要安裝14.0版本的MSBuild,為什么呢,后面會(huì)說(shuō)到。
然后在項(xiàng)目配置里面,設(shè)置源碼管理:

由于這里是https協(xié)議,所以我們要提供用戶名密碼,Jenkins會(huì)據(jù)此從遠(yuǎn)程倉(cāng)庫(kù)取代碼。那么什么時(shí)候取呢,這就要在Poll SCM(Source Code Manage,這里即git)里設(shè)置了。比如 H H 1,15 1-11 * 表示once a day on the 1st and 15th of every month except December,H可以看作任務(wù)名稱的hash值對(duì)應(yīng)的一個(gè)數(shù),所以不指定確定值的話,用這個(gè)即可。間隔表示法,H/15 * * * * 表示每15分鐘取一次。具體規(guī)則在設(shè)置時(shí)點(diǎn)文本框右邊問(wèn)號(hào)可看到。
現(xiàn)在可以執(zhí)行一下,不出意外Jenkins會(huì)拉取代碼,并放入 安裝目錄\Jenkins\workspace\任務(wù)名\ 下。接下來(lái)設(shè)置編譯步驟:

如果項(xiàng)目中引用的dll有從nuget下載獲取,這些并不會(huì)包含在SCM里,所以我們要先執(zhí)行nuget.exe restore下載相關(guān)dll。nuget.exe這個(gè)應(yīng)用程序可以到官網(wǎng)下載,目前版本是3.5。當(dāng)我們執(zhí)行這步的時(shí)候(注意尚未開始編譯),提示構(gòu)建失敗:

剛開始我以為是編譯時(shí)產(chǎn)生的問(wèn)題,經(jīng)過(guò)一番艱苦卓絕的查閱,就差把MSBuild重新研究一遍(MSBuild 保留屬性和已知屬性),終于發(fā)現(xiàn)原來(lái)是nuget導(dǎo)致的。可以參看 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項(xiàng)目,但是有些項(xiàng)目沒(méi)有直接被web項(xiàng)目引用,是生成到bin目錄下,所以這里編譯整個(gè)解決方案。筆者先用MSBuild15試之,報(bào)錯(cuò):
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”的引用程序集。若要解決此問(wèn)題,請(qǐng)安裝此框架版本的 SDK 或 Targeting Pack,或?qū)?yīng)用程序的目標(biāo)重新指向已裝有 SDK 或 Targeting Pack 的框架版本。請(qǐng)注意,將從全局程序集緩存(GAC)解析程序集,并將使用這些程序集替換引用程序集。因此,程序集的目標(biāo)可能未正確指向您所預(yù)期的框架。
改用MSBuild14沒(méi)這個(gè)錯(cuò)誤,但是在編譯Web項(xiàng)目時(shí)報(bào)錯(cuò):
error MSB4019: 未找到導(dǎo)入的項(xiàng)目“C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\WebApplications\Microsoft.WebApplication.targets”。請(qǐng)確認(rèn) <Import> 聲明中的路徑正確,且磁盤上存在該文件。
網(wǎng)上說(shuō)這是安裝Visual Studio生成的路徑,我不打算在服務(wù)器(我將Jenkins安裝在服務(wù)器上)安裝VS,從開發(fā)機(jī)上目錄C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\Microsoft\VisualStudio\v15.0\WebApplications找到這個(gè)文件,然后在服務(wù)器上新建報(bào)錯(cuò)的那個(gè)路徑,將之copy后解決。
編譯單元測(cè)試項(xiàng)目時(shí)報(bào)錯(cuò):
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拷貝到服務(wù)器,并沒(méi)有用,不知為何。想到單元測(cè)試本身就不必發(fā)布,所以新建了解決方案配置Test,在該配置下,取消單元測(cè)試項(xiàng)目的生成,然后將MSBuild的編譯參數(shù)/p:Configuration=Test。可參看 How to Exclude A Project When Building A Solution? 這樣做還有個(gè)好處,請(qǐng)看使用Web.Config Transformation配置靈活的配置文件
繼續(xù),報(bào)錯(cuò):error MSB6003: 指定的任務(wù)可執(zhí)行文件“tsc.exe”未能運(yùn)行。未能找到路徑“C:\Program Files (x86)\Microsoft SDKs\TypeScript”的一部分。從開發(fā)機(jī)拷貝,解決。
若Jenkins和web服務(wù)器不是同一個(gè)機(jī)子,我們需要用到發(fā)布配置文件,比如Web Deploy,然后增加幾個(gè)MSBuild參數(shù),這里不贅述了。
Web Deploy:先去http://www.microsoft.com/web/downloads/platform.aspx下載Microsoft Web Platform Installer,給服務(wù)器裝上,然后裝上Web Deploy3.5,大致流程可參考Web Deploy 服務(wù)器安裝設(shè)置與使用,還有一個(gè)博文【初碼干貨】在Window Server 2016中使用Web Deploy方式發(fā)布.NET Web應(yīng)用的重新梳理稍顯復(fù)雜,沒(méi)試過(guò)。
構(gòu)建完了可以設(shè)置通知,發(fā)送郵件,要即時(shí)的話,可以用釘釘。看到也有個(gè)微博插件,不過(guò)幾年沒(méi)更新了,不知是否還能用。
其它
動(dòng)態(tài)加載程序集:在MVC中,頁(yè)面是[在請(qǐng)求時(shí)]使用BuildManager動(dòng)態(tài)編譯的,BuildManager will search refrence assembies in the ‘bin’ folder and in the GAC。所以若頁(yè)面使用了我們要?jiǎng)討B(tài)加載的程序集,而程序集文件不在上述兩處,則會(huì)報(bào)錯(cuò)。具體可參看Developing a plugin framework in ASP.NET MVC with medium trust,另外文中說(shuō)的file lock不知道作者是怎么解決的。
運(yùn)行時(shí)貌似都會(huì)將bin目錄下的dll加載到臨時(shí)文件夾下(比如c:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\vs\),所以運(yùn)行時(shí)bin下的dll能刪掉,而不會(huì)提示占用。
在構(gòu)造函數(shù)中使用this會(huì)發(fā)生什么?——并沒(méi)有神奇的事情發(fā)生。。。
我們可以用requirejs等組件模塊化js代碼,使用webpack打包多個(gè)js文件合成為一個(gè)js文件,webpack會(huì)自動(dòng)分析模塊之間的依賴關(guān)系。當(dāng)然webpack不單單這個(gè)功能,可參看 入門Webpack,看這篇就夠了。 當(dāng)然在Http1.1的時(shí)代,有無(wú)必要打包(即減少請(qǐng)求次數(shù))而喪失部分緩存優(yōu)勢(shì)(針對(duì)單個(gè)文件),本人持保留態(tài)度。
1、二者都是異步模塊定義(Asynchronuous Module Definition)的一個(gè)實(shí)現(xiàn);
2、CMD和AMD都是CommonJS的一種規(guī)范的實(shí)現(xiàn)定義,RequireJS和SeaJS是對(duì)應(yīng)的實(shí)踐;
3、CMD和AMD的區(qū)別:CMD相當(dāng)于按需加載,定義一個(gè)模塊的時(shí)候不需要立即制定依賴模塊,在需要的時(shí)候require就可以了,比較方便;而AMD則相反,定義模塊的時(shí)候需要制定依賴模塊,并以形參的方式引入factory中。
.gitignore只適用于尚未添加到git庫(kù)的文件。如果已經(jīng)添加了,則需用git rm移除后再重新commit。
參考資料:
MapKey vs HasForeignKey Difference - Fluent Api
Entity Framework Code First 學(xué)習(xí)日記(8)-一對(duì)一關(guān)系
Docker 核心技術(shù)與實(shí)現(xiàn)原理
轉(zhuǎn)載請(qǐng)注明本文出處:http://www.rzrgm.cn/newton/p/6544563.html

浙公網(wǎng)安備 33010602011771號(hào)