超媒体友好型脚本

Carson Gross

我们对 REST 约束集的最后补充来自第 3.5.3 节(图 5-8)中的按需代码风格。REST 允许通过下载和执行小程序或脚本的形式来扩展客户端功能。这通过减少需要预先实现的功能数量来简化客户端。允许在部署后下载功能会提高系统可扩展性。但是,它也降低了可见性,因此只是 REST 中的一个可选约束。

--Roy Fielding - 表现层状态转移 (REST)

#脚本与 Web

超媒体驱动的应用程序 中,我们讨论了如何构建以超媒体为驱动的 Web 应用程序,这与流行的 SPA 方法形成对比,在 SPA 方法中,它们是由JavaScript 驱动的,并且在网络级别上,它们是由 RPC 驱动的

在 HDA 文章中,我们简要提到了脚本

在 HDA 中,超媒体(HTML)是构建应用程序的主要媒介,这意味着

脚本增强了现有的超媒体(HTML),但不会取代它或破坏 HDA 的基本 REST 架构。

在本文中,我们想扩展最后一个评论,并描述不“取代”或“破坏” RESTful、超媒体驱动应用程序的脚本是什么样的。这些经验法则适用于直接编写以支持 Web 应用程序的脚本,以及通用 JavaScript 库。

超媒体友好型脚本的基本规则是

下面将详细说明这些规则中的每一个。

#主要指令

HDA 的主要指令是使用 超媒体作为应用程序状态的引擎。超媒体友好型脚本方法将遵循该指令。

实际上,这意味着脚本应避免通过网络与服务器进行非超媒体交换。

因此,一般而言,超媒体友好型脚本应避免使用 fetch()XMLHttpRequest除非服务器的响应使用某种超媒体(例如 HTML),而不是数据 API 格式(例如纯 JSON)。

尊重 HATEOAS 还意味着,一般而言,应避免在 JavaScript 中存储复杂状态(而不是在 DOM 中)。

但是,最后一条语句需要进行限定:只要客户端状态直接支持比纯 HTML 允许的更复杂的前端体验(例如小部件),就可以在 JavaScript 中存储状态。

重申 Fielding 关于 REST 中脚本目的的说法

允许在部署后下载功能会提高系统可扩展性。

因此,脚本是 RESTful 系统的合法部分,以便允许创建未在基础超媒体中直接实现的附加功能,从而使超媒体(例如 HTML)更具可扩展性。

这种功能的一个很好的例子是富文本编辑器:它可能具有编辑器文档的极其复杂的 JavaScript 模型,包括选择信息、突出显示信息、代码完成等等。但是,此模型应与 DOM 的其余部分隔离,并且富文本编辑器应使用标准超媒体功能将信息公开给 DOM。例如,它应使用隐藏的输入将编辑器的内容传达给周围的 DOM,而不是需要通过 JavaScript API 调用来获取内容。

其想法是使用脚本通过提供不是标准超媒体(HTML)工具集的一部分的功能来改善超媒体体验,但要以与 HTML 相容的方式进行,而不是像许多 SPA 框架那样,将 HTML 降级为大型 JavaScript 应用程序中的 UI 描述语言。

#状态

请注意,使用超媒体作为应用程序状态的引擎并不意味着您不能有任何客户端状态。显然,上面提到的富文本编辑器示例可能具有大量的客户端状态。但也有更简单的案例,其中客户端状态是合理的,并且与超媒体驱动应用程序完全一致。

考虑一个简单的可见性切换,其中单击按钮或锚点会在另一个元素上添加一个类,使其可见。

这种短暂的客户端状态在超媒体驱动应用程序中是可以的,因为该状态纯粹是前端状态。使用这种脚本不会更新任何系统状态。如果要更改系统状态(也就是说,如果显示或隐藏元素会影响存储在服务器上的数据),则需要使用超媒体交换。

需要考虑的关键方面是,是否需要将客户端更新的任何状态与服务器同步。
如果需要,则应使用超媒体交换。如果不需要,则可以将状态保留在客户端。

#事件

JavaScript 库启用超媒体友好型脚本的一种极好方法是让它具有 丰富的自定义事件模型

触发事件的基于 JavaScript 的组件允许像 htmx 这样的超媒体导向的 JavaScript 库监听这些事件并触发超媒体交换。反过来,这使得任何 JavaScript 库都成为潜在的超媒体控制,能够通过用户选择的动作来驱动超媒体驱动应用程序。

这方面的一个很好的例子是 Sortable.js 示例,其中 htmx 监听 Sortable.js 触发的 end 事件

