[翻译]Orchard如何工作

时间:2022-05-13 04:09:55

Orchard一直是博主心中神一般的存在,由于水平比较菜,Orchard代码又比较复杂看了几次都不了了之了。这次下定决心要搞懂其工作原理,争取可以在自己的项目中有所应用。为了入门先到官网去学习一下相关的基础内容。看到这篇比较不错的入门文章,边学习边翻译一下。希望给有需要的朋友以帮助,也希望和对Orchard感兴趣的朋友一起交流。

Orchard如何工作

构建一个Web内容管理系统不同于构建一个常规web应用:前者更像构建一个应用容器。设计一个这样的系统,可扩展性应该放在第一位。这可能是一个挑战因为增加可扩展性所必需的开放式架构可能会影响应用程序的可用性:系统的所有内容都需要与包括用户界面在内的未知的模块相组合使用。(感谢王老师提供的翻译,对于原文那种巨长的读起来就像没加标点的文言文一样的句子只能找外援了。)把这些彼此不了解的小模块精心安排在一起组成一个连贯的整体正是Orchard所做的全部。

这个文档解释了Orchard架构选择并说明了这些选择的方案怎样解决同时获得扩展性或好的用户体验这个独有问题。

架构

Modules

Core

Orchard Framework

ASP.NET MVC

NHibernate

Autofac

Castle

.NET

ASP.NET

IIS or Windows Azure

Orchard基础

Orchard CMS基于一些现有的框架和类库完成。下面列出的是其中一些最根本的组成。

  • ASP.NET MVC: ASP.NET MVC是一个流行的Web开发框架,其鼓励关注点分离。

  • NHibernate: NHibernate是一个ORM工具。其负责将Orchard内容项的持久化到数据库并且将模块开发中对持久化的关注完全移除从而大大简化数据模块。可以在任意核心内容类型的源码(如Pages类)中,看到NHibernate的例子。

  • Autofac: Autofac是一个IoC容器。Orchard对于依赖注入有着重度依赖。创建一个可以注入的Orchard依赖只需编写一个继承自IDependency接口(或一个继承自IDependency更具体话的接口)的类型就可以。使用这个依赖只需要将恰当的类型作为构造函数的参数传入即可。Orchard将管理被注入的依赖对象的作用域和生存期。IAuthorizationService、RolesBasedAuthorizationService和XmlRpcHandler的代码是可以作为参考的例子。

  • Castle Dynamic Proxy: Castle被用于动态代理生成。

Orchard应用和框架在这些基础框架之上构建,作为附加的抽象层。有多种方式实现这些基础功能,使用Orchard也不需要了解NHibernate, Castle或 Autofac的实现细节。(这句话原文很抽象啊,就这么一翻译不一定对)

Orchard Framework

Orchard Framework是Orchard中最底层的部分。其包含应用的引擎或者说至少是那些不能被独立到一个模块的部分。它们是几乎大部分基础模块都需要依赖的最一般化的部分。你可以把它看作Orchard的基础类库。

启动Orchard

当一个Orchard Web应用开始运转,一个Orchard Host被创建。这个Host在应用程序域层级是单例的。

下一步,Host将使用ShellContextFactory为当前"租户"(tenant)获取Shell。"租户"是运行于相同appdomain中相互独立的应用程序实例,用于提高站点密度。这是用户可以区分的最上层的存在。Shell在"租户"这一级是单例的,可以被实际上用来代表"租户"。Shell对象高效的提供"租户"层的隔离同时让"多租户"对模块化编程模型透明。

Shell创建后将由ExtensionManager中取得可用扩展的列表。Extensions是模块和主题。默认实现下将扫描模块和主题目录用来找到扩展。

同时,Shell由ShellSettingsManager取得用于"租户"的设置列表。默认实现是由Appdata中合适的子目录下获取设置,但也可以由其他位置获取这些设置。如,Azure版本的实现中设置存储在blob storage中,因为Azure环境下Appdata不一定是可写的。

