Crystal + Kemal 实战:构建一个内存级实时应用
14MB 的镜像体积,没有任何数据库,状态全在内存里,这就是我用 Crystal 和 Kemal 搞的一个小工具。很多开发者习惯了无论项目大小都上全家桶框架,但这次我想验证一个观点:框架的选择得跟问题规模匹配,而不是靠习惯。
下一篇
用Crystal + Kemal撸一个实时应用 →
这个应用的核心逻辑很简单:一个主持人开启会话,参与者加入并私密排序,直到主持人点击“揭晓”之前,任何参与者的排序数据绝对不能出现在服务器发出的 Payload 中。这不是靠前端隐藏,而是从服务器端强制执行的隐私不变性。
为什么选 Kemal?因为它就是一个类 Sinatra 的微框架,只有路由、请求响应、WebSocket 和静态文件。对于一个不需要 ORM、不需要数据库迁移、也不需要复杂资产构建的项目来说,Kemal 刚好覆盖了所有需求,没有任何冗余。
我把业务逻辑全部抽离在纯 Crystal 对象中,完全不依赖 Kemal,这样在测试领域逻辑时根本不需要启动服务器。
一、定义不可变领域对象
为了保证数据的绝对正确,我定义了一个 Ranking 结构体。它在初始化时就强制校验:必须包含所有卡片,且不能重复。这样在后续调用中,我可以放心地使用 index! 而不需要写任何 nil 检查。
struct Ranking
getter order : Array(Motivator)
def initialize(@order : Array(Motivator))
validate!
end
# 1 为最重要,10 为最不重要
def position(motivator : Motivator) : Int32
@order.index!(motivator) + 1
end
private def validate!
unless @order.size == Motivator.values.size
raise ArgumentError.new("A ranking must order all ten cards")
end
if @order.uniq.size != @order.size
raise ArgumentError.new("A ranking must not contain a duplicate card")
end
end
end二、状态机控制权限
会话被设计成一个简单的状态机:Lobby(大厅) → Revealed(揭晓)。一旦进入揭晓状态就是终局,不可逆转。通过这种方式,我可以非常简单地在服务器端拦截非法操作。
def reveal(token : String) : Nil
raise Forbidden.new("Only the facilitator can reveal") unless token == @facilitator_token
guard_lobby!
@phase = Phase::Revealed
end
private def guard_lobby! : Nil
raise WrongPhase.new("Session is already revealed") if revealed?
end在这种架构下,服务器成为了隐私的最后一道防线。只要状态机没跳转到 Revealed,后端逻辑就绝不会把其他人的排序数据打包发给客户端。
完整源码和详细步骤可以参考这个路径:
github.com/JadeKharats/motivators/blob/main/TUTORIAL.md