无头 CMS 内部






4.83/5 (8投票s)
关于无头 CMS 和在 ASP.NET Core 上实现的技術概述
引言
在本文中,我们将了解无头 CMS,并理解它的优势以及何时适合使用。此外,我们还将列出它目前的主要局限性。为了更好地理解 HCMS 的后台工作原理,我将解释我如何设计和构建 RawCMS,一个具有 Oauth2、扩展插件系统、业务逻辑支持的 Aspnet.Core 无头 CMS。该解决方案可在 GitHub 上找到,并在 Docker Hub 上发布为演示版本。
无头 CMS
什么是无头 CMS?
传统的 CMS 结合了内容和渲染部分,而无头 CMS 则专注于内容。这似乎是一个限制,因为严格来说,您会失去一些东西。HCMS 的目的是将逻辑与内容解耦,从而便于变更管理,并将复杂的应用程序分解为多个组件,每个组件都有其单一职责。
朝着这个方向发展,HCMS 可以取代您目前称为后端的东西,并节省大量有用的 CRUD 语句创建工作。
HCMS 的诞生是为了创建多组件应用程序,您可以快速更改演示逻辑和设计,这对于您在现代网站或应用程序上工作,并且需要因为业务需求一年更改一次 UI 样式时,是一个巨大的改进。
许多供应商销售他们的产品并将其标记为“HCMS”,仅仅因为它是解耦的(而且因为它听起来很酷,可能会提高销量)。在我看来,我严格遵循原始的整体定义:无头 CMS 意味着一个 API 优先、非单体的 CMS,与界面或其他组件完全解耦。
无头 CMS 的优势
为什么使用无头 CMS?我可以简单地说,在某些场景下,解耦系统、简化前端替换和加速开发阶段可能很有用,但我有义务使用列表形式更好地解释。
- 全渠道就绪:在无头 CMS 中创建的内容是“纯粹”的,您可以在任何您想要的上下文中消费它。如果您在其上存储一些新闻内容,您也可以将其发布到公共网站或内网,将数据录入集中在一处。
- 低运营成本:无头 CMS 是产品,所以一旦您选择了一个好的产品,我认为它应该是即插即用的。此外,与定制解决方案相比,更新和错误修复可以免费从供应商那里获得。
- 缩短上市时间:无头 CMS 促进敏捷工作方式。您可以让后端和前端的多个团队参与,这可以缩短开发时间。此外,由于 HCMS 区域是数据存储的垂直解决方案,可以通过 API 进行访问,大部分工作都已完成,因此您必须专注于数据设计,而不是技术细节(例如,浪费时间思考数据负载,因为您可以免费获得 Odata 或 Grahql)。
- 垂直解决方案:HCMS 只做一件事。这使得它非常容易学习和维护。
- 灵活性:一旦您选择了您的 HCMS(无论是在本地还是云端),您的开发人员都可以使用他们喜欢的任何语言来实现前端。这意味着您可以摆脱技术限制。
无头 CMS 解决方案的局限性
与传统 CMS 相比,HCMS 还比较年轻,因此,尽管近年来涌现了许多产品,但大多数产品尚未成熟到可以完全替代传统的 API 后端。在这一段中,我想分享我发现的局限性经验。功能可能因特定产品以及它是本地部署还是 SaaS 解决方案而有很大差异。
目前,HCMS 主要有两种局限性
- 使用 HCMS 的缺点
- 所安装产品的局限性
使用 HCMS 的缺点
HCMS 需要雇佣多个团队来利用并行工作的好处。此外,由于 HCMS 没有渲染功能,所有的演示逻辑都由客户端处理。这有利于解耦,但在所有情况下,您只有一个消费者,解耦的好处并不那么显著,并且您会在数据获取过程中引入更多的复杂性和延迟。另一个问题是关于业务逻辑。在哪里实现?如果您不想在 HCMS 中实现,您必须将其放在演示层,并且如果有多个消费者,您将重复它,从而遇到逻辑存在于多个地方的问题。或者,如果您尝试将其放入 HMS,您会发现大多数云解决方案/产品不够灵活。这引出了下一个话题,HCMS 的所有局限性是什么?
HCMS 的局限性
在测试大多数重要的 HCMS 解决方案时,我遇到了许多困难的情况,以下是最常见的局限性列表。请注意,这取决于产品,有些产品可能拥有这些功能,也可能没有,但总的来说,大多数都相当普遍。
- 对外部提供商进行身份验证:大多数解决方案不允许您针对外部系统进行身份验证。我指的是最常见的情况,您有一个中央身份验证系统,所有方都传递用户令牌/票证以代表用户进行操作。换句话说,如果我有一个 oauth2 服务器,我想在前端进行身份验证,并使用令牌向内网的所有应用程序进行调用,而不仅仅是 HCMS,并且能够被识别为我自己。
- 非标准输出格式:有些使用 graphql 或 Odata,这很好,因为它提供了一种标准的数据消费方法。问题在于“有些”并不意味着“全部”,所以您在选择 HCMS 时必须注意这一点。
- 业务逻辑:在大多数情况下,无法在运行时定义业务逻辑,在某些情况下也无法扩展核心应用程序。
- 可扩展性:很难找到一个解决方案,您可以在其中编写自己的代码并更改业务逻辑或添加额外的东西。这部分是因为许多供应商将他们的 HCMS 设计为哑数据存储,部分原因是管理可扩展性的复杂性。
何时以及何地使用无头 CMS?
无头 CMS 是一个很好的机会,但我们必须了解最适合使用它的场景,以优化成本/效益比。问题是,使用普通的 HCMS,定制化相当有限,因此如果您不处于正确的场景,将很难将 HCMS 融合以满足业务需求。此外,仅仅将其用作裸数据存储就没有意义了。
何时使用 HCMS 方便
- UI 在一段时间内发生大量变化
- 大量应用程序共享相同信息,由一个团队管理
- 您对数据只有很少的业务逻辑
- 您可以雇用多个团队(后端+前端)
何时不应使用 HCMS
- 有适合您需求的垂直解决方案(例如,您想要一个博客,请使用 WordPress)
- 您有大量的业务逻辑
- 您不是数据的主人
RawCMS:构建您自己的无头 CMS
在本章中,我们将了解 RawCMS 是什么,以及我如何使用 ASP.NET Core、mongodb、Docker 和一些奇思妙想创建了一个无头 CMS。
为什么还要另一个无头 CMS?
RawCMS 的目的是生成一个没有 HCMS 常见局限性的 HCMS(以及玩一些有趣的东西来训练新技术的心愿 ;-))
RawCms 功能选择
所以我们将添加到上面的功能
- 通过 oauth2 内省(或内置身份验证系统)对其他身份验证系统进行身份验证的可能性
- 使用钩子/事件系统添加业务逻辑的可能性
- 添加自定义端点来管理非数据相关事件的可能性
- 在插件系统中添加功能的可行性
- 数据验证的可能性
- 通过多种协议公开数据,例如 WebAPI、GraphQL、Odata
架构
基本上,我将要实现的架构如下。实际上,插件部分有一些限制,并且缺少工作流管理,但其他部分已完全可用。
服务层
服务层是系统的核心部分。使用通用的 JObject
映射到 mongodb 实体,您可以将任何您想要的数据存储在 mongo 集合中,所有数据都是非类型化的。
这是该类中最相关的部分,用于解释其工作原理。
public class CRUDService
{
public JObject Get(string collection, string id)
{
//Create filter by id (all entity MUST have an id field, called _id by convention)
FilterDefinition<BsonDocument> filter = Builders<BsonDocument>.Filter.Eq
("_id", BsonObjectId.Create(id));
IFindFluent<BsonDocument, BsonDocument> results = _mongoService
.GetCollection<BsonDocument>(collection)
.Find<BsonDocument>(filter);
return ConvertBsonToJson(json);
}
public ItemList Query(string collection, DataQuery query)
{
FilterDefinition<BsonDocument> filter = FilterDefinition<BsonDocument>.Empty;
if (query.RawQuery != null)
{
filter = new JsonFilterDefinition<BsonDocument>(query.RawQuery);
}
InvokeAlterQuery(collection, filter);
IFindFluent<BsonDocument, BsonDocument> results = _mongoService
.GetCollection<BsonDocument>(collection).Find<BsonDocument>(filter)
.Skip((query.PageNumber - 1) * query.PageSize)
.Limit(query.PageSize);
long count = Count(collection, filter);
return new ConverToItemList(results, (int)count, query.PageNumber, query.PageSize);
}
public JObject Update(string collection, JObject item, bool replace)
{
//Invoke validation events
InvokeValidation(item, collection);
// create collection if not exists
EnsureCollection(collection);
FilterDefinition<BsonDocument> filter = Builders<BsonDocument>.Filter.Eq
("_id", BsonObjectId.Create(item["_id"].Value<string>()));
//Invoke presave events
InvokeProcess(collection, ref item, SavePipelineStage.PreSave);
//insert id (mandatory)
BsonDocument doc = BsonDocument.Parse(item.ToString());
doc["_id"] = BsonObjectId.Create(item["_id"].Value<string>());
//set into "incremental" update mode
doc = new BsonDocument("$set", doc);
UpdateOptions o = new UpdateOptions()
{
IsUpsert = true,
BypassDocumentValidation = true
};
if (replace)
{
_mongoService.GetCollection<BsonDocument>(collection).ReplaceOne(filter, doc, o);
}
else
{
BsonDocument dbset = new BsonDocument("$set", doc);
_mongoService.GetCollection<BsonDocument>(collection).UpdateOne(filter, dbset, o);
}
//Post save events
InvokeProcess(collection, ref item, SavePipelineStage.PostSave);
return JObject.Parse(item.ToJson(js));
}
}
身份验证
身份验证部分是通过添加身份服务器并根据 RawCms 设置使用不同的配置来完成的。这样,我们可以使用内部身份服务器(其他人从我们那里获取令牌,我们拥有用户数据)或与其他系统集成(我们在请求头中获取令牌,我们可以信任其他 oauth 系统)。
这是代码中最相关的部分。此代码在身份验证插件启动期间调用,并从数据库获取配置。省略了此类的身份验证配置之外的所有代码部分。
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
//configuration came from constructor
services.Configure<ConfigurationOptions>(configuration);
services.AddSingleton<IUserStore<IdentityUser>>(x => { return userStore; });
//... registering all identity server services for user and roles (all code omitted)
services.AddSingleton<IUserClaimsPrincipalFactory<IdentityUser>, RawClaimsFactory>();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(config.GetIdentityResources())
.AddInMemoryApiResources(config.GetApiResources())
.AddInMemoryClients(config.GetClients())
.AddAspNetIdentity<IdentityUser>()
.AddProfileServiceCustom(userStore);
if (config.Mode == OAuthMode.External)
{
OAuth2IntrospectionOptions options = new OAuth2IntrospectionOptions
{
//... set option basing on config (code omitted)
};
options.Validate();
services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme)
.AddOAuth2Introspection(x =>
{
x = options;
});
}
else
{
services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication("Bearer", options =>
{
//... set option basing on config (code omitted)
});
}
services.AddMvc(options =>
{
//this apply custom authentication like apitoken other than oauth standard
options.Filters.Add(new RawAuthorizationAttribute(config.ApiKey, config.AdminApiKey));
});
}
Lambda 表达式
Lamba 是一种简单的命令模式实现,其名称灵感来自无服务器模型,您可以在其中将函数公开为 REST 端点。基于此,您可以通过实现 lamba 来调整系统中的所有内容。每个 lambda 实例在运行时都会被发现,并根据 lambda 类型和事件进行调用,将数据上下文传递给它。
下面给出了一些 lambda 示例。
使用 lambda 添加自定义端点
public class DummyRest : RestLambda
{
public override string Name => "DummyRest";
public override string Description => "I'm a dumb dummy request";
public override JObject Rest(JObject input)
{
JObject result = new JObject()
{
{ "input",input},
{ "now",DateTime.Now},
};
return result;
}
}
验证数据
public class MyCustomValidation : SchemaValidationLambda
{
public override string Name => "My custom Validation";
public override string Description => "Provide entity validation";
public override List<Error> Validate(JObject input, string collection)
{
//do here all check with data
return ImplementCheckHere(input, collection);
}
}
保存时修改数据
public class AuditLambda : PreSaveLambda
{
public override string Name => "AuditLambda";
public override string Description => "Add audit settings";
public override void Execute(string collection, ref JObject Item)
{
if (!Item.ContainsKey("_id") || string.IsNullOrEmpty(Item["_id"].ToString()))
{
Item["_createdon"] = DateTime.Now;
}
Item["_modifiedon"] = DateTime.Now;
}
}
插件
插件系统的理念是创建一个项目,开发您的功能,将 DLL 放入 *bin* 文件夹,并使其可用于应用程序。这部分内容将在专门的文章中讨论,因为它太长且偏离主题。我只想在这里展示插件系统的原理。这也意味着您可以使用 nuget 作为分发系统或功能市场。
public class GraphQLPlugin : RawCMS.Library.Core.Extension.Plugin
{
public override string Name => "GraphQL";
public override string Description => "Add GraphQL CMS capabilities";
public override void Init()
{
Logger.LogInformation("GraphQL plugin loaded");
}
public override void ConfigureServices(IServiceCollection services)
{
//will be triggered on Startup.cs ConfigureServices
base.ConfigureServices(services);
}
private void SetConfiguration(Plugin plugin, CRUDService crudService)
{
//used to receive configuration from system
}
public override void Configure(IApplicationBuilder app, AppEngine appEngine)
{
// will be triggered on Startup.cs Configure
base.Configure(app, appEngine);
}
}
如何使用 RawCMS
为了让用户测试此解决方案,我实现了多种选项。
从 Docker 安装
这是最方便的。您可以在文档中找到 docker compose 示例,或者您可以使用 docker run,然后链接到 mongodb 实例。
docker run rawcms -p 80:8081
或使用 docker compose
version: '3'
services:
rawcms:
build: .
ports:
- "54321:54321"
links:
- mongo
environment:
- MongoSettings__ConnectionString=mongodb://mongo:27017/rawCms
- PORT=54321
- ASPNETCORE_ENVIRONMENT=Docker
mongo:
image: mongo
环境变量 MongoSettings__ConnectionString
用于将连接字符串传递给应用程序。
从 Zip Release 安装
如果您还没有准备好使用容器,您可以从 GitHub Releases 下载 zip 文件,并将其手动部署为常规的 ASP.NET Core 应用程序。
自己构建
第三种可能性是 fork 该解决方案并在本地进行操作。目前,*没有 nuget 包*可以包含在您的设置中,所以最推荐的解决方案是将 github 仓库添加为子模块或子树。
关注点
HMCS 是解耦架构和避免无用工作的绝佳机会。这可以带来减少时间和成本等好处,使所有各方独立。当然,这不是万能药,您需要了解垂直解决方案是否更方便,或者您的业务逻辑是否阻止您使用它。
我尝试实现了一个 HCMS,我们看到了一个非常重要的话题。这很有趣,我们了解了如何实现最重要的主题来克服 HCMS 目前的技术限制。
在文章的后续部分,我们将深入探讨那些重要的限制以及所应用的解决方案。敬请关注!