Hotwire:一种构建Web应用程序的全新的旧方法

2021-01-29 00:26:15

我想分享一下我在Basecamp团队遇到Hotwire的经验,该经验摘自用于构建其Hey产品的工具。我很高兴看到DHH发出有关此推文的消息,并在我的年终假期花了一些时间来研究它。

[感谢Joachim,Michael&来自INNOQ的Stefan,以及Tudor和Speakerconf队列寻求反馈。还感谢Oliver对Spring Boot团队的关注,特别感谢Bruno对Thymeleaf配置的启发。]

通过在线发送HTML而不是JSON,Hotwire是一种无需使用大量JavaScript即可构建现代Web应用程序的替代方法。

通过将数据交换层与表示层分离,Ajax允许网页以及扩展的Web应用程序动态更改内容,而无需重新加载整个页面。

因此,对于某些人来说,Hotwire的方法听起来可能很奇怪,尤其是考虑到在交付Web应用程序的复杂历史之后我们所处的位置:

我们开始时没有任何数据(完全完成的HTML内容除外)从服务器发送到浏览器。然后,我们有了JavaScript,通过指示浏览器更改页面来使HTML“动态”化。 (我们甚至不必费心讨论Java applet。)我们也完全没有HTML,它使用XSLT发送XML以供浏览器进行自我转换。然后,一个带有JavaScript的HTML页面异步请求XML并将其转换为DOM更改(这要归功于Jesse James Garrett,他使用术语“ AJAX”巧妙地封装了这些思想)。当然,XML太“冗长”,“太不可读”,“太严格”和“太灵活”,因此(在缺乏安全意识的壮观展示中)我们使用JSON代替XML。

我们拥有不断增加的复杂性和具有柔韧性的Rube Goldberg机械(俗称“单页应用”),以使我们达到目前的境界。

我不会重提可能影响Hotwire创作的开创性思想,例如Rich Hickey的“为什么简单胜于简单”,或者Stefan Tilkov的许多关于为何真正使用REST并拥有面向资源的客户端体系结构的著作,以及拥抱浏览器是一个好主意,包括(从六年前我们在GOTO Amsterdam以来)使用HTML作为数据传输格式。

我强烈建议您熟悉这些演示文稿,因为它们将与混乱的头脑混乱的声音形成鲜明的对比,后者是摆在我们面前的绝大多数基于Web的技术和方法的基础。

替代的时间轴是,在从服务器接收HTML之后,浏览器异步请求HTML片段以根据服务器上发生的用户交互或事件来动态更改页面的各个部分以进行更改。逻辑保留在服务器上。最重要的是,JavaScript的弗拉肯斯坦式噩梦(或者像它的原始概念一样经过深思熟虑的东西)仍然严格地局限于一小部分面向演示的决策以及RESTful状态转换的浏览器端协调。 HTTP / 2和HTTP / 3更快地被采用,并且不再需要SPA(因为多通道,低延迟,高度并发,异步,按需,仅需什么,服务器可推动,就消除了对SPA的需求)。 SPA的“机密”索赔)。

为了合并这两个时间表并消除现实中的主要弊端,Hotwire…

可以使快速的首次加载页面,在服务器上保持模板呈现并在任何编程语言中提供更简单,更高效的开发体验,而不会牺牲与[现在可悲的是]传统单一代码相关的任何速度或响应能力页面应用程序。

Basecamp团队在后端使用Ruby on Rails(从他们的工作中提取的第一个开源项目),因此我决定尝试使用带有JVM后端的Hotwire的上述“任何编程语言”承诺。

我精通Java,Clojure和JRuby,但我决定使用Kotlin语言来实现我的示例,并决定使用Spring Boot来驱动该应用程序(尽管其外观很现代)。我还选择了Spring WebFlux来保持反应性。

