hotwire:没有javascript的反应轨道? [深潜入热源前锋]

2021-04-13 10:37:39

是时候通过DHH&amp铸造长戏弄的新魔法了Co.并学会使用5分钟的教程超出5分钟的教程。自今年大揭幕之旅,似乎没有任何努力或javascript的建筑现代网络接口的伞名称是没有任何努力或javascript。 HTML过线的方法正在通过Rails Universe制作涟漪:今年的无数博客帖子,Reddits,截图和五个轨道电源谈判,包括您真正的博客,包括您的五个轨道展示。在这里,我想使用更多的空间来彻底解释HotWire - 以及代码示例和测试策略。正如我最喜欢的摇滚乐队会说,让我们热烈摧毁......自我毁灭学习新技巧!

看看Rails 6 App的Hotwire-Ing,无需进一步的ADO,随时可以学习这个公关。

以下文章非常详细地解释了上面的代码。它是一种适应和延伸的railsconf 2021 talk:“frontentricle Rails Frontend”,它已在线在线提供所有railsconf与会者。如果您没有会议票证,请不要担心:您可以在此处找到幻灯片牌,并且在所有会谈中都有公众,相同的页面将使用视频更新。

在过去的五年中,我一直在做纯粹的后端开发:休息和GraphQL API,WebSocket,Grpc,数据库,缓存,所有人都有屏幕。

整个前端进化已经像巨型波一样通过我:我仍然不明白为什么我们需要用反应和webpack填充每个Web应用程序。经典的HTML-First Rails方式是我的方式(或高速公路😉)。记住应用程序中JavaScript的日子是否不需要自己的MVC(或MVVM)来运行?我怀念那些日子。这些日子正在悄悄地卷土重来。

今天,我们目睹了HTML过线的兴起(是的,现在是实际的术语)。通过Phoenix LiveView引发,基于推动后端渲染模板的方法在WebSocket上推送到所有连接的客户端都在Rails社区中获得了很多牵引力,感谢刺激的宝石系列。当他今年年初向世界推出世界时,这是DHH自己的祝福。

我们是否在Web开发中站在另一个全球范式转移的边缘?返回到服务器呈现模板的简单心理模型,这次与所有铃声和反应接口的哨声略微努力?尽管我喜欢那样,我意识到这是一厢情愿的思考:大型技术过于投资于客户呈现的应用程序。 2020年代的前端开发是一个单独的资格和单独的行业,其入学要求;我们还没有办法再次成为“全堆栈”。

然而,Hotwire(看到那里的首字母缩略词?在Basecamp的部分上聪明,呵呵?)为复杂提供了急需的替代方案,或者我们应该说“复杂”,火箭科学,为浏览器进行现代客户端编程的火箭科学。

对于厌倦了唯一无法控制的API应用程序的Rails开发人员,并且谁错过了将用户体验创建为逃离按摩SQL和JSONS每周四十小时的逃生,Hotwire是一种新的空气呼吸Web开发再次有趣。

在这篇文章中,我想展示如何通过Hotwire将HTML过电线哲学应用于现有的Rails应用程序。与我最近的大多数文章一样,我将使用我的默认演示应用作为豚鼠。

这个应用程序适合任务:由Turbolinks和少数自定义(Java)脚本驱动的交互式和反应,它还拥有一个体面的系统测试覆盖范围(意味着我们可以安全地重新推荐)。我们的Hotwire-ification将在易于遵循的步骤中完成:

Turbolinks在铁路世界中已知很长一段时间;第一个主要版本早于2013年。但是,在我的早期,Rails开发人员有一个经验法则:如果你的前端表演奇怪,请尝试禁用Turbolinks。使第三方JS代码与Turbolink的假货兼容(阅读:Pushstate + Ajax)导航不是在公园散步。

当Simulusjs出来时,我停止避开Turbolinks。它通过依赖于现代DOM突变API来彻底解决了连接和断开JavaScript的问题。 Turbolinks与代码组织的刺激相结合,DOM操作产生了一部分不断的“水疗中心”经验,在反应角开发成本的一小部分中。

同样的旧Turbolinks现在被重新加入涡轮驱动器,因为它字面上驱动涡轮 - 热线包裹交易的核心。

如果您的应用程序已经使用Turbolinks(和我的dod),切换到涡轮增压器驱动器已经死了。这是关于重命名的东西。

