学习Lustre:类型安全的前端开发

Lobsters Hottest 工具

摘要

一篇博客文章,详细介绍了作者学习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

相似文章

Lisp在网页应用中的运用(2001)

Hacker News Top

Paul Graham结合自己创办Viaweb的经验,讨论了在网页应用中使用Lisp的优势,包括语言自由度、增量开发以及快速修复bug。