Hanami, Why?: Introductions
Hanami, Why?: Introductions
I mentioned in my Hello, world! post that I built this site using Hanami. This site may seem simple on the surface; it is, after all, just a blog. However, the backend is packed with features that help me day to day. I have built a tool that lets me cross-post to both Bluesky and Mastodon. I have a private journal where I can keep notes throughout the day. There is a custom analytics engine to provide me with insights on how well my blog posts are doing. There is a messaging backend that lets readers reach me through my contact form without cluttering my inbox. I have also built a task manager that syncs with both Linear and GitHub Issues and helps me track what I need to do. Most importantly, I have built an activity feed that captures my GitHub commits, journal entries, blog posts, and tasks. It helps me answer the question “What did I do between ___ and ___ dates?”, which, believe it or not, is hard for me to answer on my own, even for last week. The application is open source, and you can find it on GitHub.
我在“Hello, world!”一文中提到,我使用 Hanami 构建了这个网站。从表面上看,这可能只是一个简单的博客,但其后端却塞满了各种辅助我日常工作的特性。我构建了一个工具,可以将内容同步发布到 Bluesky 和 Mastodon;我有一个私人日志,可以随时记录笔记;我还有一个自定义的分析引擎,用于洞察博客文章的表现;我还有一个消息后端,让读者可以通过联系表单与我沟通,而不会弄乱我的收件箱。此外,我还构建了一个任务管理器,它能与 Linear 和 GitHub Issues 同步,帮助我跟踪待办事项。最重要的是,我构建了一个活动流,记录了我的 GitHub 提交、日志条目、博客文章和任务。它能帮我回答“我在某某日期到某某日期之间做了什么?”这个问题——信不信由你,即使是上周做了什么,我自己也很难回答。该应用程序是开源的,你可以在 GitHub 上找到它。
I chose Hanami for a few reasons. I had always felt trapped by Rails, which to me has a rigid way of doing things and makes you fight for anything you want to do differently. At one point I even started writing web applications in Sinatra for more freedom in how I built them. I had heard of Hanami, and had even experimented with some of the early 1.x versions, but it was not until RubyConf 2024 in Chicago that I was truly introduced to it. Today I am a maintainer on the Hanakai team (the folks behind Hanami, dry-rb and ROM), and what better way to get to know the things you build than to use them? Hanami is very different from other web frameworks in the Ruby ecosystem. I thought it might be a good idea to go over some of these differences and why I think they are beneficial. So with that, I would like to introduce you to a new series I plan on doing over the coming weeks called “Hanami, Why?”.
我选择 Hanami 有几个原因。我一直觉得被 Rails 束缚,因为它有一套僵化的做事方式,如果你想做些不同的尝试,就必须与之抗争。有一段时间,我甚至开始用 Sinatra 编写 Web 应用,以获得更大的构建自由度。我听说过 Hanami,甚至尝试过一些早期的 1.x 版本,但直到 2024 年芝加哥的 RubyConf,我才真正深入了解它。如今,我是 Hanakai 团队(Hanami、dry-rb 和 ROM 背后的团队)的维护者,还有什么比亲自使用自己构建的东西更能深入了解它们的方式呢?Hanami 与 Ruby 生态系统中的其他 Web 框架截然不同。我认为探讨这些差异以及为什么我认为它们是有益的,是一个不错的主意。因此,我想向大家介绍我计划在未来几周内推出的一个新系列:“Hanami, Why?”。
The Basics
基础知识
I think in order for us to have a fruitful conversation about Hanami, how it differs from other web frameworks, and what those differences mean, we need two things. First, we need to approach this with an open mind. As I mentioned, Hanami is very different, and I mean this in a good way. At best, you may come out of this series understanding that Hanami might actually scale better than alternatives in various circumstances. At worst, you will come out of this series with a new perspective on how things could be done differently within your own web applications. For either outcome, we must be open to difference.
我认为,为了让我们能够就 Hanami、它与其他 Web 框架的区别以及这些差异的意义进行富有成效的讨论,我们需要两样东西。首先,我们需要保持开放的心态。正如我所提到的,Hanami 非常不同,而且这是褒义上的不同。往好了说,通过这个系列,你可能会意识到在某些情况下,Hanami 的扩展性确实优于其他替代方案;往坏了说,你也会从这个系列中获得关于如何在自己的 Web 应用中以不同方式处理事务的新视角。无论结果如何,我们都必须拥抱差异。
The other thing we will need is a basic understanding of how Hanami works. I will keep this segment high level, since the Hanami documentation covers the finer points well and we will dig deeper later in this series. Hanami does its best to push you toward tiny, single-purpose abstractions. There are several important abstractions in the Hanami ecosystem, but I think the four most important abstractions are Actions, Relations, Repos, and Operations.
我们需要具备的另一件事是对 Hanami 工作原理的基本了解。我将保持这一部分的概括性,因为 Hanami 的文档已经很好地涵盖了细节,我们将在本系列的后续部分深入探讨。Hanami 竭尽全力引导你使用微小且单一用途的抽象。Hanami 生态系统中有几个重要的抽象,但我认为最重要的四个是 Actions(动作)、Relations(关系)、Repos(仓库)和 Operations(操作)。
Actions represent individual HTTP endpoints within your application. This can be daunting for some, as it means a simple set of CRUD operations is spread out amongst four separate classes. I will go into more detail in a future issue about how and why that is a good thing. Relations, repos and structs (plain data objects) are the “model” layer of a Hanami web application. Relations are the lowest level of the model layer and own the responsibility of querying your database. Repos are an optional, higher-level layer that sits on top of your relations. Repos can manage one or several relations and ensure you are not passing around a persistence layer by always returning structs. Lastly, you have operations. Operations are an implementation of the command pattern, powered by dry-operation. Operations are the perfect place to perform complex mutations and receive a Success or Failure object in return.
Actions 代表应用程序中的单个 HTTP 端点。这对某些人来说可能令人生畏,因为这意味着一组简单的 CRUD 操作被分散在四个独立的类中。我将在未来的文章中详细说明为什么这是一件好事。Relations、Repos 和 Structs(纯数据对象)构成了 Hanami Web 应用的“模型”层。Relations 是模型层的最底层,负责查询数据库。Repos 是一个可选的、位于 Relations 之上的更高层。Repos 可以管理一个或多个 Relations,并通过始终返回 Structs 来确保你不会到处传递持久化层对象。最后是 Operations。Operations 是命令模式的一种实现,由 dry-operation 提供支持。Operations 是执行复杂变更并返回 Success 或 Failure 对象的绝佳场所。
Beneath these sit lower-level system pieces that help with things like dependency injection. Thanks to dry-system we can automagically inject instances of classes directly into our actions, relations, repos, and operations. This is typically done via the Deps module which gets included in your classes with a list of the dependencies you want to inject. This offers an additional layer of clarity on what dependencies a particular class relies on, as well as where those dependencies come from. We also have a robust typing system thanks to dry-types. We will go into more detail later in this series on how I use types to validate and normalize user inputs. Lastly, as I mentioned, our operations and our actions can both use Success and Failure results thanks to dry-monads. In a moment I will show how these result objects, with pattern matching, simplify your mutations.
在这些之上,还有更底层的系统组件来辅助依赖注入等工作。得益于 dry-system,我们可以自动将类的实例注入到我们的 Actions、Relations、Repos 和 Operations 中。这通常通过 Deps 模块完成,该模块被包含在你的类中,并列出你想要注入的依赖项。这为特定类依赖哪些依赖项以及这些依赖项来自何处提供了额外的清晰度。我们还拥有得益于 dry-types 的强大类型系统。在本系列的后续部分,我们将详细介绍我如何使用类型来验证和规范化用户输入。最后,正如我提到的,得益于 dry-monads,我们的 Operations 和 Actions 都可以使用 Success 和 Failure 结果。稍后我将展示这些结果对象如何通过模式匹配来简化你的变更操作。
module MyApp
module Actions
module Users
class Create < Action
include Deps["operations.create_user"]
format :json
before :validate_params
params do
required(:user).filled(:hash) do
required(:email).filled(Types::EmailAddress)
required(:password).filled(:string, min_size?: 8)
end
end
def handle(request, response)
case create_user.call(request.params.to_h[:user])
in Success[user] then success_response(response, user)
in Failure[errors] then error_response(response, errors)
end
end
private
def error_response(response, errors)
response.status = :unprocessable_entity
response.body = errors.to_json
end
def success_response(response, user)
response.status = :created
response.body = user.to_json
end
def validate_params(request, response)
return if request.params.valid?
halt 422, error_response(response, request.params.errors.to_h)
end
end
end
end
end
Here we have an action which defines the shape of its parameters, validates them, then calls into the CreateUser operation to handle user creation. This ensures the action’s sole responsibility is HTTP logic. If CreateUser returns success then we render the user.
这里我们有一个 Action,它定义了参数的结构,对其进行验证,然后调用 CreateUser 操作来处理用户创建。这确保了 Action 的唯一职责是处理 HTTP 逻辑。如果 CreateUser 返回成功,我们就渲染用户。