Maison  >  Article  >  interface Web  >  Guide de développement de composants JavaScript Graphiques et textes modulaires Explication détaillée

Guide de développement de composants JavaScript Graphiques et textes modulaires Explication détaillée

黄舟
黄舟original
2017-03-13 16:57:432201parcourir

De nos jours, bien que la plupart des applications Web utilisent beaucoup de JavaScript, comment maintenir le focus, la robustesse et la maintenabilité des fonctions client reste un grand défi.

Bien que d'autres langages de programmation et systèmes aient naturellement adopté des principes de base tels que la séparation des préoccupations et DRY, ils développent souvent des applications côté navigateur . , ces principes sont ignorés.

Une partie de la raison de ce phénomène est l'histoire du langage JavaScript lui-même, qui a longtemps eu du mal à attirer l'attention et le traitement sérieux de la part des développeurs.

La raison la plus importante peut être causée par la différence entre le service et le client. Bien qu’il existe déjà un grand nombre de concepts de styles architecturaux à cet égard, comme ROCA, qui illustrent comment gérer cette différence. Mais il manque encore des conseils étape par étape sur la manière de mettre en œuvre ces concepts1.

Ces raisons font souvent que le code frontal est hautement procédural et relativement non structuré. Cette méthode d'appel de code direct réduit le coût d'appel, simplifiant ainsi la complexité de l'appel de code JavaScript et des navigateurs. raison pour laquelle cette méthode d’appel est autorisée à exister. Mais bientôt, le code implémenté de cette manière devient inmaintenable.

Cet article utilisera un exemple pour vous montrer le processus d'évolution d'un composant simple (widget) et voir comment il a évolué d'une base de code volumineuse et non structurée à un composant réutilisable.

Filtrer les contacts

Cet exemple de composant est utilisé pour filtrer une liste de contacts par nom. Ses derniers résultats et toute son évolution sont à retrouver dans ce dépôt GitHub. Nous encourageons les lecteurs à examiner le code soumis et à laisser de précieux commentaires.

Conformément au principe d'enrichissement progressif, nous partons d'abord d'une structure HTML de base pour décrire les données utilisées. On utilise ici le format micro h-card (microformat), qui peut jouer un rôle sémantique et donner du sens à diverses informations sur les contacts :

<!-- 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>

Veuillez noter qu'ici nous faisons peu importe que la structure DOM soit basée sur du code HTML généré par le serveur ou généré par d'autres composants, il suffit de s'assurer que nos composants peuvent s'appuyer sur cette infrastructure lors de l'initialisation. Cette structure forme en fait une structure de données basée sur DOM [{ photo, website, name, e-mail }] pour les éléments du formulaire.

Avec cette infrastructure en place, nous pouvons commencer à mettre en œuvre nos composants. La première étape consiste à fournir à l'utilisateur un champ de saisie pour saisir le nom du contact. Bien qu'il n'appartienne pas au contrat de la structure DOM, notre composant est toujours chargé de le créer et de l'ajouter dynamiquement à la structure DOM (après tout, sans notre composant, l'ajout de ce champ n'a aucun sens).

// main.js     

 var contacts = jQuery("ul.contacts");     
 jQuery(&#39;<input type="search" />&#39;).insertBefore(contacts);

(Nous utilisons jQuery ici uniquement pour des raisons de commodité et également compte tenu de sa grande convivialité. Si vous utilisez d'autres opérations DOM bibliothèque de classes , pour la même raison)

Le fichier JavaScript lui-même et le fichier jQuery dont il dépend sont référencés en bas du fichier HTML.

Ensuite, commencez à ajouter les fonctions requises. Pour les contacts qui ne correspondent pas au nom saisi dans ce champ nouvellement créé, ce composant les masquera :

// main.js     

 var contacts = jQuery("ul.contacts");     
 jQuery(&#39;<input type="search" />&#39;).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();     
         }     
    });     
 }

