正如您从标题中猜到的那样,本教程致力于 Mavo:一种新的、易于使用的方法,只需编写HTML和CSS,即可创建复杂、反应式、持久性的Web应用程序,无需编写一行JavaScript代码,也不需要服务器后端。
Mavo 由麻省理工学院计算机科学与人工智能实验室(MIT CSAIL)的 Haystack小组 开发,由 Lea Verou 领导。
我们将一起构建的应用程序是一个供外语学习者使用的抽认卡学习应用程序。它是一个功能齐全的CRUD应用程序,允许您
- 创建、删除、更新抽认卡,并通过拖放重新排列它们。
- 导入和导出抽认卡。
- 评估您对抽认卡上单词的学习情况。
以下是我们完成的应用程序的样子

在本教程中,我将指导您完成构建应用程序的整个过程。
在某些步骤的末尾,我提供了一些建议,供您尝试使用Mavo——了解更多信息——并对我们正在构建的应用程序进行一些增强。
准备好了吗?让我们开始吧!😀
静态模板
查看 CodePen 上 Dmitry Sharabin (@dsharabin) 编写的
01. 静态模板。
在 CodePen 上。
为了说明Mavo如何增强标准HTML,我们将首先创建一个纯静态HTML页面,然后使用Mavo将这个静态HTML转换为一个功能齐全的Web应用程序。
假设我们在<body>
内有以下HTML代码
<header>
<h1>Flashcards</h1>
</header>
<main>
<article>
<p>Word or phrase</p>
<p>Translation</p>
</article>
</main>
在该代码中,<article>
元素表示单个抽认卡。
让我们添加一些样式,使我们的HTML看起来更像一个真正的抽认卡应用程序。您可以查看源代码并在此处进行操作 这里。

Mavo入门
现在,我们只有静态模板。是时候添加功能了,这样它才能真正像抽认卡应用程序一样工作。Mavo来了!
为了使用Mavo,我们首先需要在页面<head>
部分包含其JavaScript和CSS文件
<head>
...
<script src="https://get.mavo.io/mavo.min.js"></script>
<link rel="stylesheet" href="https://get.mavo.io/mavo.css">
...
</head>
也许您需要支持旧版浏览器,或者希望能够读取代码?您可以在此处自定义您正在使用的Mavo的构建和版本,方法是回答几个问题。
定义Mavo应用程序
要在HTML结构上启用Mavo功能,我们必须在包含Mavo应用程序的元素上使用mv-app
属性(甚至可以是<body>
或<html>
元素,这都没问题!)。其值为应用程序的ID,在页面中应唯一。
如果我们使用没有值的mv-app
并且同一元素上没有id
或name
属性,则会自动生成诸如mavo1
、mavo2
等名称。
但是,强烈建议命名您的Mavo应用程序,因为名称在许多地方使用。
考虑到<main>
元素代表我们的Mavo应用程序,让我们向其添加mv-app
属性,并为我们的应用程序指定ID“flashcards”
<main mv-app="flashcards">
...
</main>
property属性
查看 CodePen 上 Dmitry Sharabin (@dsharabin) 编写的
02. property属性。
在 CodePen 上。
是时候告诉Mavo哪些应用程序元素重要,即我们希望哪些元素可编辑并保存。
现在我们有两个这样的元素,它们是<p>
元素。让我们向这些元素添加property
属性,以告诉Mavo它们包含数据。具有property
属性的元素称为属性。
我们可以将property
属性放在任何HTML5元素上,Mavo知道如何使其可编辑。例如,对于<span>
,您编辑其内容,但<time>
允许您通过适当的日期/时间选择器编辑其日期/时间。
您还可以扩展此规则集并通过插件以新的方式(例如,富文本)使元素可编辑。
请记住,property
属性的值应描述元素,类似于id
或class
属性
...
<p property="source">Word or phrase</p>
<p property="translation">Translation</p>
...
如果您已经有充分描述元素的class
、id
或itemprop
属性,则可以使用没有值的property
,例如<p property class="source">
。
您注意到我们应用程序中的任何变化了吗?Mavo工具栏(带有一个编辑按钮)出现在页面顶部。编辑按钮允许用户在阅读和编辑模式之间切换。现在我们的应用程序处于阅读模式。这意味着我们无法编辑页面上的数据。
Mavo工具栏是完全可自定义的,就像Mavo生成的几乎所有UI一样:您可以更改其位置、删除其默认样式、添加自定义按钮元素或使用您自己的HTML元素,等等。
我们将在本教程的后面看到一个这样的自定义示例。
访问Mavo网站的此部分以了解更多信息。
现在让我们通过点击编辑按钮切换到编辑模式。发生了什么变化?编辑按钮的文本变为正在编辑,以指示我们处于编辑模式。如果您将鼠标悬停在段落上,Mavo会通过将它们突出显示为黄色来传达您可以点击以编辑它们的信息。继续吧!点击文本并编辑它。哇!我们可以直接在页面上更改内容!

