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源码调试的设置,可以参考这一篇
参考:
Using custom VirtualPathProvider to load embedded resource Partial Views
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。