(Citation A séparée, nommée fonction rend généralement la fonction de rappel plus facile à gérer que de définir une fonction anonyme )

Veuillez noter que cet événement. La fonction handleling dépend de l'environnement DOM spécifique, qui dépend de l'élément qui a déclenché cet événement (son contexte d'exécution sera mappé sur ce pointeur). Nous allons parcourir la structure DOM à partir de cet élément pour accéder à la liste de contacts et trouver tous les éléments qui contiennent des noms (ceci est défini par la sémantique des microformats). Si le début d'un nom ne correspond pas au contenu actuellement saisi, nous traverserons à nouveau vers le haut et masquerons l'élément conteneur correspondant. Sinon, nous devons nous assurer que l'élément est toujours visible.

Tests

Ce code fournit déjà les fonctionnalités de base dont nous avons besoin, il est temps de continuer à l'améliorer en écrivant des tests 2. Dans cet exemple, l'outil que nous utilisons est QUnit.

我们首先编写一个最简单的HTML页面,它将作为我们的测试集的入口。当然我们还需要引用我们的代码以及相应的依赖项(在这个例子中就是jQuery),这和我们之前创建的普通HTML页面的方式是一样的。

<!-- test/index.html -->     

  <p id="qunit"></p>       
  <p id="qunit-fixture"></p>          

  <script src="jquery.js"></script>       
  <script src="../main.js"></script>         

  <script src="qunit.js"></script>

有了这个基础结构之后,我们就要在#qunit-fixture这个元素中加入我们的示例数据了,即一个h-card的列表,还记得我们最开始时的那一段HTML结构吗?每一个测试开始时都会重置这个元素,保证测试数据的完整,也避免任何可能的副作用产生。

我们的第一个测试保证这个组件正确地初始化,而且过滤功能和预期一样工作,能够将不满足输入条件的DOM元素隐藏起来。

// test/test_filtering.js     

 QUnit.module("contacts filtering", {   
     setup: function() { // cache common elements on the module object      
         this.fixtures = jQuery("#qunit-fixture");     
         this.contacts = jQuery("ul.contacts", this.fixtures);     
     }     
 });     

 QUnit.test("filtering by initials", function() {     
     var filterField = jQuery("input[type=search]", this.fixtures);     
     QUnit.strictEqual(filterField.length, 1);  

     var names = extractNames(this.contacts.find("li:visible"));     
     QUnit.deepEqual(names, ["Jake Archibald", "Christian Heilmann",     
             "John Resig", "Nicholas Zakas"]);     

     filterField.val("J").trigger("keyup"); // simulate user input     
     var names = extractNames(this.contacts.find("li:visible"));     
     QUnit.deepEqual(names, ["Jake Archibald", "John Resig"]);     
 });     

 function extractNames(contactNodes) {     
     return jQuery.map(contactNodes, function(contact) {     
         return jQuery(".p-name", contact).text();     
     });     
 }

(strictEqual方法能够避免JavaScript在比较对象时会默认忽略类型信息的现象,这可以避免某些微妙的错误出现。)

随后我们将这个测试文件加入我们的测试集中(在QUnit引用的下方添加这个文件的引用),在浏览器中打开这个测试集,它应该告诉我们所有的测试都已通过:

动画效果

虽然这个widget运行没问题,但还不够吸引人,因此让我们来添加一点简单的动画效果。使用jQuery可以很简单地实现这一点:只要把show和hide方法替换为相应的slideUp和slideDown方法就可以了。这一特性能够让这个朴素的示例的用户体验得到显著的提升。

但是当你再一次运行这个测试集时,结果是过滤功能这次不能正确工作了,因为全部4个联系人都依然显示在页面上:

这是由于动画效果是异步操作(就如AJAX操作一样),因此在动画结束前就已经完成了对过滤结果的检查。我们可以使用QUnit的asyncTest方法推迟检查的时间。

// test/test_filtering.js          

  QUnit.asyncTest("filtering by initials", 3, function() { // expect 3 assertions     
      // ...     
      filterField.val("J").trigger("keyup"); // simulate user input     
      var contacts = this.contacts;     
      setTimeout(function() { // defer checks until animation has completed     
          var names = extractNames(contacts.find("li:visible"));     
          QUnit.deepEqual(names, ["Jake Archibald", "John Resig"]);     
          QUnit.start(); // resumes test execution     
      }, 500);     
  });

每次都打开浏览器检查测试集的结果有些繁琐,因此我们可以使用PhantomJS,这是一个后台浏览器。将它与QUnit runner一起使用可以使测试过程自动化,并在控制台显示测试结果。

$ phantomjs runner.js test/index.html   
 Took 545ms to run 3 tests. 3 passed, 0 failed.

这种方式使得通过持续集成进行自动化测试变得更为方便(当然,它做不到跨浏览器的错误检查,因为PhantomJS只使用了WebKit内核。不过现在也出现了支持Firefox的Gecko和Internet Explorer的Trident引擎的后台浏览器。)

包含范围

目前为止,我们的代码虽然能够运行,但还不够优雅:由于浏览器不会在隔离的区间内运行JavaScript,因此这段代码会将contactsonFilter两个变量暴露到全局命名空间内,初学者需要特别当心。不过我们可以自行修改这段代码,以避免变量污染全局命名空间,由于JavaScript中唯一的限定范围机制就是函数,因此我们只需将整个文件简单地封装在一个匿名函数中,并在最后调用这个函数就可以了:

(function() {    
 var contacts = jQuery("ul.contacts");     
 jQuery(&#39;<input type="search" />&#39;).insertBefore(contacts).
on("keyup", onFilter);     
 function onFilter(ev) {     
     // ...     
 } 
 }());

这种方法被称为立即调用的函数表达式(IIFE)。

现在,我们已经有效地将变量限定为一个自包含的模块中的私有变量了。

我们还可以进一步改善代码,以防止在声明变量时因遗漏var而导致创建了新的全局变量。实现这一点只需激活strict模式,它可以避免许多代码中的陷阱3

(function() {    
 "use strict"; // NB: must be the very first statement within the function 
 // ...        
 }());

在某个IIFE容器中指定strict模式,可以确保它只在被显式调用的模块中起作用。

有了基于模块的本地变量之后,我们就可以利用这一点来引入本地别名,以达到便利性的目的,比方在我们的测试中可以这样做:

// test/test_filtering.js
 (function($) {
 "use strict";
 var strictEqual = QUnit.strictEqual;
 // ...
 var filterField = $("input[type=search]", this.fixtures);
 strictEqual(filterField.length, 1);
 }(jQuery));

现在我们有了两个别名:$strictEqual,前者是通过一个IIFE参数进行定义的,它只在这个模块内部起作用。

组件 API

虽然我们的代码已经实现了良好的结构化,不过这个组件会在启动时(例如在这段代码刚刚加载时)自动初始化。这导致了难以预测它的初始化时机,而且使得不同种类的,或是新创建的元素不能够动态地被(重新)初始化。

只需将现有的初始化代码封装在一个函数中,就可以简单地修正这一问题:

// widget.js

 window.createFilterWidget = function(contactList) { 
     $(&#39;<input type="search" />&#39;).insertBefore(contactList).
         on("keyup", onFilter);
 };

通过这种方式,我们就将这个组件的功能与它的运行程序的生命周期解耦了。初始化的责任就转交给了应用程序,在我们的示例中就是测试工具。这通常意味着需要在应用程序的上下文中加入一些“粘合代码”以管理这些组件。

请注意,我们显式地将函数赋给了全局的window对象,这是让我们的功能可以在IIFE外部访问的最简单方式。但这种方式将模块本身与某个特定的隐式上下文耦合在一起了:而window并不一定是全局对象(例如在Node.js中)。

一个更为优雅的途径是明确指出代码的哪些部分将暴露给外部,并将这些部分聚集在一处。我们可以再次利用IIFE的优势实现这一点:因为IIFE仅仅是一个函数,我们可以在它的底部返回它的公开部分(例如我们所定义的API),并将返回值赋给某个外部(全局)范围内的变量:

// widget.js
 var CONTACTSFILTER = (function($) {
 function createFilterWidget(contactList) {
     // ...
 }
 // ...
 return createFilterWidget;
 }(jQuery));

这一方式也叫做揭示模块化模式(revealing module pattern),至于使用大写是为了突出全局变量的一种约定。

封装状态

目前为止,我们的组件不仅功能良好而且结构合理,还包含了一个恰当的API。不过,如果我们继续按照这种方式引入更多的功能,就会导致对相互独立的函数的组合调用,这样很容易产生混乱的代码。对于UI组件这种注重状态的对象来说就更是如此。

在我们的示例, 我们希望允许用户决定过滤条件是否是大小写敏感的,因此我们加入了一个复选框,并相应地扩展了我们的事件处理函数:

// widget.js

 var caseSwitch = $(&#39;<input type="checkbox" />&#39;);

 // ...

 function onFilter(ev) {
     var filterField = $(this);
     // ...
     var caseSwitch = filterField.prev().find("input:checkbox");
     var caseSensitive = caseSwitch.prop("checked");

     if(!caseSensitive) {
         input = input.toLowerCase();
     }
     // ...  }

为了使组件的元素与事件处理函数相关联,这段代码增加了对某个特定DOM上下文的依赖性。解决该问题的一种选择是将DOM查找方法移至某个分离的函数中,由它根据指定的上下文决定查找哪个组件。而更加常见的方式是采用面向对象的途径。(JavaScript本身支持函数式编程与面向对象4编程两种风格,它允许开发者根据任务需求自行选择最为适合的编程风格。)

因此我们可以重写组件的方法,让它通过某个实例追踪它的所有组件:

// widget.js

 function FilterWidget(contactList) {
     this.contacts = contactList;
     this.filterField = $(&#39;<input type="search" />&#39;).
insertBefore(contactList);
     this.caseSwitch = $(&#39;<input type="checkbox" />&#39;);
 }

对API的这一改动虽然很小,影响却很大:我们现在不再通过调用createFilterWidget(…)方法,而是通过new FilterWidget(…)来初始化widget,它调用了方法的构造函数,并将上下文传递给一个新创建的对象this。为了强调new操作的必要性,按照约定,构造函数名称的首字母都是大写(这一点非常类似于其它语言中的类的命名方式)5

当然,我们需要根据这个新的结构重新实现功能,首先得加入一个方法,它根据输入内容来隐藏联系人,它的实现和之前在onFilter方法中的实现基本相同:

// widget.js

 FilterWidget.prototype.filterContacts = function(value) {
     var names = this.contacts.find("li .p-name");
     var self = this;
     names.each(function(i, node) {
         var el = $(node);
         var name = el.text();
         var contact = el.closest(".h-card");

         var match = startsWith(name, input, self.caseSensitive);
         if(match) {
             contact.show();
         } else {
             container.hide();
         }
    });
 }

(这里定义的self变量是为了在each这个回调函数中也可以访问到this对象,因为在each函数中也有它自己的this变量,这样就不能够直接访问外部范围中的this对象了。通过在内部引用self对象,它就创建了一个闭包。)

注意filterContacts函数的实现有所变化了,它不再根据上下文查找DOM,而是简单地引用之前定义在构造函数中的元素。字符串匹配功能则被抽取成一个通用目的的函数,这也表示并非所有功能都必须成为某个对象的方法:

function startsWith(str, value, caseSensitive) {
     if(!caseSensitive) {
         str = str.toLowerCase();
         value = value.toLowerCase();
     }
     return str.indexOf(value) === 0;
 }

接下来我们将连接事件处理函数,否则这个方法是永远不会被触发的:

// widget.js    
  function FilterWidget(contactList) {     
     // ...    
     this.filterField.on("keyup", this.onFilter);     
     this.caseSwitch.on("change", this.onToggle);     
 }     
  FilterWidget.prototype.onFilter = function(ev) {     
     var input = this.filterField.val(); 
          this.filterContacts(input);     
 };     
  FilterWidget.prototype.onToggle = function(ev) {     
     this.caseSensitive = this.caseSwitch.prop("checked");     
 };

现在可以重新运行我们的测试了,它除了之前那些API的小改动之外,并不需要其它的任何调整。但是一个错误出现了,这是由于this对象并非我们所预计的对象。我们已经了解到事件处理函数调用时会将相应的DOM元素作为运行上下文,因此我们需要做出一些调整,使代码能够访问到组件实例。为了实现这一点,我们利用了闭包功能以重新映射执行上下文:

// widget.js

 function FilterWidget(contactList) {
     // ...
     var self = this;
     this.filterField.on("keyup", function(ev) {
         var handler = self.onFilter;
         return handler.call(self, ev);
     });
 }

(call是一个内置的方法,它能够调用任何函数,并将任何传入的对象作为上下文,首个传入参数将对应该函数中的this对象。另一选择是apply方法,它能够接受一个隐式的arguments变量,以避免显式地引用单个的参数,它的形式是:handler.apply(self, arguments).6

最终的结果是,我们的widget中的每个方法都有着清晰的并且封装良好的职责。

jQuery API

如果使用jQuery,那么现在的API看起来还不够优雅。我们可以添加一个轻量的封装,它提供了另一种对jQuery开发者来说感觉更加自然的API。

jQuery.fn.contactsFilter = function() {
     this.each(function(i, node) {
         new CONTACTSFILTER(node);
     });
     return this;
 };

(在jQuery的插件指南中可以找到更详细的说明。)

这样一来,我们就可以使用jQuery(“ul.contacts”).contactsFilter()这种方式调用组件了。如果将这一方法定义在一个单独的层中,就可以保证我们不依赖于某些特定的系统,因为将来版本的实现也许会为其它不同的系统提供额外的API封装,甚至可能会决定移除对jQuery的依赖或选择替代品。(当然,在这个示例中,弃用jQuery也意味着我们将不得不重写代码内部实现的某些部分。)

结论与展望

希望本文能够表达出编写可维护的JavaScript组件的一些关键原则。当然,并且每个组件都要遵循这个模式,但这里所表现的一概念对于任何组件来说都提供了一些必要的核心功能。

进一步的增加或许要用到异步模块定义(AMD),它不仅改进了代码封装,而且使得模块之间的依赖更加清晰,这就允许你按需加载代码(例如通过RequireJS)。

此外,近来有一些激动人心的新特性正在开发中:下个版本的JavaScript(官方称为ECMAScript 6)将引入一个语言级别的模块系统,当然,和任何新特性一样,它是否能够被广泛接受要取决于浏览器的支持。类似的,Web Components是正在实现的一组浏览器API,它的目的是改善代码封装与可维护性,可以通过使用Polymer来感受一下其中的许多特性。但Web Components的进展如何还有待进一步观望。

  1. 对于单页面应用来说这篇规范并不太适用,因为在这种情况下服务端和客户端的角色会有很大的不同。不过对这种方式的对比已经超出了本文的范围。

  2. 或许你应该先编写测试方法。

  3. 可以使用JSLint以避免这种情况和其它一些常见问题的发生,在我们的代码库中就使用了JSLint Reporter。

  4. JavaScript使用原型而不是类,主要区别在于,类总是以某些方式表现出“独特性”,而任意对象都可以作为原型,作为创建新实例的模板。对于本文来说,这一区别基本可以忽略。

  5. 当前流行版本的JavaScript引入了Object.create方法,作为“伪经典”语法的替代品。但原型继承的核心原则还是一样的。

  6. 可以使用jQuery.proxy方法将代码改写为this.filterField.on(“keyup”, $.proxy(self, “onFilter”))

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn