组件很棒,不是吗?它们是可重用的真相来源,您可以使用它们来构建坚如磐石的前端,而无需重复代码。
您知道什么也超级酷吗?无头内容管理! 无头内容管理系统 (CMS) 产品提供内容编辑体验,同时以数据形式释放内容,这些数据可以移植到任何使用 API 的前端 UI。您可以根据自己的喜好(取决于产品)构建内容结构,并将该内容提取到您的前端应用程序中。
将这两者结合使用——分布式 CMS 解决方案和基于组件的前端应用程序——是 Jamstack 的核心原则。
但是,虽然组件和无头 CMS 本身都非常棒,但要让它们协同工作可能会很困难。我并不是说将两者连接起来很困难。在很多情况下,实际上非常简单。但是,要构建一个可重用且一致的组件系统,并使该系统与精心设计的 CMS 体验保持一致,这是一件很难实现的事情。能够自由编写内容,然后将这些内容构建成可预测的组件,正是无头内容管理如此吸引人的原因。
实现 CMS 和前端组件之间的统一
我最喜欢用一个简单的组件来演示这种复杂性:按钮。假设我们正在使用 React 来构建组件,并且我们的按钮如下所示
<Button to="/">Go Home</Button>
在 React 的美好世界里,这意味着<Button>
组件有两个props(即属性、特性、参数等)——to
和children
。children
是 React 中的一个东西,它包含开始和结束标签之间的所有内容,在本例中为“Go Home”。
如果我们要让内容编辑器中的用户能够向站点添加按钮,我们就需要一个系统,使他们能够轻松理解他们在 CMS 中的操作如何影响前端应用程序中屏幕上显示的内容。但我们也希望我们的开发人员能够使用对他们有意义的组件属性,并在他们使用的框架(例如我们示例中的 React)中工作。
我们该如何做到这一点呢?
我们可以…
…在 CMS 中使用与组件属性匹配的字段,尽管我在这方面取得的成功很少。to
和children
对试图构建按钮的内容编辑器来说没有多大意义。相信我,我试过了。我尝试过新手和经验丰富的编辑器。我尝试过帮助文本。没关系。这很令人困惑。
更有意义的是使用编辑器更有可能理解的词语,例如label
或text
代表children
,以及url
代表to
。


但那样的话,我们的代码就会不同步了。
或者如果我们…
…掩盖 CMS 中的属性。大多数无头 CMS 解决方案都允许您为字段的标签设置一个与通过 API 传递内容时使用的名称不同的值。
我们可以将字段标记为Label
和URL
,但使用children
和to
作为名称。我们可以。但我们可能不应该。还记得伊恩·马尔科姆说过的话吗?
从表面上看,掩盖属性是有道理的。这是关注点分离。编辑器看到让他们感到高兴和高效的东西,而开发人员则使用对他们有意义的名称。我喜欢它,但在理论上是这样。在实践中,它会让开发人员感到困惑。调试内容编辑器问题通常需要挖掘额外的层(即时间)才能找到标签和字段名称之间的关系。
或者为什么不…
…更改属性。开发人员灵活一点会不会更容易?毕竟,他们是设计系统的人。
是的,那是真的。但是,如果您完全遵循该规则,那么您不可避免地会在前进的路上遇到一些问题。您可能会最终与框架作斗争,或者 props 会显得有些愚蠢。
在我们的示例中,将label
和url
用作按钮的 props 对于源自 CMS 的数据来说完全没问题。但这也意味着,每当我们的开发人员想要在代码中使用按钮时,它看起来像这样
<Button label="Go Home" url="/" />
从表面上看,这似乎还可以,但它极大地限制了按钮的功能。假设我想支持其他一些功能,例如在标签内添加图标。我将需要一些额外的逻辑或另一个属性来实现它。如果我改用 React 的children
方法,它将直接生效(可能在一些自定义样式支持之后)。
好的,那么……我们该怎么办?
引入转换器
我发现的最佳方法是分别优化编辑器和开发人员的体验。创建一个适合编辑器的 CMS 体验。构建一个易于开发人员导航、理解和增强的代码库。
结果是这两种体验不会彼此保持一致。我们需要一些实用程序来转换 CMS 结构中的数据,以便能够在前端使用,而无论您使用什么框架和工具。
我将这些实用程序称为转换器。(我给东西命名是不是很厉害!?)转换器负责使用 CMS 中的数据并将其转换成组件可以轻松使用的形状。

