开场一些题外话,
今天登录这个"小菜"的博客园,感触颇多。"小菜"是我之前在QQ群里面的网名,同时也申请了这个博客园帐户,五年前的"小菜"在NET和C++某两个群里面很是的活跃,也很是热心的帮助网友尽能力所及解决技术上的问题。依稀记得当时NET群里面的"青菊、Allen、酷酷",C++群里面的"夏老师、风筝兄"等网友、哥们。时过境迁,后来由于某些缘由而慢慢淡出了QQ群里的技术交流,在这里我真的很是感谢网友"于兄"推荐我到北京某家公司上班,也很怀念当年无话不谈的网友们。
题外话有点多啊,但愿理解,直接进入主题。本人陆续写过三个WEB版的插件式框架,有基于WEBFORM平台、ASPNETMVC平台、ASPNETMVCCORE平台。今天给你们分享的是之前在工做中本身负责的一个基于ASPNETMVC平台的WEB插件框架"Antiquated"取名叫"过期的",过期是由于如今NETCORE正大行其道。
插播一个小广告,有兴趣的朋友能够看看,htttp://www.xinshijie.store.
正式进入主题以前,我想你们先看看效果,因为是图片录制,我就随便点击录制了一下。
插件框架
插件我我的的理解为大到模块小到方法甚至一个页面的局部显示均可视为一个独立的插件。站在开发者的角度来讲,结构清晰、独立、耦合度低、易维护等特色,并且可实现热插拔。固然对于插件小到方法或者局部显示的这个理念的认知也是在接触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上的支持,谢谢!