<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Rei (Rei)</title>
    <link>https://ruby-china.org/Rei</link>
    <description>中下水平 Rails 程序员</description>
    <language>en-us</language>
    <item>
      <title>Ruby 4.0 发布 🎉</title>
      <description>&lt;p&gt;发布说明： &lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/" rel="nofollow" target="_blank"&gt;https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;另外最近官网设计更新了：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/Rei/0746aba8-efb3-42f4-802c-46844c48992f.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Thu, 25 Dec 2025 19:59:20 +0800</pubDate>
      <link>https://ruby-china.org/topics/44425</link>
      <guid>https://ruby-china.org/topics/44425</guid>
    </item>
    <item>
      <title>Basecamp 上线并开源看板应用 Fizzy</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/Rei/486136c9-a89e-4d43-a16f-e18204820336.jpeg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;网站 &lt;a href="https://www.fizzy.do/" rel="nofollow" target="_blank"&gt;https://www.fizzy.do/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;源码 &lt;a href="https://github.com/basecamp/fizzy" rel="nofollow" target="_blank"&gt;https://github.com/basecamp/fizzy&lt;/a&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Wed, 03 Dec 2025 19:39:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/44404</link>
      <guid>https://ruby-china.org/topics/44404</guid>
    </item>
    <item>
      <title>【Rei on Rails】#16 Lexxy Editor - 全新的 Rails 富文本编辑器</title>
      <description>&lt;h2 id="简介"&gt;简介&lt;/h2&gt;
&lt;p&gt;一个 Basecamp 开发中的全新 Rails 富文本编辑器，视频展示如何替换 Action Text 的编辑器，以及如何实现 mention 功能。&lt;/p&gt;
&lt;h2 id="视频"&gt;视频&lt;/h2&gt;
&lt;p&gt;&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//player.bilibili.com/player.html?bvid=11bSnBoEyY" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="链接"&gt;链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/basecamp/lexxy" rel="nofollow" target="_blank"&gt;https://github.com/basecamp/lexxy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/chloerei/lexxy_demo" rel="nofollow" target="_blank"&gt;https://github.com/chloerei/lexxy_demo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 28 Nov 2025 23:47:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/44402</link>
      <guid>https://ruby-china.org/topics/44402</guid>
    </item>
    <item>
      <title>【Rei on Rails】#15 Rails Pulse 应用性能监控</title>
      <description>&lt;p&gt;介绍 Rails Pulse 应用性能监控引擎的使用&lt;/p&gt;
&lt;h2 id="视频"&gt;视频&lt;/h2&gt;
&lt;p&gt;&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//player.bilibili.com/player.html?bvid=1PfU7B2Eba" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="链接"&gt;链接&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;项目地址：&lt;a href="https://github.com/railspulse/rails_pulse" rel="nofollow" target="_blank"&gt;https://github.com/railspulse/rails_pulse&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 21 Nov 2025 23:12:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/44396</link>
      <guid>https://ruby-china.org/topics/44396</guid>
    </item>
    <item>
      <title>【Rei on Rails】#14 Administrate 搭建管理后台</title>
      <description>&lt;p&gt;thoughtbot 的后台管理库 administrate 最近发布了 1.0 版本，我试着用在了自己项目上，觉得很不错，所以录了这期视频分享：
&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//player.bilibili.com/player.html?bvid=1yWC3BmEtf" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;项目地址 &lt;a href="https://github.com/thoughtbot/administrate" rel="nofollow" target="_blank"&gt;https://github.com/thoughtbot/administrate&lt;/a&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Sat, 15 Nov 2025 17:48:48 +0800</pubDate>
      <link>https://ruby-china.org/topics/44390</link>
      <guid>https://ruby-china.org/topics/44390</guid>
    </item>
    <item>
      <title>Rails 8.1 发布</title>
      <description>&lt;p&gt;&lt;a href="https://rubyonrails.org/2025/10/22/rails-8-1" rel="nofollow" target="_blank"&gt;https://rubyonrails.org/2025/10/22/rails-8-1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;要点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Active Job 长任务中断恢复&lt;/li&gt;
&lt;li&gt;结构化事件&lt;/li&gt;
&lt;li&gt;本地 CI&lt;/li&gt;
&lt;li&gt;Markdown 渲染&lt;/li&gt;
&lt;li&gt;命令行获取密钥&lt;/li&gt;
&lt;li&gt;关联弃用声明&lt;/li&gt;
&lt;li&gt;Kamal 可以使用本地 docker registry&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Rei</author>
      <pubDate>Wed, 22 Oct 2025 19:27:52 +0800</pubDate>
      <link>https://ruby-china.org/topics/44348</link>
      <guid>https://ruby-china.org/topics/44348</guid>
    </item>
    <item>
      <title>【Rei on Rails】#010 用 Dev Container 搭建开发环境</title>
      <description>&lt;p&gt;每次接手项目搭建开发环境都感到痛苦，所以录了这期视频。&lt;/p&gt;

&lt;p&gt;B 站：&lt;a href="https://bilibili.com/video/BV1wonmzGEc6/" rel="nofollow" target="_blank"&gt;https://bilibili.com/video/BV1wonmzGEc6/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;YouTube：&lt;a href="https://www.youtube.com/watch?v=dWz6JowDiAg" rel="nofollow" target="_blank"&gt;https://www.youtube.com/watch?v=dWz6JowDiAg&lt;/a&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Tue, 30 Sep 2025 21:13:25 +0800</pubDate>
      <link>https://ruby-china.org/topics/44332</link>
      <guid>https://ruby-china.org/topics/44332</guid>
    </item>
    <item>
      <title>RubyGems 的组织管理引发争议 🍉</title>
      <description>&lt;p&gt;&lt;a href="https://news.ycombinator.com/item?id=45299170" rel="nofollow" target="_blank"&gt;https://news.ycombinator.com/item?id=45299170&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我用 gork 总结如下：&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="页面讨论总结：Ruby Central 对 RubyGems 的“攻击”"&gt;页面讨论总结：Ruby Central 对 RubyGems 的“攻击”&lt;/h3&gt;