接着Shell由当前host可用的扩展列表和当前"租户"的设置中获取CompositionStrategy对象并用它来准备IoC容器。这个操作的结果不是用于Shell的IoC容器,而是一个ShellBlueprint。其由依赖列表,控制器和记录蓝图(record blueprint)组成。

随后每个租户的ShellSettings及ShellBluePrint被传入ShellContainerFactory的CreateContainer方法并得到一个ILifetimeScope。这个ILifetimeScope基本上使IoC容器的作用域被限制在租户一级,这样模块可以直接在当前租户的作用域内获得注入的依赖而无需其他特殊操作。

依赖注入

Orchard中创建可注入依赖的标准方式是创建一个继承自IDependency或其它继承自IDependency接口的接口并实现这个接口。在使用方面来看,只需在构造函数中声明一个接口类型的形参。应用框架会自动发现所有依赖并按需要初始化实例并注入到构造函数中。

有三种不同的作用域可用于注册依赖,通过继承自不同的接口就可以在三种中选择其一。

Request: 一个依赖对象将在每次HTTP请求到来时被创建并在请求被处理完成后销毁。使你的接口继承自IDependency接口就是选择了这种作用域范围。这个对象的创建应当相当轻量。

Object: 每一次获取接口的依赖时将会创建一个新的依赖实例。实例从不被共享。使用这种方式,需要让接口继承自ITransientDependency。这个依赖对象的创建必须极为轻量。

Shell: 对于每个shell/租户只有一个实例被创建。要使用这种方式,需要让接口继承自ISingletonDependency。只有当需要为Shell的生命周期维持一个一致的状态才使用这种对象。

替换已存在的依赖

可以通过OrchardSuppressDependency特性来修饰类从而替换已存在的依赖,这个Attribute接收一个完整的类型名作为参数,其表示要替换的对象的类型。

依赖排序

有些依赖不是唯一的,相反是列表的一部分。如,handler在同一时间都是活动的。有些情况下你希望修改依赖被获取的顺序。可以通过修改模块的清单(mainfest)中Feature部分的Priority属性来实现。见下面的例子:

Features:

Orchard.Widgets.PageLayerHinting:

Name: Page Layer Hinting

Description: ...

Dependencies: Orchard.Widgets

Category: Widget

Priority: -1

ASP.NET MVC

Orchard构建于ASP.NET MVC之上,但为了添加如主题和租户隔离等支持,Orchard需要引入一个附加的间接层,其将作用在ASP.NET MVC端它需要的概念上,另外也作用于Orchard按Orchard概念等级划分其中的组成部分。

例如,当请求一个指定视图时,我们的LayoutAwareViewEngine将作为替代参与这一过程。严格来讲,这不是一个新的视图引擎,因为其不关心实际渲染,但其包含找到当前主题对应的恰当视图的逻辑,然后将渲染工作委托给实际的视图引擎。

类似的,我们也有Route Provider,Model Binder和控制器工厂类,它们都扮演ASP..NET MVC单一入口的角色并负责将请求转发给底层恰当作用域中的对象。

以路由为例,我们可能有n个路由提供程序(一般来自于各模块)以及一个Route Publisher与ASP.NET MVC进行交互。Model Binder和控制器工程的情况也类似。

内容类型系统

Orchard中的内容被一个实际的类型系统所管理,这个系统比底层.NET的类型系统功能更丰富且更动态,从而可以提供更供Web CMS系统所需的灵活性。类型必须可以在运行时动态组合并且反映出内容管理的关注点。

类型、部件和字段

Orchard可以处理任意数量的内容类型,包括那些站点管理员以无编码方式动态创建的类型。这些内容类型由内容部件聚合而成,每一个内容部件处理一个单一的关注点。这样做的原因是许多关注点跨多个内容类型。

例如,一篇博文,一个产品和一段视频可能都有一个可路由的地址,相关评论及标签。这样,可路由地址、评论和标签分别作为Orchard中一个单独的内容部件存在。这样,这些通用的管理模块可以被开发一次并被应用到任意内容类型,包括那些评论模块的作者都不知道的内容类型。

