開場一些題外話,
今天登陸這個"小菜"的博客園,感觸頗多。"小菜"是我以前在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上的支持,謝謝!