您所需要的只是将Turbolink替换在您的包裹中的@ Hotwired / Turbo-Rails .JSON和Gemfile-Reguble Turbo-Rails中的Turbolinks。

请注意,我们不需要手动启动Turbo驱动器(并且我们无法阻止它)。

一些“找到&替换“需要将数据 - turbolinks从data-turbolinks更新为data-turbo更新。

唯一需要一些时间弄清楚的变化是处理形式和重定向。以前,使用Turbolinks,我使用了远程表单(远程:true)和用JavaScript模板响应的重定向关注。 Turbo Drive拥有自己的内置支持,对劫持形式进行劫持形式,因此不再需要远程:True。但是,事实证明必须更新重定向代码。或者,更准确地说,重定向状态代码:

使用稍微模糊的难题查看其他HTTP响应代码(303)是一个聪明的选择:它允许涡轮增压依赖于本机获取API重定向:"关注"选项,因此您不必明确启动另一个请求在表单提交后获取新内容。根据规范,“如果status是303并且请求的方法没有得到或头部,则必须自动执行Get请求。将此与“如果status是301或302并且请求的方法是post” - 差异?

其他3xx状态仅适用于POST请求,同时使用Rails,我们通常使用Post,Patch,Put和Delete。

Turbo帧为页面的一部分带来无缝更新(不是整个页面,因为Turbo Drive)。我们可以说它非常类似于< iframe>但没有创建单独的窗户,DOM树和安全的噩梦。

Atcable演示应用程序(称为AnyWork)允许您创建具有多个TODO列表和聊天的仪表板。用户可以与不同列表中的项目交互:添加它们,删除它们,并将其标记为已完成。

最初,完成和删除项目由Ajax请求和自定义刺激控制器备份。我决定使用Turbo Frames重写此功能以Go HTML All-In。

我们如何分解我们的待办事项列表以处理单个项目更新?让我们将每个项目转换成框架!

<! - _item.html.rb - > <%= turbo_frame_tag dom_id(项目)do%> < div类="任何列表 - 项目<%=项目。完全的? ? "检查" :"" %> " > <%= form_for项目do | F | %> <! - ... - ... - > <%end%> <%= button_to item_path(项),方法:: delete%> <! - ... - ... - > <%end%> < / div> <%end%>

将物品容器包裹成<涡轮框架>通过帮助程序标记并通过唯一的标识符(从ActionView中查看Handy Dom_ID方法);

添加了HTML表单以使Turbo拦截提交并更新框架的内容;和

使用方法使用button_to Helper :::::::在引擎盖下创建HTML表单。

现在,无论何时在框架内提交表单,Turbo拦截提交的提交,执行Ajax请求,从响应HTML中提取具有相同ID的帧,并替换该帧的内容。

类项目控制器< ApplicationController def更新项目。更新! (item_params)渲染部分:"项目" ,当地人:{Item} END DEF DESTREAT项目。破坏!渲染部分:"项目" ,当地人:{Item}结束结束

请注意,当我们删除项目时,我们使用相同的部分响应。但我们需要删除项目的HTML节点,而不是更新。我们怎样才能这样做?我们可以用空框架回复!让我们更新我们的部分做:

<! - _item.html.rb - > <%= turbo_frame_tag dom_id(项目)do%>除非项目,否则<%。摧毁了? %> < div类="任何列表 - 项目<%=项目。完全的? ? "检查" :"" %> " > <! - ... - ... - > < / div> <%end%> <%end%>

你可能会问自己一个问题:“在将项目标记为已完成时,我们如何触发表格提交?”换句话说,如何制作复选框状态更改触发器提交表单?通过定义内联事件侦听器,我们可以这样做:

NB:使用RequestSubmit()而不是提交()很重要:前者触发了涡轮增压器可以拦截的“提交”事件,后者没有。

总之,我们可以通过改变我们的HTML模板并简化控制器代码来解决此特定功能的所有自定义JS。我很兴奋,不是吗?

我们可以进一步进一步并将我们的列表转换为帧。这将允许我们从Turbo Drive Page更新到特定节点更新,在添加新项目时。你可以在家里试试!

也可以从帧内触发整个页面或其他帧更新(请参阅文档),但目前无法更新两个独立帧。

