flowpack/decoupledcontentstore

这是Neos的两层CMS包的第二代。

安装: 7,666

依赖者: 0

建议者: 0

安全: 0

星星: 7

关注者: 12

分支: 2

开放问题: 6

类型:neos-package


README

这是Neos的两层CMS包的第二代。

此包在一个较大的实例中用于生产。

内容存储包是Neos的两层CMS解决方案的一部分。两层架构将编辑和发布与内容交付分开。这也适用于将Neos内容集成到各种其他系统中,而不会在交付期间增加开销。

第一代不是开源的;由NetworkteamSandstorm共同开发,并为几家大型客户使用。第二代(本项目)是从零开始开发的,是以开源方式基于第一代的学习成果。特别是其健壮性得到了大幅提高。

它做什么?

内容存储包将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.CloudStorageFlownative.Aws.S3)来管理资源。

这已在默认的pipelines_template.yml中实现。

如果

  • 交付层和Neos在同一数据中心(或主机)中,因此两者都可以通过最低延迟访问Redis,则应使用此设置
  • 您想要最简单的设置。

如果您使用云资产存储,请确保您永远不会删除该处的资产。对于Flownative.Aws.S3,您可以遵循“防止目标中资源取消发布的指南”

通过RSync手动同步资产到交付层

如果您无法使用云资产存储,有一个内置功能可以手动通过RSync将资产同步到交付层。

要启用此功能,您需要执行以下步骤

  1. Settings.yaml中进行配置

    Flowpack:
      DecoupledContentStore:
        resourceSync:
          targets:
            -
              host: localhost
              port: ''
              user: ''
              directory: '../nginx/frontend/resources/'
  2. 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显式同步到另一个交付层。

要启用此功能,请执行以下操作

  1. 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
  2. pipelines.yml中,在4) TRANSFER下,取消注释并调整transfer_content任务。

  3. 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