flowpack / decoupledcontentstore
这是Neos的两层CMS包的第二代。
Requires
- php: ^7.4 || ^8.0
- albertofem/rsync-lib: ~1.0
- flowpack/prunner: *
- neos/neos: ^7.3 || ^8.0
Suggests
- sandstorm/optimizedrediscachebackend: This package is only needed when this package is being used with Neos 7.3
- dev-main
- 1.x-dev
- 1.6.5
- 1.6.4
- 1.6.3
- 1.6.2
- 1.6.1
- 1.6.0
- 1.5.0
- 1.4.1
- 1.4.0
- 1.3.4
- 1.3.3
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.1
- 1.1.0
- 1.0.1
- dev-task/adjustments-for-1and1
- dev-bugfix-asset-cache-flush-after-asset-update
- dev-task-add-url-logging-in-node-renderer-orchestrator
- dev-bugfix-no-account-found-in-security-context-causes-crash
- dev-bugfix-current-content-release-id-in-content-release-manager
- dev-bugfix-full-release
- dev-feature-show-publish-triggering-user-in-overview
- dev-90-update
- dev-task-merge-1.x
- dev-feature-enumerated-node-signal
- dev-check-domain-scheme
- dev-remove-optimized-redis-cache-backend
- dev-add-auto-publish-config
- dev-bugfix-no-concurrent-runs-in-cloud-environments
- dev-redis-size
- dev-testcases-and-cache-fixes
This package is auto-updated.
Last update: 2024-09-05 14:01:52 UTC
README
这是Neos的两层CMS包的第二代。
此包在一个较大的实例中用于生产。
内容存储包是Neos的两层CMS解决方案的一部分。两层架构将编辑和发布与内容交付分开。这也适用于将Neos内容集成到各种其他系统中,而不会在交付期间增加开销。
第一代不是开源的;由Networkteam和Sandstorm共同开发,并为几家大型客户使用。第二代(本项目)是从零开始开发的,是以开源方式基于第一代的学习成果。特别是其健壮性得到了大幅提高。
它做什么?
内容存储包将Neos中的内容作为不可变内容发布发布到Redis数据库。这些发布可以原子性地切换,一个当前发布指向活动发布。
两层架构中的交付层使用当前发布,在内容存储中查找匹配的URL,并交付预渲染的内容。一个交付层与实际的Neos CMS解耦,可以用任何语言或框架实现。还可以将交付层部分集成到其他软件(例如,商店系统)作为扩展。
特性
- 将完整、只读的内容快照以所谓的内容发布的形式发布到Redis
- 允许增量发布;因此,如果进行了更改,则只需重新渲染所需页面。这与Neos内容缓存集成;因此缓存刷新工作正常。
- 与Neos工作空间发布集成,以自动将内容增量发布到内容存储
- 可配置内容存储格式,与Neos内部表示解耦。
- 可扩展性:通过自定义数据丰富内容发布。
- 允许并行渲染
- 允许将内容发布复制到不同的环境
- 允许在需要时将持久性资产同步到(例如)
- 后端模块,包含内容发布概览(当前发布,切换发布,手动发布)
本项目使用go-package prunner和其Flow Package包装器作为编排和执行内容发布的基础。
要求
- Redis
- 当此包与Neos 7.3一起使用时,需要Sandstorm.OptimizedCacheBackend(Neos 8已经具有优化的Redis后端)
- Prunner
通过以下命令启动prunner
prunner/prunner --path Packages --data Data/Persistent/prunner
将pipelines_template.yml
文件复制到您的项目,并根据需要进行调整(请参阅下面和文件中的注释以获取解释)。
渲染方法
以下流程图显示了创建内容发布的渲染管道。
┌─────────────────────┐
│ Node Rendering │
┌───────────┐ │ ┌─────────────┐ │ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ Node │ │ │Orchestrator │ │ │ Release │ │Transfer to│ │ Atomic │
│Enumeration│────▶│ └─────────────┘ │────▶│Validation │────▶│ Target │────▶│ Switch │
└───────────┘ │┌────────┐ ┌────────┐│ └───────────┘ └───────────┘ └───────────┘
││Renderer│ │Renderer││
└┴────────┴─┴────────┴┘
-
每次渲染的开始,都会对所有节点进行编号。节点编号包含所有需要在最终内容发布中的页面。
-
然后,进行渲染。同时,协调器检查页面是否已经完全渲染。如果没有,它将创建渲染任务。如果是,渲染的页面将被添加到正在进行的内容发布中。
渲染器根据协调器的指令简单地渲染页面。
协调器尝试多次渲染:有时在渲染之后,渲染没有成功工作,因为编辑器同时更改了页面;导致内容缓存刷新和输出中的“漏洞”。
-
在验证过程中,可以检查内容发布是否完全完成;检查它是否真的可以上线。
-
在传输阶段,如果需要,将完成的内容发布复制到生产Redis实例。这包括如有需要复制资产。
-
在切换阶段,内容发布将上线。
上述管道是通过prunner实现的,它协调不同的步骤。
基础设施
在这里,我们解释了使用内容存储的不同基础设施和设置约束。
- Neos内容缓存必须使用Redis。它可以使用OptimizedRedisCacheBackend。
- 内容存储需要一个单独的Redis数据库,但它可以运行在同一个服务器上。
至关重要的是,Redis必须以最低延迟为Neos和交付层提供可用性。请参阅下面的不同设置场景,了解如何实现这一点。
最小设置
最小设置如下所示
- Neos写入内容存储Redis数据库,交付层从内容存储Redis数据库读取。
- 资产(持久资源)直接写入公开可用的云存储,如S3。
┌──────────────┐ ┌──────────────┐
│ Neos Content │ │Content Store │
│Cache Redis DB│ │ Redis DB │◀───┐
└──────────────┘ └──────────────┘ │
▲ ▲ │
└────────┬─────────┘ │
│ │
╔══════╗ ╔══════════════╗
║ Neos ║ ║Delivery Layer║
╚══════╝ ╚══════════════╝
│
│
│ ┌──────────────┐
│ │Asset Storage │
└──────▶│ (S3 etc) │
└──────────────┘
在这种情况下,传输阶段不需要做任何事情,您需要配置Neos使用云存储(例如,通过Flownative.Google.CloudStorage或Flownative.Aws.S3)来管理资源。
这已在默认的pipelines_template.yml
中实现。
如果
- 交付层和Neos在同一数据中心(或主机)中,因此两者都可以通过最低延迟访问Redis,则应使用此设置
- 您想要最简单的设置。
如果您使用云资产存储,请确保您永远不会删除该处的资产。对于Flownative.Aws.S3
,您可以遵循“防止目标中资源取消发布的指南”。
通过RSync手动同步资产到交付层
如果您无法使用云资产存储,有一个内置功能可以手动通过RSync将资产同步到交付层。
要启用此功能,您需要执行以下步骤
-
在
Settings.yaml
中进行配置Flowpack: DecoupledContentStore: resourceSync: targets: - host: localhost port: '' user: '' directory: '../nginx/frontend/resources/'
-
在
pipelines.yml
中,在4) TRANSFER
下,取消注释transfer_resources
任务。
将内容发布复制到不同的Redis实例
如果
- 交付层和Neos位于不同的数据中心,因此实例之一到Redis之间的延迟更高
- 或者您需要具有不同内容状态的多个交付层,例如,具有预发布交付层和实时交付层。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Neos Content │ │Content Store │ │Content Store │
│Cache Redis DB│ │ Redis DB │ ┌ ─ ─ ─ ─ ─ ─ ─ ▶│ Redis DB │
└──────────────┘ └──────────────┘ Higher └──────────────┘
▲ ▲ │ Latency ▲
└────────┬─────────┘ │
│ │ │
╔══════╗ ╔══════════════╗
║ Neos ║─ ─ ─ ─ ─ ─ ─ ─ ┘ ║Delivery Layer║
╚══════╝ ╚══════════════╝
│
│
│ ┌──────────────┐
│ │Asset Storage │
└──────▶│ (S3 etc) │
└──────────────┘
在这种情况下,内容存储Redis数据库被Neos显式同步到另一个交付层。
要启用此功能,请执行以下操作
-
在
Settings.yaml
中配置额外的内容存储,位于Flowpack.DecoupledContentStore.redisContentStores
下。键是内容存储的内部标识符Flowpack: DecoupledContentStore: redisContentStores: live: label: 'Live Site' hostname: my-redis-hostname port: 6379 database: 11 staging: label: 'Staging Site' hostname: my-staging-redis-hostname port: 6379 database: 11
-
在
pipelines.yml
中,在4) TRANSFER
下,取消注释并调整transfer_content
任务。 -
在
pipelines.yml
中,在5) TRANSFER
下,取消注释并调整额外的contentReleaseSwitch:switchActiveContentRelease
命令。
替代方案:Redis复制
除了这里描述的显式同步外,您还可以使用 Redis Replication 来将主Redis同步到其他实例。
使用Redis复制对Neos或传输层是透明的。
要使用Redis复制,Redis 从属(即传输层的实例)需要连接到主Redis实例。
对于这里描述的显式同步,Redis实例之间不需要直接通信;但Neos需要能够到达所有实例。
增量渲染
作为对稳定性的重大改进(与v1相比),渲染管道不会区分是完整渲染还是增量渲染。要触发完整渲染,在渲染开始之前先清空内容缓存。
选项
更改资产(例如在媒体模块中)后,会触发增量渲染。您可以通过设置以下配置来选择退出此行为
Flowpack: DecoupledContentStore: startIncrementalReleaseOnAssetChange: false
渲染过程中发生编辑会发生什么?
如果编辑在渲染过程中发生更改,则内容缓存(通过标签)会被清空,这是由于此内容修改的结果。现在,有两种可能的情况
- (已修改的)文档尚未在当前渲染中渲染。在这种情况下,渲染的文档将包含最近的变化。
- 文档已经渲染并添加到内容发布中。 在这种情况下,渲染的文档将 不 包含最近的变化。
第二种情况有点危险,因为我们需要尽快重新渲染;否则我们不会收敛到一个一致的状态。
对于需要重新渲染的用例,prunner
支持一个 并发限制(即可以并行运行多少个作业) - 如果达到此限制,它还支持一个额外的 队列,也可以进行限制。
因此,pipelines.yml
中的以下行至关重要
pipelines: do_content_release: concurrency: 1 queue_limit: 1 queue_strategy: replace
因此,如果当前正在运行内容发布,我们尝试启动新的内容发布,则此任务被添加到队列(但尚未执行)。如果已经有一个渲染任务排队,则它将被较新的渲染任务替换。
这确保了在任何给定时间最多只有一个内容发布正在运行;最多只有一个内容发布在等待列表中等待渲染。 此外,我们可以确信计划中的内容发布最终将执行,因为这是prunner的工作。
可扩展性
自定义 pipelines.yml
创建自定义 pipelines.yml
是进行额外工作(例如额外枚举或渲染)的主要扩展点。
自定义文档元数据,与内容缓存集成
有时,您需要为每个单独的文档构建额外的数据结构。理想情况下,您希望此结构与内容缓存集成;即仅在页面更改时刷新它。
从性能的角度来看,在渲染本身的同时进行此操作是明智的,因为内容节点(您通常需要)已经在内存中加载。您可以在 Settings.yaml
中注册一个 Flowpack\DecoupledContentStore\NodeRendering\Extensibility\DocumentMetadataGeneratorInterface
Flowpack: DecoupledContentStore: extensions: documentMetadataGenerators: 'yourMetadataGenerator': className: 'Your\Extra\MetadataGenerator'
在实现此类时,您可以添加额外的元数据,该元数据将被序列化到Neos内容缓存中,用于每个渲染文档。
通常,您还希望添加另一个 contentReleaseWriter
,该writer读取新添加的元数据并将其添加到最终的内容发布中。下一节将说明如何实现这一点。
自定义内容发布writer
您可以完全定义内容发布在Redis中的布局,以便您的传输层使用。
通过实现自定义 ContentReleaseWriter
,您可以指定渲染内容如何在Redis中存储。
同样,这也在 Settings.yaml
中注册
Flowpack: DecoupledContentStore: extensions: contentReleaseWriters: 'yourMetadataReleaseWriter': className: 'Your\Extra\MetadataWriter'
将自定义数据写入内容发布
如果您要将自定义数据写入内容发布(使用 $redisKeyService->getRedisKeyForPostfix($contentReleaseIdentifier, 'foo')
),您还需要在设置中注册自定义键。
Flowpack: DecoupledContentStore: redisKeyPostfixesForEachRelease: foo: transfer: true
这是必要的,以便系统知道哪些键应在不同的内容存储之间同步,以及如果删除发布,应删除哪些数据。
使用参数渲染额外的节点(例如分页或筛选器)
如果您渲染了分页列表或具有可以添加到文档中的参数的筛选器(具有可预测的值列表),则可以实现一个用于枚举带有参数的额外节点的 nodeEnumerated
信号的槽。
注意:请求参数必须通过自定义路由映射到 URI,因为我们不支持渲染文档的 HTTP 查询参数。
示例
通过 Package.php
添加 nodeEnumerated
信号的槽
<?php class Package extends BasePackage { public function boot(Bootstrap $bootstrap) { $dispatcher = $bootstrap->getSignalSlotDispatcher(); $dispatcher->connect(NodeEnumerator::class, 'nodeEnumerated', MyNodeListsEnumerator::class, 'enumerateNodeLists'); } }
实现槽并根据节点类型枚举额外的节点
<?php class NodeListsEnumerator { public function enumerateNodeLists(EnumeratedNode $enumeratedNode, ContentReleaseIdentifier $releaseIdentifier, ContentReleaseLogger $logger) { $nodeTypeName = $enumeratedNode->getNodeTypeName(); $nodeType = $this->nodeTypeManager->getNodeType($nodeTypeName); if ($nodeType->isOfType('Vendor.Site:Document.Blog.Folder')) { // Get the node and count the number of pages to render // $pageCount = ... $pageCount = ceil($postCount / (float)$this->perPage); if ($pageCount <= 1) { return; } // Start after the first page, because the first page will be the document without arguments for ($page = 2; $page <= $pageCount; $page++) { $enumeratedNodes[] = EnumeratedNode::fromNode($documentNode, ['page' => $page]); } $this->redisEnumerationRepository->addDocumentNodesToEnumeration($releaseIdentifier, ...$enumeratedNodes); } } }
实际的逻辑将取决于您对节点的使用。在 PHP 中实现实际的筛选逻辑是有益的,因为它允许您在渲染过程以及额外的枚举中使用它。
扩展后端模块
- 您的包中需要一个看起来是这样的
Views.yaml
-
requestFilter: 'isPackage("Flowpack.DecoupledContentStore")'
viewObjectName: 'Neos\Fusion\View\FusionView'
options:
fusionPathPatterns:
- 'resource://Flowpack.DecoupledContentStore/Private/BackendFusion'
- 'resource://Vendor.Site/Private/DecoupledContentStoreFusion'
- 请确保您的包在 composer.json 中依赖于
flowpack/decoupledcontentstore
(这样您的 Views.yaml 才能获胜,因为 DecoupledContentStore-Package 附带了自己的 Views.yaml) - 在
Vendor.Site/Resources/Private/DecoupledContentStoreFusion
中添加一个Root.fusion
,它可以包含您的修改 - 我们目前支持以下调整
- 在页脚中添加按钮
prototype(Flowpack.DecoupledContentStore:ListFooter) { test = '<span class="align-middle inline-block text-sm pr-4 pl-16">TEST</span>' test.@position = 'before reload' }
- 添加闪存消息
// ActionController $this->addFlashMessage('sth important you have to say');
- 在页脚中添加按钮
使用不同的配置集
在某些情况下,可能需要对一些配置属性进行根本性的调整,这些属性在内容存储的消耗者网站上可能非常难以处理(安全、非破坏性)。因此,我们添加了配置属性 configEpoch
,它可以包含当前和以前的配置版本。应使用的 current
值(在消耗者网站上使用)发布到内容存储。
出于简化消耗者网站考虑,我们决定在内容存储级别而不是内容发布级别保存 configEpoch。如果您需要切换回使用以前的配置纪元版本渲染的旧发布,并且不会匹配当前发布的版本,您可以手动在当前和以前的配置纪元之间切换。在后台模块中为每个目标内容存储都有一个按钮用于此操作。显然,此按钮应谨慎使用,因为配置纪元需要始终与当前发布匹配。
示例
-
如果我们需要对内容维度的配置进行重大更改,比如我们需要添加之前不存在的 uriPrefixes。我们相应地调整配置,并在同一部署中配置 config epoch 如下
Flowpack: DecoupledContentStore: configEpoch: current: '2' previous: '1'
-
现在在消耗者网站上,我们可以采取行动来处理旧的和新的配置,并根据 Redis 中的值确定执行哪种情况。
$configEpoch = (int) $redisClient->get('contentStore:configEpoch'); $contentStoreUrl = 'https://www.vendor.de/' . ($configEpoch > 1 ? 'de-de/' : '');
开发
- 您需要安装 pnpm 作为包管理器:
curl -f https://get.pnpm.io/v6.js | node - add --global pnpm
- 在此文件夹中运行
pnpm install
- 然后运行
pnpm watch
进行开发,以及pnpm build
进行生产构建。
我们使用 esbuild 与 tailwind.css 一起构建。
渲染深入
待办事项:编写
CacheUrlMappingAspect - * 注意:此方面在交互式页面渲染期间不活动;但仅在构建内容发布时(通过批量渲染)活动。这是为了降低复杂性。
- (so when {@see DocumentRenderer} has invoked the rendering. This is to keep complexity lower
- 简化代码路径:系统在浏览页面时永远不会重用编辑器创建的内容缓存条目;但是
- 只重用先前批量渲染创建的内容缓存条目。
调试
如果您需要调试流水线的单个步骤,只需从CLI运行相应的命令,例如:./flow nodeEnumeration:enumerateAllNodes {{ .contentReleaseId }}
。
渲染测试
要执行行为测试,请安装neos/behat
包并运行./flow behat:setup
。然后
cd Packages/Application/Flowpack.DecoupledContentStore/Tests/Behavior
../../../../bin/behat -c behat.yml.dist
Behat也支持运行单个测试或单个文件 - 需要在配置文件后指定,例如。
# run all scenarios in a given folder ../../../../bin/behat -c behat.yml.dist Features/ContentStore/ # run all scenarios in the single feature file ../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature # run the scenario starting at line 66 ../../../../bin/behat -c behat.yml.dist Features/ContentStore/Basics.feature:66
在出现异常的情况下,使用--stop-on-failure
运行测试可能会有所帮助,这将在第一个错误时停止测试用例。然后,您可以检查测试数据库并手动重现错误。
此外,-vvv
是一个有用的CLI标志(额外详细) - 这在出现错误时显示完整的异常堆栈跟踪。
待办事项
- 清理旧内容发布
- 在内容存储/Redis中
- 生成旧内容格式
- (SK)错误处理测试
- 强制切换的可能性
- (AM)用户界面
- 检查待办事项 :)
旧版本中缺失的功能
data-url-next-page(或类似)不支持
许可证
GPL v3