想象一下,只要项目完成或删除(“已成功删除项目”,您还希望向用户显示闪光通知(“已成功删除”)。我们可以用涡轮框架吗?听起来我们需要将闪存邮件容器包装到框架中并将更新的HTML与项目的标记一起推送。这是我的初始想法,它没有解决问题:帧更新被选中到发起者框架。因此,我们无法更新它之外的任何内容。

与驱动器和框架相比,Turbo Stream是一种全新的技术。与那两个不同,流是明确的。全自动地没有发生任何事情,您负责在页面上应更新和何时应更新。为此,您需要使用特殊的< turbo流>元素。

如果要了解更多信息,请查看< turbo-stream&gt的源代码。这里的元素。

此元素负责替换(Action ="替换")DOM ID闪烁下的节点,其中包含在<模板和gt内部传递的新的HTML内容;标签。每当你丢弃这样的< turbo-stream>页面上的元素,它立即执行操作并销毁自己。在引擎盖下,它使用HTML自定义元素API-又是一个用于开发人员幸福的现代Web API的另一个例子(即,少javascript🙂)。

我会说Turbo Stream是旧的JavaScript模板的声明性替代品。 2010年,我们写了这篇文章:

目前,只有五个动作可用:追加,预先求建,删除和更新(仅替换节点的文本内容)。我们将讨论此限制以及如何在下面克服它。

让我们恢复初始问题:响应Todo项目的完成或删除,显示Flash通知。

我们希望用两个< turbo-fruct&gt回应;和<涡轮流>一次更新。我们怎样才能这样做?让我们为此添加新的部分模板:

<! - _item_update.html.erb - > <%=渲染项目%> <%= turbo_stream。替换"闪存警报"做%> <%= render"共享/提醒" %> <%end%>

+ flash.now [:注意] ="项目已经更新" - 渲染部分:"项目",当地人:{Item} +渲染部分:" item_update" ,当地人:{item}

不幸的是,上面的代码不按预期工作:我们没有看到任何闪存警报。在挖掘文档之后,我发现Turbo期望HTTP响应具有Text / VND.Turbo-Stream.html内容类型以激活流元素。好的,让我们这样做:

- 渲染部分:" item_update",locals:{item} +渲染部分:" item_update",locals:{item},content_type:" text / vnd.turbo-Stream .html"

我们现在有相反的情况:Flash消息工作,但项目内容未更新😞。我从Hotwire问太多了吗?通过Turbo源代码读取,我发现混合流和帧是不可能的。

在我看来,第二个选项运行反击重用HTML部分的常规页面加载和Turbo更新的想法。所以,我和第一个一起去了:

<! - _item_update.html.erb - > <%= turbo_stream。替换dom_id(项目)do%> <%=渲染项目%> <%end%> <%= turbo_stream。替换"闪存警报"做%> <%= render"共享/提醒" %> <%end%>

任务完成。但是在什么费用?我们必须为此用例添加一个新模板。而且我担心在一个真实的应用程序中,这种特设部分的数量将会随着应用程序发展而增长。

在实时更新的背景下通常提到Turbo Streams(通常与StimulesReflex相比)。

让我们看看我们如何在Turbo Stream上构建列表同步:

在Turbo之前,我必须添加自定义动作电缆通道和刺激控制器来处理广播。我还需要处理消息格式,因为我必须区分删除和完成物品。换句话说,很多代码要维护。

Turbo Streams负责这一点:Turbo-Rails Gem附带一般的Turbo :: StreamChannel和帮助程序(#turbo_Stream_from)来从HTML创建订阅:

在控制器中,我们已经拥有#broadcast_new_item和#broadcast_changes“之后的操作”钩子负责广播更新。我们现在需要的只是切换到涡轮:: StreamChannel:

def broadration_changes返回if.errors.ant.ant如果是item.destreyed? - listchannel.broadcast_to列表,键入:"删除",ID:Item.id + Turbo :: StreamsChannel.broadcast_remove_to workspace,target:earts:earts:listhel.broadcast_to列表,类型:"更新&#34 ; ID:Item.ID,DESC:Item.DESC,已完成:Item.Completed + Turbo :: StreamsChannel.Broadcast_Replace_to Workspace,Target:项目,部分:"项目/项目",当地人:{Item}结束

这种迁移顺利进行。几乎。验证广播的所有控制器单元测试(#have_broadcasted_to)失败。

不幸的是,Turbo Rails没有提供任何测试工具(呢?),所以我必须写自己,这是一个全熟悉的故事:

模块Turbo :: HakbroadcastedToturbomatcher包括Turbo :: Streams :: StreamName Def have_broadcasted_turbo_Stream_to(*流,action:,target :)#rubop:禁用命名/ predicateName target = target。回应? (:to_key)? ActionView ::记录identifier。 dom_id(target):target has_broadcasted_to(stree_name_from(streamable))。使用(a_string_matching(%(turbo-stream-action ="#34; target ="#34;#{target}")))结束rspec。配置do |配置|配置。包括turbo :: hakbroadcastedtoturbomatcher结束

它"广播删除的消息" do - 期望{techand} .to hast_broadcasted_to(listChannel.broadcasting_for(list)) - .with(type:"删除",id:item.id)+期望{project} .to have_broadcasted_turbo_stream_to(+ workspact_to(+ workspact)行动::删除,目标:项目+)结束

到目前为止,使用涡轮增长很好!已删除了一堆代码。

我们仍然没有写一行的javascript代码。这就是现实的生活吗?

在Turbo迁移期间,我偶然发现了几个使用情况,使用现有API是不够的,所以我终于不得不写一些JavaScript代码!

用例编号:实时将新列表添加到仪表板。从上一章中的项目列表中有什么不同的?标记。让我们来看看仪表板布局:

这就是来自StimulesReflex Clan的CableReady击败对手的位置:它支持超过30个操作,包括Inster_adjacent_html。

最后一个元素始终是新列表表单容器。每当我们添加新列表时,它就会在#new_list节点之前插入。您还记得涡轮溪流只支持五个行动吗?你看到问题所在的位置吗?这是我最初使用的代码:

要实现使用Turbo Streams的类似行为,我们需要在通过流添加后,添加一个黑客将列表移动到正确的位置。所以,让我们添加自己的JavaScript Spriness。

让我们首先给我们的任务是一个正式的定义:“当新列表项附加到工作区容器时,它应该在新表单元素之前定位。” “当”这里的话语意味着我们需要观察DOM并对变化作出反应。听起来不熟悉吗?是的,我们已经提到了与刺激有关的UmatationObserver API!让我们使用它。

幸运的是,我们不必编写高级JavaScript来使用此功能;我们可以使用刺激 - 使用(原谅我这种不可避免的是一个不可避免的是的)。刺激用途是刺激控制器,简单的片段来解决复杂问题的有用行为的集合。在我们的案例中,我们需要轻盈的行为。

从&#34导入{controller};刺激" ;从&#34导入{veremutation};刺激使用" ;导出默认类扩展控制器{静态目标= ["名单" ," newform" ]; connect(){[它。观察者,这个。 UnobServeRists] = Usemution(这个,{元素:这个。liststarget,childlist:true,}); }突变(条目){//通过Streams Const条目添加新列表时,应该只有一个条目,而参赛作品[0]; if(!entry.astualNodes。长度)返回; //禁用观察者,同时我们修改子列表时。 unobservelists(); //将newform移动到子列表的末尾。 liststarget。附加(这是newformtarget);这 。观察员(); }}

我们的每个仪表板都有一个非常简单的聊天:用户可以发送短信(未存储在任何地方)消息并实时接收它们。根据上下文,消息有不同的外观:我的消息有绿色边界位于左侧;其他消息是灰色的,位于右侧。但我们向连接到聊天的每个人播放相同的HTML。我们如何让用户感受到差异?这是聊天应用程序的一个非常常见的问题,通常可以通过向每个用户通道发送个性化HTML或通过增强传入的HTML客户端来解决它。我更喜欢第二个选项,所以让我们实现它。

<! - layouts / application.html.erb - > <头部> lt;%如果logged_in? %> <元名称="当前用户名和#34;内容=" <%= current_user。名称%和gt; " Data-Turbo-Track ="重新加载" > <元名称="当前 - 用户身份证"内容=" <%= current_user。 id%> " Data-Turbo-Track ="重新加载" > <%end%> <! - ... - ... - > < / head>

让用户;导出const currentuser =()=> {如果(用户)返回用户; const id = getmeta(" id"); const name = getmeta("姓名"); user = {id,name};返回用户; };函数getmeta(name){const元素=文档。头 。 QuerySelector(`meta [name ='当前 - 用户 - $ {name}']`); if(元素){return元素。 getAttribute("内容"); }}

def创建turbo :: streamschannel。 broadcast_append_to(workspace,target:cortureview ::记录identifier。dom_id(workspace,:chat_messages),部分:"聊天/消息",当地人: ......