假设除了单词及其翻译之外,抽认卡还应包含单词在句子中用法的示例。通过向抽认卡添加相应的元素来增强应用程序。
mv-multiple属性
查看 CodePen 上 Dmitry Sharabin (@dsharabin) 编写的
03. mv-multiple属性。
在 CodePen 上。
此时,我们的应用程序中只有一个抽认卡。这不太有用!对于一个有效的抽认卡应用程序,我们需要能够添加、删除和重新排列抽认卡。我们该怎么做?我们可以通过向代码中添加更多<article>
元素来创建更多抽认卡,但然后最终用户如何创建和删除抽认卡呢?
幸运的是,Mavo提供了一些使这变得轻而易举的东西:mv-multiple
属性,它告诉Mavo某些元素可以复制。它将它所使用的元素转换为可编辑的项目集合,并生成(可自定义的)UI用于添加、删除和重新排列项目。
如果在没有property
属性的元素上使用mv-multiple
,Mavo会自动向其添加property="collection"
(或collection2
、collection3
等,以确保名称唯一)。但是,建议也使用property
属性,以命名您的集合并确保在HTML更改时保留其数据。
让我们在我们的应用程序中使用mv-multiple
属性将我们孤独的抽认卡转换为抽认卡集合
<article property="flashcard" mv-multiple>
...
</article>
也可以将属性名称指定为mv-multiple
的值,如下所示:<article mv-multiple="flashcard">
。
mv-multiple
属性位于要复制的元素上,而不是集合的容器上。人们常犯的一个错误是使用<ul mv-multiple>
而不是<li mv-multiple>
,并且通常在检查元素或样式使其变得明显之前,可能会在一段时间内未被发现。
现在将应用程序切换到编辑模式。请注意,在抽认卡下方,现在有一个添加抽认卡按钮。让我们试一试:使用该按钮创建几个抽认卡。现在我们可以在应用程序中动态添加新元素,即使HTML中没有相应的元素。但这还不是全部!
请注意,<article>
上的property
属性实际上并没有使整个<article>
元素可编辑,而是充当分组元素。当您在包含其他属性的元素上使用property
属性时,就会发生这种情况。
尝试将鼠标悬停在抽认卡上,并注意其右上角附近出现的三个按钮,用于通过拖放句柄添加、删除和重新排列元素。并且通过将鼠标悬停在任何项目栏按钮上,我们可以理解它们对应哪个抽认卡:Mavo会突出显示它。难道不神奇吗?
您可以自定义Mavo生成的任何UI元素,例如,您可以通过使用mv-drag-handle
类创建自己的拖放句柄。

Mavo 为集合中的每个项目添加的按钮也支持键盘访问。 甚至重新排序:您可以将焦点放在拖动句柄上并使用方向键移动项目。
mv-storage 属性
查看 CodePen
04. mv-storage 属性 by Dmitry Sharabin (@dsharabin)
在 CodePen 上。
现在我们已经有了基本的UI,让我们尝试以下操作
- 切换到编辑模式(如果您还没有这样做)。
- 编辑第一张抽认卡的源单词和翻译。 也添加几个更多的抽认卡。
- 将应用程序切换回阅读模式。
- 最后……刷新页面。
什么?!我们的数据去哪里了?Mavo 不是应该保存它吗?发生了什么事?
实际上,我们从未告诉 Mavo是否或在哪里存储我们的数据!
为此,我们需要使用mv-storage
属性。 我们有什么选择?好吧,Mavo 为我们打开了巨大的可能性,而Mavo 插件则打开了更多可能性!
在我们的应用程序中,我们将数据存储在浏览器的localStorage
中,这是可用的一些最简单的选项之一,因此它非常适合我们的第一个 Mavo 应用程序。 我们只需要将属性mv-storage
与值local
一起添加到具有mv-app
属性的元素(也称为Mavo 根)中。
<main mv-app="flashcards" mv-storage="local">
...
</main>
看看 Mavo 工具栏。 注意到什么了吗? 另一个按钮出现了——保存按钮。
尝试再次编辑应用程序数据。 请注意,保存按钮现在已突出显示。 将鼠标悬停在保存按钮上,Mavo 将突出显示具有未保存数据的属性。 这不是很酷吗?

