开发模块化的JavaScript组件2014-09-19 infoq Frederik Dohr 译:邵思华现如今,虽然多数的web应用都使用了大量的JavaScript,但如何保持客户端功能的专注性、健壮性和可维护性依然是一个很大的挑战。尽管其它编程语言和系统都已经将关注分离和DRY这样的基本原则视为理所当然的宗旨,但往往在进行浏览器端应用开发的时候,这些原则就被忽视了。造成这一现象的部分原因是JavaScript语言本身就在不断挣扎的历史,在很长的一段时间内,它都难以获得开发者的认真关注和对待。而更重要的原因或许是源于服务端与客户端的差异造成的。虽然在这方面已经有大量的架构风格方面的概念,例如ROCA,阐述了如何管理这种差异的方式。但还是缺乏如何实现这些概念的具体步骤的指南1。这些原因经常导致前端代码的高度过程化并且相对缺乏结构性,这种直接的代码调用方式减少了调用的开销,从而简化了代码调用的复杂性,JavaScript和浏览器也是因为这一点原因而允许这种调用方式的存在。但很快,通过这种方式实现的代码就会变得得难以维护。本文将通过一个示例为你展示某个简单的组件(widget)的演化过程,看看它是如何从一个庞大的、缺乏结构性的代码库进化为一个可重用的组件的。
对联系人进行过滤
这个示例组件的作用是对一个联系人列表通过名称进行过滤。它的最新成果以及它的全部演化过程都可以在这个GitHub代码库中找到。我们鼓励读者们对提交的代码进行审阅,并且留下宝贵意见。按照渐进式增强的原则,我们首先从一个基础的HTML结构开始以描述所用到的数据。这里用到了h-card这个微格式(microformat),它能够起到语义化的作用,使得联系人的各种信息显得具有意义:
<!-- index.html --><ul><li class="h-card"><img src="http://example.org/jake.png" alt="avatar" class="u-photo"> <a href="http://jakearchibald.com" class="p-name u-url">Jake Archibald</a>(<a href="mailto:jake@example.com" class="u-email">e-mail</a>) </li><li class="h-card"><img src="http://example.org/christian.png" alt="avatar" class="u-photo"> <a href="http://christianheilmann.com" class="p-name u-url">Christian Heilmann</a>(<a href="mailto:christian@example.com" class="u-email">e-mail</a>)</li><li class="h-card"><img src="http://example.org/john.png" alt="avatar" class="u-photo"> <a href="http://ejohn.org" class="p-name u-url">John Resig</a> (<a href="mailto:john@example.com" class="u-email">e-mail</a>)</li><li class="h-card"> <img src="http://example.org/nicholas.png" alt="avatar" class="u-photo"> <a href="http://www.nczonline.net" class="p-name u-url">Nicholas Zakas</a> (<a href="mailto:nicholas@example.com" class="u-email">e-mail</a>)</li> </ul>
有一点请注意,在这里我们并不关心这个DOM结构是基于server端生成的HTML代码,或是由其它组件生成的,只要保证在初始化时,我们的组件能够依赖于这个基础结构就够了。这一结构实际上为表单项构成了一个基于DOM的数据结构 [{ photo, website, name, e-mail }]。有了这个基础结构之后,我们就可以开始实现我们的组件了。第一步是为用户提供一个输入字段,以输入联系人名称。虽然它并不属于DOM结构的契约,但我们的组件仍然要负责创建它并动态地加入到DOM结构中去(毕竟,如果没有我们的组件,那么添加这个字段就完全没有任何意义了)。
// main.jsvar contacts = jQuery("ul.contacts");jQuery("<input type="search"/>").insertBefore(contacts);
(我们在这里仅是出于便利性而使用了jQuery,同时也考虑到它的广泛使用性。如果使用其它的DOM操作类库,也是出于同样的原因。)这个JavaScript文件本身以及它所依赖的jQuery文件都会在HTML文件的底部进行引用。接下来开始加入所需的功能,对于那些不符合这个新建的字段中的输入名称的联系人,这个组件会将它们隐藏起来:
// main.jsvar contacts = jQuery("ul.contacts");jQuery("<input type="search"/>").insertBefore(contacts).on("keyup", onFilter);function onFilter(ev) {var filterField = jQuery(this);var contacts = filterField.next();var input = filterField.val();var names = contacts.find("li .p-name");names.each(function(i, node) {var el = jQuery(node);var name = el.text();var match = name.indexOf(input) === 0;var contact = el.closest(".h-card");if(match) {contact.show();} else {contact.hide();} });}(引用一个分离的、具名的函数,比起定义一个匿名函数来说,通常会使得回调函数更便于管理。)请注意,这个事件处理函数依赖于特定的DOM环境,它取决于触发这个事件的元素(它的执行上下文会映射到this指针上)。我们将从这个元素开始遍历DOM结构,以访问联系人列表,并找出所有包含名称的元素(这是由微格式的语义所定义的)。如果某个名称的开头部分与当前输入的内容不匹配,我们就再次向上遍历,将相应的容器元素隐藏起来,否则的话,就要保证该元素依然可见。