Ractive.js - Web 应用程序变得简单
已发表: 2022-03-11在当今 JavaScript 框架和库迅速增长的环境中,选择您想要开发的框架和库可能是一个相当大的挑战。 毕竟,一旦你走上了使用特定框架的道路,将代码迁移到使用不同的框架是一项非常重要的任务,你可能永远没有时间或预算来执行。
那么,为什么选择 Ractive.js?
与其他生成惰性 HTML 的工具不同,Ractive 将模板转换为默认交互应用程序的蓝图。 尽管人们可以肯定地说 Ractive 的贡献是进化而不是革命,但它的价值仍然很重要。
Ractive 之所以如此有用,是因为它提供了强大的功能,而且以一种对开发人员来说非常简单易用的方式来实现。 此外,它相当优雅、快速、不显眼且体积小。
在本文中,我们将介绍构建一个简单的 Ractive 搜索应用程序的过程,展示 Ractive 的一些关键功能以及它帮助简化 Web 应用程序和开发的方式。
什么是 Ractive.js?
Ractive 最初是为了以更优雅的方式解决数据绑定问题而创建的。 为此,它采用模板并将它们转换为 DOM 的轻量级虚拟表示,以便当数据发生变化时,真实的 DOM 会智能且有效地更新。
但很快就发现,Ractive 采用的方法和基础设施也可以用来更有效地做其他事情。 例如,它可以自动处理诸如重用事件处理程序之类的事情,并在不再需要它们时自动解除绑定。 事件委托变得不必要了。 与数据绑定一样,这种方法可以防止代码随着应用程序的增长而变得笨拙。
开箱即用地提供了双向绑定、动画和 SVG 支持等关键功能,并且可以通过插件轻松添加自定义功能。
一些工具和框架会迫使你学习新的词汇并以特定的方式构建你的应用程序,而 Ractive 对你有用,而不是相反。 它还与其他库很好地集成。
我们的示例应用程序
我们的示例应用程序将用于根据技能搜索 Toptal 开发人员数据库。 我们的应用程序将有两个视图:
- 搜索:内嵌搜索框的技能列表
- 结果:技能视图,包括开发人员列表
对于每个开发者,我们将显示他们的姓名、照片、简短描述和技能列表(每个技能将链接到相应的技能视图)。
(注意:本文末尾提供了应用程序的在线工作实例和源代码存储库的链接。)
为了保持我们对 Ractive 框架的主要关注,我们将采用一些通常不应该在生产中进行的简化:
- 默认主题。 我们将使用带有默认主题的 Bootstrap 进行样式设置,而不是自定义主题以适合您的应用程序样式。
- 依赖关系。 我们将添加我们的依赖项作为定义全局变量的单独脚本(而不是使用 ES6 模块、CommonJS 或 AMD 以及适当的加载器进行开发,并使用构建步骤进行生产)。
- 静态数据。 我们将使用我通过抓取 Toptal 网站上的公共页面准备的静态数据。
- 没有客户端路由。 这意味着当我们在视图之间切换时,URL 将保持不变。 您绝对不应该对 SPA 这样做,尽管对于一些小型交互式组件可能没问题。 Ractive 没有内置的路由器实现,但它可以与 3rd 方路由器一起使用,如本例所示。
- 在 HTML 中的 script 标签内定义的模板。 这不一定是一个坏主意,尤其是对于小型应用程序,它有一些优点(它很简单,您可以将这些客户端模板与服务器端模板一起处理,例如用于国际化)。 但是对于更大的应用程序,您可以从预编译(也就是将模板预解析为内部 JS 表示)中受益。
让我们开始使用 Web 应用程序
好的,话虽如此,让我们开始构建应用程序。 我们将以迭代的方式进行,逐个添加较小的特征,并在遇到它们时探索概念。
让我们首先创建一个包含两个文件的文件夹: index.html
和script.js
。 我们的应用程序将非常简单,并且将使用file://
协议来避免启动开发服务器的需要(尽管如果您愿意,您也可以这样做)。
搜索页面
我们将从实现搜索页面开始,用户可以在其中选择一项技能,以便在 Toptal 数据库中找到匹配的开发人员。
HTML 骨架
让我们从这个简单的 HTML 页面开始:
<html> <head> <title>Toptal Search</title> <!-- LOAD BOOTSTRAP FROM THE CDN --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> </head> <body> <!-- SOME BASIC STATIC CONTENT --> <div class="container"> <h1>Toptal Search</h1> </div> <!-- LOAD THE JAVASCRIPT LIBRARIES WE NEED --> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.9.3/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ractive/0.7.3/ractive.min.js"></script> <!-- LOAD THE DATA --> <script src="https://rawgit.com/emirotin/toptal-blog-ractive/master/data.js"></script> <!-- LOAD OUR SCRIPT --> <script src="script.js"></script> </body> </html>
如您所见,这是一个简单的 HTML5 文档。 它从 CDN、Lodash(一个很棒的数据操作库)和 Ractive.js 加载 Bootstrap。
Ractive 不需要将整个页面都用于 SPA,因此我们可以拥有一些静态内容。 在我们的例子中,它由一个容器元素和页面标题组成。
最后,我们加载我为演示准备的数据,以及包含我们程序的脚本。
好的,现在我们的 HTML 框架已经到位,让我们开始添加一些真正的功能。
技能清单
我特别喜欢 Ractive 的一件事是它如何教你思考你想要实现的最终表示(HTML),然后让你专注于编写必要的代码来实现它。
所以首先,让我们建立一个技能列表作为我们的初始视图。 这样做只需要:
- 添加将显示技能列表的 HTML 元素。
- 将一小段模板代码添加到我们的 HTML 中。
- 编写一些简短的 JavaScript 来为模板提供数据,以在我们添加的 HTML 元素中呈现它。
HTML 的 mods 包括以下内容:
<div class="container"> <h1>Toptal Search</h1> <div></div> <!-- THIS IS THE NEW HTML ELEMENT --> </div> <!-- THIS IS THE SMALL SNIPPET OF TEMPLATE CODE --> <script type="text/html"> <div class="row"> {{#each skills}} <span class="col-xs-3"> <a href="#" class="label label-primary">{{this}}</a> </span> {{/each}} </div> </script>
Ractive 没有特殊的约定来指定 HTML 元素来接收要显示的数据,但最简单的方法是为元素添加一个 ID。 为此,我通常使用“根”ID。 我们很快就会看到它在 Ractive 初始化时是如何使用的。 对于那些好奇的人,还有其他方法可以指定根元素。
带有type="text/html"
的略显尴尬的 script 元素是一个巧妙的技巧,它可以让浏览器加载一大段 HTML,而无需对其进行解析或渲染,因为浏览器会忽略未知类型的脚本。 脚本的内容是一个类似 Mustache/Handlebars 的模板(尽管 Ractive 确实有一些扩展)。
我们首先编写模板代码,假设我们可以访问技能数组。 我们使用{{#each}}
mustache 指令来定义迭代。 在指令内部,可以通过this
访问当前元素。 同样,我们假设技能变量包含一个字符串数组,因此我们只需使用插值胡须{{this}}
对其进行渲染。
好的,这就是 HTML。 但是 JavaScript 呢? 这就是将数据提供给模板的“魔法”发生的地方:
(function () { var skills = DB.skills, developers = DB.developers; var app = new Ractive({ el: '#root', template: '#tpl-app', data: { skills: _.keys(DB.skills) } }); }());
令人印象深刻,不是吗? 仅用这 10 行代码,我们就能够:
- 从“数据库”中“检索”数据。
- 实例化一个新的 Ractive 应用。
- 告诉它在元素内部渲染
.
- 告诉它使用脚本元素
获取模板(还有其他方法可以这样做)。
- 传递初始数据(我们将看到如何在运行时更改它),或者如果您习惯于 Angular 术语,则传递“范围”。
- 使用 lodash
keys
方法获取我们在“DB”中用作对象键的技能名称。
所以基本上使用这个脚本,我们告诉框架要做什么,而不是如何去做。 模板声明了如何,我个人觉得这非常神奇和自然。
希望你开始明白这个想法,所以现在让我们在现有的基础上实现一些更有用的东西。
技能搜索
当然,搜索页面需要一个搜索字段。 我们希望它提供一种交互功能,当用户在搜索框中键入时,技能列表会被过滤掉,只包括那些包含输入的子字符串的技能(过滤不区分大小写)。
与 Ractive 一样,我们从定义模板开始(同时考虑需要哪些新的上下文变量,以及在数据管理方面会发生什么变化):
<div class="container"> <h1>Toptal Search</h1> <div></div> </div> <!-- THIS IS THE SMALL SNIPPET OF TEMPLATE CODE --> <script type="text/html"> <!-- HERE'S OUR SEARCH BOX --> <div class="row"> <form class="form-horizontal col-xs-6 col-xs-offset-6"> <input type="search" class="form-control" value="{{ skillFilter }}" placeholder="Type part of the skill name here"> </form> </div> <hr> <!-- NOW INSTEAD OF DISPLAYING ALL SKILLS, WE INVOKE A TO-BE-CREATED JAVASCRIPT skills() FUNCTION THAT WILL FILTER THE SKILL LIST DOWN TO THOSE THAT MATCH THE TEXT ENTERED BY THE USER --> <div class="row"> {{#each skills()}} <span class="col-xs-3"> <a href="#" class="label label-primary">{{this}}</a> </span> {{/each}} </div> </script>
没有太多变化,但仍有相当多的东西需要学习。
首先,我们添加了一个包含搜索框的新 <div>。 很明显,我们希望将该输入连接到某个变量(除非您怀念 jQuery 汤的美好时光)。 Ractive 支持所谓的双向绑定,这意味着您的 JS 代码可以检索一个值,而无需手动从 DOM 中读取它。 在我们的例子中,这是通过使用插值 mustache value="{{ skillFilter }}"
来完成的。 Ractive 理解我们想要将此变量绑定到输入的 value 属性。 所以它会为我们观察输入并自动更新变量。 非常简洁,只有 1 行 HTML。
其次,正如上面代码片段中的注释中所解释的,现在我们将创建一个skills()
JS 函数来过滤技能列表并仅返回与用户输入的文本匹配的技能,而不是显示所有技能:
// store skill list in a variable outside of Ractive scope var skillNames = _.keys(DB.skills); var app = new Ractive({ el: '#root', template: '#tpl-app', data: { // initializing the context variable is not strictly // required, but it is generally considered good practice skillFilter: null, // Define the skills() function right in our data object. // Function is available to our template where we call it. skills: function() { // Get the skillFilter variable from the Ractive instance // (available as 'this'). // NOTE WELL: Our use of a getter here tells Ractive that // our function has a *dependency* on the skillFilter // value, so this is significant. var skillFilter = this.get('skillFilter'); if (!skillFilter) { return skillNames; } skillFilter = new RegExp(_.escapeRegExp(skillFilter), 'i') return _.filter(skillNames, function(skill) { return skill.match(skillFilter); }); } } });
虽然这很干净且易于实现,但您可能想知道它如何影响性能。 我的意思是,我们每次都要调用一个函数? 那么,每次什么? Ractive 足够聪明,只在它们的依赖项(变量)发生变化时重新渲染模板的一部分(并从中调用任何函数)(由于使用了 setter,Ractive 知道何时发生这种情况)。
顺便说一句,对于那些有兴趣更进一步的人,还有一种更优雅的方法可以使用计算属性来实现这一点,但如果你愿意,我会留给你自己玩。
结果页面
现在我们有了一个非常有用的可搜索技能列表,让我们继续查看结果视图,其中将显示匹配的开发人员列表。
在技能视图之间切换
显然有多种方法可以实现这一点。 根据是否选择了技能,我选择了两种不同视图的方法。 如果是这样,我们会显示匹配的开发人员列表; 如果没有,我们会显示技能列表和搜索框。
因此,对于初学者来说,当用户选择(即单击)技能名称时,需要隐藏技能列表,而应将技能名称显示为页面标题。 相反,在选定的技能视图上,需要有一种方法可以关闭该视图并返回技能列表。
这是我们在这条道路上的第一步:
<script type="text/html"> <!-- PARTIAL IS A NEW CONCEPT HERE --> {{#partial skillsList}} <div class="row"> <form class="form-horizontal col-xs-6 col-xs-offset-6"> <input type="search" class="form-control" value="{{ skillFilter }}" placeholder="Type part of the skill name here"> </form> </div> <hr> <div class="row"> {{#each skills()}} <span class="col-xs-3"> <!-- MAKE OUR SKILLS CLICKABLE, USING PROXY EVENTS --> <a href="#" class="label label-primary" on-click="select-skill:{{this}}">{{this}}</a> </span> {{/each}} </div> {{/partial}} {{#partial skillView}} <h2> <!-- DISPLAY SELECTED SKILL AS HEADING ON THE PAGE --> {{ currentSkill }} <!-- CLOSE BUTTON TAKES USER BACK TO SKILLS LIST --> <button type="button" class="close pull-right" on-click="deselect-skill">× CLOSE</button> </h2> {{/partial}} <!-- PARTIALS ARE NOT IN THE VIEW UNTIL WE EXPLICITLY INCLUDE THEM, SO INCLUDE THE PARTIAL RELEVANT TO THE CURRENT VIEW. --> {{#if currentSkill}} {{> skillView}} {{else}} {{> skillsList}} {{/if}} </script>
好的,这里发生了很多事情。

首先,为了适应将其实现为两个不同的视图,我将我们目前拥有的所有内容(即列表视图)移到了一个叫做局部视图的东西上。 部分本质上是一块模板代码,我们将在不同的地方(很快)包含。
然后,我们想让我们的技能可以点击,当它被点击时,我们想要导航到相应的技能视图。 为此,我们使用一种称为代理事件的东西,借此我们对物理事件(点击时,名称是 Ractive 可以理解的名称)做出反应,并将其代理到逻辑事件(选择技能,名称就是我们所说的) ) 传递参数(你可能还记得这代表这里的技能名称)。
(仅供参考,存在另一种方法调用语法来完成同样的事情。)
接下来,我们(再次)假设我们将有一个名为currentSkill
的变量,该变量将具有所选技能的名称(如果有),或者如果没有选择任何技能则为空。 所以我们定义了另一个显示当前技能名称的部分,还有一个“关闭”链接应该取消选择技能。
对于 JavaScript,主要添加的是订阅 select-skill 和 deselect-skill 事件的代码,相应地更新currentSkill
(和skillFilter
):
var app = new Ractive({ el: '#root', template: '#tpl-app', data: { skillFilter: null, currentSkill: null, // INITIALIZE currentSkill TO null // skills function remains unchanged skills: function() { var skillFilter = this.get('skillFilter'); if (!skillFilter) { return skillNames; } skillFilter = new RegExp(_.escapeRegExp(skillFilter), 'i') return _.filter(skillNames, function(skill) { return skill.match(skillFilter); }); } } }); // SUBSCRIBE TO LOGICAL EVENT select-skill app.on('select-skill', function(event, skill) { this.set({ // SET currentSkill TO THE SKILL SELECTED BY THE USER currentSkill: skill, // RESET THE SEARCH FILTER skillFilter: null }); }); // SUBSCRIBE TO LOGICAL EVENT deselect-skill app.on('deselect-skill', function(event) { this.set('currentSkill', null); // CLEAR currentSkill });
列出每种技能的开发人员
为技能准备好新视图后,我们现在可以添加一些内容——我们为该列表拥有的实际开发人员列表。 为此,我们将此视图的部分扩展如下:
{{#partial skillView}} <h2> {{ currentSkill }} <button type="button" class="close pull-right" on-click="deselect-skill">× CLOSE</button> </h2> {{#each skillDevelopers(currentSkill)}} <div class="panel panel-default"> <div class="panel-body"> {{ this.name }} </div> </div> {{/each}} {{/partial}}
希望到此为止,您已经对这里发生的事情有所了解:我们在skillView
部分中添加了一个新的迭代部分,它迭代了我们接下来要编写的新skillDevelopers
函数的结果。 对于数组中的每个开发人员(由该skillDevelopers
函数返回),我们渲染一个面板并显示开发人员的姓名。 请注意,我可以在这里使用隐式形式{{name}}
并且 Ractive 会通过从当前上下文(在我们的例子中是由{{#each}}
绑定的开发人员对象)开始搜索上下文链来找到正确的属性,但是我更喜欢明确。 Ractive 文档中提供了有关上下文和引用的更多信息。
这是skillDevelopers()
函数的实现:
skillDevelopers: function(skill) { // GET THE SKILL OBJECT FROM THE “DB” skill = skills[skill]; // SAFETY CHECK, RETURN EARLY IN CASE OF UNKNOWN SKILL NAME if (!skill) { return; } // MAP THE DEVELOPER'S IDs (SLUGS) TO THE // ACTUAL DEVELOPER DETAIL OBJECTS return _.map(skill.developers, function(slug) { return developers[slug]; }); }
扩展每个开发人员的入口
现在我们有了一个开发人员的工作列表,是时候添加更多细节了,当然还有一张漂亮的照片:
{{#partial skillView}} <h2> {{ currentSkill }} <button type="button" class="close pull-right" on-click="deselect-skill">× CLOSE</button> </h2> {{#each skillDevelopers(currentSkill)}} <div class="panel panel-default"> <div class="panel-body media"> <div class="media-left"> <!-- ADD THE PHOTO --> <img class="media-object img-circle" width="64" height="64" src="{{ this.photo }}" alt="{{ this.name }}"> </div> <div class="media-body"> <!-- MAKE THE DEVELOPER'S NAME A HYPERLINK TO THEIR PROFILE --> <a class="h4 media-heading" href="{{ this.url }}" target="_blank"> {{ this.name }}</a> <!-- ADD MORE DETAILS (FROM THEIR PROFILE) --> <p>{{ this.desc }}</p> </div> </div> </div> {{/each}} {{/partial}}
从 Ractive 方面来看,这里没有什么新东西,但是对 Bootstrap 功能的使用有点重。
显示可点击的开发人员技能列表
到目前为止,我们已经取得了不错的进展,但仍然缺少一个功能,那就是技能开发者关系的另一面; 即,我们希望显示每个开发人员的技能,并且我们希望这些技能中的每一个都成为一个可点击的链接,将我们带到该技能的结果视图。
但是等等……我确定我们在技能列表中已经有了完全相同的东西。 是的,事实上,我们做到了。 它在可点击的技能列表中,但它来自与每个开发人员的技能集不同的数组。 然而,它们非常相似,这对我来说是一个明确的信号,我们应该重用那块 HTML。 为此,partials 是我们的朋友。
(注意:Ractive 还有一种更高级的方法来处理可重用的视图块,称为组件。它们确实非常有用,但为了简单起见,我们现在不讨论它们。)
下面是我们如何使用 partials 来实现这一点(顺便提一下,我们可以在不添加任何 JavaScript 代码的情况下添加此功能!):
<!-- MAKE THE CLICKABLE SKILL LINK INTO ITS OWN “skill” PARTIAL --> {{#partial skill}} <a href="#" class="label label-primary" on-click="select-skill:{{this}}">{{this}}</a> {{/partial}} {{#partial skillsList}} <div class="row"> <form class="form-horizontal col-xs-6 col-xs-offset-6"> <input type="search" class="form-control" value="{{ skillFilter }}" placeholder="Type part of the skill name here"> </form> </div> <hr> <div class="row"> {{#each skills()}} <!-- USE THE NEW “skill” PARTIAL --> <span class="col-xs-3">{{> skill}}</span> {{/each}} </div> {{/partial}} {{#partial skillView}} <h2> {{ currentSkill }} <button type="button" class="close pull-right" on-click="deselect-skill">× CLOSE</button> </h2> {{#each skillDevelopers(currentSkill)}} <div class="panel panel-default"> <div class="panel-body media"> <div class="media-left"> <img class="media-object img-circle" width="64" height="64" src="{{ this.photo }}" alt="{{ this.name }}"> </div> <div class="media-body"> <a class="h4 media-heading" href="{{ this.url }}" target="_blank">{{ this.name }}</a> <p>{{ this.desc }}</p> <p> <!-- ITERATE OVER THE DEVELOPER'S SKILLS --> {{#each this.skills}} <!-- REUSE THE NEW “skill” PARTIAL TO DISPLAY EACH DEVELOPER SKILL AS A CLICKABLE LINK --> {{> skill}} {{/each}} </p> </div> </div> </div> {{/each}} {{/partial}}
我已经将技能列表附加到我们从“DB”中检索到的开发人员对象上,所以我只是稍微更改了模板:我将呈现技能标签的行移到了部分,并在该行原来的位置使用了这个部分。
然后,当我迭代开发人员的技能时,我可以重用这个相同的新部分来将这些技能中的每一个也显示为可点击的链接。 此外,如果你记得的话,这也代理了 select-skill 事件,传递了相同的技能名称。 这意味着我们可以将它包含在任何地方(具有适当的上下文),我们将获得一个可点击的标签,该标签会导致技能视图!
Final Touch - 预加载器
好的,我们现在有一个基本的功能应用程序。 它很干净并且运行速度很快,但仍然需要一些时间来加载。 (在我们的例子中,这部分是由于我们使用了非串联、非缩小的源,但在现实世界的应用程序中可能还有其他重要原因,例如加载初始数据)。
因此,作为最后一步,我将向您展示一个巧妙的技巧来添加一个预加载动画,该动画将在 Ractive 渲染我们的应用程序时立即删除:
<div class="container"> <h1>Toptal Search</h1> <div> <div class="progress"> <div class="progress-bar progress-bar-striped active"> Loading... </div> </div> </div> </div>
那么这里有什么魔力呢? 其实很简单。 我直接向我们的根元素添加了一些内容(它是 Bootstrap 动画进度条,但也可以是动画 GIF 或其他)。 我认为它非常聪明——当我们的脚本正在加载时,用户会看到加载指示器(因为它不依赖于 JavaScript,它可以立即显示)。 但是,一旦 Ractive 应用程序被初始化,Ractive 就会用渲染的模板覆盖根元素的内容(从而擦除预加载动画)。 这样,我们就可以实现这个效果,只需要一段静态的 HTML 和 0 行逻辑。 我觉得这很酷。
结论
想想我们在这里完成了什么,以及我们完成它的难易程度。 我们有一个非常全面的应用程序:它显示技能列表,允许快速搜索它们(甚至支持在用户在搜索框中键入时交互式更新技能列表),允许导航到特定技能并返回,以及列表每个选定技能的开发人员。 此外,我们可以单击任何开发人员列出的任何技能,将我们带到具有该技能的开发人员列表。 所有这一切都只需要不到 80 行 HTML 和不到 40 行 JavaScript。 在我看来,这令人印象深刻,充分说明了 Ractive 的强大、优雅和简单。
该应用程序的工作版本可在此处在线获得,完整的源代码已公开并可在此处获得。
当然,在这篇文章中,我们只是触及了 Ractive 框架可能实现的功能的皮毛。 如果您喜欢到目前为止所看到的内容,我强烈建议您开始使用 Ractive 的 60 秒设置,并开始自己探索 Ractive 所提供的一切。