我非常喜欢无逻辑的视图模板,但我决定首先尝试使用Thymeleaf,因为我以前从未使用过它。 las,我确定我会改用小胡子。 我希望也能尽快比较一下Vert.x版本。 Turbo Drive —显示人们认为您需要SPA的“更智能”效果 Turbo Frames —显示简单的使用UX渐进增强功能以及用其他页面的片段更新页面的一部分 Turbo Streams —显示服务器发送的事件,这些事件传输HTML模板以更新页面的一部分 为了将Turbo包含在我们的页面中,我们添加了< script> < head>元素 我们的HTML页面。 这使用unpkg服务来显示库,并允许我们以后在需要时引用Turbo对象。 Turbo Drive是一种简单的机制,使Hotwire应用程序的用户可以像浏览SPA一样对页面进行导航。

为了利用Turbo Drive,我们没有做任何特别的事情。当用户单击链接时,他们将进行导航。如果响应需要一段时间(> 500ms),则进度条将显示在页面顶部。进度条可以应用一些您自己的样式。

如果您需要在页面上的某些元素上选择停用Turbo,则可以这样做(请参见下面的“禁用Turbo”)。

似乎在Turbo Drive中还有很多功能可以通过附加的适配器进行本机移动导航,但这些示例尚未使用。

对我来说,这是Turbo的杀手feature。它允许您使用渐进增强功能来请求页面。如果Turbo不存在,不兼容或由于某些原因不可用,则用户仍可以使用该应用。重要的是,它可以让您测试是否受Turbo影响的页面。

不用浏览整个页面,我们可以用随后请求的HTML(甚至是其中的一部分)替换当前页面上的HTML的一部分。我说下一页的一部分是因为它可以是一个完整的HTML文档,可以单独作为单独的资源进行访问,因此无需在浏览器中加载Turb​​o库即可使用。

Turbo框架允许根据要求更新页面的预定义部分。捕获框架内的所有链接和表格,并在收到响应后自动更新框架内容。无论服务器提供的是完整文档,还是仅包含请求帧的更新版本的片段,都将仅从响应中提取该特定帧以替换现有内容。

在示例index.html中,我们有完全相同的HTML页面链接到“ Turbo Frame”部分和“ Regular”部分(请参见下面的“禁用Turbo”)。区别在于一个是使用Turbo Frames检索的,另一个是浏览器检索为正常页面的。

为了使用Turbo框架,将链接元素(在我们的示例中为HTML锚点< a>)包装在自定义元素< turbo-frame>中。当Turbo Frames可用并启用时,这将触发Turbo库取消常规导航,检索文档本身,对其进行解析并替换< turbo-frame>的内容。元素与检索到的文档中的匹配片段。

您可以看到匹配的< turbo-frame id =” greeting_frame”>两个文档中的标签。

检索页面后,当插入请求页面的“ turbo frame”部分时,Turbo Frames会删除相关片段周围的HTML。 (如果您不使用ThymeLeaf,请忽略“ th”参考。)

除了它不是浏览器支持的本机HTML机制(因此需要少量JavaScript)之外,这是< iframe> (或旧的< frame>)。

如果我们不希望Turbo包含某些元素,则可以明确地对其进行定向。通过使用data-turbo =" false&#34 ;,以下情况会禁用Turbo Drive和Turbo Frames。赋予任何适用的元素属性。我们使用了包含div,但可以将其用于单个a元素。

如前所述,这将检索完全相同的“问候”文档,但使用本机浏览器功能,而不是使用Turbo Drive或Turbo Frames逐步增强的功能。

现在,从锚点或表单返回的就地内容以与SPA相同的方式工作,但是需要更复杂的页面更新或服务器发送的事件或WebSockets中服务器推送的数据的表单呢? Hotwire也可以处理!

不过,不幸的是,在这里使用Turbo库使许多人感到困惑,我认为,随着时间的流逝,采用的习惯用法可能会有所发展。尽管如此,最终结果还是不错的。

在我们的示例中,我们有一个表格,要求服务器“ ping”网络主机并显示中继ping所花费的时间,或指示超时。