虽然我发现转换数据是获得 CMS 和代码库中出色体验的最流畅方法,但我没有一个明显的解决方案来解决如何(或者可能在哪里)进行这些转换。我使用了三种不同的方法,每种方法都有其优缺点。让我们来看看它们。
1. 位于组件旁边
一种方法是将转换器直接放在它们所服务的组件旁边。这通常是我在组织基于组件的项目时采用的方法——将相关文件保存在彼此附近。

这意味着我通常为每个组件创建一个目录,其中包含一组可预测的文件。index.js
充当组件的控制器。它负责导入和导出所有其他相关文件。这使得用一些基于逻辑的行为包装组件变得微不足道。换句话说,它可以在呈现组件之前转换组件的属性。以下是我们按钮示例中可能的样子
import React from "react"
import Component from "./component"
import transform from "./transformer"
const Button = props => <Component {...transform(props)} />
export default Button
transform.js
文件可能如下所示
export default input => {
return {
...input,
children: input.children || input.label,
to: input.to || input.url
}
}
在这个示例中,如果to
和children
是发送到组件的属性,它可以正常工作!但是,如果改为使用label
和url
,则它们会被转换为children
和to
。这意味着<Button>
组件(component.js
)只需要担心使用children
和to
。
const Button = ({ children, to }) => <a href={to}>{children}</a>
我个人非常喜欢这种方法。它使逻辑与组件紧密耦合。到目前为止,我发现的最大缺点是文件和转换的数量很多,因为任何给定页面的整个数据集都可以在堆栈的更早阶段进行转换,这将是……
2. 在漏斗的顶部
数据必须通过某种机制提取到应用程序中。开发人员使用这种机制检索尽可能多的当前页面或视图数据。通常,页面需要执行的查询/请求越少,其性能就越好。
换句话说,该机制通常存在于漏斗(或堆栈)的顶部附近,而不是每个组件动态地提取自己的数据。(当需要时,我使用适配器。)

检索页面数据的机制也可能负责在渲染任何组件之前转换给定页面的所有数据。
理论上,这比第一种方法更好。它减少了浏览器需要执行的工作量,这应该会提高前端性能。这意味着服务器需要做更多工作,但这通常是更好的选择。
然而,在实践中,这需要大量的工作。数据结构可能很大、很复杂且相互交织。将所有内容转换为正确的格式并在漏斗顶部传递,然后将转换后的数据传递给组件,这可能需要大量的工作。由于检索到的巨大数据块的潜在复杂性和变化,它也更难测试。使用第一种方法,测试按钮的转换逻辑非常简单。使用这种方法,您需要考虑在检索到的数据对象中任何可能出现按钮数据的地方转换按钮数据。
但是,如果您能够实现,这通常是更好的方法。
3. 中间引擎
第三种也是最后一种(也是神奇的)方法是在其他地方完成所有这些工作。在这种情况下,我们可以构建一个引擎(即一个小应用程序),它可以为我们执行转换,然后使内容可供应用程序使用。

这可能比第二种方法需要更多工作。而且它在运行额外的应用程序方面增加了成本和维护,这需要付出更多努力才能确保其稳定性。
这种方法的主要优势在于我们可以将其构建为一个抽象的引擎。换句话说,无论何时我们将数据引入任何前端应用程序,它都将通过这个中间引擎。这意味着如果我们有两个使用相同 CMS 或数据源的项目,那么第二个项目的开发工作量将大大减少。
如果您现在还没有进行任何此类操作并希望开始,我的建议是将这些方法视为垫脚石。随着应用程序的增长,它们在复杂性、维护和功能方面都会增强。从第一种方法开始,看看它能带您走多远。然后,如果您觉得可以从中受益并跃升到第二种方法,那就去做吧!如果您想冒险,那就尝试第三种方法吧!
最终,最重要的是创建编辑器和开发人员都理解和喜欢的体验。如果您能做到这一点,那么您就成功了!
我看到了中间引擎的优点,但我认为我无法承受这种工作量。不过,我已经使用漏斗顶部的解决方案取得了相当不错的成功。它还可以让您以一种方式在 CMS 上处理内容模型,您可以拥有单一内容类型,但可能基于特定属性渲染不同的组件。不幸的是,这主要是针对像 Contentful 这样的服务的响应,它限制了您可以创建的内容类型的数量,但在您可能拥有分组图像的内容类型并希望在它们的网格或滑块演示之间切换时效果很好;这些可以在前端实现为单独的组件。
或者,您可以简单地使用开发人员友好的名称,但将字段标签更改为更适合编辑器的名称。这使得内容保持一致和简单,同时为编辑器提供更好的体验。