對於一個企業級項目開發,模塊化是非常重要的。 預設Mvc框架的AreaRegistration對模塊化開發真的支持很好嗎?真的有很多複雜系統在使用預設的分區開發的嗎?我相信大部分asp.net的技術團隊最開始都研究過分區,甚至在實際項目裡面有嘗試運用,但是碰到了種種問題"各種坑",最後回頭是岸放棄了
對於一個企業級項目開發,模塊化是非常重要的。
預設Mvc框架的AreaRegistration對模塊化開發真的支持很好嗎?真的有很多複雜系統在使用預設的分區開發的嗎?我相信大部分asp.net的技術團隊最開始都研究過分區,甚至在實際項目裡面有嘗試運用,但是碰到了種種問題"各種坑",最後回頭是岸放棄了(我們的團隊也碰到了類似問題,也有人評論中說起,直接搜索asp.net mvc分區也有不少類似信息)。
有人說asp.net Mvc框架就不適合做模塊化開發,我們可以弄一個其他框架來做企業級的分模塊開發,確實現在好像已經有類似的開源項目。
但是,這個確實沒有必要,這裡對Mvc框架做了一個簡單的擴展,基本能做到分模塊開發和單個簡單Mvc項目開發沒有太大區別,並分享給大家。
本文中的慄子是使用.net4.0、Mvc4.0及Unity2.0(企業庫4.0)的。
本分區擴展集成了IoC和分區DI(依賴註入)及分區過濾器的支持。
本分區擴展框架(Fang.Mvc)在演示慄子源碼中包含完整源碼,拿到自己的項目直接引用即可使用了。
感興趣的同學請繼續,用AreaRegistration有不爽的看官請拭目以待...
一、先說一下我用Mvc不爽的地方,看大家是否有同感
1、分區類(繼承AreaRegistration)是個"特殊"類型,只能創建一個對象,而且只能簡單的"New",不能使用構造函數、屬性和方法來初始化對象
好在Mvc是開源的,我們從AreaRegistration的源碼中發現是,Mvc是通過AreaRegistration.RegisterAllAreas()初始化所有分區
從上圖可以看到Mvc先通過把每個繼承AreaRegistration的類型找出來,再通過Activator.CreateInstance創建對象。在我看來類型和對象應該是一對多的關係,分區類也一樣。我希望對分區的欄位和屬性進行不同的初始化以便可以使用同一個類型創建不同的分區。
2、分區的完整路徑提前寫死了,哪怕是換一下前面的路徑都休想
比如有個評論的功能,我希望做成一個分區。新聞的評論地址規則為/News/Comment/,博客的評論地址規則為/Blog/Comment/,使用Mvc預設分區就只能定義為/Comment/了
3、路由初始化需要使用Global.asax
其一、但是多個分區部署同一站點用誰的Global.asax呢?有人說每個Global都定義RegisterAllAreas,用誰的都行。但是對於程式員這個大多都是偏執狂的人群是很難接受的。如果哪個項目下線,正好整個站點都使用的是這個項目的Global,那就都掛了!有風險啊。
其二、那就建一個項目為主項目,其他項目都以分區的形式定義?What?誰是主項目?偏執狂糾結中...
其三、建一個項目為空項目為主項目,只有Global和RegisterAllAreas,還是覺得挺彆扭的。
其四、定義一個HttpModule在Init中執行RegisterAllAreas。這個還算靠譜,建議大家使用。本擴展框架也是定義一個HttpModule來初始化一些Mvc的配置達到擴展目的的。
4、Mvc很多種過濾器,但預設配置過濾器的方法只有兩種,一種是Attribute,一種是GlobalFilters(全局過濾器)
其一、Attribute定義過濾器太細了,要每個Controller或者Action去加,不能漏哦,特別是許可權判斷
其二、GlobalFilters攔截所有請求,如果使用分區開發的項目,每個分區有自己的過濾器需求,該需求可能與其他分區衝突(影響其他分區的Controller正常執行)。
其三、分區我希望復用,其中的Controller當然也要服用,在不同的部署中有使用不同過濾器或者過濾器參數的需求,Attribute定義的過濾器很難滿足要求。
總之我希望有一種過濾器隻影響當前部署的分區,不需要提前和分區及Controller綁定(高耦合),部署(運行時)的時候再和指定分區關聯(當然要藉助IoC容器,這裡使用的是微軟的Unity框架)。
5、Mvc4.0支持DI,如果沒有IoC容器的支持,很雞肋
其一、Mvc4.0預設的DI基本上沒有什麼作用,必須擴展。
其二、每個分區有不同的DI(依賴註入)需求,我希望每個分區可以配置成不同的。
6、使用預設的分區開發出現重名的控制器錯誤或者視圖渲染出錯
6.1 在主項目中增加分區,如果新加分區的控制器(Controller)類名與主項目中控制器同名,導致主項目的控制器出錯,但是分區的控制器正常
這是個挺要命的問題,新加分區不出錯,主項目的控制器出錯,這個問題很難定位,我們一般是哪裡出錯找哪裡。但是我們在主項目的控制器里找不到錯誤,而且這個控制器原來是正常工作的。新加了分區功能,但他"幹掉"原來正常的功能!!!(很恐怖不是嗎?如果知道 原因當然好處理,關鍵是很多人不會去懷疑新加並正常運行的分區,新分區的開發人員也可能會"理直氣壯"的否認是自己的問題。)
6.2 新加分區的視圖出錯
一種原因是預設分區新加視圖的位置和普通項目不一樣,搞不好弄錯位置
還有一種原因,主項目的視圖與新分區的視圖衝突,這個就有點噁心了
總之預設的分區開發總是和普通的項目(Mvc)開發有很多的不同,導致分模塊開發的困難和故障,甚至使人敬而遠之,徹底不分區。
二、Mvc分區擴展方法
1、我們繼續從源碼查找分區初始化的過程
RegisterAllAreas調用CreateContextAndRegister
但是CreateContextAndRegister是一個internal保護的方法,不讓我們用啊!好在這個方法沒有幾行代碼,我們直接再寫一個類似方法就可以了。
2、Mvc最重要的就是路由規則,這個也要搞清楚
這個簡單,隨便建一個分區就可以看得很清楚
原來Mvc是通過RegisterArea調用AreaRegistrationContext的MapRoute方法來設置路由規則的;而AreaRegistrationContext對象創建就更簡單,直接New出來的(參考前面CreateContextAndRegister的源碼截圖)!
現在基本上都搞清楚了,可以動手進行擴展了。
3、首先對分區擴展類(Area),我們來增加一些功能
我這裡定義了一個類型Fang.Mvc.Area繼承AreaRegistration。這個是可以按個人愛好加一些功能的,最好是一些常用功能,每個分區項目的個性化功能可以通過再次繼承這個類型來擴展
3.1 擴展分區基本屬性
通過Name屬性實現分區名的配置(註意:同一個站點下分區名不能重覆)。
通過Path屬性分區路徑首碼,就是分區的路由規則前都加一個這個路徑。其一、大家知道,如果路由規則衝突化,Mvc無法找到正常的路由規則,這個很要命,增加一個部署是配置的路徑就很有必要了。其二、我的需求比較奇特,我希望同一個站點下可以部署多個相同的分區,就像前面說的我們要給/News/和/Blog/都部署Comment分區,訪問路徑不一樣,訪問的資料庫或者數據表也可以不一樣,對已數據源不一樣就靠DI了,後面再繼續說。
通過NameSpaces屬性配置當前分區查找Controller的範圍。如果沒有配置就按Mvc預設處理,按當前分區類的命名空間。
這樣我們的每個分區是獨立的(通過NameSpaces隔離),每個分區既可以獨立運行調試和可以和其他分區部署到一起也不會相互衝突。和普通簡單的Mvc項目幾乎沒有區別,大大減少開發和學習成本。當然有人會說,用這個分區擴展框架不是還要現學容器框架(Unity),這個確是事實。但是容器框架(Unity)的作用不僅僅是管理好幾個分區,容器的Ioc、Aop等作用對項目開發那是如虎添翼,容器技術幾乎是大型系統開發必備工具,小小的分區框架集成了容器本身也是該框架的一大亮點。
4、定義一個路由規則註冊類(AreaRoute)來達到把Area的配置應用到路由註冊
AreaRoute通過調用AreaRegistrationContext的對應方法註冊
AreaRoute調用Area的CheckName、CheckUrl和CheckNameSpaces來實現Area的配置
把分區的路由規則保存到Area的_routes列表中備用(這裡不詳述作用,不在本文探討範圍內)
5、現在開始搞定分區過濾器
5.1 在Area類增加屬性Filters配置分區過濾器
5.2 定義AreaFilterProvider類型來調用Area的Filters
AreaFilterProvider繼承Mvc框架的IFilterProvider介面
在分區初始化時把AreaFilterProvider添加到Mvc的FilterProviders.Providers中,任務完成。
6、該輪到期盼已久DI容器了
6.1 首先定義Fang.Mvc.Container類來封裝一下Unity容器
這個使用其他容器工具也是可以的,甚至封裝一個工廠,支持多種容器也未不可。
至於有人說Unity有"bug",不適合做DI。我使用Unity容器有幾年了,還沒完全搞明白Unity,我認為Unity容器還是挺博大精深的,足夠我用了。至於"bug"也算不上bug,最多是坑,多踩踩就平了。至於Unity(及企業庫)有多挺博大精深,不在本文探討範圍,敢興趣的可以自己研究,找高手探討。
找我交流也行(但是我還在學習中,認識可能還膚淺或者有錯誤,最好不要誤導了大家)。
6.2 增加屬性DependencyContainer配置DI(依賴註入)容器的名字
6.3 定義AreaDependencyResolver類來實現分區配置和DI配置的結合
AreaDependencyResolver繼承Mvc框架的IDependencyResolver介面
接下來就簡單了,按Http上線文信息獲取分區配置,調用容器工廠,調用容器實現GetService方法即可。
7、萬事俱備了,借HttpModule的東風吹起來
定義類AreaMergeModule繼承asp.net的IHttpModule,在Init方法中一通調用初始化以上擴展的功能的初始化。
調用容器工廠獲取Mvc容器,在容器中獲取所有的Area對象並逐個初始化,再初始化AreaDependencyResolver。
三、重頭戲開場,炒"慄子"
1、一個站點配置兩個分區,分別調用不同的數據源
源碼路徑:\Test\MvcApplication1
1.1 先看分區及容器配置
配置文件:\Test\MvcApplication1\ConfigFiles\unity.config
以上使用分區類MvcApplication1.RouteConfig定義兩個分區(A和B),每個分區的名字和路徑等都配置不一樣。
另外還額外定義了兩個依賴註入的容器。名字分別為兩個分區配置的依賴註入的容器名。
以上還可以看出分區類和分區的Controller沒有依賴關係,事實上只要路由規則沒有特殊路由及其他要求,可以使用定義好的預設分區類Area。而且分區定義是按命名空間,事實上原有複雜系統可以在不修改原項目的情況下按邏輯(命名空間)拆分為多個分區,以後看情況再逐個分區拆分為獨立項目,優化項目結構。
如果有同學說你上面的配置看不懂,不好意思要補課了,找度娘,搜索"Unity2.0容器配置"(小心有一個叫Unity3d的東西是做游戲的,和這個沒關係)。
1.2 再看HttpModule配置
配置文件:\Test\MvcApplication1\Web.config
system.web的httpModules在經典模式下起作用,system.webServer的modules在集成模式下起作用,如果不確定兩個都配上就Ok了。
1.3 分區路由配置(與Mvc預設路由配置及分區配置對比)
配置文件:\Normaltest\MvcApplication4\App_Start\RouteConfig.cs
配置文件:\Test\MvcApplication1\App_Start\RouteConfig.cs
以上三種路由配置中,可以看出新改良的分區擴展居然比Mvc預設分區配置和靜態路由配置更相似
首先使用該擴展配置路由沒有新的學習成本
其次原非分區項目要改成分區項目建一個分區類(擴展後的),把路由規則直接複製過來,意味遷移成本非常低。
1.4 再看一下Controller怎麼用DI
代碼文件:\Test\MvcApplication1\Controllers\HomeController.cs
HomeController執行依賴一個數據源(Config),但是HomeController並沒有定義和調用獲取數據源的方法,只是把數據源(Config屬性)聲明為Dependency的。
1.5 最後看測試效果
例子有點簡單,完全說明問題,兩個不同路徑(分區路徑)的Url都訪問到HomeController而且數據源(Config屬性)自動初始化成功,而且這兩個地址調用了不同的數據源。
2、分區過濾器的慄子
源碼路徑:\Test\Site
2.1 先看分區及容器配置
配置文件:\Test\Site\ConfigFiles\unity.config
這次配置更複雜了一點,定了三個分區,使用兩個分區類。配置了一個過濾器,而且有兩個分區(不同分區類)引用了該過濾。(依賴註入配置同前一個例子,沒必要說了)
2.2 其他配置和上一個例子相似,直接看三個分區效果
對此不想多說了,一切都在以上的熱乎乎的慄子里,我的目的已經達到了。你的目的達到了嗎?
模塊開發是個系統性問題,後續我還將發表使用Mvc分區擴展框架更好的解決實際開發問題文章,敬請期待...
四、高潮來了,奉送源碼
瞬間高潮就結束了,是不是有些遺憾啊。本框架是從搜房內部框架中拆分出來的一部分。隨著.net開源社區的快速發展,搜房作為廣泛使用.net的大型互聯網公司之一,正在逐步以新的姿態擁抱開源。
我們鼓勵(甚者獎勵)搜房的程式員參與開源項目或者把內部通用系統脫敏整理後在網上開源。