部件本身可以有属性和内容字段。内容部件也可以以与部件相同的方式被复用:一个具体的字段类型往往被用于多个部件和内容类型。部件与字段的不同在于它们所处理的规模和它们的语义。

字段是比部件更精细构成。例如,字段类型可能描述一个电话号码或一个坐标,而部件一般用来描述一个完整的关注点像评论和标签。

但是这里重要的不同在于语义方面:如果对象是实现"is a"关系你应该写一个部件,反之如果对象是"has a"关系应该写一个字段。

例如,衬衫is a商品,它has aSKU和价格。你不会说衬衫有一个商品或衬衫是一个价格或SKU。

通过这个例子你知道,衬衫这个内容类型由商品部件构成,而商品部件由一个名为"price"的金额字段和一个名为SKU的字符串字段构成。

另一个不同是,对于每一个内容类型其中只有一个特定类型的部件,从"is a"关系的观点来看这是合理的。而一个部件可以有任意数量的特定类型的字段。另一种解释方式,部件中的存储字段的方式就像一个字典,字段的名称为键,字段的类型为值。而文档类型是一个部件类型的列表(不包含部件的名字)。

另一种在部件和字段间做选择的方法是:如果你认为用户对于一个内容类型中的某对象的可能需要多于一个实例,这个对象应该被作为一个字段。

内容类型剖析

正如我们所见,一个内容类型由内容部件构成。从代码角度来看,内容部件一般与下面的概念相关:

  • 一个记录,部件数据POCO的表现

  • 实际组成是一个模型类,其继承自ContentPart<T>,其中T是记录类型

  • Repository,Repository不需要模块作者来实现,Orchard会使用一个泛型的Repository。

  • 处理器。处理器实现了IContentHandler接口并且由如OnCreated、OnSaved等一系列事件处理器构成。基本上来说,它们在内容项生命周期的每个部分添加钩子函数来执行一系列任务。在它们的构造函数中可以添加处理来参与内容项的实际构成。在ContentHandler基类有一个Filter集合用来给内容类型添加通用行为。

    如,Orchard提供了一个StorageFilter,可以很简单声明一个内容部件的持久化应该怎样被处理:只需Filters.Add(StorageFilter.For(myPartRepository));,然后Orchard会处理好myPartRepository的数据持久化到数据库的操作。

    另一个处理器的例子是ActivatingFilter,其负责完成把部件焊接到类型上的实际操作:这样调用Filters.Add(new ActivatingFilter<BodyAspect>(BlogPostDriver.ContentType.Name));其将正文内容部件添加到博文对象中。

  • 驱动。驱动是更友好、更专门化的处理器(结果肯定是灵活性更低)并且与特定的内容部件类型相关联(它们继承自ContentPartDriver<T>,其中T是内容部件类型)。换句话说,处理器不必仅限于一个内容部件类型。驱动可以看作针对一个特定部件的控制器。它们通常构建通过主题引擎被渲染的外观。

内容管理器

所有Orchard中的内容都通过ContentManager对象来访问,这是为什么你可以使用你之前并不知道类型的内容的原因。

ContentManager提供方法查询内容存储,管理内容版本以及管理内容发布状态。

事务

Orchard针对每个HTTP请求自动创建一个事务。这意味着一个请求中发生的所有操作都是一个“包围”事务的一部分。如果请求过程中有代码终止事务,所有数据操作都将被回滚。相反,如果事务从未被显式取消,在请求的最后所有操作将被提交而无需显式提交。

请求生命周期

这一小节,我们将以请求一篇指定的博文作为例子。

当对一篇特定博文的请求到达时,程序首先查找由各种模块提供的可用路由,在其中找到博客模块匹配的路由。这个路由可以解析对博文控制器中item操作的请求,后者将由内容管理器中查找博文。然后基于请求的主要目标由内容管理器中获得一个Page Object Model (POM)(通过调用 BuildDisplay方法),这样博文就被由内容管理器中检索出来了。