<form class="sortable" hx-post="/items" hx-trigger="end">
  <div class="htmx-indicator">Updating...</div>
  <div><input type='hidden' name='item' value='1'/>Item 1</div>
  <div><input type='hidden' name='item' value='2'/>Item 2</div>
  <div><input type='hidden' name='item' value='3'/>Item 3</div>
  <div><input type='hidden' name='item' value='4'/>Item 4</div>
  <div><input type='hidden' name='item' value='5'/>Item 5</div>
</form>

当拖放操作完成时,Sortable.js 会触发 end 事件。htmx 通过 hx-trigger 属性监听此事件,然后发出 HTTP 请求,与服务器交换超媒体。这将这个 Sortable.js 拖放驱动的窗口小部件变成了一个新的、强大的超媒体控制。

#岛屿

Web 开发的最新趋势是 “岛屿” 的概念

岛屿架构鼓励在服务器渲染的网页中进行少量、集中的交互。

在需要更复杂的脚本方法以及需要在正常的超媒体交换机制之外与服务器进行通信的情况下,最超媒体友好的方法是使用岛屿架构。这将非超媒体组件与超媒体驱动应用程序的其余部分隔离开。

事件是将非超媒体驱动的岛屿集成到更广泛的超媒体驱动应用程序中的干净方法,使您可以将“内部”岛屿转换为“外部”超媒体控制,就像上面的 Sortable.js 示例一样。

Deniz Akşimşek 观察到,通常更容易将非超媒体岛屿嵌入到更大的超媒体驱动应用程序中,而不是反过来。

#内联脚本

超媒体友好型脚本的最后一条规则是内联脚本:将脚本直接写入超媒体中,而不是将脚本放在外部文件中。与这里列出的其他规则相比,这是一种有争议的概念,我们认为它是超媒体友好型脚本的“可选”规则:值得考虑,但不是必需的。

这种脚本方法虽然很独特,但已被一些 HTML 脚本库采用,特别是 Alpine.jshyperscript

以下是一些 hyperscript 示例,展示了内联脚本

<button _="on click toggle .visible on the next <section/>">
    Show Next Section
</button>
<section>
    ....
</section>

正如按钮所说,当单击此按钮时,它会在 section 元素上切换 .visible 类。

这种内联超媒体脚本方法的主要优势在于,从概念上讲,超媒体本身得到了强调,而不是超媒体的脚本。

将此代码与 JSX 组件 进行对比,在 JSX 组件中,脚本语言(JavaScript)是核心概念,超媒体/HTML 嵌入了其中

class Button extends React.Component {
    constructor(props) {
        // ...
    }
    toggleVisibilityOnNextSection() {
        // ...
    }
    render() {
        return <button onClick={this.toggleVisibilityOnNextSection}>{this.props.text}</button>;
    }
}

在这里,您可以看到 JavaScript 是使用中的主要技术,超媒体/HTML 用作 UI 描述机制。在这种情况下,HTML 是超媒体这一事实几乎无关紧要。

话虽如此,内联脚本和 JSX 方法确实具有一个共同的优势:两者都满足 行为局部性 (LoB) 设计原则。它们都将行为局部化到相关的元素或组件,这使得更容易查看这些元素和组件的作用。

当然,对于内联脚本,应该对直接在超媒体中完成的脚本数量有一个软限制。您不希望用脚本淹没超媒体,使其难以理解超媒体文档的“形状”。

使用像调用库函数或使用 hyperscript 行为 这样的技术,您可以使用内联脚本,同时将实现提取到单独的文件或位置。

内联脚本不是超媒体友好型脚本的必要条件,但它是一个值得考虑的选择,可以替代更传统的脚本/超媒体拆分。

#实用主义

当然,在现实世界中,有许多有用的 JavaScript 库违反了 HATEOAS,并且没有触发事件。这通常使它们难以适应超媒体驱动应用程序。尽管如此,这些库可能提供了在其他地方难以找到的关键功能。

在这种情况下,我们主张实用主义:如果可以轻松地修改库使其超媒体友好或以超媒体友好的方式对其进行包装,这可能是一个不错的选择。您永远不会知道,上游作者可能会 考虑拉取请求 来帮助改进他们的库。

但是,如果不是,并且没有好的替代方案,那么就按照库的设计使用它。

尝试将超媒体不友好的库与应用程序的其余部分隔离开,但一般来说,不要在维护概念纯度上花费太多 复杂性预算:对于那一天来说,邪恶就足够了。

</>