Mvc 模塊化開發


      在Mvc中,標准的模塊化開發方式是使用Areas,每一個Area都可以注冊自己的路由,使用自己的控件器與視圖。但是在具體使用上它有如下兩個限制

      1.必須把視圖文件放到主項目的Areas文件夾下才能生效,否則運行時會發生找不到視圖的錯誤。

      2.在實際開發中,這種開發方式只能建立一個項目,所有的開發工作都在這個項目里完成,非常不利於團隊大規模開發。

 

      顯然,上面的兩點限制嚴重制約了插件化開發實際運用。為了實現真正的插件化開發,大家積極的思考研究,又找到了如下幾種方式

      1.MVC Portable Areas

      這種開發方式,是使用單獨的項目進行Areas開發,然后將所有頁面,樣式,腳本等資源以“嵌入的資源”的方式編譯到dll中。這樣被主項目引用后,就不會發生找不到資源的情況了。另外,還有一個名為Razor Generator的插件來幫助做這個事情。

      這種開發方式也有個嚴重的問題,即嚴重減慢了開發效率。每當你更改項目里的任意一點元素,包括樣式,腳本,視圖,都需要重新編譯項目才能生效。而在標准的開發方式中,這些元素都是即時生效的。原因就是在運行時,系統尋找的是dll中的資源,而不是項目里的文件。

      一般來講,用這種方式進行模塊項目發布,可能會更合適。

      MVC Portable Areas

      Portable Area disadvantages

 

      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相關職位,移動互聯網領域優先

      參考:

      MVC 插件式開發

      Using custom VirtualPathProvider to load embedded resource Partial Views

      如何用MEF實現Asp.Net MVC框架

      基於ASP.NET MVC3 Razor的模塊化/插件式架構實現

      自定義VirtualPathProvider映射ASP.NET MVC View


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM