发布于 2026-01-06 0 阅读
0

Marko:设计 UI 语言 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

Marko:设计用户界面语言

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

FLUURT 是为 Marko UI 框架构建的新编译器/运行时的代号。介绍性文章已经概述了它的主要特性以及它可能对您有价值的原因。

本文将详细探讨FLUURT全新标签原语语法的设计决策过程,该语法为FLUURT高度灵活的响应式组合提供了强大支持。某些语法乍看之下可能有些不寻常,但正如您将看到的,它们至关重要。它对开发者体验的影响与库的优化能力同样深远。

目前仍处于早期开发阶段,可能并非最终版本。我们仍然欢迎大家的建议和反馈。


基础

Marko 一直将自身视为 HTML 的超集。它最初是作为一种模板语言开发的,随着发展不断添加更高级的功能。这意味着许多强大的概念可以直接在标记语言中表达,但也意味着存在一些不一致之处和特殊规则。

我们很早就意识到,如果想让最终用户充分利用这门语言的全部功能,就必须解决这些问题。因此,探索工作从基础的 HTML 语义开始。

标签

Marko 中的大多数元素都是标签。我们支持原生内置标签,例如 `<script>`<div>和 ` <script> <form>`。此外,我们还有一些 Marko 特有的标签,例如<for>用于迭代的 `<script>`、<if>用于条件渲染的 `<script>` 和<await>用于异步渲染的 `<script>`。另外,我们也支持自定义标签,例如 `<script>` <my-tag>,它可以加载自定义组件。这些组件是用户定义的模板,类似于顶级应用程序模板,但它们可以在整个应用程序中重复使用,并由 Marko 运行时进行管理。

属性

属性是标签的修饰符,用于提供输入配置。Marko 扩展了 HTML 的标签概念,也扩展了属性。除了字符串之外,Marko 还支持分配给属性的 JavaScript 表达式。

替代文字

构建一种语言

单凭这一点,就足以构成一种强大的应用模板化方式。我们可以将代码作为组件复用,并传递动态数据。然而,HTML 缺少一些构建核心语言所需的其他功能。

我们真正需要做的是将函数调用语义引入 HTML 标签。Marko 长期以来一直在朝着这个方向努力,但我们现在才真正实现它。我们已经有了作为输入的属性,但我们需要简化其余的用户体验。

标签变量(返回值)

模板中的标签会创建 DOM 节点,但目前我们只能向其中传递值。如何从标签中获取值呢?

我们可以绑定事件。我们可以向下传递一些数据,供子节点调用或使用其值进行扩展。然而,对于 DOM 节点引用,或者任何你想传递的数据,我们认为内置此功能非常重要。以下是一些潜在的使用示例:

替代文字

为什么要用斜杠?Marko 的简写语法已经使用了很多符号。我们知道我们需要一个单独的结束符号。冒号原本:似乎是最显而易见的选择,但考虑到我们即将推出的 TypeScript 支持,情况就不同了。

替代文字

现在,我们可以像大多数库一样,使用重载属性来处理这种情况。但是,我们更倾向于使用清晰的语法,因为它简洁明了,而且正如您将看到的,这种语法将用于多个标签。

最后一点是理解作用域的工作原理。我们决定对变量使用标签作用域,也就是说,它们对同级变量及其所有后代变量都是可见的。如果需要将变量提升到更高层级,则需要使用单独的声明标签(稍后会详细介绍)。

标签参数(回调/渲染属性)

虽然可以将函数传递给标签,但我们需要一种方法来处理子元素的渲染。Marko 和大多数模板 DSL 都明确区分了数据和渲染。Marko 没有引入特殊的控制流,而是提供了一种机制,允许组件调用其子模板并传递数据。

这种现象在控制流程组件中很常见。
替代文字

在这个例子中,`{{ value}} item` 和 ` index{{value}}` 由父组件提供,并且仅供子组件使用。这与标签变量不同,标签变量会暴露给所有同级组件。这一点很重要,因为子组件可能会多次渲染,每次渲染时都会有不同的值。

默认属性

我们最后意识到的这一点,可能看起来更像是一种语法糖,而不是真正的语法改进。但为了简洁起见,有时最好只传递一个未命名的参数。有时你并不需要一堆命名属性。我们建议使用赋值给标签的方式来实现这一点:

替代文字

然而,这种小小的便利却开启了无限可能。

