开场一些题外话,今天登陆这个"小菜"的博客园,感触颇多。"小菜"是我以前在QQ群里面的网名,同时也申请了这个博客园账户,五年前的"小菜"在NET和C++某两个群里面非常的活跃,也非常热心的帮助网友尽能力所及解决技术上的问题。依稀记得当时NET群里面的"青菊、Allen、酷酷",C++群里面的"夏老师、风筝兄"等网友、哥们。时过境迁,后来因为某些原因而慢慢淡出了QQ群里的技术交流,在这里我真的非常感谢网友"于兄"推荐我到北京某家公司上班,也很怀念当年无话不谈的网友们。
题外话有点多啊,希望理解,直接进入主题。本人陆续写过三个WEB版的插件式框架,有基于WEBFORM平台、ASPNETMVC平台、ASPNETMVCCORE平台。今天给大家分享的是以前在工作中自己负责的一个基于ASPNETMVC平台的WEB插件框架"Antiquated"取名叫"过时的",过时是因为现在NETCORE正大行其道。
正式进入主题之前,我想大家先看看效果,由于是图片录制,我就随便点击录制了一下。
插件框架
插件我个人的理解为大到模块小到方法甚至一个页面的局部显示都可视为一个独立的插件。站在开发者的角度来说,结构清晰、独立、耦合度低、易维护等特点,而且可实现热插拔。当然对于插件小到方法或者局部显示的这个理念的认知也是在接触NOP之后才有的,因为在此之前基于WEBFORM平台实现的插件框架仅仅是按模块为单位实现的插件框架。以上仅是我个人理解,不喜勿喷。
框架 (framework)是一个框子——指其约束性,也是一个架子——指其支撑性。是一个基本概念上的结构,用于去解决或者处理复杂的问题,这是百度百科的定义。通俗的讲,框架就是一个基础结构,比如建筑行业,小区的设计,房屋的地基结构等。IT行业软件系统也类似,框架承载了安全、稳定性、合理性等等特点,一个好的基础框架应该具有以上特点。本文的意图是跟大家一起讨论一个框架的实现思路,并不是去深入的研究某个技术点。
实现思路 应用框架,设计的合理性我觉得比设计本身重要,本人接触过多个行业,看到过一些内部开发框架,为了设计而过于臃肿。本人以前写过通信类的框架,如果你完全采用OO的设计,那你会损失不少性能上的问题。言归正传,插件应用框架我们可以理解为一个应用框架上面承载了多种形式上的独立插件的热插拔。应用框架你最好有缓存,我们可以理解为一级缓存、日志、认证授权、任务管理、文件系统等等基础功能并且自身提供相关默认实现,对于后期的定制也应该能够轻松的实现相关功能点的适配能力。应用框架也并不是所谓的完全是从无到有,我们可以根据业务需求,人力资源去选择合适的WEB平台加以定制。微软官方的所有WEB平台都是极具扩展的基础平台,统一的管道式设计,让我们可以多维度的切入和定制。作为一个应用框架肯定也会涉及大量的实体操作对象,这时候我们可能会遇到几个问题,实体的创建和生命周期的管理。如果我们采用原始的New操作,即便你能把所有创建型设计模式玩的很熟,那也是一件比较头痛的事。对于MVC架构模式下的特殊框架ASPNETMVC而言,之所以用"特殊"这个词加以修饰,是因为ASPNETMVC应该是基于一个变体的MVC架构实现,其中的Model也仅仅是ViewModel,所以我们需要在领域模型Model与ViewModel之间做映射。以上是个人在工作中分析问题的一些经验和看法,如有不对,见谅!
"Antiquated"插件框架参考NOP、KIGG等开源项目,根据以上思路分析使用的技术有:MVC5+EF6+AUTOMAPPER+AUTOFAC+Autofac.Integration.Mvc+EnterpriseLibrary等技术,
算是一个比较常见或者相对标准的组合吧,Antiquated支持多主题、多语言、系统设置、角色权限、日志等等功能。
项目目录结构
项目目录结构采用的是比较经典的"三层结构",此三层非彼三层,当然我是以文件目录划分啊。分为基础设施层(Infrastructures)、插件层(Plugins)、表示层(UI),看图
目录解说:
Infrastructures包含Core、Database、Services、PublicLibrary三个工程,其关联关系类似于"适配"的一种关系,也可理解为设计模式里面的适配器模式。Core里面主要是整个项目的基础支撑组件、默认实现、以及领域对象"规约"。
SQLDataBase为EF For SqlServer。Services为领域对象服务。PublicLibrary主要是日志、缓存、IOC等基础功能的默认实现。
Plugins文件夹包含所有独立插件,Test1为页面插件,显示到页面某个区域。Test2为Fun插件里面仅包含一个获取数据的方法。
UI包括前台展示和后台管理
Framwork文件夹主要是ASPNETMVC基础框架扩展。说了这么多白话,接下来我们具体看看代码的实现和效果。
整个应用框架我重点解说两个部分基础部分功能和插件。我们先看入口Global.asax,一下关于代码的说明,我只挑一些重要的代码加以分析说明,相关的文字注释也做的比较详细,代码也比较简单明了,请看代码
基础部分
protected void Application_Start() { // Engine初始化 EngineContext.Initialize(DataSettingsHelper.DatabaseIsInstalled()); // 添加自定义模型绑定 ModelBinders.Binders.Add(typeof(BaseModel), new AntiquatedModelBinder()); if (DataSettingsHelper.DatabaseIsInstalled()) { // 清空mvc所有viewengines ViewEngines.Engines.Clear(); // 注册自定义mvc viewengines ViewEngines.Engines.Add(new ThemableRazorViewEngine()); } // 自定义元数据验证 ModelMetadataProviders.Current = new AntiquatedMetadataProvider(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); DataAnnotationsModelValidatorProvider .AddImplicitRequiredAttributeForValueTypes = false; // 注册模型验证 ModelValidatorProviders.Providers.Add( new FluentValidationModelValidatorProvider(new AntiquatedValidatorFactory())); // 注册虚拟资源提供程序 var viewResolver = EngineContext.Current.Resolve<IAntiquatedViewResolver>(); var viewProvider = new ViewVirtualPathProvider(viewResolver.GetEmbeddedViews()); HostingEnvironment.RegisterVirtualPathProvider(viewProvider); }
我们往往在做系统或者应用框架开发的时候,一般会去找基础框架给我们提供的合适切入点实现全局初始化。相信玩ASP.NET的朋友应该对Global.asax这个cs文件比较熟悉,或者说他的基类HttpApplication,大概说一下这个HttpApplication对象,HttpApplication的创建和处理时机是在运行时HttpRuntime之后,再往前一点就是IIS服务器容器了,所以HttpApplication就是我们要找的切入点。
EngineContext初看着命名挺唬人的,哈哈,其实还是比较简单的一个对象,我们暂时管它叫"核心对象上下文"吧,个人的一点小建议,我们在做应用框架的时候,最好能有这么一个核心对象来管理所有基础对象的生命周期。先上代码
/// <summary> /// 初始化engine核心对象 /// </summary> /// <returns></returns> [MethodImpl(MethodImplOptions.Synchronized)] public static IEngine Initialize(bool databaseIsInstalled) { if (Singleton<IEngine>.Instance == null) { var config = ConfigurationManager.GetSection("AntiquatedConfig") as AntiquatedConfig; Singleton<IEngine>.Instance = CreateEngineInstance(config); Singleton<IEngine>.Instance.Initialize(config, databaseIsInstalled); } return Singleton<IEngine>.Instance; }
它的职责还是比较简单,以单例模式线程安全的形式负责创建和初始化核心对象Engine,当然它还有第二个职责封装Engine核心对象,看代码
public static IEngine Current { get { if (Singleton<IEngine>.Instance == null) { Initialize(true); } return Singleton<IEngine>.Instance; } }
麻烦大家注意一个小小的细节,EngineContext-Engine这两个对象的命名,xxxContext某某对象的上下文(暂且这么翻译吧,因为大家都这么叫)。我们阅读微软开源源码比如ASPNETMVC WEBAPI等等,经常会碰到这类型的命名。个人理解,
Context是对逻辑业务范围的划分、对象管理和数据共享。我们接着往下看,Engine里面到底做了哪些事情,初始化了哪些对象,上代码。
/// <summary> /// IEngine /// </summary> public interface IEngine { /// <summary> /// ioc容器 /// </summary> IDependencyResolver ContainerManager { get; } /// <summary> /// engine初始化 /// </summary> /// <param name="config">engine配置</param> /// <param name="databaseIsInstalled">数据库初始化</param> void Initialize(AntiquatedConfig config, bool databaseIsInstalled); /// <summary> /// 反转对象-泛型 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> T Resolve<T>() where T : class; /// <summary> /// 反转对象 /// </summary> /// <param name="type"></param> /// <returns></returns> object Resolve(Type type); IEnumerable<T> ResolveAll<T>(); }
其一初始化IDependencyResolver容器,这个IDependencyResolver非MVC框架里面的内置容器,而是我们自定义的容器接口,我们后续会看到。其二基础对象全局配置初始化。
其三后台任务执行。其四提供容器反转对外接口,当然这个地方我也有那么一点矛盾,是不是应该放在这个地方,而是由IOC容器自己来对外提供更好呢?不得而知,暂且就这么做吧。看到这里,我们把这个对象取名为engine核心对象应该还是比较合适吧。
下面我们重点看看IDependencyResolver容器和任务Task
/// <summary> /// ioc容器接口 /// </summary> public interface IDependencyResolver : IDisposable { /// <summary> /// 反转对象 /// </summary> /// <param name="type"></param> /// <returns></returns> object Resolve(Type type); object ResolveUnregistered(Type type); void RegisterAll(); void RegisterComponent(); void Register<T>(T instance, string key) where T:class; /// <summary> /// 注入对象 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="existing"></param> void Inject<T>(T existing); T Resolve<T>(Type type) where T:class; T Resolve<T>(Type type, string name); bool TryResolve(Type type, out object instance); T Resolve<T>(string key="") where T:class; IEnumerable<T> ResolveAll<T>(); }
容器接口本身的功能没有过多要说的,都是一些标准的操作,玩过容器的应该都比较熟悉。接下来我们重点看看容器的创建和适配。容器的创建交由IDependencyResolverFactory工厂负责创建,IDependencyResolverFactory接口定义如下
public interface IDependencyResolverFactory { IDependencyResolver CreateInstance(); }
IDependencyResolverFactory工厂就一个方法创建容器,由它的实现类DependencyResolverFactory实现具体的对象创建,看代码
public class DependencyResolverFactory : IDependencyResolverFactory { private readonly Type _resolverType; public DependencyResolverFactory(string resolverTypeName) { _resolverType = Type.GetType(resolverTypeName, true, true); } // 从配置文件获取ioc容器类型 public DependencyResolverFactory() : this(new ConfigurationManagerWrapper().AppSettings["dependencyResolverTypeName"]) { } // 反射创建容器对象 public IDependencyResolver CreateInstance() { return Activator.CreateInstance(_resolverType) as IDependencyResolver; } }
<add key="dependencyResolverTypeName" value="Antiquated.PublicLibrary.AutoFac.AutoFacDependencyResolver, Antiquated.PublicLibrary"/>我把配置节点也一并贴出来了,代码逻辑也比较简单,一看就明白了,整个创建过程算是基于一个标准的工厂模式实现,通过反射实现容器对象创建。接下来我们看看创建出来的具体ioc容器DefaultFacDependencyResolver,看代码。
public class DefaultFacDependencyResolver : DisposableResource, Core.Ioc.IDependencyResolver, // 这就是我们上面贴出来的容器接口 IDependencyResolverMvc // MVC内置容器接口对象,实现mvc全局容器注入 { // autofac容器 private IContainer _container; public IContainer Container { get { return _container; } } public System.Web.Mvc.IDependencyResolver dependencyResolverMvc { get => new AutofacDependencyResolver(_container); } public DefaultFacDependencyResolver() : this(new ContainerBuilder()) { } public DefaultFacDependencyResolver(ContainerBuilder containerBuilder) { // build容器对象 _container = containerBuilder.Build(); } // ...... 此处省略其他代码 }
DefaultFacDependencyResolver顾名思义就是我们这个应用框架的默认容器对象,也就是上面说的应用框架最好能有一套基础功能的默认实现,同时也能轻松适配新的功能组件。比如,我们现在的默认IOC容器是Autofac,当然这个容器目前来说还
是比较不错的选择,轻量级,高性能等。假如哪天Autofac不再更新,或者有更好或者更适合的IOC容器,根据开闭原则,我们就可以轻松适配新的IOC容器,降低维护成本。对于IOC容器的整条管线差不多就已经说完,下面我们看看任务
IBootstrapperTask的定义。
/// <summary> /// 后台任务 /// </summary> public interface IBootstrapperTask { /// <summary> /// 执行任务 /// </summary> void Execute(); /// <summary> /// 任务排序 /// </summary> int Order { get; } }
IBootstrapperTask的定义很简单,一个Execute方法和一个Order排序属性,接下来我们具体看看后台任务在IEngine里面的执行机制。
public class Engine : IEngine { public void Initialize(AntiquatedConfig config, bool databaseIsInstalled) { // 省略其他成员... ResolveAll<IBootstrapperTask>().ForEach(t => t.Execute()); } // ...... 此处省略其他代码 }
代码简单明了,通过默认容器获取所有实现过IBootstrapperTask接口的任务类,执行Execute方法,实现后台任务执行初始化操作。那么哪些功能可以实现在后台任务逻辑里面呢?当然这个也没有相应的界定标准啊,我的理解一般都是一些公共的
基础功能,需要提供一些基础数据或者初始化操作。比如邮件、默认用户数据等等。比如我们这个应用框架其中就有一个后台任务Automapper的映射初始化操作,看代码
public class AutoMapperStartupTask : IBootstrapperTask { public void Execute() { if (!DataSettingsHelper.DatabaseIsInstalled()) return; Mapper.CreateMap<Log, LogModel>(); Mapper.CreateMap<LogModel, Log>() .ForMember(dest => dest.CreatedOnUtc, dt => dt.Ignore()); // ...... 此处省略其他代码 } }
到此基础部分我挑选出了Engine、ioc、task这几部分大概已经说完当然Engine还包括其他一些内容,比如缓存、日志、全局配置、文件系统、认证授权等等。由于时间篇幅的问题,我就不一一介绍了。既然是插件应用框架,那肯定就少不了插件的
讲解,下面我们继续讲解第二大部分,插件。
插件部分
IPlugin插件接口定义如下
/// <summary> /// 插件 /// </summary> public interface IPlugin { /// <summary> /// 插件描述对象 /// </summary> PluginDescriptor PluginDescriptor { get; set; } /// <summary> /// 安装插件 /// </summary> void Install(); /// <summary> /// 卸载插件 /// </summary> void Uninstall(); }
IPlugin插件接口包含三个成员,一个属性插件描述对象,和安装卸载两个方法。安装卸载方法很好理解,下面我们看看PluginDescriptor的定义
/// <summary> /// 插件描述对象 /// </summary> public class PluginDescriptor : IComparable<PluginDescriptor> { public PluginDescriptor() { } /// <summary> /// 插件dll文件名称 /// </summary> public virtual string PluginFileName { get; set; } /// <summary> /// 类型 /// </summary> public virtual Type PluginType { get; set; } /// <summary> /// 插件归属组 /// </summary> public virtual string Group { get; set; } /// <summary> /// 别名,友好名称 /// </summary> public virtual string FriendlyName { get; set; } /// <summary> /// 插件系统名称,别名的一种 /// </summary> public virtual string SystemName { get; set; } /// <summary> /// 插件版本 /// </summary> public virtual string Version { get; set; } /// <summary> /// 插件作者 /// </summary> public virtual string Author { get; set; } /// <summary> /// 显示顺序 /// </summary> public virtual int DisplayOrder { get; set; } /// <summary> /// 是否安装 /// </summary> public virtual bool Installed { get; set; } // 省略其他代码... }
从PluginDescriptor的定义,我们了解到就是针对插件信息的一些描述。对于插件应用框架,会涉及到大量的插件,那么我们又是如果管理这些插件呢?我们接着往下看,插件管理对象PluginManager。
// 程序集加载时自执行 [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")] namespace Antiquated.Core.Plugins { /// <summary> /// 插件管理 /// </summary> public class PluginManager { // ...... 此处省略其他代码 private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); private static readonly string _pluginsPath = "~/Plugins"; /// <summary> /// 插件管理初始化操作 /// </summary> public static void Initialize() { using (new WriteLockDisposable(Locker)) { try { // ...... 此处省略其他代码 // 加载所有插件描述文件 foreach (var describeFile in pluginFolder.GetFiles("PluginDescribe.txt", SearchOption.AllDirectories)) { try { // 解析PluginDescribe.txt文件获取describe描述对象 var describe = ParsePlugindescribeFile(describeFile.FullName); if (describe == null) continue; // 解析插件是否已安装 describe.Installed = installedPluginSystemNames .ToList() .Where(x => x.Equals(describe.SystemName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault() != null; // 获取所有插件dll文件 var pluginFiles = describeFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories) .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName)) .Where(x => IsPackagePluginFolder(x.Directory)) .ToList(); //解析插件dll主程序集 var mainPluginFile = pluginFiles.Where(x => x.Name.Equals(describe.PluginFileName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault(); describe.OriginalAssemblyFile = mainPluginFile; // 添加插件程序集引用 foreach (var plugin in pluginFiles.Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))) PluginFileDeploy(plugin); // ...... 此处省略其他代码 } catch (Exception ex) { thrownew Exception("Could not initialise plugin folder", ex);; } } } catch (Exception ex) { thrownew Exception("Could not initialise plugin folder", ex);; } } } /// <summary> /// 插件文件副本部署并添加到应用程序域 /// </summary> /// <param name="plug"></param> /// <returns></returns> private static Assembly PluginFileDeploy(FileInfo plug) { if (plug.Directory.Parent == null) throw new InvalidOperationException("The plugin directory for the " + plug.Name + " file exists in a folder outside of the allowed Umbraco folder heirarchy"); FileInfo restrictedPlug; var restrictedTempCopyPlugFolder= Directory.CreateDirectory(_restrictedCopyFolder.FullName); // copy移动插件文件到指定的文件夹 restrictedPlug = InitializePluginDirectory(plug, restrictedTempCopyPlugFolder); // 此处省略代码... var restrictedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(restrictedPlug.FullName)); BuildManager.AddReferencedAssembly(restrictedAssembly); return restrictedAssembly; } /// <summary> /// 插件安装 /// </summary> /// <param name="systemName"></param> public static void Installed(string systemName) { // 此处省略其他代码.... // 获取所有已安装插件 var installedPluginSystemNames = InstalledPluginsFile(); // 获取当前插件的安装状态 bool markedInstalled = installedPluginSystemNames .ToList() .Where(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) .FirstOrDefault() != null; // 如果当前插件状态为未安装状态,添加到待安装列表 if (!markedInstalled) installedPluginSystemNames.Add(systemName); var text = MergeInstalledPluginsFile(installedPluginSystemNames); // 写入文件 File.WriteAllText(filePath, text); } /// <summary> /// 插件卸载 /// </summary> /// <param name="systemName"></param> public static void Uninstalled(string systemName) { // 此处省略其他代码.... // 逻辑同上 File.WriteAllText(filePath, text); } }
从PluginManager的部分代码实现来看,它主要做了这么几件事,1:加载所有插件程序集,:2:解析所有插件程序集并初始化,:3:添加程序集引用到应用程序域,4:写入插件文件信息,最后负责插件的安装和卸载。以上就是插件管理的部分核心代码,代码注释也比较详细,大家可以稍微花点时间看下代码,整理一下实现逻辑。麻烦大家注意一下中间标红的几处代码,这也是实现插件功能比较容易出问题的几个地方。首先我们看到这行代码[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")],这是ASP.NET4.0及以上版本新增的扩展点,其作用有两点,其一配合BuildManager.AddReferencedAssembly()实现动态添加外部程序集的依赖,其二可以让我们的Initialize插件初始化函数执行在我们的Global.asax的Application_Start()方法之前,因为微软官方描述BuildManager.AddReferencedAssembly方法必须执行在Application_Start方法之前。最后还有一个需要注意的小地方,有些朋友可能想把插件副本文件复制到 应用程序域的DynamicDirectory目录,也就是ASP.NET的编译目录,如果是复制到这个目录的话,一定要注意权限问题,CLR代码访问安全(CAS)的问题。CAS代码访问安全是CLR层面的东西,有兴趣的朋友可以去了解一下,它可以帮助我们在日后的开发中解决不少奇葩问题。
插件业务逻辑实现
首先声明,MVC实现插件功能的方式有很多种,甚至我一下要讲解的这种还算是比较麻烦的,我之所以选择一下这种讲解,是为了让我们更全面的了解微软的web平台,以及ASPNETMVC框架内部本身。后续我也会稍微讲解另外一种比较简单的实现方式。我们继续,让我们暂时先把视线转移到Global.asax这个文件,看代码。
/// <summary> /// 系统初始化 /// </summary> protected void Application_Start() { // 此处省略其他代码... // 注册虚拟资源提供程序 var viewResolver = EngineContext.Current.Resolve<IAntiquatedViewResolver>(); var viewProvider = new ViewVirtualPathProvider(viewResolver.GetEmbeddedViews()); //注册 HostingEnvironment.RegisterVirtualPathProvider(viewProvider); }
通过EngineContext上下文对象获取一个IAntiquatedViewResolver对象,IAntiquatedViewResolver这个对象到底是什么?怎么定义的?我们继续往下看。
public interface IAntiquatedViewResolver { EmbeddedViewList GetEmbeddedViews(); }
IAntiquatedViewResolver里面就定义了一个方法,按字面意思的理解就是获取所有嵌入的views视图资源,没错,其实它就是干这件事的。是不是觉得插件的实现是不是有点眉目了?呵呵。不要急,我们接着往下看第二个对象ViewVirtualPathProvider对象。
/// <summary> /// 虚拟资源提供者 /// </summary> public class ViewVirtualPathProvider : VirtualPathProvider { /// <summary> /// 嵌入的视图资源列表 /// </summary> private readonly EmbeddedViewList _embeddedViews; /// <summary> /// 对象初始化 /// </summary> /// <param name="embeddedViews"></param> public ViewVirtualPathProvider(EmbeddedViewList embeddedViews) { if (embeddedViews == null) throw new ArgumentNullException("embeddedViews"); this._embeddedViews = embeddedViews; } /// <summary> /// 重写基类FileExists /// </summary> /// <param name="virtualPath"></param> /// <returns></returns> public override bool FileExists(string virtualPath) { // 如果虚拟路径文件存在 return (IsEmbeddedView(virtualPath) || Previous.FileExists(virtualPath)); } /// <summary> /// 重写基类GetFile /// </summary> /// <param name="virtualPath"></param> /// <returns></returns> public override VirtualFile GetFile(string virtualPath) { // 判断是否为虚拟视图资源 if (IsEmbeddedView(virtualPath)) { // 部分代码省略... // 获取虚拟资源 return new EmbeddedResourceVirtualFile(embeddedViewMetadata, virtualPath); } return Previous.GetFile(virtualPath); } }
定义在ViewVirtualPathProvider中的成员比较核心的就是一个列表和两个方法,这两个方法不是它自己定义,是重写的VirtualPathProvider基类里面的方法。我觉得ViewVirtualPathProvider本身的定义和逻辑都很简单,但是为了我们能更好的理解这么一个虚拟资源对象,我们很有必要了解一下它的基类,虚拟资源提供程序VirtualPathProvider这个对象。
VirtualPathProvider虚拟资源提供程序,MSDN上的描述是,提供了一组方法,使 Web 应用程序可以从虚拟文件系统中检索资源,所属程序集是System.Web。System.Web这个大小通吃的程序集除开ASP.NETCORE,之前微软所有的WEB开发平台都能看到它神一样的存在。吐槽了一下System.Web,我们接着说VirtualPathProvider对象。
public abstract class VirtualPathProvider : MarshalByRefObject { // 省略其他代码... protected internal VirtualPathProvider Previous { get; } public virtual bool FileExists(string virtualPath); public virtual VirtualFile GetFile(string virtualPath); }
从VirtualPathProvider对象的定义来看,它是跟文件资源相关的。WEBFORM平台的请求资源对应的是服务器根目录下面的物理文件,没有就会NotFound。如果我们想从数据库或者依赖程序集的嵌入的资源等地方获取资源呢?没关系VirtualPathProvider可以帮我解决。VirtualPathProvider派生类ViewVirtualPathProvider通过Global.asax的HostingEnvironment.RegisterVirtualPathProvider(viewProvider)实现注册,所有的请求资源都必须经过它,所以我们的插件程序集嵌入的View视图资源的处理,只需要实现两个逻辑FileExists和GetFile。我们不防再看一下ViewVirtualPathProvider实现类的这两个逻辑,如果是嵌入的资源,就实现我们自己的GetFile逻辑,读取插件视图文件流。否则交给系统默认处理。
说到这里,可能有些朋友对于FileExists和GetFile的执行机制还是比较困惑,好吧,索性我就一并大概介绍一下吧,刨根问底是我的性格,呵呵。需要描述清楚这个问题,我们需要关联到我们自定义的AntiquatedVirtualPathProviderViewEngine的实现,AntiquatedVirtualPathProviderViewEngine继承自VirtualPathProviderViewEngine,我们先来看下VirtualPathProviderViewEngine的定义
public abstract class VirtualPathProviderViewEngine : IViewEngine { // 省略其他代码... private Func<VirtualPathProvider> _vppFunc = () => HostingEnvironment.VirtualPathProvider; public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) { // 省略其他代码... GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, CacheKeyPrefixView, useCache, out viewLocationsSearched); } private string GetPath(ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations) { // 省略其他代码... // 此方法里面间接调用了FileExists方法 } protected virtual bool FileExists(ControllerContext controllerContext, string virtualPath) { return VirtualPathProvider.FileExists(virtualPath); } protected VirtualPathProvider VirtualPathProvider { get { return _vppFunc(); } set { if (value == null) { throw Error.ArgumentNull("value"); } _vppFunc = () => value; } } }
我们从VirtualPathProviderViewEngine的代码实现,可以看出FindView方法间接地调用了FileExists方法,而FileExists方法的实现逻辑是通过VirtualPathProvider对象的FileExists方法实现,比较有趣的是VirtualPathProvider属性来源于System.web.Hosting名称空间下的静态属性HostingEnvironment.VirtualPathProvider,大家是否还记得我们的Global.asax里面就注册了一个VirtualPathProvider对象的派生类?我还是把代码贴出来吧,HostingEnvironment.RegisterVirtualPathProvider(viewProvider),有点意思了,那是不是我们的AntiquatedVirtualPathProviderViewEngine的FileExists方法逻辑就是我们VirtualPathProvider派生类里面的实现?没错就是它,我说是你们可能不相信,我们继续贴代码,下面我们看看System.web.hosting下面的HostingEnvironment对象的部分实现。
public static void RegisterVirtualPathProvider(VirtualPathProvider virtualPathProvider) { // 省略其他代码... HostingEnvironment.RegisterVirtualPathProviderInternal(virtualPathProvider); } internal static void RegisterVirtualPathProviderInternal(VirtualPathProvider virtualPathProvider) { // 我们的派生类赋值给了_theHostingEnvironment它 HostingEnvironment._theHostingEnvironment._virtualPathProvider = virtualPathProvider; virtualPathProvider.Initialize(virtualPathProvider1); }
我们的Global.asax调用的RegisterVirtualPathProvider方法,其内部调用了一个受保护的方法RegisterVirtualPathProviderInternal,该方法把我们的VirtualPathProvider派生类赋值给了_theHostingEnvironment字段。现在我们是不是只要找到该字段的包装属性,是不是问题的源头就解决了。看代码
public static VirtualPathProvider VirtualPathProvider { get { if (HostingEnvironment._theHostingEnvironment == null) return (VirtualPathProvider) null; // 省略代码... return HostingEnvironment._theHostingEnvironment._virtualPathProvider; } }
看到_theHostingEnvironment字段的包装属性,是不是感觉豁然开朗了。没错我们的AntiquatedVirtualPathProviderViewEngine里面的FileExtis实现逻辑就是我们自己定义的ViewVirtualPathProvider里面实现的逻辑。到此FileExists的执行机制就已经全部介绍完毕,接下来继续分析我们的第二个问题GetFile的执行机制。不知道细心的朋友有没有发现,我上文提到的我们这个应用框架的ViewEgine的实现类AntiquatedVirtualPathProviderViewEngine是继承自VirtualPathProviderViewEngine,查看MVC源码的知,此对象并没有实现GetFile方法。那它又是什么时机在哪个地方被调用的呢?其实如果我们对MVC框架内部实现比较熟悉的话,很容易就能定位到我们要找的地方。我们知道View的呈现是由IView完成,并且ASPNETMVC不能编译View文件,根据这两点,下面我们先看看IView的定义。
public interface IView { // view呈现 void Render(ViewContext viewContext, TextWriter writer); }
IView的定义非常干净,里面就一个成员,负责呈现View,为了直观一点,我们看看IView的唯一直接实现类BuildManagerCompiledView的定义,看代码
public abstract class BuildManagerCompiledView : IView { // 其他成员... public virtual void Render(ViewContext viewContext, TextWriter writer) { // 编译view文件 Type type = BuildManager.GetCompiledType(ViewPath); if (type != null) { // 激活 instance = ViewPageActivator.Create(_controllerContext, type); } RenderView(viewContext, writer, instance); } protected abstract void RenderView(ViewContext viewContext, TextWriter writer, object instance); }
由BuildManagerCompiledView的定义可以看出,IView的Render方法,做了三件事。1.获取View文件编译后的WebViewPage类型,2.激活WebViewPage,3.呈现。GetFile的调用就在BuildManager.GetCompiledType(ViewPath);这个方法里面,BuildManager所属程序集是System.web。我们继续查看System.web源代码,最后发现GetFile的调用就是我们在Global.asax里面注册的ViewVirtualPathProvider对象的重写方法GetFile方法。看代码,由于调用堆栈过多,我就贴最后一部分代码。
public abstract class VirtualPathProvider : MarshalByRefObject { private VirtualPathProvider _previous; // 其他成员... public virtual VirtualFile GetFile(string virtualPath) { if (this._previous == null) return (VirtualFile) null; return this._previous.GetFile(virtualPath); } }
现在大家是不是彻底弄明白了VirtualPathProvider对象的提供机制,以及在我们的插件应用框架里面的重要作用?好了,这个问题就此告终,我们继续上面的插件实现。
接下来我们继续看插件的安装与卸载。
安装
可以参看上面PluginManager里面Installed方法的代码逻辑。需要注意的一点是,为了实现热插拔效果,安装和卸载之后需要调用HttpRuntime.UnloadAppDomain()方法重启应用程序域,重新加载所有插件。到此为止整个插件的实现原理就已经结束了。心塞,但是我们的介绍还没有完,接下来我们看下各独立的插件的目录结构。
插件实例
以上是两个Demo插件,没有实际意义,Test1插件是一个显示类的插件,可以显示在你想要显示的各个角落。Test2插件是一个数据插件,主要是获取数据用。Demo里面只是列举了两个比较小的插件程序集,你也可以实现更大的,比如整个模块功能的插件等等。看上图Test1插件的目录结构,不知道细心的朋友有没有发现一个很严重的问题?熟悉ASPNETMVC视图编译原理的朋友应该都知道,View的编译需要web.config文件参与,View的编译操作发生在System.Web程序集下的AssemblyBuilder对象的Compile方法,获取web.config节点是在BuildProvidersCompiler对象里面,由于System.web好像没有开源,代码逻辑乱,我就不贴代码了,有兴趣的朋友可以反编译看看。我们回到Test1插件的目录结构,为什么Test1这个标准的ASPNETMVC站点Views下面没有web.config文件也能编译View文件?其实这里面最大的功臣还是我们上面详细解说的VirtualPathProvider对象的实现类ViewVirtualPathProvider所实现的FileExists和GetFile方法。当然还有另外一位功臣也是上面有提到过的VirtualPathProviderViewEngine的实现类AntiquatedVirtualPathProviderViewEngine,具体代码我就不贴了,我具体说下实现原理。对于ASPNETMVC基础框架而言,它只需要知道是否有这个虚拟路径对应的视图文件和获取视图文件,最后编译这个视图。我们可以通过这个特点,如果是插件视图,由ViewEngin里面自己实现的FindView或者FindPartialView所匹配的View虚拟路径(这个路径就算是插件视图返回的也是根目录Views下的虚拟路径)结合FileExists和GetFile实现插件View视图的生成、编译到最后呈现。如果是非插件视图,直接交给ASPNETMVC基础框架执行。根目录views下需要有配置View编译的基类。
下面我们看下怎么实现新的插件。你的插件可以是一个类库程序集也可以是一个完整的ASP.NETMVC网站。以Test1为例,1.首先我们需要新建PluginDescribe.txt文本文件,该文本文件的内容主要是为了我们初始化IPlugin实现类的PluginDescriptor成员。我们来具体看下里面的内容。
FriendlyName: Test Test1Plugin Display SystemName: Test.Test1Plugin.Display Version: 1.00 Order: 1 Group: Display FileName: Test.Test1Plugin.Display.dll
2.新建一个类xxx,名称任取,需要实现IPlugin接口,同样以Test1插件为列
public class Test1Plugin : BasePlugin, IDisplayWindowPlugin { public string Name { get { return "Test.Test1Plugin.Display"; } } public void GetDisplayPluginRoute(string name, out string actionName, out string controllerName, out RouteValueDictionary routeValues) { actionName = "Index"; controllerName = "Test1"; routeValues = new RouteValueDictionary { {"Namespaces", "Antiquated.Plugin.Test.Test1Plugin.Display.Controllers"}, {"area", null}, {"name", name} }; } } public interface IDisplayWindowPlugin: IPlugin { string Name { get; } void GetDisplayPluginRoute(string name, out string actionName, out string controllerName, out RouteValueDictionary routeValues); }
3.如果有view视图,必须是嵌入的资源,理由我已经介绍的很清楚了。
4.如果有需要可以实现路由注册,我们看下Test1的RouteProvider实现。
Public class RouteProvider : IRouteProvider { public void RegisterRoutes(RouteCollection routes) { routes.MapRoute("Plugin.Test.Test1Plugin.Display.Index", "Plugins/Test1/Index", new { controller = "Test1", action = "Index" }, new[] { "Test.Test1Plugin.Display.Controllers" } ); } public int Priority { get { return 0; } } }
5.也是最后一步,需要把程序集生成到网站根目录的Plugins文件下。以上就是整个插件实现的原理和逻辑。说句题外话,ASP.NETMVC实现插件的方式有很多种,甚至有更简单的方式,我之所以挑选这一种,是觉得这种实现方式可以更多的了解整个脉络。下面我们来稍微了解一下另一种实现方式。
ASP.NETMVC插件方式实现二
1.插件管理部分还是基于以上,PlginManage的管理方式,包括加载、初始化、安装、卸载等功能。
2.我们在Global.asax里面不需要注册和重写VirtualPathProvider对象。
3.独立的插件工程如果有View视图文件,不需要添加到嵌入的资源,但是需要按目录结构复制到根目录Plugins下面,另外必须要添加web.config文件并且也复制到根目录Plugins里面的目录下面。webconfig需要指定View编译所需要的条件。
4.Action里面的View返回,直接指定网站根目录相对路径。
大家有没有觉得方式二很简单?好了插件就介绍到这了。本来打算多介绍几个基础模块,多语言、认证授权、多主题等。由于篇幅问题,我最后稍微说一下多主题的实现。
多主题实现
对于web前端而言,多主题其实就是CSS样式实现的范畴,我们的应用框架实现的多主题就是根据不同的主题模式切换css样式实现。看图
位于网站根目录下的Themes文件夹里面的内容就是网站主题的目录结构。每个主题拥有自己独立的样式文件styles.css和Head.cshtml视图文件。视图文件里面的内容很简单,就是返回style.css文件路径。结合Themes目录结构,我注重介绍一下多主题的实现原理。主题切换其实际是css主样式文件切换即可,那么我们怎么实现视图的主样式文件切换?很简单,重写ViewEngin的FindPartialView和FindView方法逻辑,通过视图实现css文件的切换和引入。
1.自定义ViewEngin引擎的view路径模板。
2.ViewEngin的FindPartialView逻辑。
哎终于写完了,发文不易,麻烦大家多多点赞。谢谢。
最后我想多说几句,现在NET行情很差,至少长沙现在是这样。主要是因为Net生态太差,最近长沙NET社区好像要搞一个技术大会,意在推广NetCore,在此祝愿开发者技术发布会议圆满成功。同时也希望大家为Netcore生态多做贡献。下一次我会继续分享在以前工作中自己实现的一个应用框架,这个框架是基于ASP.NETCORE实现的。
最后感谢大家支持,源码在后续会上传github上去,因为还在整理。
码字不易,如果有帮助到您,也麻烦给点rmb上的支持,谢谢!
原文地址:https://www.cnblogs.com/adair-blog/p/10743701.html