Home >Web Front-end >JS Tutorial >JavaScript Component Development Guide Modular Graphics and Text Detailed Explanation

JavaScript Component Development Guide Modular Graphics and Text Detailed Explanation

黄舟
黄舟Original
2017-03-13 16:57:432245browse

Nowadays, although most web applications use a lot of JavaScript, how to maintain the focus, robustness and maintainability of client functions is still a big challenge.

Although otherprogramming languagesand systems have taken basic principles such as separation of concerns and DRY as a matter of course, browser-side application development is often carried out , these principles are ignored.

Part of the reason for this phenomenon is that the JavaScript language itself has a history of struggling. For a long time, it has been difficult to get serious attention and treatment from developers.

The more important reason may be caused by the difference between the service side and the client side. Although there are already a large number of architectural style concepts in this regard, such as ROCA, that illustrate how to manage this difference. But there is still a lack of guidance on the specific steps on how to implement these concepts1.

These reasons often lead to a high degree of proceduralization and relative lack of structure in front-end code. This direct code calling method reduces the cost of calling, thus simplifying the complexity of code calling. JavaScript and browsers are also because This reason allows this calling method to exist. But soon, the code implemented this way becomes unmaintainable.

This article will use an example to show you the evolution process of a simple component (widget) and see how it evolved from a large, unstructured code base to a reusable component. .

Filtering contacts

The function of this sample component is to filter a contact list by name. Its latest results and its entire evolution can be found in this GitHub repository. We encourage readers to review submitted code and leave valuable comments.

In accordance with the principle of progressive enhancement, we first start with a basic HTML structure to describe the data used. The h-card microformat (microformat) is used here, which can play a semantic role and make various information about contacts meaningful:

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

There is one point Please note that here we do not care whether the DOM structure is based on HTML code generated by the server or generated by other components. It is enough to ensure that our components can rely on this infrastructure during initialization. This structure actually forms a DOM-based data structure [{ photo, website, name, e-mail }] for the form items.

With this infrastructure in place, we can start implementing our components. The first step is to provide the user with an input field to enter the contact name. Although it does not belong to the contract of the DOM structure, our component is still responsible for creating it and dynamically adding it to the DOM structure (after all, without our component, adding this field has no meaning at all).

// main.js     

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

(We use jQuery here only for convenience, but also considering its widespread use. If you use other DOM operationclass libraries, for the same reason.)

The JavaScript file itself and the jQuery file it depends on will be referenced at the bottom of the HTML file.

Next, start adding the required functions. For those contacts that do not match the input name in this newly created field, this component will hide them:

// 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();     
         }     
    });     
 }

(quote a detached A named function usually makes the callback function easier to manage than defining an anonymous function)

Please. Note that this Event handling function depends on the specific DOM environment, which depends on the element that triggered the event (its execution context will be mapped to this pointer). We will traverse the DOM structure starting from this element to access the contact list and find all elements that contain names (this is defined by the semantics of the microformats). If the beginning of a name does not match the currently entered content, we will traverse upward again and hide the corresponding container element. Otherwise, we must ensure that the element is still visible.

Testing

This code already provides the basic functionality we need, it’s time to continue enhancing it by writing tests2. In this example, the tool we are using is 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”))

The above is the detailed content of JavaScript Component Development Guide Modular Graphics and Text Detailed Explanation. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn