学习Lustre:类型安全的前端开发
摘要
一篇博客文章,详细介绍了作者学习Lustre(一个用于Gleam的类型安全函数式前端框架)的经历,重点关注其Model-View-Update架构以及构建Web应用程序的好处。
<p><a href="https://lobste.rs/s/uf4j0n/learning_lustre_type_safe_frontend">评论</a></p>
查看缓存全文
缓存时间: 2026/06/18 12:02
# 学习 Lustre:使用 Gleam 进行类型安全的前端开发 - 博客
来源:https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true
最近我得到了一份实习机会!他们让我为信用评分分析应用构建前端,唯一的问题是我之前完全没有前端开发经验。说实话,我不想用 JavaScript 来做这件事,无论是语言还是生态系统对我来说都太混乱了。我不想放弃使用 Gleam 时拥有的**类型安全**和**可预测性**,因此经过一番研究,我终于决定尝试一下 [Lustre](https://lustre.hexdocs.pm/index.html)!
---
## 什么是 Lustre? (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#what-is-lustre)
Lustre 是一个用 Gleam 构建 Web 应用的声明式、函数式框架。它设计上注重简洁,并且不需要使用宏或模板。正如其[文档](https://lustre.hexdocs.pm/index.html#philosophy) 所述:
> 现代前端开发既困难又复杂。其中一些复杂性是必要的,但很多是偶然产生的,或者是因为有太多选择。Lustre 与 Gleam 的设计理念相同:在可能的情况下,应该只有一种做事方式!
---
## Model-View-Update 架构 (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#the-model-view-update-architecture)
受 Elm 和 Erlang 的启发,Lustre 使用消息传递来管理状态,一个 Lustre 应用由三个主要部分组成:
1. **Model**:你的应用状态。它将传递给你的 `view` 函数,用于决定 UI 的样子。
2. **View**:渲染你的 HTML 元素。用户交互和外部事件会产生消息,这些消息必须由你的 `update` 函数处理。
3. **Update**:更新你的应用状态。你可以对 UI 接收到的消息进行模式匹配,并更新 Model。
```gleam
pub type Message
pub type Model
fn init(_props) {
todo as "build initial model"
}
fn view(model: Model) {
todo as "render your UI"
}
fn update(model: Model, message: Message) {
todo as "update current model"
}
```
### Message (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#message)
消息描述了应用中的**状态变化**。它们通过 Gleam 的[自定义类型](https://tour.gleam.run/data-types/custom-types/) 定义,通常遵循约定:`主语 动词 宾语`。
```gleam
pub type Message {
/// 用户更新了邮箱字段
UserTypedEmail(email: String)
/// 用户更新了密码字段
UserTypedPassword(password: String)
/// 用户向服务器提交了登录凭据
UserClickedSubmit
/// API 验证了用户凭据
ApiReturnedSession(result: Result(session.Session, rsvp.Error(String)))
}
```
### Model (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#model)
你的应用状态是全局的,不局限于当前页面。如果必要,你可以在 Model 中存储**页面特定**的状态,我在实习项目中就是这样实现的。你的 Model 可以在项目的根模块中定义,并存储一个 `page` 字段用于当前页面的状态。
```gleam
// client.gleam
pub type Model {
Model(
/// 当前用户会话
session: session.Session,
/// 当前路由
route: route.Route,
/// 当前页面模型
page: page.Page,
/// 所选语言
lang: lang.Language,
)
}
```
[modem](https://hexdocs.pm/modem) 包提供了拦截内部链接导航的功能,并通过提供的处理程序将这些导航发送到你的 `update` 函数。你需要在初始化时设置其功能。
```gleam
pub fn init(opts: Init) -> #(Model, effect.Effect(Message)) {
let route = route.parse(opts.uri)
let #(page, effect) = page.init(route)
let init_modem = {
use uri <- modem.init()
let route = route.parse(uri)
// 当 `modem` 包拦截到链接时,
// 会发送此消息,需要在应用的 `update` 函数中妥善处理。
UserNavigatedTo(route)
}
// Lustre 应用的 `init` 函数必须提供初始 Model
// 以及一个初始化完成后要执行的副作用。
let effect = effect.batch([effect, init_modem])
#(Model(route:, page:), effect)
}
```
每个页面可以实现自己的 `view` 和 `update` 函数。这样,每个页面负责自己的状态管理和 HTML 渲染。当用户在应用中导航时,Model 中的 `route` 和 `page` 字段都会更新。
```gleam
pub fn update(model: Model, message: Message) {
case model, message {
Model(route: route.Login, page: page.Login(page), ..), LoginMessage(page_message) ->
handle_login_message(model, page, page_message)
Model(route: route.Dashboard, page: page.Dashboard(page), ..), DashboardMessage(page_message) ->
handle_dashboard_message(model, page, page_message)
_, _ -> todo
}
}
```
### View (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#view)
你的 `view` 函数是**纯函数**,这意味着相同的 Model 总会渲染出相同的 HTML。Lustre 提供了一个模块来构建页面的骨架,最酷的地方在于它只是常规的 Gleam 代码,你只需要导入 `html` 模块并访问其函数即可。这里我在应用 Model 的 `session` 字段上进行[模式匹配](https://tour.gleam.run/flow-control/case-expressions/),以决定此标签指向哪个路由。
```gleam
pub fn view(model: Model) {
case session {
session.Authenticated(..) -> {
let attributes = [
route.href(route.Dashboard),
attribute.class("font-bold bg-primary text-primary-foreground"),
]
html.a(attributes, [
html.text("Dashboard"),
])
}
session.Guest -> {
let attributes = [
route.href(route.Login),
attribute.class("py-2 px-4 rounded-md hstack"),
attribute.class("font-bold bg-primary text-primary-foreground"),
]
html.a(attributes, [
icon.log_in([class("size-4")]),
html.text("Login"),
])
}
session.Pending(..) -> {
let attributes = [
attribute.class("flex gap-2 items-center"),
attribute.class("font-bold bg-primary text-primary-foreground"),
]
html.div(attributes, [
// 等待身份验证时渲染旋转动画
html.span([attr.aria_busy(True), attr.data("spinner", "small")], []),
html.p([], [html.text("Loading")]),
])
}
}
}
```
### Update (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#update)
你的 `update` 函数接受两个参数:
1. 当前应用 Model。
2. 接收到的消息。
你可以对参数进行模式匹配来决定接下来做什么,并相应地更新应用状态。Gleam 支持对多个值进行模式匹配。
```gleam
pub fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
case model, message {
// 导航
model, UserNavigatedTo(route:) -> handle_navigation(model, route)
// 语言选择
model, NavbarMessage(message: navbar.UserSelectedLanguage(lang:)) -> #(
Model(..model, lang:),
effect.none(),
)
// 会话管理 ------------------------------------------------
// 如果服务器成功验证用户身份,
// 初始化其会话,并将其重定向到正确的路由。
Model(session: session.Pending(on_success:, ..), ..), UserRestoredSession(result: Ok(session)) -> {
let route = on_success
let #(page, page_effect) = page.init(route)
let redirect = modem.push(route.path(route), option.None, option.None)
let effect = effect.batch([page_effect, redirect])
#(Model(..model, session: route:, page:), effect)
}
// 如果失败,将会话初始化为 Guest,并相应重定向用户,
// 通常为登录页面。
Model(session: session.Pending(on_failure:, ..), ..), UserRestoredSession(result: Error(..)) -> {
let session = session.Guest
let route = on_failure
let #(page, page_effect) = page.init(route)
let redirect = modem.push(route.path(route), option.None, option.None)
let effect = effect.batch([page_effect, redirect])
#(Model(..model, session:, route:, page:), effect)
}
}
}
```
由于在应用中导航也会产生消息,你可以轻松控制给定用户可以访问哪些页面。
```gleam
fn handle_navigation(
model: Model,
route: route.Route,
) -> #(Model, Effect(Message)) {
// 如果路由没有变化,则不做任何操作
use <- bool.guard(model.route == route, #(model, effect.none()))
let protected = route.is_protected(route)
let route = case model.session, route {
// 如果路由要求用户已认证,则将其重定向到登录页面。
session.Guest, _ | session.Pending(..), _ if protected -> route.Login
// 如果用户已经认证,但正在导航到登录页面,
// 则将其重定向到 Dashboard。
session.Authenticated(..), route.Login -> route.Dashboard
_, _ -> route
}
let #(page, effect) = page.init(route)
#(Model(..model, route:, page:), effect)
}
```
再次强调,模式匹配通常就是解决大多数问题所需要的全部。Gleam 的设计重点在于只有一种做事方式,这有助于保持项目简洁,最重要的是,保持可预测性。
## 编译项目 (https://blog.kacaii.dev/3mn6n3lvibc2b?auth_completed=true#compiling-the-project)
所有设置完成后,你可以使用 `lustre_dev_tools` (https://lustre-dev-tools.hexdocs.pm/lustre/dev.html) 来编译应用,将所有必要的 CSS、JS 和 HTML 打包在一起。
```bash
gleam run -m lustre/dev build
# Compiled in 0.08s
# Running lustre/dev.main
# Creating JavaScript bundle...
✅ Bundle successfully built.
✅ HTML generated.
# Copying 6 assets...
✅ Assets copied.
✅ Build complete!
```
编译完成后,你可以从后端提供这些文件,从而得到一个功能完整的应用。如果你已经喜欢 Gleam 在后端的工作方式,我非常推荐你也将其用于前端!<3
相似文章
@Mayhem4Markets: https://x.com/Mayhem4Markets/status/2069090022117019928
两大主流LLM服务框架SGLang和vLLM的详细技术对比,涵盖KV缓存管理(RadixAttention vs PagedAttention)的架构差异、吞吐量、延迟以及自托管环境的部署考量。
用 Rust/WASM 构建 LLM 的开源边缘语义缓存——对架构的合理性检查?[D]
提议使用 Rust/WASM 在 CDN 边缘构建一个轻量级的开源 LLM 语义缓存,以降低延迟和 API 成本,并寻求社区对架构和用例有效性的反馈。
Lisp在网页应用中的运用(2001)
Paul Graham结合自己创办Viaweb的经验,讨论了在网页应用中使用Lisp的优势,包括语言自由度、增量开发以及快速修复bug。
@0x_kaize: https://x.com/0x_kaize/status/2068775813785506091
关于在使用 GLM 5.2 模型时避免速率限制和降低成本的指南,涵盖提示批处理、缓存、免费模型替代方案、努力水平、上下文窗口管理和自托管。
@blackanger: 从 Epic Lora 里蒸馏了不少好的实践
An experiment on using Rust's type system as an AI coding specification, distilling practices from Epic Lora.