Datastar: Web Framework for the Future?
Datastar:面向未来的 Web Framework?
Chris Malek - 技术文章
Datastar:面向未来的 Web Framework?
2025-01-15 [最后修改时间:2025-01-20] /posts/data-star-first-impressions/ map[email:chris.malek@cedarhillsgroup.com location:San Diego, CA name:Chris Malek]
目录
- TLDR
- 介绍
- 我在寻找什么?
- 重新审视 Datastar 和一个转折点
- 什么是 Datastar?
- Actions - GETS/POSTS/PUTS/DELETES
- 重新思考 Web 开发
- 服务器要求
- 结论
- 进一步阅读
# TLDR Datastar 是一个新的 hypermedia framework,它可以更简单高效地构建实时 web 应用程序。它优先考虑服务器端逻辑,使用 "signals" 来自动更新 UI,并利用 Server-Sent Events 实现闪电般的速度。如果你正在寻找传统 JavaScript frameworks 或 HTMX 的精简替代方案,Datastar 值得探索。 但是,它要求你以全新的视角进行 web 开发,拥抱服务器驱动的架构和 reactive programming。
# 介绍 我最近一直在深入研究 hypermedia,寻找可以用来构建新产品并帮助客户快速创建概念验证和 web 工具的 frameworks 和 libraries。 在撰写本文时,HTMX 基本上受到了 hypermedia 领域的所有关注。它的演示效果非常好,示例也很棒。但是,本文不是关于 HTMX 的。 我相信 hypermedia 和 HTMX 提供了一个有希望的方向,但是,当我尝试使用 HTMX 开发一个新产品时,我感到很苦恼,因为在搞清楚项目结构、HTML 结构(结合过多的 HTMX tags)以及意识到 HTMX 无法交互式地处理前端时遇到了挑战,为此,你必须引入像 AlpineJS 这样的东西。(我说过我讨厌 javascript 吗?)。HTMX 很酷,但我认为在你用它开始一个新项目之前,你可能需要看看 Datastar。 在评估 HTMX 时,我过去曾看过 Datastar,但在我第二次查看并开始感觉到 HTMX 的一些不足之处之前,我并没有理解它相对于 HTMX 的潜力。你的结果可能会有所不同。
## 我的背景 首先,让我们了解我带有偏见的观点。每个人的背景都不同,我认为了解我的出发点非常重要。
- 我是 PeopleSoft ERP 平台的专家,创建 "enterprise" 应用程序
- PeopleSoft 是一个大型 ERP 系统,被许多大型组织使用。它非常强大,但有点过时。由于它是大多数公司数据的中心,因此它不会很快消失,但 SAAS apps 正在慢慢地蚕食它。
- 我大部分时间都在创建业务 CRUD 应用程序,这些应用程序供业务用户和学生使用。
- 这些应用程序始终是 "config" 驱动的,因为 PeopleSoft 可以轻松地创建设置/配置表和 GUI 来管理数据。将 PeopleSoft 想象成以设置表的形式存在的巨大的 "feature flags"。
- PeopleSoft 完全抽象了前端。你永远不用担心前端。后端控制前端。PeopleSoft 是 30 年前构建的,它的 meta-data 架构允许从 client-server 架构移植到 web 架构。开发人员可以部署应用程序,而无需担心 JavaScript 甚至 HTML 和 CSS。这太棒了。
- 这使我在职业生涯的大部分时间里都远离了前端。我一直是一名后端开发人员,但是使用 PeopleSoft,后端开发人员可以轻松地部署面向用户的前端应用程序。 所以我习惯于处理和向客户交付 100% 服务器端但又面向用户的解决方案。你不需要前端开发人员来部署 PeopleSoft 应用程序。
- 这些应用程序包含一些最敏感的公司数据,例如工资单、传记数据、学生数据、财务数据等。你不能在这些应用程序中 "快速行动并破坏事物"。你必须非常小心数据和业务逻辑。
- 在这些应用程序中,你不能信任在浏览器中运行的代码,并且你的后端代码必须保护对数据的访问。
- 我使用 Go 来做我的大部分 side projects。我喜欢 Go 的简洁性、速度和类型安全。
# 我在寻找什么? 我一直在寻找一个 framework 或一组 libraries 来构建一个现代高效的新产品。我一直在研究 Hypermedia 来做到这一点。 我的大致要求是:
- 一个现代 web framework,它高效且可以处理实时更新。
- 一个可以处理前端和后端,但尽可能 100% 依赖后端的 framework。
- 一个可以处理前端状态和交互的 framework。
- 简单,简单,简单
- 一个我可以在后端使用 Go 的 framework。
- 快速开发和原型设计
- 尽可能避免 Javascript 和 NPM,或者完全从开发的角度来看。
- Javascript 让我感到恶心 🤢,每次我看到 NPM 时,我都会感到头痛 😵💫,我的直觉告诉我该逃跑。我不喜欢 JavaScript ecosystem。
- 一个 "stable" 的平台,我可以部署一些东西,让它在多年内正常工作,而无需担心它。
- 快速部署到云端,例如 Fly.io。
- 自由地使用我想要的任何 CSS framework,因为它们似乎会随着风的变化而变化。
- 避免现代 web apps 的 split-team、JSON API 方法,在这种方法中,前端和后端是不相连的。
之前我一直在研究 Phoenix,但不想转移到另一种语言。如果你在 google 上搜索 "hypermedia",你会看到很多关于 HTMX 的文章,并且 GitHub 上似乎有很多使用它的项目。我认为它是目前最流行的 hypermedia library。我一直在努力研究 Live Golang Library。
## 我对 HTMX 的第一印象 当然,我研究了 HTMX,并开始对它的潜力感到兴奋。因此,在部署了一些客户用于一些一次性任务的相当简单的 "web 工具" 之后,我开始开发一个真正的应用程序。 我为自己和客户创建了一些简单的工具。我不需要任何前端状态或交互。我只是从服务器更新 UI,其中大部分是由于 field change 或点击而产生的新 HTML fragments。在这些简单的情况下,HTMX 可以很好地处理这些问题。 在我对更复杂的应用程序进行 HTMX prototyping 时,HTML 代码变成了一团糟,其中一些非平凡的部分使用了 HTMX tags。我发现自己很难理解项目结构和大量的 HTMX tags,以及如何管理前端和后端。我还需要一些前端功能和状态,而 HTMX 并不是为处理这些而设计的。使用 HTMX,你必须导入 AlpineJS,我开始出现荨麻疹,因为我讨厌 JavaScript 😢。 当我开始处理应用程序中更复杂的部分时,我感觉 HTMX 有点碍事了。 我开始看到一个巨大的 lint ball 正在建立。我的直觉告诉我,我正在朝着错误的方向前进。我按下了暂停按钮,开始寻找替代方案,因为我感觉自己工作得太辛苦了,代码变得太复杂了。
# 重新审视 Datastar 和一个转折点 我一直忙于在客户现场工作,我暂停了对 HTMX alterntives 的研究或一些 non-trival TODO application examples。然后有一天,在我的 YouTube feed 上,我看到了一个 对 Datastar 创建者的采访,它让我再次审视 Datastar。如果我没有使用 HTMX 的一些经验,我就不会 "准备好" 接受那次采访,并且理解他所提出的一些观点。 以下是 AI 生成的采访要点。我用粗体标出的那些要点让我再次审视 Datastar:
- Delaney 探索了 HTMX 之外的 hypermedia,重点关注实时应用程序。
- HTMX 被视为解决了 1999 年的 hypermedia 问题,但并没有推动当前的界限。
- Server-sent events (SSE) 提供有效的实时更新,超越了传统的 AJAX。
- Datastar 是 HTMX 的模块化替代方案,旨在简化代码并提高性能。
- 游戏开发的效率可以激发 web 开发的速度和能力。
- SSE 可以实现微秒级的更新,挑战了 HTMX 中轮询的局限性。
- event-driven architecture 对于可扩展、高效的实时应用程序至关重要。
- 与 HTMX 相比,Datastar 的声明式方法降低了复杂性。
- 强调服务器控制,SSE 可以优化和简化 web 交互。
- Delaney 提倡一种范式转变,即更智能地使用 hypermedia 技术。
在评估 HTMX 时,我之前曾看过 Datastar 的 documentation。我可能是从 Reddit 上的一些讨论中找到它的。但是,我之前很难掌握它的用途,并且发现它的文档令人困惑且密集。说实话,当我第一次阅读 Datastar docs 时,它只是超出了我的理解能力。 我还没有准备好理解它。它声称是 HTMX 和 AlpineJS 的更好替代品。另一件让我反感的事情是,该项目没有大量的贡献者。但是,在观看了 Datastar 作者的采访后,我意识到他可能有一些深刻的见解,我应该再看一遍。他也是 HTMX 的贡献者。 我最初发现 Datastar 令人困惑的两件事是:
- 使用 SSE (Server-Sent Events) 进行实时更新。
- 我没有使用 SSE 的经验,也不明白它如何在实时应用程序中使用。我模糊地记得读到过它们不具备可扩展性,或者它们的连接容易断开。我已经好几年没有看过它们了。我没有使用它们的经验。
- "signals" 的概念用于 reactive programming。
- 我没有意识到这可以极大地简化你的代码。
signal
这个术语令人困惑,我第一次阅读时根本不明白。我没有 reactive programming 的经验。事实证明,signals
可以帮助我避免大量的前端代码和状态管理,但我一开始并没有意识到这一点。
那次采访让我再次查看了 documentation,documentation 已经进行了一些更新。在我花了一些时间阅读和重新阅读文档并查看 examples 后,然后尝试了一些我自己的 "hello world" 示例,然后灵感就来了。 Datastar 可能是我一直在寻找的 library。在我开始剥开一些洋葱皮后,它看起来很有希望。
# 什么是 Datastar? 似乎作者 delaneyj 正在采用 HTMX 和 Hypermedia 的一些基本 primitives,并使它们更容易使用,而 Datastar 是他的答案。至少这是他们的主张。在撰写本文时,我仍在用它创建我的第一个应用程序。我还没有准备好给出完整的评论。但我对它的潜力感到兴奋。 似乎作者也是 Go 的忠实粉丝,这对我很有帮助,因为任何示例和 libraries 都将包含 Go 示例。 首先,让我澄清一下。我不是 Datastar 的专家。我只是刚开始学习它。我也 NOT 是 Datastar 的贡献者或作者。 我没有功劳。我只是想传播关于它的消息,因为我不认为它受到了应有的关注。 根据我目前对 Datastar 的理解,有一些关键概念构成了 Datastar 的基础:
- Signals:Reactive programming primitives,当数据更改时自动更新 UI。
- 我们将很快探索这些到底是什么。
- 作为开发人员,你将决定你想要哪些 signals,并将一些特殊的 tags 放在 HTML elements 上,这些 tags 将触发服务器将更新发送回 signals。这将与某种服务器状态相关联。
- Server-Sent Events (SSE):高效的数据流,用于实时更新和页面更改。
- 这些只是从服务器发送回客户端的 repsonses。它们只是 HTTP 上的文本,通常是更新 UI 的 HTML fragments。你可以做很多其他事情,但我们不要超前。
- Actions:HTTP verbs (GET, POST, PUT, DELETE),它会触发服务器端逻辑和 UI 更新。
- 这些是你放在 HTML elements 上的 HTML tags,这些 tags 会触发服务器将更新发送回 signals 或更新 UI 的新 HTML fragments。
- Fragments:HTML snippets,它基于服务器端逻辑和用户交互来更新 UI。
- 你的服务器端必须经过组织,才能发送回将更新 UI 的 HTML fragments。
你在 HTML 中包含 Datastar JavaScript library,然后你可以开始使用这些概念来构建你的应用程序。你还需要组织你的服务器来处理 SSE requests 和 GET/POST/PUT/DELETE requests。 你的后端选择并不重要。
## 与 HTMX 比较 我主要将其与 HTMX 进行比较,因为这是我目前的视角,并且 HTMX 受到了大量的关注。
- 使用 HTMX 构建一个真正的应用程序,你需要:
- 前端
- HTML
- HTMX JavaScript 和 Tags 来处理对后端更新的触发
- AlpineJS(或其他 JavaScript framework)来处理前端逻辑、交互和状态。
- 后端
- 依赖于你的 UX 的 HTML fragments
- 路由和代码来处理 GET/POST/PUT/DELETE
- 前端
- 使用 Datastar 构建一个真正的应用程序,你需要:
- 前端
- HTML
- Datastar JavaScript 来处理对后端更新的所有触发,以及所有 UI 状态和交互。
- 后端
- 依赖于你的 UX 的 HTML fragments
- 路由和代码来处理 GET/POST/PUT/DELETE
- SSE 路由来处理对 signals 的更新
- 前端
因此,仅从查看依赖项来看,Datastar 为你提供了一个 JavaScript library,它可以处理前端(signals)上的状态,并使 HTML 属性执行 actions (GET/POST/PUT/DELETE) 并处理来自服务器的更新。服务器 100% 负责生成 HTML fragments 和对 signals 的更新。
- Datastar 在一个 library 下提供了 HTMX 和 AlpineJS 的好处。你获得了两全其美。
- 你可以抛弃前端 framework(如 React 或 Vue.js)会提供的大部分功能,并使用 Datastar。(大胆的主张)
- 你的服务器 100% 负责生成 HTML snippets 和模板,这与你使用 HTMX 所做的非常相似。
- 它是后端无关的,可以与 Go、Node.js、PHP 等一起使用。我更喜欢 Go,但这无关紧要。
- 它严重依赖 Server-Sent Events (SSE) 进行实时更新,但在你揭开 SSE 的面纱后,它只是带有某些不同 headers 的文本 HTTP responses。
## 什么是 "Signal",什么是 "Reactive Programming"?
我认为我在最初阅读文档时错过的一件大事是 signal
的概念。这并非由 Datastar 发明,我相信它是使用另一位开发人员的 library 在 Datastar 中实现的。
我相信我只是落后了,你可能已经知道什么是 signal
。我将尝试解释它。从根本上理解 signal 代表什么以及可以为你做什么,是 Datastar 力量的源泉。 它使创建用户界面更加简单和可维护。
在我们讨论 signals 之前,让我们先讨论一下 reactive programming
,因为它们是相关的。Reactive programming 使你的应用程序代码自动 "react" 数据变化,并将这些变化自动地在应用程序中传播。你不用告诉计算机如何逐步执行操作,而是告诉计算机当数据变化时应该发生什么,计算机会找出如何执行操作。它允许你定义数据源和数据消费者之间的关系。当数据源发生变化时,数据消费者会自动更新。在一个非 reactive 的系统中,当数据源发生变化时,你必须手动更新数据消费者。这通常以 "on-change" Javascript events 和函数的形式来绑定所有数据和 UI。
好吧,好吧,这仍然太多的术语了!!!!
## 通过电子表格理解 Reactive Programming 我认为理解 reactive programming 的最佳方式是考虑电子表格。
- 像 Microsoft Excel 或 Google Sheets 这样的电子表格应用程序是这里最好的例子。
- 如果你有在工程或金融领域使用复杂电子表格的任何经验,那么你已经使用过 reactive programming。
- 我不是在谈论将 Excel 用作 CSV 查看器。
- 我是在谈论将它用作进行计算的工具,并且你 "构建层" 的中间计算以获得最终结果。
- 通常,你需要中间计算来进行其他计算,或者只是进行错误检查。这导致了一系列相互依赖的计算。这在电子表格单元格的公式中表示,这些公式引用了其他单元格。对于复杂的计算,你可以拥有一个 "pipeline" 或 spiderweb 的计算,它们是相互依赖的。
- 我拥有工程学位,并且在大学期间和大学毕业后为工程公司工作,我曾使用 Excel 进行一些非常复杂的计算以及 HVAC 冷却和管道系统的工程建模。Excel 是一个很棒的工具。
电子表格的惊人之处在于它是 reactive 的。 当你更改单元格中的值时,所有依赖的单元格都会自动更新。这是 reactive programming 的本质。在电子表格中,你定义了 "cells" 或数据元素之间的关系,并且底层 framework 会在数据更改时传播更改。这非常强大,可以简化你的代码并使其更易于维护。
这是一个粗略的示意图,其中箭头表示单元格之间的依赖关系:
Datastar 通过
signal
的概念在 web 应用程序中为你提供了一些相同的功能。
我从概念上将 Datastar signal
视为 "cell" 或 HTML elements 之间的链接。我一开始没有建立这种联系。
- 在 Datastar 中,signals 用于在数据更改时更新 UI。
- 它还可以触发后端 posts/gets/puts/deletes。
Signals
是 Datastar 应用程序的一部分。你将 signals
放在页面上,UI 可以自动更新。请参阅 模型绑定示例 和 Signals 更改示例
服务器可以向下发送对 "signals" (电子表格单元格内容) 的更新,甚至可以向下发送更新 UI 的新 HTML fragment。用电子表格术语来说,这就像从服务器添加新的单元格、图表等。
你可以用 signals 做更多的事情。如果你阅读了文档仍然不理解,我建议你重新阅读它们。我不得不阅读它们几次才能理解。
# Actions - GETS/POSTS/PUTS/DELETES 当/如果你开始研究 HTMX,你会看到你可以使用 GET/POST/PUT/DELETE 在服务器上触发 actions。这在 Datastar 中也是一样的。你可以使用 signal 触发这些 actions。 HTMX 和 Datastar 都会触发这个对服务器的请求来更新或获取更新的 UI elements。不同之处在于,Datastar 使用 SSE 将更新发送回客户端。我一直在挠头,直到我开始阅读更多关于它的信息并查看示例。 SSE 非常简单。它只是文本,我以前可能读到过,但由于我没有真正的开发工作经验,所以我不理解。我每天都在使用 HTTP web services,并且对 HTTP 的工作原理有充分的了解。 你可以向 HTML elements 添加一些 "tags" (单击按钮、更改输入等),然后当用户与页面交互时,服务器可以向下发送对 signals 的更新。 来自 Datastar 示例:
<div id="contact_1">
<label>First Name: John</label>
<label>Last Name: Doe</label>
<label>Email: joe@blow.com</label>
<div>
<button data-on-click="@get('/examples/click_to_edit/contact/1/edit')">
Edit
</button>
<button data-on-click="@get('/examples/click_to_edit/contact/1/reset')">
Reset
</button>
</div>
</div>
在浏览器中运行的 Datastar Javascript library 通过保持打开的连接(直到服务器关闭它)连接到服务器。服务器可以向下发送对 signals 的更新。服务器还可以向下发送更新 UI 的新 HTML fragments。 Datastar 和 HTMX 具有类似的概念,但 Datastar 开箱即用地构建,以使用 element 的 "ID" 处理更新页面的任何部分。这在 HTMX 中是可能的,但需要一些额外的工作/tags。 基本上,Datastar "actions" 可以做 HTMX 可以做的任何事情。
## 理解 SSE - 它只是文本 首先,让我们快速理解 SSE。正如我们将看到的,SSE 只是文本。它不是某种神奇的协议。它只是 HTTP 上的文本,带有一些特殊的 headers,并且浏览器开箱即用地支持它。 Datastar 利用 SSE,并且 Javascript library 以某种方式期望和解释来自服务器的 responses。服务器可以向下发送对 signals 的更新,或将更新 UI 的新 HTML fragments。服务器也可以在完成后关闭连接。 有关权威来源,请参阅 Datastar SEE 参考 在 Datastar 中,你可以向 HTML elements 添加一些 "tags" (按钮 "on click"、输入 "on change" 等),这会导致浏览器向服务器发送一个请求,以打开 SSE 连接。该连接保持打开状态,直到服务器关闭它。服务器可以向下发送对 signals 的更新,或将更新 UI 的新 HTML fragments。 对于大多数 CRUD 应用程序,你将发送将更新 UI 的 HTML fragments。然后关闭连接。如果你正在制作某种实时仪表板,你将保持连接打开,并在服务器发现数据发生变化时向下发送对 UI 的更新。服务器可能会监视数据库或某些其他数据源,并在它们更改时向下发送对 UI 的更新。 首先,让我们看一下最简单的例子,它最像 HTMX 示例,并且更符合 CRUD 应用程序的要求。 你将在 HTML element 上添加一些属性,这将触发对服务器的 SSE 调用。为简单起见,我们假设它是一个按钮点击。
<button id="button1" data-on-click="@get('/example/buttonpress')">
点击我
</button>
这将触发浏览器通过 SSE header 的 Accept: text/event-stream
向服务器发出的 HTTP 调用。
GET /example/buttonpress HTTP/1.1
Host: example.com
Accept: text/event-stream
Cache-Control: no-cache
可以选择发送额外的数据,Datastar 将自动发送页面上的任何本地 signals。这种自动 signal 发送是 Datastar 的一项功能,HTMX 中没有这项功能,我没有意识到它有多么强大。在上面的 HTTP 示例中,我没有显示任何 signals,因为在这个简单的示例中不需要它们。
- 在这里,浏览器将保持连接打开并监听来自服务器的更新。
- 服务器可以向下发送对
signals
的更新,或将更新 UI 的新 HTML fragments。- 在此示例中,我们将重点关注 HTML Fragments
- 服务器发送回一个
event
为Datastar-merge-fragments
的 response,以及将更新 UI 的新 HTML fragment 的data
。- 在这种情况下,服务器 "知道" 它的唯一工作是当按钮被按下时发回一些 HTML 并关闭连接。
- HTTP response 看起来像这样:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: close
event: Datastar-merge-fragments
data: fragments <div id="button1">Button Pressed and removed.</div>
在上面的示例中,服务器发回一个新的 HTML fragment,它将替换被单击的按钮。在浏览器中运行的 Datastar JavaScript 将匹配 element 的 ID
,并将其替换为新的 HTML fragment。
服务器可以发送几个 fragments 来更新页面的任何部分。HTMX 可以做到这一点,但我认为 Datastar 是开箱即用地构建来处理这个问题的。
- 举一个例子,在什么情况下 SSE 连接保持打开,并且服务器向下发送对 signals 的更新?
- 想象一下这种情况,你有一个 web 页面,它正在跟踪食品配送车辆的位置。
- 驾驶员在他的手机上安装了一个应用程序,该应用程序向上报告到中央服务器的位置。
- 我可以转到一个 web 页面,它将允许我跟踪驾驶员,直到订单进入 "已送达" 状态。
- 服务器正在监视车辆的 GPS 位置,并向下发送对 signals 的更新,以更新地图上卡车的位置。服务器还可以向下发送更新 UI 的新 HTML fragments。
- 浏览器保持一个 SSE 连接打开,并且服务器可以向下发送更新。
- 来自服务器的 HTTP response 看起来像这样,其中每个
event
和data
对之间都有一些时间间隔。
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
event: Datastar-merge-fragments
data: fragments <div id="truckstatus">The Truck is under a bridge</div>
event: Datastar-merge-fragments
data: fragments <div id="truckstatus">The truck is at Jersey Mikes and the driver is enjoying a sandwich</div>
在 HTMX 中,你必须实现轮询,它可以工作,但不如 SSE 有效。 如果你查看 进度条示例,你可以看到那里有一个像这样的 SEE endpoint:
GET https://data-star.dev/examples/progress_bar/data
它会发送一系列更新到 title 和 id="progress_bar"
的 div
。当浏览器接收到更新时,它会实时更新 UI。浏览器保持一个 SSE 连接打开,并且服务器可以向下发送对 signals 的更新。服务器还可以向下发送更新 UI 的新 HTML fragments。
HTTP/1.1 200 OK
cache-control: no-cache
connection: keep-alive
content-type: text/event-stream
date: Thu, 16 Jan 2025 05:36:26 GMT
fly-request-id: 01JHPSSQHJMTZ82JYZXE5T43BM-sjc
server: Fly/3f202fc64 (2025-01-13)
transfer-encoding: chunked
via: 1.1 fly.io
event: Datastar-merge-fragments
retry: 1000
data: fragments <div id="progress_bar"><svg width="200" height="200" viewbox="-25 -25 250 250" style="transform: rotate(-90deg)"><circle r="90" cx="100" cy="100" fill="transparent" stroke="#e0e0e0" stroke-width="16px" stroke-dasharray="565.48px" stroke-dashoffset="565px"></circle> <circle r="90" cx="100" cy="100" fill="transparent" stroke="#6bdba7" stroke-width="16px" stroke-linecap="round" stroke-dashoffset="559px" stroke-dasharray="565.48px"></circle> <text x="44px" y="115px" fill="#6bdba7" font-size="52px" font-weight="bold" style="transform:rotate(90deg) translate(0px, -196px)">1%</text></svg></div>
event: Datastar-merge-fragments
retry: 1000
data: selector title
data: fragments <title>1%</title>
event: Datastar-merge-fragments
retry: 1000
data: fragments <div id="progress_bar"><svg width="200" height="200" viewbox="-25 -25 250 250" style="transform: rotate(-90deg)"><circle r="90" cx="100" cy="100" fill="transparent" stroke="#e0e0e0" stroke-width="16px" stroke-dasharray="565.48px" stroke-dashoffset="565px"></circle> <circle r="90" cx="100" cy="100" fill="transparent" stroke="#6bdba7" stroke-width="