单击保存按钮并刷新页面(在刷新页面之前无需切换到阅读模式)。 您的数据还在吗? 太棒了! 我们离目标又近了一步——一个功能齐全的抽认卡应用程序。
mv-autosave 属性
现在每次需要保存数据时,我们都必须单击保存按钮? 这可能更安全,以防止破坏宝贵的数据,但它通常会带来不便。 我们能否自动保存数据? 当然! 为了在每次更改数据时自动保存数据,我们可以在 Mavo 根上使用mv-autosave
属性。 其值是节流保存的时间(秒)。 让我们将mv-autosave="3"
添加到应用程序的根元素中
<main mv-app="flashcard" mv-storage="local" mv-autosave="3">
...
</main>
如果mv-autosave="3"
,Mavo 最多每三秒保存一次。 这对于保存更改历史记录的后端(例如,GitHub、Dropbox)尤其有用,以防止导致该历史记录无用的泛滥。
要禁用节流并立即保存,我们可以使用mv-autosave="0"
或仅使用mv-autosave
,这也会从 UI 中删除保存按钮(因为在这种情况下它没有用处)。
再次更改数据并查看保存按钮。 看到了吗? 一开始它被突出显示,但在 3 秒后——它没有被突出显示。 我们所有的数据现在都自动保存了!
因此,现在我们应用程序的主要部分看起来像这样
<main mv-app="flashcards" mv-storage="local" mv-autosave="3">
<article property="flashcard" mv-multiple>
<p property="source">Word or phrase</p>
<p property="translation">Translation</p>
</article>
</main>
我们几乎完成了应用程序的 alpha 版本。 现在轮到您让应用程序变得更好。 不用担心,您拥有所有必要的知识。
增强应用程序,以便抽认卡可以由最终用户组织到与各种主题相关的不同组中,例如,用户可以将所有与服装相关的抽认卡收集到一个组中,将所有与厨房用具相关的抽认卡收集到另一个组中,等等。
💡 提示!
有很多方法可以实现这个目标,由您决定遵循哪种方法。 但是,在继续之前,我想让您考虑一些问题
- 您将使用哪个 HTML 元素作为分组元素? 如果用户可以看到抽认卡组的名称(主题名称)并可以将组折叠到标题,那将很方便。
- 您将向该元素添加哪些 Mavo 属性(如果有)? 该元素将是属性还是集合?
- 最终用户能否添加新主题、删除和重新排列主题、更改主题标题以及在不同主题之间移动抽认卡?
如果您决定不将抽认卡组织成组,而是仅仅用对应于各种主题的标签标记它们怎么办? 嗯,这完全没问题。 使用标签的解决方案也适用。 为了练习,也尝试完成这种方法。
mv-bar 属性
查看 CodePen
05. mv-bar 属性 by Dmitry Sharabin (@dsharabin)
在 CodePen 上。
由于我们的应用程序在本地存储数据,因此默认情况下,应用程序的用户将无法与其他用户共享他们的卡片。 如果我们让他们导出他们的抽认卡并导入其他人的抽认卡,那不是很好吗? 谢天谢地,这些功能已经在 Mavo 中实现,我们可以非常轻松地将它们添加到我们的应用程序中!
mv-bar
属性控制将显示在工具栏中的按钮(如果有)。 它通常在 Mavo 根(具有mv-app
属性的元素)上指定。 按钮由其 ID(非常逻辑)表示:edit
、import
、export
等。
因为我们只想向默认集添加几个按钮,所以我们可以使用所谓的相对语法,它允许我们向默认集添加和删除按钮,而无需显式列出所有内容。 我们只需要以with
关键字开头mv-bar
属性的值即可。
通过这样做,我们将得到以下内容
<main mv-app="flashcards"
mv-storage="local"
mv-autosave="3"
mv-bar="with import export">
...
</main>
试用一下这些功能:添加一些抽认卡,尝试将它们导出到文件中。 然后删除现有的抽认卡并从之前导出的文件中导入抽认卡。
表达式和 MavoScript
查看 CodePen
06. 表达式和 MavoScript by Dmitry Sharabin (@dsharabin)
在 CodePen 上。
现在让我们为我们的应用程序添加一些统计信息,例如抽认卡的数量! 听起来很有趣吗? 我希望如此。😀
为此,我们需要了解一些关于 Mavo 的新知识。
我们可以动态地引用我们在Mavo 应用程序中定义的任何属性的值(包括 HTML 属性中),方法是在括号内放置其名称,如下所示:[propertyName]
。 这是一个简单表达式的示例,它允许我们动态计算事物,并在它们发生变化时做出反应。
Mavo 的表达式语法称为MavoScript。 它类似于电子表格公式,让我们执行计算和其他操作(使用数字、文本、列表等),但旨在更具可读性并适应嵌套关系。 您可以在文档中详细了解 Mavo 表达式和 MavoScript。
现在让我们进行实验,并在flashcard
属性内部添加[source]
表达式,例如,在两个属性之间:source
和translation
。
...
<p property="source">Word or phrase</p>
[source]
<p property="translation">Translation</p>
...
我们的应用程序发生了什么变化? 抽认卡source
属性的值现在在页面上显示了两次。
切换到编辑模式并尝试更改source
属性的值。 你看到了吗? 在您更改属性值时,页面内容会更新! 这就是我之前说 Mavo 允许我们开发响应式 Web 应用程序的原因。
这确实很酷,但不幸的是,在我们的例子中,它并没有什么用处:我们无法使用此表达式来计算抽认卡的数量——我们总是只会得到一个值。
如果我们将[source]
表达式放在flashcard
属性外部会怎样? 我们将得到类似这样的东西
...
[source]
<article property="flashcard" mv-multiple>
...
</article>
...
这与之前的情况有什么不同? 若要查看差异,请添加一些抽认卡(如果您还没有添加)。 现在,我们不再是一个值,而是一个逗号分隔值的列表:所有抽认卡的source
属性。 这正是我们正在寻找的:列表中项目的数量对应于应用程序中抽认卡的数量。
有道理吗? 嗯,是的,但如果我们计算抽认卡的数量,而不是其source
属性的值的数量,会不会更有逻辑? 毕竟,即使在我们填写其来源或翻译之前,添加的抽认卡也存在。 我建议您执行以下操作:让我们用[flashcard]
替换[source]
表达式
...
[flashcard]
<article property="flashcard" mv-multiple>
...
</article>
...
注意到区别了吗? 我们仍然有一个列表,但其值不是简单值,而是对象,即包含与每个抽认卡相关的所有数据的复杂值。 好消息是这些对象的数量等于抽认卡的数量,因为每个抽认卡都有一个,即使它完全为空。 因此,现在我们每个抽认卡都有一个对象,但我们如何计算它们并显示总数呢?
现在让我们熟悉MavoScript 函数,并找到可以让我们计算抽认卡数量的函数。 请记住,我们有一个抽认卡的列表,因此我们需要找到一个可以让我们计算列表中项目数量的函数。 这里就是——count()
函数正是这样做的!
但是我们如何在表达式中使用函数呢?是否有我们需要知道的规则?当然,有一些。
- 表达式用括号表示。
- 不要嵌套括号。
让我们尝试使用count()
函数来计算闪卡的数量。
...
[count(flashcard)]
<article property="flashcard" mv-multiple>
...
</article>
...
这正是我们想要达到的目标——现在我们的应用中有一些统计数据了!是不是很酷?

