jQuery 确实帮助我们用更少的代码做更多的事情,但有一件事它没有解决,那就是如何组织代码。一开始这可能不是问题,但随着应用的年龄和功能的增长,它的组织(或缺乏组织)成为问题。
在本章中,我们将介绍一些经过验证的组织 JavaScript 的方法。在本章中,我们将:
- 学习一些面向对象的技术,使我们的代码易于理解和维护
- 使用事件来解耦代码,并确保不相关的部分不需要直接相互对话
- 快速看一下编写 JavaScript 单元测试,特别是 Jasmine,它是一个用于测试 JavaScript 代码的行为驱动开发框架
软件体系结构模式,如模型视图控制器****MVC等,之所以流行,主要是因为它们正面解决了代码组织的问题。模型视图控制器将应用程序分为三个主要部分。模型是处理应用程序数据的部分。控制器从模型中获取数据并将其反馈给视图,它从视图中获取用户输入并将其反馈给模型。这种模式最重要的一点是,你不应该把责任混为一谈。模型从不包含控制器代码,控制器从不包含视图,等等。这称为关注点分离或 SoC。如果应用程序的任何部分违反此规则,您的应用程序将陷入紧密耦合、相互依赖的代码的混乱状态。
我们不需要为了从 MVC 中获得一些好处而全力以赴。我们可以用它来指导我们的发展。首先,它帮助我们回答这个问题:这段代码应该去哪里?让我们看一个例子。我们需要编写代码,从我们的 web 服务中检索成员数据,并将其呈现给用户进行选择。我们应该如何进行?您的第一个冲动可能是编写如下代码:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<title>Chapter08-Clean Code</title>
</head>
<body>
<div>Super Coding Club Members</div>
<hr/>
<ul id="myList"></ul>
<hr/>
<script type="text/javascript">
// Hook the document ready event and
$(document).ready(function () {
// get the user data
$.getJSON('users.json', function (members) {
var index, htmlTemplate = '';
// make sure there are some members
if (members && members.length) {
// create the markup
for (index = 0; index < members.length; index += 1) {
htmlTemplate += '<li>' + members[index].name.first + '</li>';
}
// render the member names
$('#myList').html(htmlTemplate);
}
return members;
}).fail(function (error) {
alert("Error: " + error.status + ": " + error.statusText);
});
});
</script>
</body>
</html>
此代码达到了我们的要求,但有几个问题。首先,关注点没有分离。虽然我们不努力创建一个 MVC 应用程序,但我们至少应该努力拥有沿着模型、视图和控制器线分解的功能。在本例中,我们的模型由$.getJSON()
方法调用表示。它直接绑定到我们的控制器代码,在本例中,控制器代码获取模型数据并从中创建 HTML 模板。最后,我们的视图代码采用 HTML 模板,并使用$.html()
方法呈现它。
此代码也是紧密耦合的一个示例。代码的每一部分都直接依赖于下一部分,没有办法将它们分开。紧密耦合的代码更难测试。没有简单的方法来测试这段代码的功能。看看吧。代码位于 document ready 事件中;为了开始测试代码的功能,您必须模拟该事件。一旦您模拟了 document ready 事件,您还需要以某种方式模拟getJSON()
方法,因为其余的代码都隐藏在其中。
使前面的代码示例难以理解的一个原因是它没有被分解成逻辑单元。在 JavaScript 中,我们没有像其他面向对象语言那样的类,但我们有对象甚至文件来将逻辑相关的代码单元保持在一起。
我们从函数开始分解代码。不要让一个函数做所有事情,而要努力拥有多个函数,每个函数只做一件事。当函数做了太多的事情时,它们就会变得很难理解。注释可能有助于解释代码在做什么,但编写良好的代码应该注释自己。函数有助于清晰地区分不同的功能。
您可能想知道为什么这些都很重要,特别是因为我们有可以满足需求的工作代码。这很重要,因为典型的程序花在维护上的时间远远多于编写上的时间。因此,编写可维护的代码非常重要。让我们再试一次:
<script type="text/javascript">
function showHttpError(error) {
alert("Error: " + error.status + ": " + error.statusText);
}
function getMembers(errorHandler) {
return $.getJSON('users.json').fail(errorHandler);
}
function createMemberMarkup(members) {
var index, htmlTemplate = '';
members.forEach(function (member) {
htmlTemplate += '<li>' + member.name.first + '</li>';
});
return htmlTemplate;
}
function renderMembers($ptr, membersMarkup) {
$ptr.html(membersMarkup);
}
function showMembers() {
getMembers(showHttpError)
.then(function (members) {
renderMembers($('#members'), createMemberMarkup(members));
});
}
// Hook the document ready event
$(document).ready(function () {
showMembers();
});
</script>
代码的第二个版本比原始版本长,虽然没有注释,但更清晰。将代码分离为单独的函数可以很容易地理解它在做什么。
在一个完整的 MVC 应用程序中,我们可以为每个关注点创建单独的类,然后将每个函数移动到它所属的类中。但我们不需要这么正式。首先,我们没有 JavaScript 中的类,但是我们有非常强大的对象,可以包含函数。因此,让我们再试一次,这次使用 JavaScript 对象将代码打包:
<script type="text/javascript">
var members = {
showHttpError: function (error) {
alert("Error: " + error.status + ": " + error.statusText);
},
get: function (errorHandler) {
return $.getJSON('users.json').fail(errorHandler);
},
createMarkup: function (members) {
var index, htmlTemplate = '';
members.forEach(function (member) {
htmlTemplate += '<li>' + member.name.first + '</li>';
});
return htmlTemplate;
},
render: function ($ptr, membersMarkup) {
$ptr.html(membersMarkup);
},
show: function () {
var that = this;
that.get(that.showHttpError)
.then(function (members) {
that.render($('#members'), that.createMarkup(members));
});
}
};
// Hook the document ready event
$(document).ready(function () {
members.show();
});
</script>
在这个版本中,我们将所有代码捆绑到成员对象中。这使得我们可以轻松地移动代码,并帮助我们将其视为一个单一的、内聚的单元。将代码放入对象中也可以使用this
构造。这是否是一种进步是有争议的。我见过许多 JavaScript 开发人员将所有对象名称以长格式输入,只是为了避免考虑这一点。请注意,我们还缩短了大多数方法名称。在许多方法名称中使用member
一词是多余的,因为对象的名称是member
。另外,请注意,我们现在将函数称为“方法”。当函数是类或对象的一部分时,它就是一个方法。
通过这本书,我们使用了事件,但它们总是由 jQuery 或浏览器触发,而不是由我们触发。现在,这种情况即将改变。事件对于代码的解耦非常有用。想象一个场景,在这个场景中,您和另一个开发人员正在一起开发一个应用程序。另一个开发人员将为您的代码提供数据。与前面通过单个 Ajax 调用提供数据的示例不同,数据源将间歇性可用。您的代码,即数据读取器,将在数据可用时将其呈现给页面。我们有几种方法可以做到这一点。
数据源可以提供一种轮询方法供我们调用。我们将反复调用此方法,直到源为我们提供一些新数据。然而,我们知道投票效率很低。在我们调用轮询服务的大部分时间里,不会有任何新数据,并且我们会在不返回任何新数据的情况下消耗 CPU 周期。
我们可以为数据源提供一个方法,它可以在有新数据时调用该方法。这解决了效率低下的问题,因为只有在有新数据时才会调用我们的代码。想想这样做会带来怎样的维护噩梦。当数据源只向一个依赖模块提供数据时,这似乎很容易。但是,如果第二个或第三个模块也需要数据源呢?我们必须不断更新数据源模块。并且,如果数据读取器模块中的任何一个发生变化,则可能还需要更新数据源。我们该怎么办?
我们可以使用自定义事件。使用自定义事件会松开耦合。双方都不直接打电话给对方。相反,当数据源有新数据时,它会触发自定义事件。任何想要新数据的数据读取器都只需为自定义事件注册一个处理程序。
下面是使用自定义事件的一些好处。首先,联轴器松动。其次,可以为事件注册的数据读取器的数量没有限制。第三,如果事件是在没有侦听器的情况下触发的,则不会有任何中断。最后,如果有一堆读卡器注册了,但是没有新的数据出现,那么什么都不会发生。
对于自定义事件,我们必须小心的一件事是,记住在完成事件处理程序时释放它们。如果我们忘记了,可能会发生两件事。首先,我们可能会导致内存泄漏。内存泄漏是指 JavaScript 无法释放内存块,因为在我们的例子中,事件处理程序仍然持有对它的引用。
随着时间的推移,浏览器将开始变得越来越迟钝,因为它在最终崩溃之前就开始耗尽内存。其次,我们的事件处理程序可能被调用太多次。当钩住事件的代码被多次调用而事件从未被释放时,就会发生这种情况。在您知道之前,事件处理程序会被调用两次、三次或更多次,而不是只调用一次。
定制事件的最后一个好处是,我们已经知道实现它们所需的大部分内容。为了执行常规浏览器事件,代码与我们所学的只是略有不同。让我们先来看一个简单的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="../libs/jquery-2.1.1.js"></script>
<title>Chapter 8 Simple Custom Events</title>
</head>
<body>
<button id="eventGen">Trigger Event</button>
<button id="releaseEvent">Release Event Handlers</button>
<div id="console-out" style="width: 100%">
</div>
<script>
var customEvent = "customEvent";
function consoleOut(msg){
console.info(msg);
$('#console-out').append("<p>" + msg + "</p>");
}
function customEventHandler1(eventObj) {
return consoleOut("Custom Event Handler 1");
}
function customEventHandler2(eventObj) {
return consoleOut("Custom Event Handler 2");
}
$(document).ready(function () {
// Notice we can hook the event before it is called
$(document).on(customEvent, customEventHandler1);
$(document).on(customEvent, customEventHandler2);
// generate a new custom event for each click
$('#eventGen').on('click', function () {
$.event.trigger(customEvent);
});
// once we release the handlers, they are not called again
$('#releaseEvent').on('click', function () {
consoleOut("Handlers released");
$(document).off(customEvent, customEventHandler1);
$(document).off(customEvent, customEventHandler2);
});
});
</script>
</body>
</html>
在本例中,我们在页面上放置两个按钮。第一个按钮触发事件,第二个按钮释放事件处理程序。按钮下方是用于显示消息的宽<div>
。
脚本标记中的代码首先创建一个名为customEvent
的变量,该变量保存自定义事件的名称。活动的名称由您决定,但我建议使用包含贵公司反向域名的内容,因为您不希望将来的浏览器版本破坏您的代码,因为它使用相同的活动名称。然后,我们有两个事件处理程序,它们没有做任何特别有趣的事情。
最后,在 DocumentReady 事件处理程序中,我们钩住定制事件两次。我们使用第一个按钮的 click 事件触发定制事件,使用第二个按钮的 click 事件释放处理程序。
在本例中,我们没有展示的一件事是如何将数据传递给自定义事件处理程序代码。幸运的是,这并不难,所以让我们用另一个例子来说明:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="../libs/jquery-2.1.1.js"></script>
<title>Chapter 8 Custom Events 2</title>
</head>
<body>
<div> <button id="stop">Stop Polling</button> <span id="lastNewUser"></span></div>
<hr/>
<div id="showUsers">
</div>
<script>
var users = [];
var newUser = "com.therockncoder.new-user";
function showMessage(msg) {
$('#lastNewUser').show().html(msg + ' welcome to the group!').fadeOut(3000);
}
function init() {
function getUserName(user) {
return user.name.first + " " + user.name.last;
}
function showUsers(users) {
var ndx, $ptr = $('#showUsers');
$ptr.html("");
for (ndx = 0; ndx < users.length; ndx += 1) {
$ptr.append('<p>' + getUserName(users[ndx]) + '</p>')
}
}
function addNewNameToList(eventObj, user, count) {
console.info("Add New Name to List = " + getUserName(user));
users.push(user);
showUsers(users);
}
function welcomeNewUser(eventObj, user) {
var name = getUserName(user);
showMessage(name);
console.info("got New User " + name);
}
$(document).on(newUser, addNewNameToList);
$(document).on(newUser, welcomeNewUser);}
function startTimer() {
init();
var handleId = setInterval(function () {
$.getJSON('http://api.randomuser.me/?format=json&nat=us').success(function (data) {
var user = data.results[0].user;
$.event.trigger(newUser, [user]);
});
}, 5000);
$('#stop').on('click', function(){
clearInterval(handleId);
showMessage('Cancelled polling');
});
}
$(document).ready(startTimer);
</script>
</body>
</html>
让我们浏览一下代码,以确保我们了解它在做什么。
用户界面由一个标记为停止的单个按钮和一个水平标尺组成。成员将显示在水平规则下方,消息将显示在按钮旁边。
在脚本标记中,代码首先为用户创建一个空数组,并创建一个字符串来保存自定义事件的名称。代码由三个主要功能组成:showMessage
、init
和startTimer
。当 document ready 事件触发时,它调用init
方法,然后调用startTimer
方法。
startTimer
方法每隔 5 秒对随机用户 web 服务进行重复调用。每次调用它时,它都会给我们一个随机的新用户。在init
方法中,我们为自定义事件建立了两个处理程序:addNewNameToList
和welcomeNewUser
。每个方法都获取事件提供的用户数据,并对其执行不同的操作。要停止示例程序,请单击停止按钮,它将清除间隔计时器。
单元测试是一个有趣的话题。似乎每个人都同意编写测试是件好事,但实际上很少有人这么做。对单元测试的全面检查需要一整本书,但是,希望我们能够涵盖足够多的单元测试内容,向您展示如何将它们添加到代码中,并解释为什么应该这样做。
我首选的单元测试框架是 Jasmine,http://jasmine.github.io/ 。这并不是要轻视任何其他可用的优秀框架,但 Jasmine 在前端和后端工作,在浏览器和命令行中工作,是主动维护的,并且可以从测试框架中完成所有需要的工作。请记住,虽然代码是为 Jasmine 编写的,但这些原则可以应用于任何 JavaScript 单元测试框架。在研究如何编写单元测试之前,我们应该从为什么要编写单元测试开始。
起初,JavaScript 很少在网站上使用。当它被使用时,它只是做一些小事情,如果 JavaScript 被禁用的话,这些小事情就没有了。因此,清单很小,主要是客户端表单验证和简单的动画。
如今,情况发生了变化。许多网站在禁用 JavaScript 的情况下根本无法运行。其他人将使用有限的功能运行,通常会显示缺少的 JavaScript 警告。因此,既然我们如此依赖它,测试它是有意义的。
我们编写单元测试有三个主要原因:验证我们的应用程序是否正常运行,验证它在修改后是否继续正常工作,最后,在使用测试驱动开发(TDD)或行为驱动开发时指导我们的开发(BDD)。我们将关注第一点,验证我们的应用程序是否正常运行。我们将使用 Jasmine 行为驱动的 JavaScript 测试框架来实现这一点。Jasmine 从命令行或浏览器运行。我们将仅从浏览器使用它。
为了使我们的示例简短,我们将运行一个典型的单元测试示例:算术计算器。它将能够加、减、乘和除两个数字。为了测试计算器,我们需要三个文件:SpecRunner.html
、calculator.js
和calculator-spec.js
。SpecRunner.html
加载其他两个文件和 Jasmine 框架。以下是SpecRunner.html
文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.2.0</title>
<!-- Jasmine files -->
<link rel="shortcut icon" type="image/png" href="../libs/jasmine-2.3.4/jasmine_favicon.png">
<link rel="stylesheet" href="../libs/jasmine-2.3.4/jasmine.css">
<script src="../libs/jasmine-2.3.4/jasmine.js"></script>
<script src="../libs/jasmine-2.3.4/jasmine-html.js"></script>
<script src="../libs/jasmine-2.3.4/boot.js"></script>
<!-- System Under Test -->
<script src="calculator.js"></script>
<!-- include spec files here... -->
<script src="spec/calculator-spec.js"></script>
</head>
<body>
</body>
</html>
在SpecRunner.html
中没有什么特别具有挑战性。除了 Jasmine 运行所需的文件之外,您还可以放置应用程序,而不是全部,只放置单元测试能够运行所需的部分和单元测试文件。按照传统,单元测试文件与正在测试的文件具有相同的名称,末尾添加了-spec
。
以下是calculator.js
文件:
var calculator = {
add: function(a, b){
return a + b;
},
subtract: function(a, b){
return a -b;
},
multiply: function(a, b){
return a * b;
},
divide: function(a, b){
return a / b;
}
};
我们的计算器简单易懂。它是一个 JavaScript,有四个函数:加、减、乘和除。测试它的功能应该同样简单,事实上也是如此。以下是测试计算器的spec
文件:
describe("Calculator", function() {
it("can add numbers", function(){
expect(calculator.add(12, 3)).toEqual(15);
expect(calculator.add(3000, -100)).toEqual(2900);
});
it("can subtract numbers", function(){
expect(calculator.subtract(12, 3)).toEqual(9);
});
it("can multiply numbers", function(){
expect(calculator.multiply(12, 3)).toEqual(36);
});
it("can divide numbers", function(){
expect(calculator.divide(12, 3)).toEqual(4);
});
});
Jasmine 的单元测试框架围绕三种方法工作:describe
、it
和expect
。describe
是一个包含测试套件的全局函数。它的第一个参数是一个包含测试套件名称的字符串。第二个参数是作为测试套件的函数。接下来是it
。
与describe
类似,it
是 Jasmine 中的一个全局函数,it
持有一个规范,或 spec。传递给it
的参数也是一个字符串和一个函数。按照传统,字符串的编写方式是,当使用it
读取时,它会完成一个描述规范的句子。该函数使用一个或多个期望值执行测试。
使用expect
Jasmine 函数创建期望。expect
在其他单元测试框架中相当于assert
。它被传递一个参数,这个参数是要测试的值,它调用 Jasmine 中的实际值。与期望相关联的是matcher
函数。matcher
函数传递了预期值。
茉莉花中expect
和matcher
功能的组合是它最美丽的东西之一。如果书写正确,它们读起来就像一个英语句子。这使得编写测试和以后读取测试都很容易。Jasmine 还附带了大量匹配器,可以帮助您准确地写出所需的期望值。还有一个not
操作符,它将反转任何matcher function
的逻辑。
要运行示例测试,只需启动SpecRunner.html
页面。它将加载 Jasmine、我们想要测试的代码部分和规范。在单元测试传统中,页面要么是绿色的,这意味着我们所有的规范都通过了,要么是红色的,这意味着至少有一个规范失败了。
Jasmine 的目标是帮助我们找到并修复损坏的测试。例如,如果我们的 add 规范失败了,我们会看到测试套件的名称和失败的规范一起以红色显示:“Calculator can add numbers.”下面是失败的预期。它将向我们展示它的价值以及我们所期望的价值。
在示例代码中,我们通过调用describe
方法来创建测试套件。我们有规格说明,每个都测试计算器的一个方面。请注意我们的规范是如何措辞的,以便它们读起来像句子一样。我们的第一个规范是“它可以添加数字”。当我们的测试通过时,我们将看到测试套件显示单词“Calculator”和下面列出的每个规范,说明测试内容。
茉莉还有setup
和teardown
功能。setup
函数在测试套件中的每个规范运行之前执行。teardown
函数在每个规范运行后运行。我们在示例代码中没有使用这两种方法,但它们在更复杂的测试中非常有用,特别是当需要准备对象并在之后清理对象时。
Jasmine 是一个完整的单元测试框架。我们几乎没有触及它能做什么的表面。另外,请记住,虽然从浏览器运行 Jasmine 很方便,但从命令行运行它会带来更多的功能。然后,它能够集成到您的网站的建设过程。但这只是一个介绍;为了充分理解 Jasmine 如何帮助您编写更干净的代码,并帮助您测试 jQuery,您需要在代码中尝试它。
在这一章中我们讨论了很多想法。开发人员经常忘记 JavaScript 是一种面向对象的语言,但它不是一种基于类的语言。要记住的最重要的事情之一是分离关注点的技术。它是保持代码可理解性和可维护性的基石。通过学习 SoC,我们了解了将代码分解为逻辑单元和使用事件来解耦代码的主题。在本章结束时,我们学习了如何使用流行的开源工具 Jasmine 对代码进行单元测试。
现在,我们已经学会了如何组织代码,现在是时候将注意力转向人们对 jQuery 的其他抱怨:性能。在下一章中,我们将学习如何度量代码的性能,以及我们可以做哪些简单的事情来加速代码。相当多的博客文章表明,jQuery 应该为许多网站的速度缓慢负责。如果您对 JavaScript 或 jQuery 不太了解,很容易得出这个结论。幸运的是,学习一些可以显著提高 jQuery 代码速度的经验法则并不困难。