如果您熟悉 Marko,可能听说过标签参数,它们用 `<tag>` 表示( )。问题在于,它们与典型的属性产生了一种奇怪的冲突,并引入了一种只能在 Marko 内置的 flow 标签中使用的新语法。而 `default` 属性则可以被任何标签利用。


构建我们的原始

通过这些新增功能,我们现在能够描述许多仅靠简单的 HTML 无法实现的概念。其核心在于能够创建用于状态管理的基本元素。虽然这看起来有点像 HTML 中的 JSX,但实际上我们的限制要严格得多。我们只允许声明式语句。即便如此,我们仍然具备实现目标所需的灵活性。

标签<let>

我们决定以一种 JavaScript 开发人员熟悉的方式来建模库中的核心状态原子。let这是一种在 JavaScript 中定义可变值的方法,代表了我们的核心响应式原子。

替代文字

细心的人会注意到,这些实际上是使用了默认属性的标签变量。您将初始值传递给<let>标签,并返回指定的变量名。

然后,这些变量可以按预期在事件处理程序中使用,或者作为其他基本元素定义的一部分。

标签<const>

与 JavaScript 类似,`<div>`标签<const>代表无法重新绑定的内容。在我们的例子中,这些内容指的是静态值和动态表达式。它们在我们的模板环境中充当固定不变的真值。我们可以根据使用情况对这些情况进行静态分析,以确保最终用户无需担心哪些内容会更新。

替代文字

可能需要一些时间来适应doubleCount这个例子中的更新。然而,它的一致性在于它与目标之间的关系始终count不变。

标签<effect>

最后一个核心标签原语是 effect 标签。这是库用来产生副作用的机制。我们同样使用了 default 参数。

替代文字

Marko 的效果会自动跟踪响应式依赖项,仅在受影响的状态更新时才进行更新。因此,我们也提出了一种<mount>标签,它不跟踪依赖项,仅在模板的相应部分挂载时运行,并在移除时进行清理。


整合

这种方法最大的优势在于其极强的可扩展性。当你编写自己的行为时,你的用户可以使用完全相同的 API。

替代文字

基本上,你的想象力就是极限。

Marko 还有其他一些语法我没有详细介绍,其中最重要的是它的动态组件和子模板渲染机制。这些对于编写自定义标签非常重要,但超出了本文的讨论范围。

相反,我想从消费者的角度来探讨这对开发意味着什么。最终,我们将获得网页创作的“所见即所得”体验。在 Marko 中,组件导入会被自动检测。通过将状态置于模板级别,组件组合就变成了一种层级式的考量。

那么,我们来做个比较吧。我将使用 Fluurt 的新语法,并将其与 Svelte、React Hooks 和 React Classes 进行比较。考虑一个组件,它封装了一个从 CDN 加载到页面上的第三方图表 (1):
步骤 1

现在我们需要添加一个新的输入来显示和隐藏这个图表。我们可以简单地将其包裹在一个<if>标签中,生命周期(包括处置)都会自动正确处理(2):
步骤 2

如果以后我们想把它拆分成单独的组件,我们可以把代码剪切粘贴到一个新文件中,把输入传递给标签,它就能立即工作(3):
步骤 3

这是一个简单的示例,但代码就这些。我移动它的时候甚至不需要做任何修改。最重要的是,随着组件变得越来越复杂,这种共置模式也能以同样的方式扩展。


结论

正如你所见,设计一门语言需要考虑很多方面。它可能始于确定正确的语法,但也会延伸到理解语法和语义的含义。我们的目标是在可扩展性方面保持一致性,避免需要$在不同地方指定使用方式和函数调用方式。

我们相信这对于构建我们想要打造的高效生产环境至关重要。减少代码量不仅仅是统计 Git 提交中的代码行数,而是真正减少代码量。这些模式不仅能减少初始代码量,还能降低重构的开销。

我建议您尝试使用您选择的库,完成上一节中的三步示例。您需要在多个地方应用第二步中的条件语句。您需要重构代码,将其移至不同的文件中。此外,还需要添加额外的代码块包装器和导入语句。

这些都是设计编程语言时需要考虑的因素。它超越了技术实现或字符数的限制,归根结底在于我们如何有效地组织思路和传达意图。更重要的是,在编程中,要认识到它代表着一份鲜活的文档,一场持续的对话,可以由一个人或多人进行。


标签 API 概述:


请访问Marko 的 Github 页面关注我们的 Twitter 账号加入我们的 Discord 服务器,以获取最新动态。

文章来源:https://dev.to/ryansolid/marko-designing-a-ui-language-2hni