我希望您已经做好准备,可以继续尝试使用Mavo。
改进应用程序,以便统计数据不仅显示应用程序中闪卡的总数,而且还显示每个主题中闪卡的数量(如果有主题)。
💡提示!
想要根据某些条件过滤列表?where
运算符将提供帮助。
自我评估功能
我们已经有一个应用程序,可以让我们创建、编辑和存储多个闪卡。但是我们如何跟踪哪些闪卡我们已经学习过,哪些闪卡我们需要更多练习呢?任何值得尊敬的闪卡应用程序都需要一个自我评估功能。让我们研究一下如何添加它!
假设在我们的应用程序中,我们有两个用于自我评估的按钮:Bad
和 Good
。每次最终用户点击按钮时,我们到底希望发生什么?嗯,这个想法相当简单。
- 点击
Bad
按钮表示用户尚未学习该单词,我们希望我们的应用程序将相应的闪卡移动到列表的开头,以便他们可以在启动应用程序后尽快看到它。 - 点击
Good
按钮表示用户已经学习了该单词,相应的闪卡需要移动到列表的末尾,让他们使用其他尚未学习的闪卡。
“您确定我们可以在没有 JavaScript 的情况下做到这一点吗?” 您可能会问。是的!Mavo 非常强大,能够为我们提供所需的所有工具!
现在我们知道了要实现什么,让我们首先设置 UI,然后继续下一步。我们的标记看起来像这样
...
<article property="flashcard" mv-multiple>
...
<section>
<h2>Evaluate Yourself</h2>
<button>Bad</button>
<button>Good</button>
</section>
</article>
...