博文有其自己的控制器,但对于所有的内容类型不一定如此。如,动态内容类型通过更通用的来自Core Routable部分的ItemController被处理。ItemController中的Display操作完成和博文控制器中几乎同样的的事情:由内容管理器中获取内容项然后用其生成一个POM。

接着布局视图引擎将根据当前的主题并使用模型类型结合Orchard对视图命名的约定来解析得到恰当的视图。

在视图内部,可能发生如区域定义这样更动态的形态构造。

实际渲染由样式引擎来完成,其查找对应的模板或产生外观的方法(shape method)来以出现顺序递归的绘制POM出现的每一个“形状(shape)”

小工具

小工具是拥有Widget内容部件和Widget版型的内容类型。如同其他内容类型一样,小工具也是由部件和字段组成。这意味着它们可以和其他内容类型使用相同的编辑方式和渲染逻辑。它们同样共享相同的生成模块,这意味着人恶化已存在的内容部件都可能被作为小部件的一部分使用而几乎不需要改动成本。

小工具通过小工具层被添加到页面。层是一系列小工具。层有一个名字,一套决定它们应该出现在站点中的哪些页面上的规则,一个定义小工具及小工具相关绘制区域位置和排序的列表以及设置。

每个层中附加的规则使用IronRuby表达式来表示。这些表达式可以使用程序中任何IRuleProvider的实现。Orchard附带了2个开箱即用的实现:url和认证。

站点设置

Orchard中站点是一个内容项,其可以让模块来焊接附加部件。通过这种方式模块来提供站点设置功能。

站点设置是租户隔离的。

事件总线

Orchard及其模块通过创建用于依赖的接口来公开扩展点,这些接口的实现可以被注入。

实现针对扩展点的插件既可以通过实现扩展点接口完成,也可以通过实现一个具有相同名称和相同方法的接口来完成。换句话说,Orchard不要求严格的强类型接口一致,这样插件可以扩展一个扩展点而无需对声明扩展点接口的程序集产生依赖。

这正是Orchard事件总线的一个实现。当一个扩展点调用注入的实现,一个消息被发布到事件总线上。监听事件总线的对象之一将消息分发到继承自名称一致的接口的类的方法中。

命令

Orchard站点上的许多操作既可以通过命令行来执行也可以通过管理界面来执行。这些命令是通过实现ICommandHandler的类型中使用CommandName特性装饰的方法来公开的。

Orchard命令行工具通过模拟web站点环境以及通过反射检查程序集的方式在运行时发现可用的命令。这个命令运行的环境与实际运行站点的环境尽可能的接近。

搜索与索引

默认情况下,搜索和索引使用Lucene实现,默认实现也可以被替换为其它索引引擎。

缓存

Orchard中缓存依赖于ASP.NET缓存,但我们也公开一个可以通过调用ICache类型依赖对象中Get方法来使用帮助API。Get方法接收一个键和一个用于在缓存还没包含请求项时生成缓存项值的函数。

使用Orchard缓存API的主要好处是它是租户透明的。

文件系统

Orchard文件系统是抽象的,存储可以根据环境被定向到物理文件系统或如Azure Blob Storage这样的外部存储。Media模块是使用抽象文件系统的一个例子。

用户和角色

Orchard中User是内容项(虽然不是可被路由的),这使它很易于用来构建资料模块,例如可以使用附加字段来扩展它。Role作为内容部件被焊接到User上。

权限

每个模块可以公开一套权限以及默认情况下这些权限应怎样被授予Orchard的默认角色。

任务

模块可以通过调用IScheduledTaskManager类型依赖对象上的CreateTask方法来安排一个任务。任务可以通过实现IScheduledTaskHandler来被执行。Process方法可以检查任务类型名称并决定怎样处理它。

任务运行于来自ASP.NET线程池的一个单独线程上。

通知

