>  기사  >  웹 프론트엔드  >  JavaScript 구성 요소 개발 가이드 모듈형 그래픽 및 텍스트 자세한 설명

JavaScript 구성 요소 개발 가이드 모듈형 그래픽 및 텍스트 자세한 설명

黄舟
黄舟원래의
2017-03-13 16:57:432214검색

요즘 대부분의 웹 애플리케이션은 JavaScript를 많이 사용하지만 클라이언트 기능의 초점, 견고성 및 유지 관리 가능성을 어떻게 유지하는지는 여전히 큰 과제입니다.

다른 프로그래밍 언어 ​​ 및 시스템에서는 관심사 분리 및 DRY와 같은 기본 원칙을 당연히 취했지만 브라우저 측 애플리케이션을 개발하는 경우가 많습니다. . , 이러한 원칙은 무시됩니다.

이런 현상이 나타나는 이유 중 하나는 오랫동안 개발자들로부터 진지한 관심과 대우를 받기 위해 애썼던 JavaScript 언어 자체의 역사 때문입니다.

더 중요한 이유는 서비스와 클라이언트의 차이 때문일 수 있습니다. ROCA와 같이 이와 관련하여 이러한 차이를 관리하는 방법을 보여주는 아키텍처 스타일 개념이 이미 많이 있습니다. 그러나 이러한 개념을 구현하는 방법에 대한 단계별 지침이 여전히 부족합니다1.

이 기사에서는 예제를 사용하여 간단한 구성 요소(위젯)의 진화 과정을 보여주고 구조화되지 않은 대규모 코드 기반에서 재사용 가능한 구성 요소로 어떻게 발전했는지 살펴보겠습니다.

연락처 필터링

이 예제 구성 요소는 이름별로 연락처 목록을 필터링하는 데 사용됩니다. 최신 결과와 전체 발전 내용은 이 GitHub 저장소에서 확인할 수 있습니다. 독자들이 제출된 코드를 검토하고 귀중한 의견을 남기는 것이 좋습니다.

점진적 향상 원칙에 따라 먼저 기본 HTML 구조부터 사용되는 데이터를 설명합니다. 여기서는 h-card 마이크로포맷(micro

for

mat)이 사용되어 의미론적 역할을 하고 연락처에 대한 다양한 정보를 의미 있게 만들 수 있습니다.

<!-- 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 구조가 서버에서 생성된 HTML 코드를 기반으로 하는지 아니면 다른 구성 요소에서 생성되는지 여부는 중요하지 않습니다. 초기화 중에 구성 요소가 이 인프라에 의존할 수 있다는 것을 보장하는 것만으로도 충분합니다. 이 구조는 실제로 양식 항목에 대한 DOM 기반 데이터 구조

[{ photo, website, name, e-mail }]

를 형성합니다. 이 인프라가 준비되면 구성 요소 구현을 시작할 수 있습니다. 첫 번째 단계는 사용자에게 연락처 이름을 입력할 수 있는 입력 필드를 제공하는 것입니다. DOM 구조의 계약에 속하지 않더라도 우리 구성 요소는 여전히 이를 생성하고 DOM 구조에 동적으로 추가하는 역할을 담당합니다(결국 구성 요소가 없으면 이 필드를 추가하는 것은 전혀 의미가 없습니다).

// main.js     

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

(여기에서는 편의상 jQuery를 사용하고 폭넓은 사용성을 고려하여 사용합니다. 다른

DOM 연산

클래스 라이브러리를 사용하는 경우에도 마찬가지입니다. ) JavaScript 파일 자체와 그것이 의존하는 jQuery 파일은 HTML 파일 하단에서 참조됩니다.

다음으로, 새로 생성된 필드의 입력 이름과 일치하지 않는 연락처에 대해 필요한 기능을 추가하기 시작합니다.

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

( 함수

는 일반적으로

익명 함수 ) 를 정의하는 것보다 콜백 함수 를 관리하기 쉽게 만듭니다. 이 이벤트 처리 함수는 이벤트를 트리거한 요소에 따라 달라지는 특정 DOM 환경에 따라 달라집니다(해당 실행 컨텍스트는 this 포인터에 매핑됩니다). 이 요소부터 시작하여 DOM 구조를 탐색하여 연락처 목록에 액세스하고 이름이 포함된 모든 요소를 ​​찾습니다(이는 마이크로포맷의 의미에 따라 정의됩니다). 이름의 시작 부분이 현재 입력된 내용과 일치하지 않으면 다시 위쪽으로 이동하여 해당 컨테이너 요소를 숨깁니다. 그렇지 않으면 해당 요소가 계속 표시되는지 확인해야 합니다.

테스트이 코드는 이미 우리에게 필요한 기본 기능을 제공하므로, 이제 테스트

2

를 작성하여 계속해서 기능을 향상시킬 때입니다. 이 예에서 우리가 사용하는 도구는 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”))

위 내용은 JavaScript 구성 요소 개발 가이드 모듈형 그래픽 및 텍스트 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.