&lt;p&gt;这个 Hacker News 页面（&lt;a href="https://news.ycombinator.com/item?id=45299170" rel="nofollow" target="_blank"&gt;https://news.ycombinator.com/item?id=45299170&lt;/a&gt;）讨论了一个 Ruby 生态系统的重大争议：Ruby Central（Ruby 社区的非营利组织）突然移除 RubyGems 和 Bundler 的长期维护者，撤销他们的访问权限，并将控制权集中到全职员工手中。原帖由用户 jolux 于 22 小时前发布，链接到 Ellen Dash（网名 duckinator）撰写的 PDF 文档《Goodbye RubyGems》（&lt;a href="https://pup-e.com/goodbye-rubygems.pdf" rel="nofollow" target="_blank"&gt;https://pup-e.com/goodbye-rubygems.pdf&lt;/a&gt;）。该文档将此事件描述为“敌意接管”，强调了缺乏沟通和对社区信任的破坏。目前线程有 209 条评论，整体情绪偏向批评和担忧，焦点在于治理问题、社区影响以及未来方向。讨论中涌现出对 Ruby 社区“友好”文化的怀念（如“MINASWAN”原则：Matz is nice and so we are nice），并有一些辩论转向更广泛的组织政治。&lt;/p&gt;
&lt;h4 id="主要主题概述"&gt;主要主题概述&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;治理与组织变革&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;许多评论批评 Ruby Central 的行动缺乏透明度和事先通知。Ruby Central 事后发布声明（&lt;a href="https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/" rel="nofollow" target="_blank"&gt;https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/&lt;/a&gt;），称这是为了“加强管理”，但被视为公关式回应，没有道歉。&lt;/li&gt;
&lt;li&gt;有人提到最近的治理提案（受 Homebrew 启发），旨在正式化决策流程，但事件发生前未完成。猜测包括安全担忧（如供应链攻击），但缺乏证据支持。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;对 Ruby Central 行动的批评&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原作者 Ellen Dash 分享了她十年维护经历，并宣布辞职，强调社区优先而非企业控制。高度赞同的评论称此为“敌意接管”，并指出被移除的维护者多为前承包商，与 Ruby Central 的说法矛盾。&lt;/li&gt;
&lt;li&gt;DHH（David Heinemeier Hansson，Rails 创始人）在 X 上发帖（&lt;a href="https://x.com/dhh/status/1969168477475786830" rel="nofollow" target="_blank"&gt;https://x.com/dhh/status/1969168477475786830&lt;/a&gt;）辩护 Ruby Central 的长期贡献，但承认事件处理不当，引发进一步辩论。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;社区影响与个人经历&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;评论者表达对维护者的同情，许多人分享 Ruby 社区的往日回忆，感慨其友好氛围的衰退。一些人担心这会影响 Ruby 的吸引力，导致开发者流失。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;备选方案与未来方向&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;建议包括社区自建替代品或分叉（如 Rails Foundation 独立注册表），但 DHH 发帖表示反对。Homebrew 维护者 Mike McQuaid 积极调解，已进行多轮通话（&lt;a href="https://bsky.app/profile/mikemcquaid.com/post/3lz7klsyue22f" rel="nofollow" target="_blank"&gt;https://bsky.app/profile/mikemcquaid.com/post/3lz7klsyue22f&lt;/a&gt;），试图促成对话。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="整体情绪与共识"&gt;整体情绪与共识&lt;/h4&gt;
&lt;p&gt;线程情绪以负面为主，共识是 Ruby Central 需要改善沟通和包容性，但对事件根源（如安全 vs. 权力集中）存在分歧。调解努力显示出社区修复意愿，但若无实质改变，可能加剧分裂。讨论活跃，适合 Ruby 开发者关注生态动态。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Sat, 20 Sep 2025 14:42:12 +0800</pubDate>
      <link>https://ruby-china.org/topics/44321</link>
      <guid>https://ruby-china.org/topics/44321</guid>
    </item>
    <item>
      <title>Geeknote 开源</title>
      <description>&lt;p&gt;原文链接：&lt;a href="https://geeknote.net/Rei/posts/3256" rel="nofollow" target="_blank"&gt;https://geeknote.net/Rei/posts/3256&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;很高兴的宣布，Geeknote 已经开源，仓库位于 &lt;a href="https://github.com/chloerei/geeknote" rel="nofollow" target="_blank"&gt;https://github.com/chloerei/geeknote&lt;/a&gt; 。&lt;/p&gt;
&lt;h2 id="为什么开源？"&gt;为什么开源？&lt;/h2&gt;
&lt;p&gt;开源的想法由来已久，其中一个原因是开源很酷。我管理的另一个社区 &lt;a href="https://ruby-china.org/topics" title=""&gt;Ruby China&lt;/a&gt; 是开源的，Geeknote 借鉴的网站之一 &lt;a href="https://dev.to/" rel="nofollow" target="_blank" title=""&gt;dev.to&lt;/a&gt; 是开源的，所以我也想把 Geeknote 开源。 &lt;/p&gt;

&lt;p&gt;另一个原因是我想给 Ruby on Rails 社区提供一个新手友好的开源项目。Rails 的开源项目不少，知名的例如 &lt;a href="https://github.com/discourse/discourse" rel="nofollow" target="_blank" title=""&gt;Discourse&lt;/a&gt;、&lt;a href="https://gitlab.com/gitlab-org/gitlab" rel="nofollow" target="_blank" title=""&gt;GitLab&lt;/a&gt;、&lt;a href="https://github.com/chatwoot/chatwoot" rel="nofollow" target="_blank" title=""&gt;Chatwoot&lt;/a&gt; 等。但这些项目由于历史积累和业务复杂度，其实不适合新手阅读。&lt;/p&gt;
&lt;h2 id="什么是新手友好？"&gt;什么是新手友好？&lt;/h2&gt;
&lt;p&gt;Geeknote 是新手友好的，有两个原因。&lt;/p&gt;

