超媒体友好脚本

Carson Gross

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

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

脚本与Web

超媒体驱动应用中,我们讨论了如何以超媒体驱动的方式构建Web应用,与流行的SPA方法(即JavaScript驱动且在网络层是RPC驱动的)形成对比。

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

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

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

在本文中,我们希望扩展最后一点,描述不"取代"或"破坏"RESTful超媒体驱动应用的脚本是什么样子。这些经验法则既适用于直接支持Web应用的脚本,也适用于通用JavaScript库。

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

下面将详细阐述每条规则。

首要原则

HDA的首要原则是使用超媒体作为应用状态引擎(HATEOAS)。超媒体友好的脚本方法将遵循这一原则。

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

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

遵循HATEOAS还意味着,通常应避免在JavaScript中存储复杂状态(而非DOM中)。

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

重申Fielding关于脚本在REST中的目的:

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

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

富文本编辑器是这类功能的好例子:它可能有编辑器文档的极其复杂的JavaScript模型,包括选择信息、高亮信息、代码补全等。然而,该模型应与DOM的其余部分隔离,富文本编辑器应使用标准超媒体特性向DOM公开其信息。例如,它应使用隐藏输入将编辑器内容传达给周围的DOM,而非要求通过JavaScript API调用获取内容。

思路是通过脚本提供超媒体工具集中不具备的功能和功能,从而改善超媒体体验,但要以与HTML良好配合的方式实现,而非像许多SPA框架那样将HTML降级为更大JavaScript应用中的UI描述语言。

状态

注意,使用超媒体作为应用状态引擎并不意味着不能有任何客户端状态。显然,上述富文本编辑器示例可能有大量客户端状态。但更简单的情况中,客户端状态是合理且完全符合超媒体驱动应用的。

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

这种短暂的客户端状态在超媒体驱动应用中没问题,因为状态纯粹是前端的。这种脚本不会更新系统状态。如果系统状态要变更(即显示或隐藏元素对存储在服务器上的数据有影响),则需要使用超媒体交换。

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

事件

JavaScript库实现超媒体友好脚本的一个极佳方式是拥有丰富的自定义事件模型

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

Sortable.js示例是个好例子,其中htmx监听Sortable.js触发的end事件:

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

end事件在拖放完成时由Sortable.js触发。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/>">
    显示下一部分
</button>
<section>
    ....
</section>

如所述,点击此按钮会切换section元素上的.visible类。

这种超媒体脚本内联方法的主要优点是,概念上强调超媒体本身,而非超媒体的脚本。

将此代码与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且不触发事件。这通常使它们难以适应超媒体驱动应用。尽管如此,这些库可能提供其他地方难以找到的关键功能。

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

但如果没有,也没有好的替代方案,那么就按设计使用JavaScript库。

尝试将非超媒体友好的库与应用其余部分隔离,但总的来说,不要在维护概念纯度上花费太多复杂性预算:一天的难处一天当就够了。

</>