非涡轮增压形式非常简单。我们不得不再次添加data-turbo =" false"属性以防止Turbo库处理这种形式:

我们的响应/ pinger资源请求的控制器能够产生常规的HTML响应,从而在浏览器中出现一个新页面(如预期的那样)。

方法pingerPage注释为产生text / html。它返回ThymeLeaf呈现的ping视图名称,并作为对HTTP请求的响应返回。

现在,如果我们希望表单的结果更像Turbo Frame或SPA一样处理并包含在请求文档中怎么办?

我们有一个常规的HTML表单(这次没有“ disable Turbo”指令),然后我们可以在页面上的其他任何地方更改带有响应的元素。 Turbo Streams允许该元素对响应执行以下操作:追加,添加,替换,更新和删除。

我们希望在某种程度上模拟命令行ping,因此我们使用append操作。这是由服务器驱动的。第二种方法pingerStream注释为产生text / vnd.turbo-stream.html。

ThymeLeaf与SpringBoot的集成在处理自定义媒体类型方面并不出色,但是如果我们配置自定义视图解析器并为视图文件使用稍有不同的文件扩展名(类似于Bruno Drugowick在他的示例中所做的那样)即将发生。在我们的示例中,任何扩展名为.turbo-stream.html的文件都将应用Turbo Stream媒体类型。

还要注意,produces批注属性仅用于内容协商。它使我们能够根据用户代理接受哪种媒体类型作为对请求的响应而具有独特的功能。它不设置响应的内容类型,因为它可以包含多种媒体类型,并且因为该内容类型是由构造响应实体的任何对象(通常是View Resolver)设置的,但它也可以是依赖于返回值的操作方法本身类型。

因此,我们可以看到pingerStream作为状态返回200 OK,它还将Content-Type响应标头设置为text / vnd.turbo-stream.html,并且它提供了响应主体,该主体是符合Turbo Stream的文档媒体类型-包含HTML 5模板元素的XML类混合(请参阅下文)。

我们的Turbo Stream文档告诉Turbo将模板的内容附加到请求文档中目标ID为ping的元素中。它以HTML列表项的形式提供模板的内容,其ping时间以毫秒为单位(或“超时”)。

当请求文档收到此消息时,Turbo会识别Turbo Stream媒体类型,从模板中提取HTML,然后将操作应用于目标元素。在我们的示例中,将HTML列表项附加到请求文档中已经存在的HTML有序列表。

这样做的影响是,每当用户单击页面上的Ping按钮时,Ping时间就会添加到列表中。

我从来都不是WebSocket的忠实拥护者(肯定从来没有通过WAN来使用),并且使用HTTP / 2和SSE方法,几乎​​没有令人信服的理由再使用WebSockets。就是说,除非您不能完全使用HTTP / 2(我的意思是,这只有十年了,而我们现在已经有了HTTP / 3,所以如果像CDN和负载均衡器这样的中介中间人,这可能仍然是一个问题。 ),或者除非您必须处理糟糕的旧浏览器版本(这是一个完全人为的问题,因为声称其员工“无法”使用任何最新,最安全的浏览器版本的公司都已装满了)。

请记住,我们使用unpkg服务可以访问我们现在用于连接到事件流的Turbo值(直到HTML可以在没有JavaScript的情况下做到这一点之前,这才是必需的)。

< script type =" text / javascript"> if(window [" EventSource"]&& window [" Turbo"]){Turbo.connectStreamSource(new EventSource(" / load"))); } else {console.warn(" SSE上的Turbo Streams不可用"); }< / script>

在我们的样本中,我们有一个平均系统负载流(以及检查负载的UTC时间),每3秒生成一次项目。我们希望该流中的项目可以显示在我们的网页上,而无需用户进行任何交互,并且希望该数据在流中的每个项目到达时进行更新。

我们有一个控制器,类似于ping示例,该控制器读取该流并将其作为事件流(文本/事件流)响应映射到Turbo Stream文档。