&lt;p&gt;首先，Geeknote 的业务不复杂。作为一个博客社区，核心模型就是用户 - 文章 - 评论，理解起来比较简单。但因为 Geeknote 已经运营了四年，有一些技术债，所以也不是周末项目那么简单，对于现实项目也有参考价值。&lt;/p&gt;

&lt;p&gt;其次，Geeknote 尽量使用 Rails 默认栈的技术。例如，使用 Hotwire 而不是前后端分离，使用 Minitest 而不是 Rspec，使用 Rails auth generator  而不是 devise。我优先&lt;a href="https://boringtechnology.club/index_zh_TW.html" rel="nofollow" target="_blank" title=""&gt;选择无聊的技术&lt;/a&gt;，这样更容易维护，有更多时间花在产品上。&lt;/p&gt;
&lt;h2 id="如何使用？"&gt;如何使用？&lt;/h2&gt;
&lt;p&gt;要启动开发环境，只需要用 vscode 打开项目目录，会自动提示启动 devcontainer 环境。&lt;/p&gt;

&lt;p&gt;要部署到服务器，可以参考&lt;a href="https://github.com/chloerei/geeknote/wiki/Deployment" rel="nofollow" target="_blank" title=""&gt;项目 Wiki&lt;/a&gt;。目前文档比较简陋，后面我会根据需要更新。&lt;/p&gt;

&lt;p&gt;项目基于 MIT License 开源，你可以用于搭建自己的社区，或者 Fork 添加自己的功能。如果发现 Bug，可以打开 Issue 或者 Pull Request；如果想要贡献新功能，建议开发前先到 &lt;a href="https://github.com/chloerei/geeknote/discussions" rel="nofollow" target="_blank" title=""&gt;Discussion&lt;/a&gt; 发帖，Geeknote 接受新功能倾向于保守。&lt;/p&gt;

&lt;p&gt;有其他疑问欢迎评论留言。Just for fun!&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Wed, 17 Sep 2025 21:07:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/44316</link>
      <guid>https://ruby-china.org/topics/44316</guid>
    </item>
    <item>
      <title>RubyConf China 2025 视频已上线</title>
      <description>&lt;p&gt;RubyConf China 2025 今年在成都举办，演讲视频如下：&lt;/p&gt;

&lt;p&gt;BiliBili: &lt;a href="https://space.bilibili.com/552654808/lists/6317427" rel="nofollow" target="_blank"&gt;https://space.bilibili.com/552654808/lists/6317427&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;YouTube: &lt;a href="https://www.youtube.com/playlist?list=PLTUHmtFhYC6hzBV3xfRMw5sN_-Ly2YUL2" rel="nofollow" target="_blank"&gt;https://www.youtube.com/playlist?list=PLTUHmtFhYC6hzBV3xfRMw5sN_-Ly2YUL2&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;抱歉视频过了这么久才上线，原因是前期准备的时候沟通失误，片头制作的需求没给到设计师伙伴，没有预留档期制作，导致工期大大超出预期。明年我会吸取教训，在准备阶段确认素材的准备工作，并且准备替代方案。&lt;/p&gt;

&lt;p&gt;声音经过一些降噪处理，可能不够清晰，推荐打开自动字幕观看。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Wed, 10 Sep 2025 19:21:25 +0800</pubDate>
      <link>https://ruby-china.org/topics/44308</link>
      <guid>https://ruby-china.org/topics/44308</guid>
    </item>
    <item>
      <title>Turbo Stream Broadcast - 被低估的 Rails 功能</title>
      <description>&lt;p&gt;原文链接：&lt;a href="https://geeknote.net/Rei/posts/3253" rel="nofollow" target="_blank"&gt;https://geeknote.net/Rei/posts/3253&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;我一直认为，把 Rails 作为前后端分离项目的后端是一种浪费。类似于在 Mac 上安装 Windows 系统，没有什么可以阻止你这样做，而且看起来很美好：Windows 系统更大众化，Mac 硬件做工好，似乎是强强联合。但是深度 Mac 用户会知道，这丢失了很多软硬件一体化设计的优势。&lt;/p&gt;

&lt;p&gt;Rails 优势在于前后端紧密集成，有的功能只有在全栈环境下才能使用，Turbo Stream Broadcast 就是其中一个。Broadcast 在&lt;a href="https://turbo.hotwired.dev/handbook/streams#integration-with-server-side-frameworks" rel="nofollow" target="_blank" title=""&gt;官方文档&lt;/a&gt;中只有一个小节的介绍，并且没有完整示例，可能很多人会忽略。我在掌握这个功能之后，发现 broadcast 对于开发实时更新的应用提供了很大便利。&lt;/p&gt;

&lt;p&gt;最近我&lt;a href="https://geeknote.net/Rei/posts/3252" rel="nofollow" target="_blank" title=""&gt;开发了一个 AI Chat 应用&lt;/a&gt;，其中核心功能是聊天部分：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/Rei/0ab824bc-1939-4918-b607-de2c773cba98.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这个功能是通过 broadcast 实现的。当用户发送一条消息时，服务端会保存这条消息，同时生成一个和 AI 交互的后台任务，在任务执行过程中又会创建新的消息。消息的创建和更新都通过 broadcast 更新到页面上。&lt;/p&gt;

&lt;p&gt;关键部分的代码如下：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/message.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:conversation&lt;/span&gt;

  &lt;span class="n"&gt;broadcasts_to&lt;/span&gt; &lt;span class="ss"&gt;:conversation&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/rooms/show.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"messages"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="vi"&gt;@conversation&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/messages/_message.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  ...
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这些代码，message 对象就会在创建、更新和删除的时候通过 action cable 发送消息，在页面更新内容。&lt;/p&gt;