mv-action 属性
查看 Dmitry Sharabin 的 CodePen
07. mv-action 属性 (@dsharabin)
在 CodePen 上。
Mavo 操作允许我们创建我们自己的控件,当用户与它们交互时,以自定义方式修改数据。听起来很有希望,对吧?
要定义自定义操作,我们需要在 Mavo 应用程序内的适当元素上使用mv-action
属性。每次点击该元素时都会执行该操作。这正是我们想要的。
对于<form>
元素,当表单提交时会执行自定义操作。
mv-action
属性的值是一个表达式。我们可以使用 MavoScript 提供给我们的任何表达式函数和语法,以及一些其他函数来促进数据操作,例如add()
、set()
、move()
或delete()
。需要注意的是,与以响应方式计算的普通表达式不同,这些表达式仅在每次触发操作时计算。
Mavo 期望mv-action
属性的值为表达式,因此**不需要用括号括起来**:mv-action="expression"
。此外,如果我们包含它们,它们将被视为表达式的一部分。
因此,我们需要在集合中移动闪卡,Mavo 有一个合适的函数可以让我们做到这一点——move()
函数。它的第一个参数指的是我们要移动的项目,第二个参数是它在集合中的位置。请记住,集合的元素从0开始编号。
想要了解更多关于move
函数(及其变体)以及自定义操作的信息,请参阅文档。
让我们实现我们之前讨论的大纲的第一点:在进行自我评估时,最终用户点击Bad
按钮,相应的闪卡移动到集合的开头,即成为第一个。所以在代码中,我们有
...
<article property="flashcard" mv-multiple>
...
<button mv-action="move(flashcard, 0)">Bad</button>
...
</article>
...
请注意,在mv-action
属性中,我们引用了属性内部的flashcard
属性,因为我们希望处理当前的闪卡。
如果我们尝试实现大纲的第二点,我们将面临一个问题。您能否建议具体是什么问题?
让我们记住,如果最终用户点击Good
按钮,相应的闪卡将移动到集合的末尾,即成为最后一个。要使闪卡成为集合中的最后一个,我们需要知道集合中的项目数量。
谢天谢地,我们之前已经解决了这个任务并实现了相应的功能。但是我们能否使用该解决方案来解决我们当前的问题?不幸的是,我们不能:正如我们所知,我们只能在闪卡属性外部通过其名称引用闪卡的集合(并评估其大小)。但在我们的例子中,我们需要在内部进行操作:我们需要为其编写表达式的Good
按钮位于flashcard
属性内部。
那我们该怎么办?我很高兴您提出这个问题。Mavo 有解决方案。
使用 meta 元素保存中间值
查看 Dmitry Sharabin 的 CodePen
08. <meta> 元素 (@dsharabin)
在 CodePen 上。
因此,一方面,我们知道[count(flashcards)]
表达式如果在闪卡属性外部计算,则会给出闪卡的数量。另一方面,我们需要在flashcard
属性内部使用该值。
为了解决这个难题,我们需要在flashcard
属性外部计算闪卡的数量,并以某种方式保存结果以便能够在应用程序的其他地方使用它,准确地说是在flashcard
属性内部。对于这种情况,在 Mavo 中,有所谓的计算属性。
为了保存中间结果以便我们可以引用它,我们需要在代码中使用一个 HTML 元素。建议为此目的使用<meta>
元素,如下所示:<meta property="propertyName" content="[expression]">
。使用此元素的优点是它在编辑模式之外是隐藏的,在语义上和视觉上都是。
请记住,默认情况下不会保存计算属性。
现在让我们在我们的应用程序中添加flashcardCount
计算属性。请记住,我们必须将其放在flashcard
属性外部,但之后我们可以从任何地方引用它。
...
<meta property="flashcardCount" content="[count(flashcard)]">
<article property="flashcard" mv-multiple>
...
</article>
...
只差一步就可以完成自我评估功能的实现:如果最终用户点击Good
按钮,相应的闪卡将移动到集合的末尾,即成为最后一个。让我们在应用程序的代码中添加相关的操作。
...
<meta property="flashcardCount" content="[count(flashcard)]">
<article property="flashcard" mv-multiple>
...
<button mv-action="move(flashcard, flashcardCount)">Good</button>
</article>
...
我们完成了!恭喜!😀
还有另一种解决此任务的方法:借助$all
特殊属性。如果$all
属性位于集合内部,则它表示集合本身。因此,在这种情况下不需要使用任何计算属性。尝试自己实现该解决方案。
只剩下最后一件事需要我们修复。还记得我们在应用程序中添加一些统计数据的部分吗?还记得我们构建用于评估应用程序中闪卡数量的表达式:[count(flashcard)]
吗?相反,我们现在可以(也应该)使用我们定义的计算属性。对应用程序进行相应的更改。
要点
那么到目前为止我们学到了什么?让我们回顾一下。为了将任何静态 HTML 页面转换为 Mavo 应用程序,我们需要
- 在页面的
<head>
部分包含 Mavo JavaScript 和 CSS 文件。 - 将
mv-app
属性添加到 Mavo 根元素。 - 通过向它们添加
property
属性来告诉 Mavo 应用程序的哪些元素很重要。 - 将
mv-multiple
属性放在将被复制并转换为集合的元素上。 - 通过向 Mavo 根添加
mv-storage
属性来告诉 Mavo 在哪里存储我们的数据。 - 决定 Mavo 是否应该自动保存我们的数据。如果是,则向 Mavo 根添加
mv-autosave
属性。我们还知道
- Mavo 工具栏是完全可自定义的。
mv-bar
属性控制哪些按钮将出现在那里。 - 表达式允许我们在其他元素中显示属性的当前值并执行计算。表达式的值(和类型)取决于表达式在代码中所处的位置。Mavo 的表达式语法称为 MavoScript。
- 自定义操作允许创建以自定义方式修改数据的控件。要定义自定义操作,请在 Mavo 应用程序内的适当元素上设置
mv-action
属性。 - 值为表达式的属性称为计算属性。为了保存中间结果以便能够在应用程序的其他地方引用它,建议使用
<meta>
元素。
结语替代
所以我们构建了我们的应用。它已经是完美的了吗?当然不是,没有什么东西是完美的!还有很多东西可以改进,还有很多功能可以添加(借助 Mavo,我们甚至可以使我们的应用多语言化!)。继续,进一步增强它,不要犹豫尝试一些新东西!
到目前为止,我们了解到的关于 Mavo 的知识仅仅是冰山一角,还有很多东西需要学习。我鼓励你仔细研究一下,阅读文档,查看示例(在Mavo 网站上,或在 CodePen 上:由Lea Verou制作和一些由我自己制作的),并创建新的东西!祝你好运!😉
致谢
我要感谢两位很棒的人。首先,我要衷心感谢Lea Verou,她不仅激励我撰写本教程(并帮助我实现了它),而且一直激励着我,她让网页开发的世界变得更美好。我从未见过如此有天赋的人,我很高兴有机会和她一起做一些事情!
我也要感谢James Moore。他在 Udemy 上的课程“JavaScript 初学者函数式编程”中使用的示例促使我制作了自己的抽认卡学习应用版本。他是一位很棒的老师!