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