&lt;p&gt;其中最浓缩的代码是这一行：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;broadcasts_to&lt;/span&gt; &lt;span class="ss"&gt;:conversation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初见可能比较难理解，将它展开成完整参数的形式：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;broadcasts_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;:conversation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;inserts_by: :append&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updates_by: :replace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"messages/message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些参数的意思是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;将 message 的变动发布到关联的 conversation 频道。&lt;/li&gt;
&lt;li&gt;渲染 message 的时候使用 &lt;code&gt;messages/message&lt;/code&gt; 模版。&lt;/li&gt;
&lt;li&gt;新增时插入到 &lt;code&gt;#messages&lt;/code&gt; 元素。&lt;/li&gt;
&lt;li&gt;更新时替换 &lt;code&gt;dom_id(message)&lt;/code&gt; 元素。&lt;/li&gt;
&lt;li&gt;删除时删除 &lt;code&gt;dom_id(message)&lt;/code&gt; 元素。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Broadcast 默认提供 CRUD 的 turbo stream action，前面例子的消息内容流式更新使用了自定义的 turbo stream action。为避免文章篇幅过长不在这里展开，有兴趣的可以看项目源码：&lt;a href="https://github.com/chloerei/llmrpg" rel="nofollow" target="_blank"&gt;https://github.com/chloerei/llmrpg&lt;/a&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;broadcasts_to&lt;/code&gt; 就像 &lt;code&gt;belongs_to&lt;/code&gt;，不了解细节的时候会觉得它充满魔法，了解细节之后会觉得原来如此，然后自觉使用默认配置。&lt;/p&gt;

&lt;p&gt;对应的，前端部分需要插入 &lt;code&gt;&amp;lt;%= turbo_stream_from @conversation %&amp;gt;&lt;/code&gt; 来订阅 &lt;code&gt;@conversation&lt;/code&gt; 频道的消息，这样它就能在后台数据变动的时候更新页面内容。&lt;/p&gt;

&lt;p&gt;Broadcast 功能需要视图、控制器、模型、后台任务等模块高度集成，才能实现这么精炼的代码。试想一下如果没有这个功能，自己就需要解决 websocket 服务端/客户端开发、通信协议，还有两端开发带来的沟通问题。而使用 Rails 全栈，只需要 &lt;code&gt;broadcasts_to&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果你需要为应用增加一些实时功能，推荐使用 turbo stream broadcast。你也许会对 Rails 高集成带来的开发效率有全新的认识。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Mon, 11 Aug 2025 17:27:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/44242</link>
      <guid>https://ruby-china.org/topics/44242</guid>
    </item>
    <item>
      <title>LLMRPG - 用于角色扮演的大语言聊天应用</title>
      <description>&lt;p&gt;最近做了一个用于角色扮演的大语言聊天应用，想跟大家分享。&lt;/p&gt;

&lt;p&gt;这个应用可以给 AI 设定各种角色，然后跟这些角色进行聊天。支持私聊和群聊，并且自己可以扮演其中的角色参与聊天。&lt;/p&gt;

&lt;p&gt;示例：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/Rei/6df0fb2a-3063-40b6-9a0c-6071fd85db74.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;为了方便体验，我部署了一个在线版本：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.llmrpg.com/" rel="nofollow" target="_blank"&gt;https://app.llmrpg.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;注意在线版本的数据不加密，我可能会观察使用情况来改进应用。&lt;/p&gt;

&lt;p&gt;如果你注重隐私，这个项目已经开源，可以尝试自己部署：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/chloerei/llmrpg" rel="nofollow" target="_blank"&gt;https://github.com/chloerei/llmrpg&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="使用方法"&gt;使用方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;注册账号。&lt;/li&gt;
&lt;li&gt;到“角色”创建角色定义并上传头像。&lt;/li&gt;
&lt;li&gt;到“房间”创建聊天室并添加参与角色。&lt;/li&gt;
&lt;li&gt;在角色列表下拉框选择自己要扮演的角色（可选）。&lt;/li&gt;
&lt;li&gt;开始聊天。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="为什么开发这个项目"&gt;为什么开发这个项目&lt;/h2&gt;
&lt;p&gt;这个项目源自我学习 LLM 应用开发过程的一个脚本，在调用 API 的时候，我尝试给 AI 加上各种角色设定，输出就变得有趣起来。以往编写的程序都会产出固定输出，例如下单就会产生订单，发帖就会产生帖子。但是与 AI 聊天，不能预知它会产生什么结果，有时会有种创造生命的错觉。&lt;/p&gt;

&lt;p&gt;由于脚本还是比较单调，我想如果加上图形界面，给角色加上头像，就可以提供更高的沉浸感。于是就可以做成了基于 Web 的聊天应用。&lt;/p&gt;

&lt;p&gt;这个项目主要提供的是情绪价值。例如可以调戏动漫人物、向历史伟人请教问题，或者是做一个虚拟的自己，问自己的初心是什么。这一切都取决于自己的想象力。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️提醒：AI 的对话是虚幻的，不要和现实混淆。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="未来"&gt;未来&lt;/h2&gt;
&lt;p&gt;当前版本功能还很简陋，我想尽快发布出来收集反馈。&lt;/p&gt;

&lt;p&gt;有一些功能已在计划中，例如支持更多的 AI 后端和模型切换（目前只支持 DeepSeek）。&lt;/p&gt;