模块可以通过调用INotifier类型依赖对象上的方法来将消息显示到管理界面上。一个请求中可以创建多个通知作为请求的一部分。

本地化

应用程序及其模块的本地化通过调用T方法并传入字符串资源来完成:@T("This string can be localized")。查看使用本地化帮助函数获得更多细节和指导。Orchard资源管理器可以由位于应用程序指定位置的PO文件加载本地化资源字符串。

内容项的本地化通过一个不同的机制来实现:内容项的本地化版本是物理上独立的内容,并且通过一个专用的部件相连接。

当前使用的文化由文化管理器决定。默认实现是返回站点设置中配置的文化,另外可替换的一个实现是由用户档案或浏览器设置中获取。

日志

日志通过ILogger类型依赖的对象实现。不同的实现可以将日志项发送到不同的存储类型。Orchard的实现使用了Castle.Core.Logging进行日志。

Orchard核心

Orchard.Core程序集包含了一系列Orchard运行必须的模块。其它模块可以安全的依赖这些永远可用的模块。

核心模块的例子如订阅,导航以及路由。

模块

Orchard默认分发版本包含了大量内置的模块,如博客以及页面,同时许多第三方模块也在被开发。

模块就是一个ASP.NET MVC区域以及一个用于扩展Orchard的manifest.txt文件。

一个模块一般包含事件处理器,内容类型以及它们默认的呈现模版包括一些管理界面。

当模块项目的csproj文件被改动或csproj文件关联的文件被改动时,模块可以由源代码被动态编译。这带来了一种"记事本"开发方式,即不需要开发者显式执行编译,甚至使用如Visual Studio这样的IDE。

模块必须被放置到Modules文件夹(Orchard.Web/Modules/MyModule)且文件夹名称必须与项目产生的编译后的DLL的名称匹配。所以,如果你有一个名为My.Custom.Module.csproj的自定义模块项目,这个项目编译产生My.Custom.Module.dll,这样模块根目录必须被命名为My.Custom.Module。[~/Modules/My.Custom.Module/]

主题

Orchard中一个基本的设计原则是Orchard生成的所有HTML,包括模块产生的标记可以被主题中的内容所替换。约定定义了那个文件应该进入主题文件层次的哪个位置。

Orchard中整个渲染机制都是建立于形状(shape)之上。主题引擎的工作就是找到当前的主题并让主题决定绘制每个形状的最佳方式。每个形状都有一个默认的渲染方式,可能来自模块定义的视图文件夹中的一个模板,也可能是一段绘制形状的方法代码。默认的渲染方式可能被当前的主题覆盖。主题通过自身的渲染形状的模板或绘制形状的代码来代替默认的从而实现覆盖。

主题可以拥有父级,这样子主题可以专门针对父主题的一部分或改造父主题的一部分。Orchard中有一个名为Theme Machine的基础样式,其已经过设计可以轻松的用于作为父主题。

主题可以像模块那样包含代码:它们可以有自己的csproj文件同样也能享受到动态编译的好处。这样主题可以定义自己的形状绘制方法,而且也可以像管理界面添加它们所有的任何设置项。

当前主题的选择通过实现IThemeSelector的类实现,这个类的方法对所有请求返回一个主题名称和优先级。这样可以有多个选择器对主题的选择提供选项。Orchard默认带有四种IThemeSelector的实现:

  • SiteThemeSelector返回当前针对租户或站点配置的主题,优先级低。

  • AdminThemeSelector在当前URL为管理URL时接管并返回管理主题,优先级高。

  • PreviewThemeSelector使用正在被预览的主题覆盖站点当前主题,当且仅当当前用户是开启主题预览的用户。

  • SafeModeThemeSelector是应用在“安全模式”中时唯一可用的选择器,安全模式一般出现在程序配置期间。这个选择器优先级极低。

主题选择器的一个例子是,当用户代理被识别为属于一个移动设备时更换一套移动主题。

原文版权归Orchard基金会团队,由hystar翻译,转载请保留链接