这次,我们不必理会纯HTML到新页面的响应,我们只是用Turbo Stream文档来回答,该文档指示浏览器替换现有的HTML元素(而不是在ping示例中附加)。

该控制器还将Thymeleaf与WebFlux / Reactor Core集成在一起,将值流(而不是单个值)包装在ReactiveDataDriverContextVariable中,以供模型在插值到模板期间使用。这必须在模板中使用Thymeleaf“ each”属性附加一个元素。

这会将Thymeleaf置于“ SSE”模式,然后将模板作为事件流和每次刷新数据驱动缓冲区的方式返回(我们为缓冲区使用了1的大小,但是您可以对“页面”使用更高的值数据)导致响应发送到浏览器。打开“检查”工具上的“网络”选项卡并检查负载资源,将显示“事件流”选项卡,您可以查看来自服务器的响应流。

到现在为止,我对Hotwire感到满意。它提供了浏览器端逻辑的简单性,逐步增强和轻触感。它有一个参与的社区。它拥有一支精明的团队,他们正在使用它来构建自己的产品。

我尚未探索刺激物,但是当我这样做时,将是另一篇文章。乍一看,它看起来也很棒,并且提供了足够的客户端状态处理,而其他JavaScript库中却没有膨胀。

我也将探索Turbo在移动应用程序上下文中的工作方式以及Strada框架(尚未发布)。

但是,我确实对Turbo Streams有一点关注。问题是Turbo Stream响应不是HTML。它们也不是XML。通过提取template元素中的所有内容,它们肯定包含HTML(使用HTML 5的Content Template元素)。因此,使用自定义媒体类型。我个人认为这与Hotwire其余方法不一致。对于早期采用者,它也越来越引起混乱。

我觉得还有一些事情需要探索,因此可以使用HTML文档,以便可以使用HTML 5的模板和广告位的全部功能在传统的(全页请求)或Turbo Stream请求中使用相同的响应实体。 Turbo库中的上下文智能。

尽管如此,我还是要感谢Basecamp团队以及来自社区的贡献者,他们将这个意想不到的梦想工具整合在一起,表示由衷的感谢。我期待在实际项目中“激怒”使用它,并将其介绍给我在Secure Code Warrior的团队。

对于那些使用与Reactor Core集成(通过WebFlux)的Kotlin协同例程的示例,这些示例提供了公平的参考,说明控制器动作如何使用低模板,遵循反应式,同时使用大多数人熟悉的代码样式。

Kotlin库将这些暂停函数映射到Spring WebFlux对我们使用Reactor的用途。如果我们使用的是Java 8+(或者我们想直接使用Kotlin的Reactor Core),则可以使用Mono T / FluxT。这些函数的返回类型。

定制的Thymeleaf配置和TemplateSelectorModifier展示了如何在使用内置的Spring内容协商功能时使用相同的基本控制器操作,并且在使用HTML视图进行响应的操作与使用Turbo Stream进行响应的操作之间,声明性语句仅存在微小差异。视图。

因为“ ping”是一个I / O活动,并且我正在使用oldskewl阻止Socket来执行网络I / O,所以我们应该以响应方式实现这些功能,因此在等待时它们不会阻止应用程序服务器事件循环响应来自被ping的主机的响应。

如果您看一下最终的ping()函数是否起作用,您会看到许多不错的Kotlin协程和其他语法糖,它们提供了从非阻塞上下文中调用阻塞代码的机制,并记录了代码执行所需的时间,与Disposable资源一起使用,在非阻塞上下文中制造延迟,以及让非阻塞代码处理在阻塞代码中引发的异常。使用阻塞套接字会带来一定的权衡和限制(例如,只有64个线程可用于阻塞I / O),因此实际的实现可能会转向非阻塞I / O组件,例如Java 7的NIO2 AsyncronousSocketChannel。我可能会更新样本以使用该样本,但这不是练习的重点。