&lt;p&gt;有更多意见建议，欢迎到项目的讨论区留言：&lt;a href="https://github.com/chloerei/llmrpg/discussions" rel="nofollow" target="_blank"&gt;https://github.com/chloerei/llmrpg/discussions&lt;/a&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 08 Aug 2025 17:24:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/44240</link>
      <guid>https://ruby-china.org/topics/44240</guid>
    </item>
    <item>
      <title>用 HTTP + JSON 直接访问 GraphQL API</title>
      <description>&lt;p&gt;原文链接：&lt;a href="https://geeknote.net/Rei/posts/3191" rel="nofollow" target="_blank"&gt;https://geeknote.net/Rei/posts/3191&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;有一天我需要访问 &lt;a href="https://fly.io/docs/networking/custom-domain-api/" rel="nofollow" target="_blank" title=""&gt;Fly 的 API&lt;/a&gt; 以支持自动签发 SSL 证书，Fly API 基于 GraphQL。我一向不太喜欢 GraphQL，精神洁癖让我不想增加一个 GraphQL Client 依赖。我想到 GraphQL 底层基于 HTTP 和 JSON，为何不直接访问接口？以下就是用 Ruby 实现过程。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fly 前开发者认为“GraphQL 拖慢了所有人、所有事物的速度。” —— &lt;a href="https://fly.io/blog/the-exit-interview-jp/" rel="nofollow" target="_blank"&gt;https://fly.io/blog/the-exit-interview-jp/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;首先 Graph QL API 需要提供一个 endpoint，对于 Fly API 来说是 &lt;code&gt;https://api.fly.io/graphql&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://api.fly.io/graphql"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以直接对 endpoint 发起 HTTP POST 请求，但会因为缺失验证信息返回错误：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; 验证错误&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要增加验证信息需要传递一个 auth token，token 可以在 fly 的控制台获取：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"Authorization"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;FLY_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; Query 错误&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来要传递查询内容。GraphQL 查询的 JSON 格式为：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;QUERY STRING...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;var1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，query 是支持变量的，但是需要通过 variables 字段传递。例如 Fly 的查询证书接口的查询字符串为：&lt;/p&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$appName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$appName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;certificates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;clientStatus&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;$appName&lt;/code&gt; 是变量，那么查询的代码可以写作：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;query: &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
    query($appName: String!) {
      app(name: $appName) {
        certificates {
          nodes {
            createdAt
            hostname
            clientStatus
          }
        }
      }
    }
&lt;/span&gt;&lt;span class="no"&gt;  EOF&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;variables: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;appName: &lt;/span&gt;&lt;span class="s2"&gt;"my-app-name"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"Authorization"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;FLY_API_TOKEN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;#=&amp;gt; json data...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的裸 JSON 内容是：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;certificates&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nodes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2020-03-04T14:17:14Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hostname&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clientStatus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;createdAt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2020-03-05T15:28:41Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hostname&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exemplum.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clientStatus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意的是，返回的 json data 放在 data 字段内，需要增加一层解析：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JSON.parse(response.body)["data"]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在把上面的代码整理一下，放进一个类里：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# client = Graphql::Client.new("http://localhost:3000/graphql", { "Authorization" =&amp;gt; "Bearer token..." })&lt;/span&gt;
&lt;span class="c1"&gt;# response = client.execute("query { ... }", { name: "value" })&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Graphql::Client&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="vi"&gt;@endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;
    &lt;span class="vi"&gt;@headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;query: &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;variables: &lt;/span&gt;&lt;span class="n"&gt;variables&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了方便调用，我还封装了一个 Fly::Client 类：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Fly::Client&lt;/span&gt;
  &lt;span class="nc"&gt;GraphqlClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Graphql&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.fly.io/graphql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Authorization"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FLY_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_certs&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;GRAPHQL&lt;/span&gt;&lt;span class="sh"&gt;
      query($appName: String!) {
        app(name: $appName) {
          certificates {
            nodes {
              createdAt
              hostname
              clientStatus
            }
          }
        }
      }
&lt;/span&gt;&lt;span class="no"&gt;    GRAPHQL&lt;/span&gt;

    &lt;span class="n"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;appName: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FLY_APP_NAME"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="no"&gt;GraphqlClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这个类可以方便的调用 Fly API：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Fly&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_certs&lt;/span&gt;
&lt;span class="c1"&gt;# =&amp;gt; json data ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是用 HTTP + JSON 直接访问 GraphQL API 的方法。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Wed, 26 Feb 2025 10:43:19 +0800</pubDate>
      <link>https://ruby-china.org/topics/44068</link>
      <guid>https://ruby-china.org/topics/44068</guid>
    </item>
    <item>
      <title>RubyConf China 2024 视频已上线</title>
      <description>&lt;p&gt;抱歉今年视频上得晚了。&lt;/p&gt;

&lt;p&gt;以下是播放列表：&lt;/p&gt;

&lt;p&gt;BiliBili: &lt;a href="https://space.bilibili.com/552654808/lists/4537933" rel="nofollow" target="_blank"&gt;https://space.bilibili.com/552654808/lists/4537933&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;YouTube: &lt;a href="https://www.youtube.com/playlist?list=PLTUHmtFhYC6jKjjVPcYIXaiyCdmkKE-oW" rel="nofollow" target="_blank"&gt;https://www.youtube.com/playlist?list=PLTUHmtFhYC6jKjjVPcYIXaiyCdmkKE-oW&lt;/a&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Sat, 11 Jan 2025 20:47:58 +0800</pubDate>
      <link>https://ruby-china.org/topics/44015</link>
      <guid>https://ruby-china.org/topics/44015</guid>
    </item>
    <item>
      <title>Rails 8: The Demo</title>
      <description>&lt;p&gt;原先的 15 分钟 demo 增加到了 30 分钟，内容也多了很多：Trix，Importmap，Kamal，Auth generator，PWA。
&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//www.youtube.com/embed/X_Hw9P1iZfQ" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 15 Nov 2024 13:50:26 +0800</pubDate>
      <link>https://ruby-china.org/topics/43949</link>
      <guid>https://ruby-china.org/topics/43949</guid>
    </item>
    <item>
      <title>Rails 开发者应该拥抱 Web Component</title>
      <description>&lt;p&gt;原文地址： &lt;a href="https://geeknote.net/Rei/posts/3084" rel="nofollow" target="_blank"&gt;https://geeknote.net/Rei/posts/3084&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文有两个视频演示，查看原文会获得更好体验。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;Rails 8 将会继续将 Hotwire 作为默认，我觉得这很好。Hotwire 是以服务端渲染为核心的前端方案，由于服务端是数据的根源，大部分应用可以通过服务端渲染解决问题而不用考虑数据同步。&lt;/p&gt;

