Heim > Artikel > Web-Frontend > JavaScript Component Development Guide Modulare Grafiken und Text Detaillierte Erläuterung
Obwohl die meisten Webanwendungen heutzutage viel JavaScript verwenden, ist es immer noch eine große Herausforderung, den Fokus, die Robustheit und die Wartbarkeit der Client-Funktionen aufrechtzuerhalten.
Obwohl andere Programmiersprachen Grundprinzipien wie Separation of Concerns und DRY als selbstverständlich angesehen haben, entwickeln sie häufig browserseitige Anwendungen . , diese Grundsätze werden ignoriert.
Ein Grund für dieses Phänomen ist die Geschichte der JavaScript-Sprache selbst, die lange Zeit darum gekämpft hat, ernsthafte Aufmerksamkeit und Behandlung von Entwicklern zu erhalten.
Der wichtigere Grund kann durch den Unterschied zwischen der Dienstleistung und dem Kunden verursacht werden. Allerdings gibt es diesbezüglich bereits eine Vielzahl architektonischer Stilkonzepte, wie zum Beispiel ROCA, die veranschaulichen, wie dieser Unterschied gemeistert werden kann. Es fehlt jedoch immer noch eine Schritt-für-Schritt-Anleitung zur Umsetzung dieser Konzepte1.
Diese Gründe führen häufig dazu, dass der Front-End-Code stark prozedural und relativ unstrukturiert ist. Diese direkte Code-Aufrufmethode reduziert die Komplexität des Code-Aufrufs, da dies auch der Fall ist Grund, warum diese aufrufende Methode existieren darf. Doch bald wird der auf diese Weise implementierte Code nicht mehr wartbar.
Dieser Artikel zeigt Ihnen anhand eines Beispiels den Entwicklungsprozess einer einfachen Komponente (Widget) und zeigt, wie sie sich von einer großen, unstrukturierten Codebasis zu einer wiederverwendbaren Komponente entwickelt hat.
Diese Beispielkomponente wird verwendet, um eine Kontaktliste nach Namen zu filtern. Die neuesten Ergebnisse und die gesamte Entwicklung finden Sie in diesem GitHub-Repository. Wir ermutigen die Leser, den eingereichten Code zu überprüfen und wertvolle Kommentare zu hinterlassen.
Gemäß dem Prinzip der progressiven Verbesserung beginnen wir zunächst mit einer grundlegenden HTML-Struktur zur Beschreibung der verwendeten Daten. Hier wird das h-Card-Mikroformat (microformat) verwendet, das eine semantische Rolle spielen und verschiedene Informationen zu Kontakten aussagekräftig machen kann:
<!-- 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>
Bitte beachten Sie, dass wir dies hier tun Es ist egal, ob die DOM-Struktur auf dem vom Server generierten oder von anderen Komponenten generierten HTML-Code basiert. Es reicht aus, um sicherzustellen, dass unsere Komponenten während der Initialisierung auf diese Infrastruktur zurückgreifen können. Diese Struktur bildet tatsächlich eine DOM-basierte Datenstruktur [{ Foto, Website, Name, E-Mail }] für die Formularelemente.
Sobald diese Infrastruktur vorhanden ist, können wir mit der Implementierung unserer Komponenten beginnen. Der erste Schritt besteht darin, dem Benutzer ein Eingabefeld zur Eingabe des Kontaktnamens zur Verfügung zu stellen. Obwohl es nicht zum Vertrag der DOM-Struktur gehört, ist unsere Komponente dennoch dafür verantwortlich, es zu erstellen und dynamisch zur DOM-Struktur hinzuzufügen (schließlich hat das Hinzufügen dieses Felds ohne unsere Komponente überhaupt keine Bedeutung).
// main.js var contacts = jQuery("ul.contacts"); jQuery('<input type="search" />').insertBefore(contacts);
(Wir verwenden jQuery hier nur aus Bequemlichkeitsgründen und auch unter Berücksichtigung seiner breiten Verwendbarkeit. Wenn Sie andere DOM-Operationen Klassenbibliothek verwenden, aus demselben Grund)
Auf die JavaScript-Datei selbst und die jQuery-Datei, von der sie abhängt, wird am Ende der HTML-Datei verwiesen.
Als Nächstes beginnen Sie mit dem Hinzufügen der erforderlichen Funktionen. Für diejenigen Kontakte, die nicht mit dem Eingabenamen in diesem neu erstellten Feld übereinstimmen, werden sie von dieser Komponente ausgeblendet:
// main.js var 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(); } }); }
(Zitat A separat, Die benannte Funktion macht die Rückruffunktion normalerweise einfacher zu verwalten als die Definition einer anonymen Funktion )
Bitte beachten Sie, dass dieses Ereignis Die Funktion „Handling“ hängt von der spezifischen DOM-Umgebung ab, die von dem Element abhängt, das dieses Ereignis ausgelöst hat (sein Ausführungskontext wird diesem Zeiger zugeordnet). Wir werden die DOM-Struktur ausgehend von diesem Element durchqueren, um auf die Kontaktliste zuzugreifen und alle Elemente zu finden, die Namen enthalten (dies wird durch die Semantik der Mikroformate definiert). Wenn der Anfang eines Namens nicht mit dem aktuell eingegebenen Inhalt übereinstimmt, gehen wir erneut nach oben und verstecken das entsprechende Containerelement. Andernfalls müssen wir sicherstellen, dass das Element weiterhin sichtbar ist.
Dieser Code bietet bereits die grundlegende Funktionalität, die wir benötigen. Es ist an der Zeit, ihn durch das Schreiben von Tests 2 weiter zu verbessern. In diesem Beispiel verwenden wir das Tool 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,因此这段代码会将contacts和onFilter两个变量暴露到全局命名空间内,初学者需要特别当心。不过我们可以自行修改这段代码,以避免变量污染全局命名空间,由于JavaScript中唯一的限定范围机制就是函数,因此我们只需将整个文件简单地封装在一个匿名函数中,并在最后调用这个函数就可以了:
(function() { var contacts = jQuery("ul.contacts"); jQuery('<input type="search" />').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参数进行定义的,它只在这个模块内部起作用。
虽然我们的代码已经实现了良好的结构化,不过这个组件会在启动时(例如在这段代码刚刚加载时)自动初始化。这导致了难以预测它的初始化时机,而且使得不同种类的,或是新创建的元素不能够动态地被(重新)初始化。
只需将现有的初始化代码封装在一个函数中,就可以简单地修正这一问题:
// widget.js window.createFilterWidget = function(contactList) { $('<input type="search" />').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 = $('<input type="checkbox" />'); // ... 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 = $('<input type="search" />'). insertBefore(contactList); this.caseSwitch = $('<input type="checkbox" />'); }
对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.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的进展如何还有待进一步观望。
对于单页面应用来说这篇规范并不太适用,因为在这种情况下服务端和客户端的角色会有很大的不同。不过对这种方式的对比已经超出了本文的范围。
或许你应该先编写测试方法。
可以使用JSLint以避免这种情况和其它一些常见问题的发生,在我们的代码库中就使用了JSLint Reporter。
JavaScript使用原型而不是类,主要区别在于,类总是以某些方式表现出“独特性”,而任意对象都可以作为原型,作为创建新实例的模板。对于本文来说,这一区别基本可以忽略。
当前流行版本的JavaScript引入了Object.create方法,作为“伪经典”语法的替代品。但原型继承的核心原则还是一样的。
可以使用jQuery.proxy方法将代码改写为this.filterField.on(“keyup”, $.proxy(self, “onFilter”))
Das obige ist der detaillierte Inhalt vonJavaScript Component Development Guide Modulare Grafiken und Text Detaillierte Erläuterung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!