Mvc 模塊化開發
在Mvc中,標準的模塊化開發方式是使用Areas,每一個Area都可以注冊自己的路由,使用自己的控件器與視圖。但是在具體使用上它有如下兩個限制
1.必須把視圖文件放到主項目的Areas文件夾下才能生效,否則運行時會發生找不到視圖的錯誤。
2.在實際開發中,這種開發方式只能建立一個項目,所有的開發工作都在這個項目里完成,非常不利于團隊大規模開發。
顯然,上面的兩點限制嚴重制約了插件化開發實際運用。為了實現真正的插件化開發,大家積極的思考研究,又找到了如下幾種方式
1.MVC Portable Areas
這種開發方式,是使用單獨的項目進行Areas開發,然后將所有頁面,樣式,腳本等資源以“嵌入的資源”的方式編譯到dll中。這樣被主項目引用后,就不會發生找不到資源的情況了。另外,還有一個名為Razor Generator的插件來幫助做這個事情。
這種開發方式也有個嚴重的問題,即嚴重減慢了開發效率。每當你更改項目里的任意一點元素,包括樣式,腳本,視圖,都需要重新編譯項目才能生效。而在標準的開發方式中,這些元素都是即時生效的。原因就是在運行時,系統尋找的是dll中的資源,而不是項目里的文件。
一般來講,用這種方式進行模塊項目發布,可能會更合適。
2.模擬Areas
這個名字是我自己起的,是通過獨立的項目來模擬主項目Areas部份。在具體使用上,是將普通的Mvc項目建立到主項目的Areas文件夾下,然后手工刪除除Model,Controller,Views外的所有文件,再手工建立Areas注冊文件。這種開發方式比較巧妙的將視圖放到了Areas能找到的目錄,又是通過獨立項目的方式進行開發,基本滿足了模塊化開發的需要。
但是這種模擬開發方式仍有一個小的瑕疵。如果一個解決方案很大,包括了多個主項目,此時就無法實現主項目共用子模塊,因為無法將一個子模塊同時放到多個項目的Areas中去。
ASP.NET MVC 4 pluggable application modules
我目前在工作中使用的是第2種開發方式。對于無法共享子模塊的問題,目前只是將代碼復制多份來解決。這顯然不是一個好的辦法,但也是沒有辦法的辦法。
PS:如果VS支持虛擬目錄就好了。
最近翻閱園子,發現菜鳥一個同學通過自定義VirtualPathProvider類實現了模塊化開發,感覺很不錯,遂仔細研讀,頗有收獲,現分享如下。
3.自定義VirtualPathProvider
這類方式的基本思路是,改變Mvc中默認尋找文件的方式,讓其到我指定的目錄查找,將找到的文件返回。但是具體實現上,我與菜鳥一個有所不同。當然,我是學習他的,是他的簡化版。
菜鳥一個同學是重量級實現方案,其不僅重寫了尋找過程,還自定義了文件過濾機制。另外,其模塊注冊過程是在主項目中完成的。
我的方案是輕量級實現方案,在延用Areas方式的基礎上,重寫了文件的尋找方式。模塊注冊過程是在子項目中完成的。
下面主要介紹我的方案。菜鳥一個同學的方案可以去他的博客中研究。
在我的案例中,MvcApplication1是主項目,MvcApplication2是模塊項目,項目文件夾與項目名同名,兩個項目文件夾放置在同級目錄。
一.什么是VirtualPathProvider
MSND上的說明是:提供了一組方法,以實現用于Web 應用程序的虛擬文件系統。
簡單的講,當一個請求申請某個文件時,如果不存在這個文件,默認會返回404錯誤,但是這個類可以動態將別的資源作為這個資源返回回去。比如將另一個目錄下的同名或不同名文件返回,甚至動態生成一個文件然后返回。
二.注冊模塊路徑
在我們的需求中,文件不是不存在,只是不在Areas目錄下而以。所以我們要做的就是將請求的文件切換到實際目錄下然后返回。那么第一步就是要告訴系統文件的真正路徑。
在這里我定義了IAreaVirtualPathRegistration接口,只有一個方法GetPath,就是返回模塊與路徑的對應關系
public interface IAreaVirtualPathRegistration { List<KeyValuePair<string, string>> GetPath(); }
這里我沒有用字典的原因是我允許同一個模塊名有多個不同的目錄。如果使用了字典數據結構,后面的配置會覆蓋前面的配置。
這里配置的路徑,是相對于主項目的項目文件夾的路徑。
MvcApplication2的注冊文件如下
public class MvcApplication2AreaVirtualPathRegistration: IAreaVirtualPathRegistration { public List<KeyValuePair<string, string>> GetPath() { var pathList = new List<KeyValuePair<string, string>>(); pathList.Add(new KeyValuePair<string, string>("MvcApplication2", "MvcApplication2")); return pathList; } }
三.自定義VirtualPathProvider
名字就叫AreaVirtualPathProvider好了
public class AreaVirtualPathProvider : VirtualPathProvider
定義一個basePath字段,記錄主項目的物理路徑
private readonly string basePath = Path.GetFullPath(HostingEnvironment.MapPath("~") + @"..");
定義了areaVirtualPathList字段,并在靜態構造函數中獲取項目中所有注冊的模塊路徑關系
private static List<KeyValuePair<string, string>> areaVirtualPathList = new List<KeyValuePair<string, string>>(); static AreaVirtualPathProvider() { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { foreach (var type in assembly.GetExportedTypes()) { if (Array.Exists(type.GetInterfaces(), t => t.Name.Equals("IAreaVirtualPathRegistration"))) { var areaVirtualPathRegistration = assembly.CreateInstance(type.FullName) as IAreaVirtualPathRegistration; foreach (var areaVirtualPath in areaVirtualPathRegistration.GetPath()) { var key = @"/Areas/" + areaVirtualPath.Key; var value = areaVirtualPath.Value; areaVirtualPathList.Add(new KeyValuePair<string, string>(key, value)); } } } } }
定義了GetRealPath方法,將請求的虛擬路徑轉換為本地物理路徑,這個方法是核心方法
private string GetRealPath(string virtualPath) { if (virtualPath.StartsWith("~")) { virtualPath = VirtualPathUtility.ToAbsolute(virtualPath); } foreach (var areaVirtualPath in areaVirtualPathList) { if (virtualPath.StartsWith(areaVirtualPath.Key, StringComparison.OrdinalIgnoreCase)) { var realPath = Path.Combine(basePath, virtualPath.Replace(areaVirtualPath.Key, areaVirtualPath.Value)); if (File.Exists(realPath)) { return realPath; } } } return null; }
可以看到,實現其實很簡單,即將虛擬路徑中關于Areas的路徑部份替換為所配置的實際路徑。由于虛擬路徑中對于模塊項目的請求都會自動帶上/Areas/段,所以在上一步中需要為areaVirtualPath的Key的前面增加一個Areas。
下面,就是重寫VirtualPathProvider的相關方法了
首先重寫FileExists方法
public override bool FileExists(string virtualPath) { var realPath = GetRealPath(virtualPath); if (realPath != null) { return true; } return base.FileExists(virtualPath); }
可以看到,這種重寫方式,保留了默認的調用,即對于模塊項目的請求,使用自定義方式,對于主項目的請求,由于獲取的結果是null,最后還是使用默認方式。
重寫GetCacheDependency方法
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { var realPath = GetRealPath(virtualPath); if (realPath != null) { var filePathList = new List<string>(); foreach (var virtualPath1 in virtualPathDependencies) { filePathList.Add(GetRealPath(virtualPath1.ToString())); } return new CacheDependency(filePathList.ToArray(), utcStart); } return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); }
重寫GetFileHash方法
public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) { var realPath = GetRealPath(virtualPath); if (realPath != null) { var filePathList = new List<string>(); foreach (var virtualPath1 in virtualPathDependencies) { filePathList.Add(GetRealPath(virtualPath1.ToString())); } return string.Join(string.Empty, filePathList.ToArray()).GetHashCode().ToString(); } return base.GetFileHash(virtualPath, virtualPathDependencies); }
重寫GetFile方法,這個也是核心方法
public override VirtualFile GetFile(string virtualPath) { var realPath = GetRealPath(virtualPath); if (realPath != null) { var viewStream = new FileStream(realPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); var webConfigFileStream = new FileStream(GetWebConfigFullPath(virtualPath), FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return new AreaVirtualFile(virtualPath, CorrectView(virtualPath, viewStream, webConfigFileStream)); } return base.GetFile(virtualPath); }
在談這個方法之前先說一下Mvc中的View。我們每天寫的cshtml其實只是一個半成品,框架還會為我們自動加上父類聲明,引用的命名空間等。這些文件中缺少的部份一般定義在Web.config中。
在GetFile返回的文件中,也需要包含這些內容。
在上面的代碼中,viewStream變量指向請求的View文件,webConfigFileStream變量指向對應的Web.config文件。Web.config文件通過GetWebConfigFullPath方法獲取
private string GetWebConfigFullPath(string viewVirtualPath) { var realPath = Path.GetDirectoryName(GetRealPath(viewVirtualPath)); while (realPath.Contains("\\")) { var webConfigPath = realPath + @"\Web.config"; if (File.Exists(webConfigPath)) { return webConfigPath; } realPath = realPath.Substring(0, realPath.LastIndexOf('\\')); } return Path.GetFullPath(HostingEnvironment.MapPath("~/Views/Web.Config")); }
可以看到,從cshtml所在文件夾開始逐級向上查找Web.config,如果找到則返回,如果一直沒有找到,則使用主項目的View的Web.config。
拿到視圖文件和Web.config文件后,通過CorrectView方法將必要內容插入到cshtml文件中。這個方法太長,就不貼了。
最后,創建一個AreaVirtualFile對象并返回。
public class AreaVirtualFile : VirtualFile { private readonly Stream stream; public AreaVirtualFile(string virtualPath, Stream stream) : base(virtualPath) { this.stream = stream; } public override bool IsDirectory { get { return false; } } public override Stream Open() { return stream; } }
以上,就是整個方案的全部內容。
對于這個解決方案,我有一點表示不解。我翻看了Mvc的源碼,發現其并沒有實現自己的VirtualPathProvider,那么對于我的自己實現的VirtualPathProvider,為什么GetFile方法不能使用默認實現,而必須是返回加工之后的文件呢?我功力不夠,源碼看的我很混亂,貌似其優先使用了自己的一套文件查找系統,如果找不到才使用VirtualPathProvider。
或者,還有更優的解決方案?
PS:項目實例下載
PPS:對于.Net源碼調試的設置,可以參考這一篇
PPPS:公司倒閉,本人失業,急求.Net相關職位,移動互聯網領域優先
參考:
Using custom VirtualPathProvider to load embedded resource Partial Views

浙公網安備 33010602011771號