&lt;p&gt;不过能做不表示最优，还是有一些问题需要在客户端处理，这通常是涉及客户端状态和前端渲染。举个例子，多选输入框。当前 Geeknote 的标签输入使用了 &lt;a href="https://github.com/josefarias/hotwire_combobox" rel="nofollow" target="_blank" title=""&gt;hotwire_combobox&lt;/a&gt; 这个库，它充分利用了 hotwire 服务端渲染的特性，用一种聪明的方式实现了多选输入框。但如果网络状况不好，会发现输入会有较大延迟：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/Rei/16625425-a826-41d7-9222-88adf581ae07.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我没有人为降低网络延迟，从国内访问境外网站就是这个状态。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这里的问题在于在将输入转为 chip 的时候，hotwire_combobox 使用了服务端渲染，而这实际可以在客户端完成，因为展示补全列表的时候已经获得渲染所需要的数据了。当然 hotwire_combobox 可能以后会改为客户端渲染，这时候会发现需要处理两个问题：客户端状态和渲染。由于 hotwire 并不包含客户端渲染的功能，如果纯用 JavaScript 处理会非常痛苦。考虑一下如何管理可选项、已输入、当前选择，并且反应到 UI 上？&lt;/p&gt;

&lt;p&gt;这时候就要考虑前端方案了。&lt;/p&gt;
&lt;h2 id="前端组件"&gt;前端组件&lt;/h2&gt;
&lt;p&gt;在过去十年的前端框架混战中，“组件”概念取得了共识。所谓组件，是指一块包含了样式、状态和功能的前端模块。组件可以包含更多组件，组件间也可以通信。以组件的方式管理前端代码是当今前端框架的主流。&lt;/p&gt;

&lt;p&gt;所以在选择前端方案时，我会更多考虑基于组件的前端方案。&lt;/p&gt;
&lt;h2 id="可选的方案"&gt;可选的方案&lt;/h2&gt;&lt;h3 id="View Component"&gt;View Component&lt;/h3&gt;
&lt;p&gt;如果搜索 &lt;code&gt;Rails Component&lt;/code&gt; 你会发现 &lt;a href="https://viewcomponent.org/" rel="nofollow" target="_blank" title=""&gt;ViewComponent&lt;/a&gt; 这个 gem。它本质上是对象化的服务端局部模版，提供了更好的测试接口，但不解决客户端状态和渲染。&lt;/p&gt;
&lt;h3 id="React/Vue 等"&gt;React/Vue 等&lt;/h3&gt;
&lt;p&gt;React/Vue 和其他流行的前端框架，可以很方便的构建前端组件。它们通常提供了响应式属性、声明式模版，可以很好处理客户端状态和渲染。并且这些框架的社区庞大，有大量现成的 UI 库和组件库。&lt;/p&gt;

&lt;p&gt;但目前主流的前端框架有一个问题，互操作性不好。React 的组件需要用在 React 项目，Vue 的组件需要用在 Vue 项目。当然，也可以通过增加适配器让不同框架的组件互操作，但很少有人这么做。通常选择了一个框架后，整个应用就都会选择基于这个框架的组件。&lt;/p&gt;

&lt;p&gt;也由于这个问题，在 Rails View 中使用 React/Vue 也显得格格不入，需要到处写初始化代码。根据我的经验，在 Rails View 中引入了 React/Vue 之后最终都会走向前后端分离，因为这些组件和 Rails View 无法交互，与其耦合在一起不如彻底分离。&lt;/p&gt;

&lt;p&gt;有的人可能觉得这样很好，但我希望前端组件用于增强而不是替换 Rails View，因为 Rails View 擅长于服务端渲染的部分。&lt;/p&gt;
&lt;h3 id="Web Component"&gt;Web Component&lt;/h3&gt;
&lt;p&gt;最终我把目光放在 &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components" rel="nofollow" target="_blank" title=""&gt;Web Component&lt;/a&gt;，一个浏览器的标准 API。Web Component 允许开发者创建自定义元素，并且像浏览器内置元素一样使用它。&lt;/p&gt;

&lt;p&gt;举个例子，可以这样使用自定义元素：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/posts"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;my-combobox&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"tabs"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Ruby,JavaScript"&lt;/span&gt; &lt;span class="na"&gt;suggeust-url=&lt;/span&gt;&lt;span class="s"&gt;"/tags/suggests"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/my-combobox&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;my-combobox&amp;gt;&lt;/code&gt; 是一个自定义元素，它用起来像浏览器内置元素，也会随着表单提交而提交。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;我还没有实现 &lt;code&gt;&amp;lt;my-combobox&amp;gt;&lt;/code&gt;，也许会在将来实现。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;对于 Rails 项目来说更好的是，可以在 Turbo Stream/Broadcast 环境中使用自定义元素，而不需要额外的初始化代码。&lt;/p&gt;

&lt;p&gt;直接用浏览器 API 也可以创建自定义元素，但我建议先从 Lit 库开始。&lt;/p&gt;
&lt;h2 id="Lit 简介"&gt;Lit 简介&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://lit.dev/" rel="nofollow" target="_blank" title=""&gt;Lit&lt;/a&gt; 是一个开发 Web Component 的库，主要开发人员来自 Google。根据&lt;a href="https://lit.dev/docs/#why-should-i-choose-lit" rel="nofollow" target="_blank" title=""&gt;官网的介绍&lt;/a&gt;，Lit 的开发团队参与了 Web Component 标准的定制。&lt;/p&gt;

&lt;p&gt;Lit 相比浏览器原生接口提供了更多方便开发者的功能，例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;样式作用域。&lt;/li&gt;
&lt;li&gt;反应式属性。&lt;/li&gt;
&lt;li&gt;声明式模版。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以下是 Lit 实现一个计数器组件的例子：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyCounter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;`
    label {
      color: green;
    }
  `&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
        &amp;lt;label&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/label&amp;gt;
        &amp;lt;button @click="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;Increment&amp;lt;/button&amp;gt;
    `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-counter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MyCounter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要注意的是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;组件内的 Style 只对组件内部元素有效，所以 class name 可以写得很简洁。&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;${}&lt;/code&gt; 插值，通过 &lt;code&gt;@event&lt;/code&gt; 绑定事件。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;count&lt;/code&gt; 属性发生变化时，只会更新需要变更的部分。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些特性能大大简化前端组件的开发。&lt;/p&gt;
&lt;h2 id="实践"&gt;实践&lt;/h2&gt;
&lt;p&gt;最近我开发了项目 &lt;a href="https://www.geekslide.com/" rel="nofollow" target="_blank" title=""&gt;Geekslide&lt;/a&gt;，Lit 用于 UI 组件和幻灯片编辑/播放。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/Rei/edc956e4-6b67-4460-8417-a0bda9e37c1d.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;如果没有 Lit，这样的交互纯手工写会非常麻烦。由于 Web Component 出色的互操作性，我可以只在需要的部分使用 Lit 而不用整个替换 Rails View。&lt;/p&gt;

&lt;p&gt;我打算在未来更新更多 Lit 实践方面的内容。&lt;/p&gt;
&lt;h2 id="资源"&gt;资源&lt;/h2&gt;
&lt;p&gt;最后推荐一些资源。&lt;/p&gt;

&lt;p&gt;学习 Lit 最好的去处就是官方文档：&lt;a href="https://lit.dev/docs/" rel="nofollow" target="_blank"&gt;https://lit.dev/docs/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adobe 的 UI 库有 Web Component 版本：&lt;a href="https://opensource.adobe.com/spectrum-web-components/" rel="nofollow" target="_blank"&gt;https://opensource.adobe.com/spectrum-web-components/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FontAwsome 团队正在开发一个基于 Web Component 的 UI 库：&lt;a href="https://backers.webawesome.com/" rel="nofollow" target="_blank"&gt;https://backers.webawesome.com/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;如果你需要开发高交互的应用，又不想放弃 Rails View 默认栈，不妨试一下 Web Component / Lit。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Thu, 17 Oct 2024 22:51:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/43918</link>
      <guid>https://ruby-china.org/topics/43918</guid>
    </item>
    <item>
      <title>Importmap 还是 jsbundling？我全都要</title>
      <description>&lt;p&gt;原文地址： &lt;a href="https://geeknote.net/Rei/posts/3049" rel="nofollow" target="_blank"&gt;https://geeknote.net/Rei/posts/3049&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;从 Rails 7 开始，Importmap 成为处理 JavaScript 加载的默认机制。它可以充分利用 HTTP/2 的并行下载和缓存机制，避免打一个大包每次改动都需要下载所有代码。&lt;/p&gt;

&lt;p&gt;对于 js 依赖，Importmap 提供了一个 &lt;code&gt;pin&lt;/code&gt; 功能，例如运行：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/importmap pin local-time
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Importmap 就会从 CDN 下载 &lt;code&gt;local-time&lt;/code&gt;  的 js 文件放到 &lt;code&gt;vendor/javascript&lt;/code&gt; 目录，自动添加 &lt;code&gt;config/importmap.rb&lt;/code&gt; 配置，随后就可以在 js 文件里面导入：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import LocalTime from "local-time"
LocalTime.start()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但某些 js 库预设开发者会使用打包工具，没有将源码打包成一个完整的包，而是拆分了很多文件，这时候用 &lt;code&gt;importmap pin&lt;/code&gt; 就会遇到问题。例如 Lit，如果执行：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/importmap pin lit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会看到输出：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pinning "lit" to vendor/javascript/lit.js via download from https://ga.jspm.io/npm:lit@3.2.0/index.js
Pinning "@lit/reactive-element" to vendor/javascript/@lit/reactive-element.js via download from https://ga.jspm.io/npm:@lit/reactive-element@2.0.4/reactive-element.js
Pinning "lit-element/lit-element.js" to vendor/javascript/lit-element/lit-element.js.js via download from https://ga.jspm.io/npm:lit-element@4.1.0/lit-element.js
Pinning "lit-html" to vendor/javascript/lit-html.js via download from https://ga.jspm.io/npm:lit-html@3.2.0/lit-html.js
Pinning "lit-html/is-server.js" to vendor/javascript/lit-html/is-server.js.js via download from https://ga.jspm.io/npm:lit-html@3.2.0/is-server.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 Lit 引用了很多子包。糟糕的是，即使下载了这么多包，导入还是不完整的，如果在 js 代码中 `import { LitElement } from "lit"，会在浏览器中报错：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET http://localhost:3000/assets/css-tag.js net::ERR_ABORTED 404 (Not Found) 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为 &lt;code&gt;@lit/reactive-element&lt;/code&gt; 这个包中有很多可选模块没有下载下来。但如果下载所有可选模块，那么 importmap 配置会膨胀得很厉害。有一个 PR 正在处理（&lt;a href="https://github.com/rails/importmap-rails/pull/235" rel="nofollow" target="_blank" title=""&gt;#235&lt;/a&gt;），不好说能不能解决，因为问题在于库作者没有考虑不打包导入的需求。&lt;/p&gt;

&lt;p&gt;那么不妨改变一下思路，先用 jsbunding 将依赖打包，然后再用 importmap 导入。以下展示如何实现。&lt;/p&gt;
&lt;h2 id="实现"&gt;实现&lt;/h2&gt;
&lt;p&gt;假设已经使用 Rails 创建了项目，并且默认使用了 importmap：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new myapp
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;代码在 Rails 8.0.0.beta1 测试，但应该可用于 Rails 7+。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;接下来安装 jsbundling：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./bin/bundle add jsbundling-rails
./bin/rails javascript:install:esbuild
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时你会看到 js 编译错误，因为 jsbundling 和 importmap 的默认配置有冲突，接下来会修复冲突。&lt;/p&gt;

&lt;p&gt;删除 &lt;code&gt;app/views/layouts/application.html.erb&lt;/code&gt; 内这行内容：&lt;/p&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_include_tag&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"data-turbo-track"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"reload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;package.json&lt;/code&gt;，将内容改为：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "build": "esbuild app/assets/javascripts/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意路径改为 &lt;code&gt;app/assets/javascripts/*.*&lt;/code&gt;，这是以后放置需要 esbuild 编译的 js 文件的目录。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;config/applicatoin.rb&lt;/code&gt; 里面添加内容：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;excluded_paths&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app/assets/javascripts"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建文件夹 &lt;code&gt;app/javascript/src/&lt;/code&gt;，添加文件 &lt;code&gt;app/assets/javascripts/lit.js&lt;/code&gt;，内容为：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 yarn 安装 lit 包：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add lit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;config/importmap.rb&lt;/code&gt; 内添加配置：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;pin&lt;/span&gt; &lt;span class="s2"&gt;"lit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"lit.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在启动开发进程 &lt;code&gt;./bin/dev&lt;/code&gt; ，你会看到 esbuild 将 lit 编译到 &lt;code&gt;app/assets/builds/lit.js&lt;/code&gt;。打开浏览器查看页面源码，importmap 的内容增加了：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"imports": {
  ...
  "lit": "/assets/lit-9c62c803.js",
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个工作流程是：esbuild 将 &lt;code&gt;app/assets/javascripts&lt;/code&gt; 的源码编译到 &lt;code&gt;app/assets/build&lt;/code&gt;，&lt;code&gt;app/assets/build&lt;/code&gt; 的内容会被 assets pipeline 处理，在 &lt;code&gt;config/importmap.rb&lt;/code&gt; 中添加导入名和文件名的映射，模块就可以被应用的 js 代码导入。&lt;/p&gt;

&lt;p&gt;现在，你可以在 js 中 &lt;code&gt;import { LitElement } from "lit"&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;本文使用 esbuild 和 importmap 结合的方式，解决 impotmap 无法处理复杂依赖的问题。虽然这破坏了 nobuild 的期望，还是能利用到细粒度缓存的优点。在 importmap 普遍被 js 包兼容前，不妨用这个方法处理复杂依赖。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Sat, 12 Oct 2024 22:15:27 +0800</pubDate>
      <link>https://ruby-china.org/topics/43907</link>
      <guid>https://ruby-china.org/topics/43907</guid>
    </item>
    <item>
      <title>Rails World 2024 Opening Keynote - David Heinemeier Hansson</title>
      <description>&lt;p&gt;&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//www.youtube.com/embed/-cEn_83zRFw" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=-cEn_83zRFw" rel="nofollow" target="_blank" title=""&gt;https://www.youtube.com/watch?v=-cEn_83zRFw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Summary:&lt;/p&gt;

&lt;p&gt;Progress is our path,&lt;br&gt;
Complexity builds the bridge,&lt;br&gt;
Simplicity waits.&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Rails 8.0"&gt;Rails 8.0&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Propshaft&lt;/li&gt;
&lt;li&gt;Solid Trifecata&lt;/li&gt;
&lt;li&gt;Thruster&lt;/li&gt;
&lt;li&gt;Kamal 2&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="Rails 8.1"&gt;Rails 8.1&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Action Notifier&lt;/li&gt;
&lt;li&gt;Active Record Search&lt;/li&gt;
&lt;li&gt;House (MD)&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 27 Sep 2024 20:12:43 +0800</pubDate>
      <link>https://ruby-china.org/topics/43897</link>
      <guid>https://ruby-china.org/topics/43897</guid>
    </item>
    <item>
      <title>authentication-zero 是 devise 良好替代</title>
      <description>&lt;p&gt;首先 Rails 8 也许会增加一个用户认证生成器，用来生成基本的用户注册、登录机制。 &lt;a href="https://github.com/rails/rails/issues/50446" rel="nofollow" target="_blank"&gt;https://github.com/rails/rails/issues/50446&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;然后我看了评论提及的现有的生成器 &lt;a href="https://github.com/lazaronixon/authentication-zero" rel="nofollow" target="_blank"&gt;https://github.com/lazaronixon/authentication-zero&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;新项目试用了一下，生成的代码非常简洁，跟我自己写的差不多了，避免了手工复制自己的代码。&lt;/p&gt;

&lt;p&gt;为什么不推荐 devise，理由：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;内部逻辑非常抽象，改起来很困难。&lt;/li&gt;
&lt;li&gt;定制的时候需要把控制器和模版复制到项目中，那么就跟生成器差不多了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以在 Rails 8 自带的生成器出来前，可以先用着 authentication-zero。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Tue, 14 May 2024 10:13:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/43684</link>
      <guid>https://ruby-china.org/topics/43684</guid>
    </item>
    <item>
      <title>Rei on Rails #10 - 如何实现自定义域名</title>
      <description>&lt;p&gt;将一些要点分享出来希望对大家有帮助。&lt;/p&gt;

&lt;p&gt;哔哩哔哩： &lt;a href="https://www.bilibili.com/video/BV1iu4y157jS/" rel="nofollow" target="_blank" title=""&gt;https://www.bilibili.com/video/BV1iu4y157jS/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;YouTube： &lt;a href="https://www.youtube.com/watch?v=gyL-yWL2ZH8" rel="nofollow" target="_blank" title=""&gt;https://www.youtube.com/watch?v=gyL-yWL2ZH8&lt;/a&gt;&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Sun, 26 Nov 2023 22:13:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/43490</link>
      <guid>https://ruby-china.org/topics/43490</guid>
    </item>
  </channel>
</rss>
