CoffeeScript-jQuery-Rails-和-Node-编程指南-全-
CoffeeScript、jQuery、Rails 和 Node 编程指南(全)
原文:
zh.annas-archive.org/md5/9466a6beba70d7161a7e3c71ee57e6cc译者:飞龙
前言
JavaScript 是由 Brendan Eich 在 1995 年左右在 Netscape 工作时编写的一种古怪的小语言。它是第一种基于浏览器的脚本语言,当时只能在 Netscape Navigator 中运行,但它最终进入了大多数其他 Web 浏览器。当时,网页几乎完全由静态标记组成。JavaScript(最初命名为 LiveScript)的出现是为了满足使页面动态化以及将完整脚本语言的力量带给浏览器开发者的需求。
语言的设计决策很大程度上是由对简单性和易用性的需求驱动的,尽管在当时,其中一些决策是出于 Netscape 的纯粹营销原因。选择“JavaScript”这个名字是为了将其与 Sun 微系统公司的 Java 联系起来,尽管 Sun 实际上与此无关,而且它在概念上与其名称相似之处甚少。
除了一个方面,即它的语法大部分是从 Java、C 和 C++ 中借用的,以便让来自这些语言的程序员感到熟悉。但尽管看起来相似,实际上它是一个完全不同的动物,并且与更奇特的语言(如 Self、Scheme 和 Smalltalk)具有相似的特征。其中包括动态类型、原型继承、一等函数和闭包。
因此,我们最终得到了一种看起来与当时的主流语言非常相似的语言,并且可以被诱导出类似的行为,但它的核心思想却截然不同。这导致它在很多年里被严重误解。许多程序员从未将其视为一种“严肃”的编程语言,因此在编写浏览器代码时,没有应用几十年来积累的许多最佳开发实践。
那些深入研究这种语言的人肯定会发现很多奇怪之处。Eich 本身也承认,这种语言大约在 10 天内就完成了原型设计,尽管他所创造出的东西令人印象深刻,但 JavaScript 并非没有(许多)缺陷。这些缺陷也并没有真正帮助提升其知名度。
尽管存在所有这些问题,JavaScript 仍然成为了世界上使用最广泛的编程语言之一,如果不是仅仅因为互联网的爆炸式增长和 Web 浏览器的普及。跨众多浏览器的支持似乎是一件好事,但它也由于语言和 DOM 实现上的差异而造成了混乱。
大约在 2005 年,人们创造了 AJAX 这个术语来描述一种由浏览器中引入的 XMLHTTPRequest 对象所实现的 JavaScript 编程风格。这意味着开发者可以编写客户端代码,直接通过 HTTP 与服务器通信,并在不重新加载页面的情况下更新页面元素。这实际上标志着语言历史上的一个转折点。突然之间,它开始被用于“严肃”的 Web 应用程序中,人们开始以不同的眼光看待这种语言。
2006 年,约翰·雷斯吉(John Resig)将 jQuery 发布到世界。它旨在简化客户端脚本、DOM 操作和 AJAX,以及抽象化浏览器之间的许多不一致性。它成为许多 JavaScript 程序员的必备工具。到目前为止,它被世界上排名前 10,000 的网站中的 55%所使用。
2009 年,瑞安·达尔(Ryan Dahl)创建了 Node.js,这是一个基于 Google V8 JavaScript 引擎的事件驱动网络应用程序框架。它迅速变得非常受欢迎,尤其是在编写 Web 服务器应用程序方面。其成功的一个大因素是您现在可以在服务器上以及浏览器上编写 JavaScript。围绕这个框架出现了一个庞大而杰出的社区,目前 Node.js 的前景看起来非常光明。
2010 年初,杰里米·阿什肯纳斯(Jeremy Ashkenas)创建了 CoffeeScript,这是一种编译成 JavaScript 的语言。其目标是创建更干净、更简洁、更符合语言习惯的 JavaScript,并使其更容易使用语言更好的特性和模式。它去除了 JavaScript 中许多语法冗余,减少了行噪声,通常创建出更短、更清晰的代码。
受到 Ruby、Python 和 Haskell 等语言的影响,它借鉴了这些语言的一些强大而有趣的功能。尽管它看起来可能相当不同,但 CoffeeScript 代码通常与其生成的 JavaScript 非常接近。它迅速成为一夜之间的成功,很快被 Node.js 社区采用,并被包含在 Ruby on Rails 3.1 中。
布兰登·艾奇也对 CoffeeScript 表示了钦佩,并将其作为他希望在未来 JavaScript 版本中看到的一些事物的例子。
本书不仅介绍了这门语言,还探讨了为什么您应该在可能的情况下用 CoffeeScript 而不是 JavaScript 来编写代码。它还探讨了使用 jQuery 和 Ruby on Rails 在浏览器中使用 CoffeeScript,以及在服务器上使用 Node.js。
本书涵盖的内容
第一章, 为什么选择 CoffeeScript?,介绍了 CoffeeScript 并深入探讨了它与 JavaScript 之间的区别,特别是 CoffeeScript 旨在改进的 JavaScript 部分。
第二章,运行 CoffeeScript,简要介绍了 CoffeeScript 的堆栈及其通常的打包方式。您将学习如何使用 Node.js 和 npm 在 Windows、Mac 和 Linux 上安装 CoffeeScript。您还将了解 CoffeeScript 编译器(coffee),以及熟悉一些有助于日常 CoffeeScript 开发的工具和资源。
第三章,CoffeeScript 和 jQuery,介绍了使用 jQuery 和 CoffeeScript 进行客户端开发。我们还开始使用这些技术实现本书的示例应用程序。
第四章,CoffeeScript 和 Rails,首先简要概述了 Ruby on Rails 及其与 JavaScript 框架的历史。我们介绍了 Rails 3.1 中的 Asset Pipeline 以及它是如何与 CoffeeScript 和 jQuery 集成的。然后我们转向使用 Rails 为我们的示例应用添加后端。
第五章,CoffeeScript 和 Node.js,首先简要概述了 Node.js、其历史和哲学。然后演示了使用 Node.js 用 CoffeeScript 编写服务器端代码是多么容易。然后我们使用 WebSockets 和 Node.js 实现了示例应用的最后一部分。
您需要这本书的内容
要使用这本书,您需要一个运行 Windows、Mac OS X 或 Linux 的计算机和一个基本的文本编辑器。在整个书中,我们将从互联网下载一些我们需要的软件,所有这些软件都将免费且为开源。
这本书面向的对象
这本书是为希望了解更多关于 CoffeeScript 的现有 JavaScript 程序员而写的,或者是有一些编程经验并希望学习更多使用 CoffeeScript 进行 Web 开发的人。它还作为 jQuery、Ruby on Rails 和 Node.js 的绝佳入门书籍。即使您有使用一个或多个这些框架的经验,这本书也会向您展示如何使用 CoffeeScript 使您的体验变得更好。
习惯用法
在这本书中,您会发现多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词如下所示:"您会看到if语句的子句不需要被括号括起来"。
代码块如下设置:
gpaScoreAverage = (scores...) ->
total = scores.reduce (a, b) -> a + b
total / scores.length
当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
create: (e) ->
$input = $(event.target)
val = ($.trim $input.val())
任何命令行输入或输出都如下所示:
coffee -co public/js -w src/
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"页脚将有一个清除已完成按钮"。
注意
警告或重要注意事项如下所示。
小贴士
小技巧和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者的反馈对我们开发您真正能从中获得最大收益的标题非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有多个方面可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在 www.PacktPub.com 的账户下载您购买的任何 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.PacktPub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/support,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从 www.packtpub.com/support 选择您的标题来查看任何现有勘误。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面提供的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章。为什么选择 CoffeeScript?
CoffeeScript 编译成 JavaScript 并紧密遵循其惯例。完全有可能将任何 CoffeeScript 代码重写为 JavaScript,而不会看起来有太大差异。那么,为什么你想使用 CoffeeScript 呢?
作为一名经验丰富的 JavaScript 程序员,你可能会认为学习一门全新的语言根本不值得花费时间和精力。
但最终,代码是为程序员而写的。编译器并不关心代码的外观或其含义的清晰度;要么它能运行,要么就不能。我们作为程序员的目标是编写易于表达、易于阅读、引用、理解、修改和重写的代码。
如果代码过于复杂或充满了不必要的仪式,它将更难理解和维护。CoffeeScript 让我们能够清晰地表达思想,并编写更易于阅读的代码。
认为 CoffeeScript 与 JavaScript 非常不同是一种误解。虽然这里和那里可能有一些极端的语法差异,但本质上,CoffeeScript 被设计用来打磨 JavaScript 的粗糙边缘,揭示隐藏在下面的美丽语言。它引导程序员走向 JavaScript 所谓的“优点”,并对构成良好 JavaScript 的因素持有强烈的观点。
CoffeeScript 社区的一个格言是:“它只是 JavaScript”,我也发现,真正理解这门语言的最佳方式是查看它生成的输出,这实际上是一段非常可读且易于理解的代码。
在本章中,我们将突出两种语言之间的某些差异,通常集中在 JavaScript 中 CoffeeScript 试图改进的地方。
以这种方式,我不仅想给你一个关于语言主要功能的概述,还想让你在使用 CoffeeScript 时,一旦开始更频繁地使用它,就能从其生成的代码中调试 CoffeeScript,以及能够转换现有的 JavaScript。
让我们从 CoffeeScript 在 JavaScript 中修复的一些事情开始。
CoffeeScript 语法
CoffeeScript 的一个优点是,你通常会编写比在 JavaScript 中更短、更简洁的程序。这其中的部分原因是因为语言中添加了强大的功能,但它也对 JavaScript 的一般语法进行了一些调整,使其变得更加优雅。它去掉了所有分号、花括号和其他通常导致 JavaScript 中大量“行噪声”的冗余。
为了说明这一点,让我们看一个例子。以下表格的左侧是 CoffeeScript;右侧是生成的 JavaScript:
| CoffeeScript | JavaScript |
|---|
|
fibonacci = (n) ->
return 0 if n == 0
return 1 if n == 1
(fibonacci n-1) + (fibonacci n-2)
alert fibonacci 10
|
var fibonacci;
fibonacci = function(n) {
if (n === 0) {
return 0;
}
if (n === 1) {
return 1;
}
return (fibonacci(n - 1)) + (fibonacci(n - 2));
};
alert(fibonacci(10));
|
要运行本章中的代码示例,你可以使用优秀的 Try CoffeeScript 在线工具,网址为 coffeescript.org。它允许你输入 CoffeeScript 代码,然后会在侧边栏中显示等效的 JavaScript 代码。你还可以直接从浏览器中运行代码(通过点击左上角的 运行 按钮)。如果你更喜欢在计算机上安装 CoffeeScript 后先运行示例,请跳到下一章,然后在安装好 CoffeeScript 后返回。此工具在以下屏幕截图中有展示:

起初,这两种语言可能看起来相当不同,但希望随着我们逐步了解这些差异,你会发现它们仍然是 JavaScript,只是进行了一些小的调整和很多语法糖。
分号和大括号
正如你可能已经注意到的,CoffeeScript 去掉了行尾的所有尾随分号。如果你想在一行上放置两个表达式,仍然可以使用分号。它还去掉了代码块(如 if 语句、switch 和 try..catch 块)的括号(也称为花括号)。
空白
你可能想知道解析器是如何确定你的代码块开始和结束的位置的。CoffeeScript 编译器通过使用语法空白来实现这一点。这意味着缩进被用来定义代码块,而不是大括号。
这可能是该语言最具争议性的特性之一。如果你这么想,在几乎所有语言中,程序员都倾向于使用代码块的缩进来提高可读性,那么为什么不将其作为语法的一部分呢?这不是一个新概念,主要是从 Python 借鉴的。如果你有任何关于显著空白语言的体验,你将不会对 CoffeeScript 的缩进有任何困难。
如果你没有这样做,可能需要一些时间来适应,但这样做可以使代码读起来非常清晰,易于扫描,同时还能减少很多按键。我敢打赌,如果你花时间克服一些最初的顾虑,你可能会逐渐爱上块缩进。
注意
块可以用制表符或空格缩进,但要注意保持一致性,使用其中一种,否则 CoffeeScript 将无法正确解析你的代码。
括号
你会看到 if 语句的子句不需要用括号括起来。对于 alert 函数也是如此;你会看到单个字符串参数在函数调用后没有括号。在 CoffeeScript 中,带有参数的函数调用、if..else 语句的子句以及 while 循环的子句中的括号都是可选的。
虽然带有参数的函数不需要括号,但在可能存在歧义的情况下使用它们仍然是一个好主意。CoffeeScript 社区提出了一种很好的习惯:将整个函数调用用括号括起来。以下表格展示了 CoffeeScript 中 alert 函数的使用:
| CoffeeScript | JavaScript |
|---|
|
alert square 2 * 2.5 + 1
alert (square 2 * 2.5) + 1
|
alert(square(2 * 2.5 + 1));
alert((square(2 * 2.5)) + 1);
|
函数是 JavaScript 中的第一类对象。这意味着当你不使用括号引用函数时,它将返回函数本身,作为一个值。因此,在 CoffeeScript 中,当你调用无参数的函数时,仍然需要添加括号。
通过对 JavaScript 语法进行这些小的调整,CoffeeScript 可以显著提高代码的可读性和简洁性,并且可以节省你很多键盘输入。
但它还有一些其他的技巧。大多数写过相当多 JavaScript 的程序员可能会同意,最频繁输入的短语之一可能是函数定义 function(){}。函数确实是 JavaScript 的核心,尽管它有很多瑕疵。
CoffeeScript 有很好的函数语法
你可以将函数作为第一类对象来处理,同时也能创建匿名函数,这是 JavaScript 最强大的功能之一。然而,语法可能非常尴尬,使得代码难以阅读(尤其是当你开始嵌套函数时)。但 CoffeeScript 有一个解决方案。看看下面的代码片段:
| CoffeeScript | JavaScript |
|---|
|
-> alert 'hi there!'
square = (n) -> n * n
|
var square;
(function() {
return alert('hi there!');
});
square = function(n) {
return n * n;
};
|
在这里,我们创建了两个匿名函数,第一个只是显示一个对话框,第二个将返回其参数的平方。你可能已经注意到了那个有趣的 -> 符号,并且可能已经猜出了它的作用。是的,这就是在 CoffeeScript 中定义函数的方法。我遇到过这个符号的几个不同名称,但似乎最被接受的术语是一个瘦箭头或者只是一个箭头。这与我们将要讨论的粗箭头相反。
注意,第一个函数定义没有参数,因此我们可以省略括号。第二个函数有一个参数,它被括号包围,放在 -> 符号前面。根据我们现在的知识,我们可以制定一些简单的替换规则,将 JavaScript 函数声明转换为 CoffeeScript。它们如下:
-
将
function关键字替换为-> -
如果函数没有参数,可以省略括号
-
如果函数有参数,将整个参数列表(带括号)放在
->符号前面 -
确保函数体正确缩进,然后删除包围的大括号
不需要 return
您可能已经注意到,在这两个函数中,我们都省略了 return 关键字。默认情况下,CoffeeScript 将返回函数中的最后一个表达式。它将在所有执行路径上尝试这样做。CoffeeScript 会尝试将任何语句(返回无值的代码片段)转换为返回值的表达式。CoffeeScript 程序员通常会通过说“一切都是表达式”来引用这种语言特性。
这意味着您不再需要键入 return,但请记住,在许多情况下,这可能会微妙地改变您的代码,因为您将始终返回某些内容。如果您需要在函数的最后一条语句之前从函数中返回一个值,您仍然可以使用 return。
函数参数
函数参数也可以接受一个可选的默认值。在下面的代码片段中,您将看到指定的可选值是在生成的 JavaScript 的主体中赋值的:
| CoffeeScript | JavaScript |
|---|
|
square = (n=1) ->
alert(n * n)
|
var square;
square = function(n) {
if (n == null) {
n = 1;
}
return alert(n * n);
};
|
在 JavaScript 中,每个函数都有一个类似数组的结构,称为 arguments,每个传递给函数的参数都有一个索引属性。您可以使用 arguments 将可变数量的参数传递给函数。每个参数都将是一个 arguments 的元素,因此您不需要通过名称引用参数。
虽然 arguments 对象在某种程度上类似于数组,但它实际上不是一个“真正的”数组,并且缺少大多数标准数组方法。通常,您会发现 arguments 并不提供检查和操作其元素所需的功能,就像它们与数组一起使用时那样。
这迫使许多程序员使用一种技巧,通过使 Array.prototype.slice 复制 argument 对象元素,或者使用 jQuery.makeArray 方法创建一个标准数组,然后可以像正常数组一样使用。
CoffeeScript 从表示为 splats 的参数中借用创建数组的模式,用三个点 (...) 表示。这些在下面的代码片段中显示:
CoffeeScript:
gpaScoreAverage = (scores...) ->
total = scores.reduce (a, b) -> a + b
total / scores.length
alert gpaScoreAverage(65,78,81)
scores = [78, 75, 79]
alert gpaScoreAverage(scores...)
JavaScript:
var gpaScoreAverage, scores,
__slice = [].slice;
gpaScoreAverage = function() {
var scores, total;
scores = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
total = scores.reduce(function(a, b) {
return a + b;
});
return total / scores.length;
};
alert(gpaScoreAverage(65, 78, 81));
scores = [78, 75, 79];
alert(gpaScoreAverage.apply(null, scores));
注意,在函数定义中,参数后面跟着 ...。这告诉 CoffeeScript 允许变量参数。函数可以通过参数列表或跟在 ... 后面的数组来调用。
var 关键字去哪里了?
在 JavaScript 中,您通过在声明前缀 var 关键字来创建局部变量。如果您省略它,变量将在全局作用域中创建。
您将在这些示例中看到,我们不需要使用 var 关键字,并且 CoffeeScript 在生成的 JavaScript 中在函数顶部创建了实际的变量声明。
如果您是经验丰富的 JavaScript 开发者,您可能会想知道您将如何创建全局变量。简单的答案是您不能。
许多人(可能包括 CoffeeScript 的作者)可能会认为这是一个好事,因为在大多数情况下应该避免全局变量。不过,别担心,我们将在稍后讨论创建顶级对象的方法。但这也巧妙地引出了 CoffeeScript 的另一个好处。
CoffeeScript 处理作用域更好
看一下下面的 JavaScript 代码片段。注意,一个名为salutation的变量在两个地方被定义,在函数内部,以及在函数第一次被调用之后:
| JavaScript |
|---|
|
var greet = function(){
if(typeof salutation === 'undefined')
salutation = 'Hi!';
console.log(salutation);
}
greet();
salutation = "Bye!";
greet();
|
在 JavaScript 中,当你省略声明变量时的var关键字,它立即成为全局变量。全局变量在所有作用域中都是可用的,因此可以从任何地方覆盖,这通常会导致混乱。
在前面的例子中,greet函数首先检查salutation变量是否已定义(通过检查typeof是否等于undefined,这是在 JavaScript 中检查变量是否已定义的常见方法)。如果之前没有定义,它将不使用var关键字创建它。这将立即将变量提升到全局作用域。我们可以在片段的其余部分看到这一点的后果。
第一次调用greet函数时,字符串Hi!将被记录。在问候语更改后再次调用函数,控制台将记录Bye!。因为变量被泄露为全局变量,其值在函数作用域之外被覆盖。
这种语言的奇怪“特性”已经成为许多疲惫的程序员头疼的原因,因为他们忘记在某处包含一个var关键字。即使你打算声明一个全局变量,这通常被认为是一个糟糕的设计选择,这就是为什么 CoffeeScript 不允许这样做。
CoffeeScript 会始终向任何变量声明添加var关键字,以确保它不会意外地成为全局声明。实际上,你永远不应该自己输入var,如果这样做,编译器会报错。
顶级变量关键字
当你在 JavaScript 脚本的最顶层声明var时,它仍然在全局范围内可用。当你包含许多不同的 JavaScript 文件时,这也可能造成混乱,因为你可能会覆盖在早期脚本中声明的变量。
在 JavaScript 和随后的 CoffeeScript 中,函数作为闭包工作,这意味着它们创建自己的变量作用域,同时也可以访问它们的封装作用域变量。
经过多年的发展,一个常见的模式开始出现,即库作者将整个脚本包裹在一个匿名闭包函数中,并将它分配给一个单独的变量。
CoffeeScript 编译器做类似的事情,并将脚本包裹在一个匿名函数中,以避免其作用域泄露。在以下示例中,JavaScript 是运行 CoffeeScript 编译器的输出:
| CoffeeScript | JavaScript |
|---|
|
greet = -> salutation = 'Hi!'
|
(var greet;
greet = function() {
var salutation;
return salutation = 'Hi!';
}).call(this);
|
这里您可以看到 CoffeeScript 如何将其函数定义包装在其自己的作用域中。
然而,在某些情况下,您可能希望一个变量在整个应用程序中可用。通常,将属性附加到现有的全局对象可以实现这一点。当您在浏览器中时,您只需在全局 window 对象上创建一个属性。
在浏览器端的 JavaScript 中,window 对象代表一个打开的窗口。它对所有其他对象都是全局可用的,因此可以用作全局命名空间或其他对象的容器。
当我们谈论对象时,让我们谈谈 CoffeeScript 使其变得更好的 JavaScript 的另一个部分:定义和使用对象。
CoffeeScript 有更好的对象语法
JavaScript 语言有一个奇妙而独特的对象模型,但创建对象和从它们继承的语法和语义始终有点繁琐,并且被广泛误解。
CoffeeScript 以简单而优雅的语法清理了这一点,这种语法并没有偏离惯用的 JavaScript 太远。以下代码演示了 CoffeeScript 如何将其类语法编译成 JavaScript:
CoffeeScript:
class Vehicle
constructor: ->
drive: (km) ->
alert "Drove #{km} kilometres"
bus = new Vehicle()
bus.drive 5
JavaScript:
var Vehicle, bus;
Vehicle = (function() {
function Vehicle() {}
Vehicle.prototype.drive = function(km) {
return alert("Drove " + km + " kilometres");
};
return Vehicle;
})();
bus = new Vehicle();
bus.drive(5);
在 CoffeeScript 中,您使用 class 关键字来定义对象结构。在底层,这创建了一个函数对象,其原型上添加了函数方法。constructor: operator 将创建一个构造函数,当您的对象使用 new 关键字初始化时将被调用。
所有其他函数方法都是使用 methodName: () -> 语法声明的。这些方法是在对象的原型上创建的。
注意
您注意到我们的 alert 字符串中的 #{km} 吗?这是字符串插值语法,它借鉴了 Ruby。我们将在本章后面讨论这个问题。
继承
关于对象继承呢?虽然这是可能的,但在 JavaScript 中通常非常痛苦,以至于大多数程序员甚至都不愿意这样做,或者使用具有非标准语义的第三方库。
在这个例子中,您可以看到 CoffeeScript 如何使对象继承变得优雅:
CoffeeScript:
class Car extends Vehicle
constructor: ->
@odometer = 0
drive: (km) ->
@odometer += km
super km
car = new Car
car.drive 5
car.drive 8
alert "Odometer is at #{car.odometer}"
JavaScript:
Car = (function(_super) {
__extends(Car, _super);
function Car() {
this.odometer = 0;
}
Car.prototype.drive = function(km) {
this.odometer += km;
return Car.__super__.drive.call(this, km);
};
return Car;
})(Vehicle);
car = new Car;
car.drive(5);
car.drive(8);
alert("Odometer is at " + car.odometer);
这个例子并不包含编译器将生成的所有 JavaScript 代码,但足以突出有趣的部分。extends 操作符用于设置两个对象及其构造函数之间的继承链。注意使用 super 后调用父类变得多么简单。
如您所见,@odometer 被翻译为 this.odometer。@ 符号只是 this 的快捷方式。我们将在本章后面进一步讨论这个问题。
感到不知所措了吗?
在我看来,class 语法是您在 CoffeeScript 和其编译后的 JavaScript 之间找到的最大差异的地方。然而,大多数时候它都能正常工作,一旦您理解了它,您就很少需要担心细节。
扩展原型
如果你是一位经验丰富的 JavaScript 程序员,仍然喜欢自己完成所有这些工作,你不需要使用 class。CoffeeScript 仍然提供了通过 :: 符号访问原型的便捷快捷方式,这在生成的 JavaScript 中将被 .prototype 替换,如下面的代码片段所示:
| CoffeeScript | JavaScript |
|---|
|
Vehicle::stop=-> alert'Stopped'
|
Vehicle.prototype.stop(function() {
return alert('Stopped');
});
|
CoffeeScript 解决的一些其他问题
JavaScript 有很多其他的小烦恼,CoffeeScript 使其变得更加友好。让我们看看其中的一些。
保留字和对象语法
在 JavaScript 中,你通常会需要使用保留字,或者 JavaScript 中使用的关键字。这通常发生在 JavaScript 中的字面量对象键作为数据,如 class 或 for,然后你需要用引号括起来。CoffeeScript 会自动为你引用保留字,通常你甚至不需要担心它。
| CoffeeScript | JavaScript |
|---|
|
tag =
type: 'label'
name: 'nameLabel'
for: 'name'
class: 'label'
|
var tag;
tag = {
type: 'label',
name: 'nameLabel',
"for": 'name',
"class": 'label'
};
|
注意,我们不需要花括号来创建对象字面量,并且在这里也可以使用缩进。在使用这种样式时,只要每行只有一个属性,我们甚至可以省略尾随逗号。
我们也可以用这种方式编写数组字面量:
| CoffeeScript | JavaScript |
|---|
|
dwarfs = [
"Sneezy"
"Sleepy"
"Dopey"
"Doc"
"Happy"
"Bashful"
"Grumpy"
]
|
var dwarfs;
dwarfs = ["Sneezy", "Sleepy", "Dopey", "Doc", "Happy", "Bashful", "Grumpy"];
|
这些特性结合起来,使得编写 JSON 变得轻而易举。比较以下示例以查看差异:
CoffeeScript:
"firstName": "John"
"lastName": "Smith"
"age": 25
"address":
"streetAddress": "21 2nd Street"
"city": "New York"
"state": "NY"
"postalCode": "10021"
"phoneNumber": [
{"type": "home", "number": "212 555-1234"}
{"type": "fax", "number": "646 555-4567"}
]
JavaScript:
({
"firstName": "John",
"lastName": "Smith",
"age": 25,
"address": {
"streetAddress": "21 2nd Street",
"city": "New York",
"state": "NY",
"postalCode": "10021"
},
"phoneNumber": [
{
"type": "home",
"number": "212 555-1234"
}, {
"type": "fax",
"number": "646 555-4567"
}
]
});
字符串连接
对于处理大量字符串的语言,JavaScript 在构建字符串方面一直做得不太好。变量和表达式值通常被期望插入到字符串的某个位置,这通常是通过使用 + 运算符进行连接来完成的。如果你曾经尝试在字符串中连接几个变量,你会很快知道这很快变得繁重且难以阅读。
CoffeeScript 具有内置的字符串插值语法,这与许多其他脚本语言类似,但它是专门从 Ruby 中借用的。以下代码片段展示了这一点:
| CoffeeScript | JavaScript |
|---|
|
greet = (name, time) ->
"Good #{time} #{name}!"
alert (greet 'Pete', 'morning')
|
var greet;
greet = function(name, time) {
return "Good " + time + " " + name + "!";
};
alert(greet('Pete', 'morning'));
|
你可以在 #{} 内写入任何表达式,其字符串值将被连接。请注意,你只能在双引号字符串 "" 中使用字符串插值。单引号字符串是字面量,将按原样表示。
等价性
JavaScript 中的等价运算符 ==(及其相反的 !=)充满了危险,很多时候并不像你期望的那样工作。这是因为它会在比较之前首先尝试将不同类型的对象强制转换为相同的类型。
它也不是传递的,这意味着它可能根据操作符左右是否是类型而返回不同的 true 或 false 值。请参考以下代码片段:
'' == '0' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
由于其不一致和奇怪的行为,JavaScript 社区中的尊敬成员建议完全避免使用它,而应使用身份运算符,即===来代替。这个运算符在两个对象类型不同时总是返回false,这与许多其他语言中==的工作方式一致。
CoffeeScript 会始终将==转换为===,将!=转换为!===,如下面的实现所示:
| CoffeeScript | JavaScript |
|---|
|
'' == '0'
0 == ''
0 == '0'
false == 'false'
false == '0'
false == undefined
false == null
null == undefined
|
'' === '0';
0 === '';
0 === '0';
false === 'false';
false === '0';
false === void 0;
false === null;
null === void 0;
|
存在运算符
当你在 JavaScript 中尝试检查一个变量是否存在并且有值(不是null或undefined)时,你需要使用这个古怪的习惯用法:
typeof a !== "undefined" && a !== null
CoffeeScript 有一个很好的快捷方式,即存在运算符?,除非变量是undefined或null,否则它将返回false。
| CoffeeScript | JavaScript |
|---|
|
broccoli = true;
if carrots? && broccoli?
alert 'this is healthy'
|
var broccoli;
broccoli = true;
if ((typeof carrots !== "undefined" && carrots !== null) && (broccoli != null)) {
alert('this is healthy');
}
|
在这个例子中,由于编译器已经知道 broccoli 是已定义的,?运算符将只检查它是否有null值,而它将检查carrots是否是undefined以及null。
存在运算符有一个方法调用变体:?.或者简称为“soak”,它将允许你在方法链中吞咽null对象上的方法调用,如下所示:
| CoffeeScript | JavaScript |
|---|
|
street = person?.getAddress()?.street
|
var street, _ref;
street = typeof person !== "undefined" && person !== null ? (_ref = person.getAddress()) != null ? _ref.street : void 0 : void 0;
|
如果链中的所有值都存在,你应该得到预期的结果。如果其中任何一个应该是null或undefined,你将得到一个未定义的值,而不是抛出TypeError。
尽管这是一个强大的技术,但它也可能被轻易滥用,并使代码难以推理。如果你有长的方法链,可能很难确切知道 null 或 undefined 值是从哪里来的。
迪米特法则,一个著名的面向对象设计原则,可以用来最小化这种复杂性并提高代码的解耦。它可以概括如下:
-
你的方法可以直接调用其类中的其他方法
-
你的方法可以直接调用其自己的字段上的方法(但不能调用字段的字段)
-
当你的方法接受参数时,你的方法可以直接调用这些参数上的方法
-
当你的方法创建局部对象时,该方法可以调用这些局部对象上的方法
注意
虽然这并不是一个“严格的法律”,因为它不应该被违反,但它更类似于自然法则,即遵循这个法则的代码也往往更加简单和松散耦合。
现在我们已经花了一些时间来探讨 JavaScript 的一些不足和烦恼,CoffeeScript 是如何解决的,让我们来关注一些 CoffeeScript 添加的其他强大功能;一些是从其他脚本语言借用的,一些是语言独有的。
列表推导式
在 CoffeeScript 中,遍历集合的方式与 JavaScript 的命令式方法大不相同。CoffeeScript 从函数式编程语言中汲取灵感,使用列表推导式来转换列表,而不是通过迭代元素来遍历。
while 循环
while 循环仍然存在,并且工作方式大致相同,只不过它可以作为一个表达式使用,这意味着它将返回一个值数组:
CoffeeScript:
multiplesOf = (n, times) ->
times++
(n * times while times -= 1 > 0).reverse()
alert (multiplesOf 5, 10)
JavaScript:
var multiplesOf;
multiplesOf = function(n, times) {
times++;
return ((function() {
var _results;
_results = [];
while (times -= 1 > 0) {
_results.push(n * times);
}
return _results;
})()).reverse();
};
alert(multiplesOf(5, 10));
注意,在前面的代码中,while 循环体位于条件之前。这是 CoffeeScript 中的一种常见惯用语,如果循环体只有一行的话。你也可以用 if 语句和列表推导式做同样的事情。
我们可以通过使用 until 关键字稍微提高前面代码的可读性,它基本上是 while 的否定,如下所示:
CoffeeScript:
multiplesOf = (n, times) ->
times++
(n * times until --times == 0).reverse()
alert (multiplesOf 5, 10)
JavaScript:
var multiplesOf;
multiplesOf = function(n, times) {
times++;
return ((function() {
var _results;
_results = [];
while (--times !== 0) {
_results.push(n * times);
}
return _results;
})()).reverse();
};
alert(multiplesOf(5, 10));
for 语句在 CoffeeScript 中的工作方式与 JavaScript 不同。CoffeeScript 用列表推导式替换了它,这些推导式主要借鉴了 Python 语言,并且与你在 Haskell 等函数式语言中找到的构造非常相似。推导式提供了一种更声明式的方式来过滤、转换和聚合集合,或者对每个元素执行操作。最好的说明方式是通过一些示例:
CoffeeScript:
flavors = ['chocolate', 'strawberry', 'vanilla']
alert flavor for flavor in flavors
favorites = ("#{flavor}!" for flavor in flavors when flavor != 'vanilla')
JavaScript:
var favorites, flavor, flavors, _i, _len;
flavors = ['chocolate', 'strawberry', 'vanilla'];
for (_i = 0, _len = flavors.length; _i < _len; _i++) {
flavor = flavors[_i];
alert(flavor);
}
favorites = (function() {
var _j, _len1, _results;
_results = [];
for (_j = 0, _len1 = flavors.length; _j < _len1; _j++) {
flavor = flavors[_j];
if (flavor !== 'vanilla') {
_results.push("" + flavor + "!");
}
}
return _results;
})();
虽然它们相当简单,但推导式具有非常紧凑的格式,并且用很少的代码做了很多事情。让我们将其分解为其独立的各个部分:
[action or mapping] for [selector] in [collection] when [condition] by [step]
推导式最好从右到左阅读,从 in 集合开始。selector 名称是在遍历集合时赋予每个元素的临时名称。for 关键字前面的子句描述了你想如何使用 selector 名称,通过将其作为参数调用方法、选择其上的属性或方法,或者为其赋值。
when 和 by 守卫子句是可选的。它们描述了迭代应该如何被过滤(只有当元素的后续 when 条件为 true 时才会返回元素),或者使用 by 后跟一个数字来选择集合的哪些部分。例如,by 2 将返回所有偶数编号的元素。
我们可以通过使用 by 和 when 重新编写我们的 multiplesOf 函数:
CoffeeScript:
multiplesOf = (n, times) ->
multiples = (m for m in [0..n*times] by n)
multiples.shift()
multiples
alert (multiplesOf 5, 10)
JavaScript:
var multiplesOf;
multiplesOf = function(n, times) {
var m, multiples;
multiples = (function() {
var _i, _ref, _results;
_results = [];
for (m = _i = 0, _ref = n * times; 0 <= _ref ? _i <= _ref : _i >= _ref; m = _i += n) {
_results.push(m);
}
return _results;
})();
multiples.shift();
return multiples;
};
alert(multiplesOf(5, 10));
[0..n*times] 语法是 CoffeeScript 的范围语法,它借鉴了 Ruby。它将创建一个包含从第一个到最后一个数字之间所有元素的数组。当范围有两个点时,它将是包含的,意味着范围将包含指定的起始和结束元素。如果它有三个点(…),它将只包含中间的数字。
当我开始学习 CoffeeScript 时,列表推导是最大的新概念之一。这是一个非常强大的功能,但确实需要一些时间来习惯并学会以列表推导的方式思考。每当您想写一个使用低级 while 循环结构的代码时,请考虑使用列表推导。它们在处理集合时提供了您可能需要的几乎所有功能,并且与内置的 ECMAScript 数组方法(如 .map() 和 .select())相比,它们非常快。
您可以使用列表推导来遍历对象中的键值对,使用 of 关键字,如下面的代码所示:
CoffeeScript:
ages =
john: 25
peter: 26
joan: 23
alert "#{name} is #{age} years old" for name, age of ages
JavaScript:
var age, ages, name;
ages = {
john: 25,
peter: 26,
joan: 23
};
for (name in ages) {
age = ages[name];
alert("" + name + " is " + age + " years old");
}
条件子句和逻辑别名
CoffeeScript 引入了一些非常棒的逻辑和条件功能,其中一些也借鉴了其他脚本语言。unless 关键字是 if 关键字的逆;if 和 unless 可以采用后缀形式,这意味着语句可以放在行的末尾。
CoffeeScript 还为一些逻辑运算符提供了简单的英文别名。它们如下所示:
-
is对应于== -
isnt对应于!= -
not对应于! -
and对应于&& -
or对应于|| -
true也可以表示yes或on -
false可以表示no或off
将所有这些放在一起,让我们看看一些代码来演示它:
CoffeeScript:
car.switchOff() if car.ignition is on
service(car) unless car.lastService() > 15000
wash(car) if car.isDirty()
chargeFee(car.owner) if car.make isnt "Toyota"
JavaScript:
if (car.ignition === true) {
car.switchOff();
}
if (!(car.lastService() > 15000)) {
service(car);
}
if (car.isDirty()) {
wash(car);
}
if (car.make !== "Toyota") {
chargeFee(car.owner);
}
数组切片和拼接
CoffeeScript 允许您使用 .. 和 ... 语法轻松地从数组中提取部分元素。[n..m] 将选择包括 n 和 m 在内的所有元素,而 [n…m] 将仅选择 n 和 m 之间的元素。
[..] 和 […] 都会选择整个数组。它们在以下代码中使用:
| CoffeeScript | JavaScript |
|---|
|
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
alert numbers[0..3]
alert numbers[4...7]
alert numbers[7..]
alert numbers[..]
|
var numbers;
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
alert(numbers.slice(0, 4));
alert(numbers.slice(4, 7));
alert(numbers.slice(7));
alert(numbers.slice(0));
|
CoffeeScript 确实喜欢它的省略号。它们被 splats、范围和数组切片使用。以下是一些快速提示,如何识别它们:如果 … 在函数定义或函数调用的最后一个参数旁边,它是一个 splat。如果它被包含在不是索引数组的方括号中,它是一个范围。如果它是索引数组,它是一个切片。
解构或模式匹配
解构 是一个在许多函数式编程语言中都能找到的强大概念。本质上,它允许您从复杂对象中提取单个值。它可以简单地允许您一次性分配多个值,或者处理返回多个值的函数;如下所示:
CoffeeScript:
getLocation = ->
[
'Chigaco'
'Illinois'
'USA'
]
[city, state, country] = getLocation()
JavaScript:
var city, country, getLocation, state, _ref;
getLocation = function() {
return ['Chigaco', 'Illinois', 'USA'];
};
_ref = getLocation(), city = _ref[0], state = _ref[1], country = _ref[2];
当您运行此代码时,您将得到三个变量 city、state 和 country,它们的值是从 getLocation 函数返回的数组中的相应元素分配的。
您可以使用解构从对象和哈希中提取值。对象中的数据可以嵌套多深没有限制。以下是一个例子:
CoffeeScript:
getAddress = ->
address:
country: 'USA'
state: 'Illinois'
city: 'Chicago'
street: 'Rush Street'
{address: {street: myStreet}} = getAddress()
alert myStreet
JavaScript:
var getAddress, myStreet;
getAddress = function() {
return {
address: {
country: 'USA',
state: 'Illinois',
city: 'Chicago',
street: 'Rush Street'
}
};
};
myStreet = getAddress().address.street;
alert(myStreet);
在这个例子中,{address: {street: ---}} 这一部分描述了你的模式,基本上是找到所需信息的位置。当我们把 myStreet 变量放入我们的模式中,我们告诉 CoffeeScript 将该位置的价值分配给 myStreet。虽然我们可以使用嵌套对象,但我们也可以混合使用解构对象和数组,如下面的代码所示:
CoffeeScript:
getAddress = ->
address:
country: 'USA'
addressLines: [
'1 Rush Street'
'Chicago'
'Illinois'
]
{address:
{addressLines:
[street, city, state]
}
} = getAddress()
alert street
JavaScript:
var city, getAddress, state, street, _ref;
getAddress = function() {
return {
address: {
country: 'USA',
addressLines: ['1 Rush Street', 'Chicago', 'Illinois']
}
};
};
_ref = getAddress().address.addressLines, street = _ref[0], city = _ref[1], state = _ref[2];
alert(street);
在这里,在前面的代码中,我们正在从 addressLines 获取的数组值中提取元素,并给它们命名。
=> 和 @
在 JavaScript 中,this 的值指的是当前正在执行函数的所有者,或者函数是方法的对象。与其他面向对象的语言不同,JavaScript 还有函数不是紧密绑定到对象的概念,这意味着 this 的值可以随意(或意外地)更改。这是语言的一个非常强大的功能,但如果使用不当,也可能导致混淆。
在 CoffeeScript 中,@ 符号是 this 的快捷方式。每当编译器看到类似 @foo 的内容时,它将用 this.foo 替换它。
虽然在 CoffeeScript 中仍然可以使用这个功能,但通常不推荐使用,更习惯使用 @。
在任何 JavaScript 函数中,this 的值是函数附加到的对象。然而,当你将函数传递给其他函数或重新将函数附加到另一个对象时,this 的值将改变。有时这正是你想要的,但通常你希望保持 this 的原始值。
为了这个目的,CoffeeScript 提供了 =>,或称胖箭头,它将定义一个函数,同时捕获 this 的值,以便函数可以在任何上下文中安全地调用。这在使用回调时特别有用,例如在 jQuery 事件处理器中。
以下示例将说明这个想法:
CoffeeScript:
class Birthday
prepare: (action) ->
@action = action
celebrate: () ->
@action()
class Person
constructor: (name) ->
@name = name
@birthday = new Birthday()
@birthday.prepare () => "It's #{@name}'s birthday!"
michael = new Person "Michael"
alert michael.birthday.celebrate()
JavaScript:
var Birthday, Person, michael;
Birthday = (function() {
function Birthday() {}
Birthday.prototype.prepare = function(action) {
return this.action = action;
};
Birthday.prototype.celebrate = function() {
return this.action();
};
return Birthday;
})();
Person = (function() {
function Person(name) {
var _this = this;
this.name = name;
this.birthday = new Birthday();
this.birthday.prepare(function() {
return "It's " + _this.name + "'s birthday!";
});
}
return Person;
})();
michael = new Person("Michael");
alert(michael.birthday.celebrate());
注意,birthday 类上的 prepare 函数接受一个 action 函数作为参数,当生日发生时将被调用。因为我们使用箭头函数传递这个函数,所以它的作用域将固定到 Person 对象。这意味着即使它不存在于运行函数的 Birthday 对象中,我们仍然可以引用 @name 实例变量。
Switch 语句
在 CoffeeScript 中,switch 语句采用不同的形式,看起来不像 JavaScript 的 Java 启发式语法,而更像 Ruby 的 case 语句。你不需要调用 break 来避免跌入下一个 case 条件。
它们具有以下形式:
switch condition
when … then …
….
else …
这里,else 是默认情况。
就像 CoffeeScript 中的其他一切一样,它们是表达式,并且可以将其分配给一个值。
让我们看看一个例子:
CoffeeScript:
languages = switch country
when 'france' then 'french'
when 'england', 'usa' then 'english'
when 'belgium' then ['french', 'dutch']
else 'swahili'
JavaScript:
var languages;
languages = (function() {
switch (country) {
case 'france':
return 'french';
case 'england':
case 'usa':
return 'english';
case 'belgium':
return ['french', 'dutch'];
default:
return 'swahili';
}
})();
CoffeeScript 不强制你添加默认的 else 子句,尽管始终添加一个是一个好的编程实践,以防万一。
连接比较
CoffeeScript 从 Python 中借鉴了链式比较。这些基本上允许你像在数学中那样编写大于或小于的比较,如下所示:
| CoffeeScript | JavaScript |
|---|
|
age = 41
alert 'middle age' if 61 > age > 39
|
var age;
age = 41;
if ((61 > age && age > 39)) {
alert('middle age');
}
|
块字符串、块注释和字符串
大多数编程书籍都以注释开始,我想以注释结束。在 CoffeeScript 中,单行注释以 # 开头。注释不会出现在你的生成输出中。多行注释以 ### 开始和结束,并且它们会被包含在生成的 JavaScript 中。
你可以使用 """ 三重引号来跨多行扩展字符串。
摘要
在本章中,我们从 JavaScript 的角度开始研究 CoffeeScript。我们看到了它如何帮助你编写比在 JavaScript 中通常要短、更干净、更优雅的代码,并避免许多其陷阱。
我们意识到,尽管 CoffeeScript 的语法似乎与 JavaScript 很不同,但实际上它与生成的输出映射得相当紧密。
之后,我们深入探讨了 CoffeeScript 的一些独特而美妙的新增功能,如列表推导、解构赋值以及它的类语法,以及许多更方便、更强大的功能,如字符串插值、范围、展开和数组切片。
我在本章的目标是说服你们,CoffeeScript 是 JavaScript 的一个更优越的替代品,我已经通过展示它们之间的差异来尝试做到这一点。尽管我之前说过“它只是 JavaScript”,但我希望你们能欣赏到 CoffeeScript 本身就是一个美妙且现代的语言,它从其他伟大的脚本语言中汲取了卓越的影响。
我还可以写很多关于语言之美的内容,但我感觉我们已经达到了可以深入一些真实世界的 CoffeeScript 并从中欣赏它的地步,换句话说。
那么,你准备好了吗?让我们开始吧,然后安装 CoffeeScript。
第二章:运行 CoffeeScript
在本章中,我们将讨论如何在您的开发环境中安装并运行 CoffeeScript。
CoffeeScript 可以轻松安装在 Mac、Windows 或 Linux 上。根据您是否希望安装过程简单直接,或者希望走在技术前沿,有各种方法可以使它运行。但在我们深入细节之前,了解 CoffeeScript 通常不会独立存在,它使用一些优秀的 JavaScript 工具和框架来完成其魔法功能是很好的。让我们简要地讨论典型的 CoffeeScript 栈。
CoffeeScript 栈
在 CoffeeScript 的早期历史中,其编译器是用 Ruby 编写的。后来,它变成了自托管的;语言编译器是用其自身编写的。这意味着 CoffeeScript 的编译器是用 CoffeeScript 代码编写的,然后可以编译成 JavaScript,然后可以运行来再次编译 CoffeeScript。这不是很令人困惑吗?
不进一步深入这个壮举是什么,这也意味着为了运行 CoffeeScript,我们需要能够在您的计算机上独立执行 JavaScript,而不需要浏览器。
Node.js,或简称 Node,是一个用于编写网络服务器应用程序的 JavaScript 框架。它是使用 Google 的 V8 构建的,这是一个可以在没有网络浏览器的情况下运行 JavaScript 的引擎——非常适合 CoffeeScript。它已经成为安装 CoffeeScript 的首选方式。
将 CoffeeScript 与 Node.js 配对有很多好处。这不仅意味着你可以编译在浏览器中运行的 JavaScript,而且你还可以获得一个完整的 JavaScript 网络应用程序服务器框架,其中包含数百个为它编写的有用库。
就像在 Node.js 中的 JavaScript 一样,您可以在服务器上编写和执行 CoffeeScript,用它来编写网络服务器应用程序,甚至可以用作正常的日常系统脚本语言。
注意
核心的 CoffeeScript 编译器没有对 Node 的依赖,从技术上讲可以在任何 JavaScript 环境中执行。然而,使用编译器的 coffee 命令行实用程序是一个 Node.js 包。
下图展示了 CoffeeScript 编译器的工作原理:

Node.js 和 npm
Node.js 有自己的包管理系统,称为 npm。它用于安装和管理在 Node.js 生态系统中运行的包、库及其依赖项。它也是安装 CoffeeScript 最常见的方式,CoffeeScript 本身也作为一个 npm 包提供。因此,在设置好 Node.js 和 npm 之后,安装 CoffeeScript 实际上非常简单。
根据您的操作系统以及您是否需要编译源代码,安装 Node.js 和 npm 有不同的方法。接下来的每个部分都将涵盖您操作系统的安装说明。
小贴士
Node.js 维基包含了大量关于在众多平台上安装和运行 Node 的信息。如果你在本章遇到任何问题,可以查看它,因为它有很多关于故障排除的技巧,并且经常更新;链接是 github.com/joyent/node/wiki/Installation。
Node.js、npm 和 CoffeeScript 在 Windows 上
Node.js 社区一直在推动良好的原生 Windows 支持,并且安装非常容易。
要这样做,首先访问 Node.js 网站 (nodejs.org),然后点击 下载 按钮。你会看到一些可用的选项,但请选择 Windows 安装程序 选项,如下面的截图所示:

这将下载一个 .msi 文件。一旦下载完成,安装过程将非常简单;只需接受条款并点击 继续。如果你看到以下屏幕,那么你就已经成功安装了 Node:

在这一点上,你可能需要注销 Windows 或重启,以便 $PATH 变量的更改生效。完成此操作后,你应该能够打开 DOS 命令提示符并运行以下命令:
node –v
这应该会输出一个版本号,这意味着你可以继续了。让我们也检查一下 npm 是否运行正常。在命令行工具中,输入以下命令:
npm
你应该会看到以下类似的截图:

现在,为了继续安装 CoffeeScript,只需输入以下命令:
npm install coffee-script
如果一切顺利,你应该会看到以下类似的截图:

这里,我使用了 -g 标志,它为所有用户安装 npm 软件包。一旦你安装了 CoffeeScript,我们就可以使用 coffee 命令来测试它,如下所示:

这是 CoffeeScript 解释器,正如你所见,你可以用它来即时运行 CoffeeScript 代码。要退出,只需使用 Ctrl + C。
就这样!在 Windows 上安装 Node.js 真的非常快和简单。
在 Mac 上安装 CoffeeScript
在 Mac 上安装 Node.js 有两种方法,要么是从 Node.js 网站下载 .pkg 文件并使用 Apple 的安装程序应用安装,要么是使用 Homebrew 命令行软件包管理器。
最简单的方法是安装 .pkg 文件,让我们先来了解一下。安装 Homebrew 可能需要更多的工作,但如果你更喜欢使用命令行工具并从源代码构建 CoffeeScript,那么这是值得的。
使用 Apple 安装程序
访问 Node.js 网站(nodejs.org),然后点击下载按钮。您将看到一些可用的选项,但请选择Macintosh 安装程序选项,如下面的截图所示:

这将下载一个.pkg文件。一旦下载完成,安装过程将非常简单;只需选择您的目标位置,接受许可协议,然后点击继续。您应该选择使用为这台电脑的所有用户安装选项来安装它,如下面的截图所示:

如果您看到以下屏幕,那么您已经成功安装了 Node:

您还将安装 npm,我们将使用它来安装 CoffeeScript。跳转到使用 npm 安装 CoffeeScript部分。
使用 Homebrew
许多开发者更喜欢在 Mac 上使用命令行工具,Homebrew 软件包管理器已经变得相当流行。它的目的是让您轻松安装 Mac OS X 不自带的一些 Unix 工具。
如果您更喜欢使用 Homebrew 安装 Node.js,您需要在您的系统上安装 Homebrew。您可能还需要 XCode 命令行工具来构建 Node.js 源代码。Homebrew 维基百科包含了如何安装和运行的说明,请参阅github.com/mxcl/homebrew/wiki/installation。
如果您已经安装了 Homebrew,您可以使用brew命令安装 Node.js,如下面的截图所示:

如您从输出中看到的,Homebrew 没有安装 npm,没有它我们无法安装 CoffeeScript。要安装 npm,您只需在终端中复制并粘贴以下命令:
curl http://npmjs.org/install.sh |sh
在 npm 安装后,您应该看到以下类似的屏幕:

使用 npm 安装 CoffeeScript
现在我们已经安装了 npm,我们应该能够安装 CoffeeScript。只需在终端中输入以下命令:
npm install –g coffee-script
-g标志让 npm 全局安装 CoffeeScript;一旦完成,您现在可以使用coffee命令来测试 CoffeeScript 是否工作,如下面的截图所示:

就这么简单!在 Mac 上安装 CoffeeScript 相当容易。
在 Linux 上安装 CoffeeScript
在 Linux 上使用 CoffeeScript 安装 Node.js 的方式取决于您安装了哪个发行版。大多数流行的发行版都有相应的软件包,如果没有,您也可以尝试从源代码构建 CoffeeScript,如下一节所述。
我只有 Debian 基础发行版的包管理器的经验,并且已经使用 apt-get 包管理器成功安装了 CoffeeScript 与 Node.js。然而,您应该能够按照描述遵循其他发行版的说明。
Ubuntu、MintOS 和 Debian 上有 Node.js 的 apt-get 软件包,但在安装之前,您需要添加它们的源。以下各节将探讨安装每个软件包的说明。
Ubuntu 和 MintOS
在命令行工具中输入以下内容(您可能需要足够的权限来使用 sudo):
sudo apt-get install python-software-properties
sudo apt-add-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs npm
Debian
在 Debian 上,您通常需要登录到 root 终端来安装软件包。登录后,输入以下命令:
echo deb http://ftp.us.debian.org/debian/ sid main > /etc/apt/sources.list.d/sid.list
apt-get update
apt-get install nodejs npm
其他发行版
Node.js 的维基页面 github.com/joyent/node/wiki/Installing-Node.js-via-package-manager 包含了在包括 Fedora、openSUSE、Arch Linux 和 FreeDSB 在内的各种 Linux 和 Unix 发行版上安装的说明。
使用 npm 安装 CoffeeScript
在您的包管理器完成其操作后,现在您应该已经安装了 Node.js 和 npm。您可以通过使用 npm -v 命令来验证这一点。现在,您可以通过输入以下命令使用 npm 安装 CoffeeScript:
npm install –g coffee-script
-g 标志告诉 npm 全局安装该软件包。
以下截图显示了如何使用 -v 命令安装 CoffeeScript:

就这样!在 Linux 上安装 CoffeeScript 非常简单。
从源代码构建 Node.js
如果您不想使用包管理器或安装程序,或者您的操作系统没有提供,或者您想获取 Node.js 的最新版本,那么您也可以从源代码构建 Node.js。但请注意,这个过程通常充满危险,因为源代码通常需要系统上的某些依赖项来构建。
在 Linux 或 Unix 上构建
在 Linux 或 Unix 环境中构建时,您需要确保已安装以下源依赖项:
-
Python–版本 2.6 或版本 2.7:您可以通过在命令提示符中输入
python --version来检查您是否已安装 Python,以及已安装的版本。 -
libssl-dev:这通常可以使用内置的包管理器安装。它已经在 OS X 上安装好了。
我将向您展示如何使用最新的源代码构建 Node.js。源代码由流行的 Git 版本控制系统管理,并托管在 github.com 的仓库中。要从您的 github 拉取最新源代码,您需要确保您已安装 Git。您可以使用 apt-get 如下安装:
apt-get install git-core
一旦您有了这些先决条件,您应该能够在命令行工具上构建 node。在命令行工具中输入以下命令:
git clone https://github.com/joyent/node.git
cd node
git checkout v0.6.19
./configure
make
sudo make install
哇!如果一切顺利,您应该能够使用 npm 安装 CoffeeScript:
npm install –g coffee-script
在 Windows 上构建
虽然在 Windows 上可以构建 Node.js,但我强烈建议你只运行安装程序。在这本书中提到的所有安装方式中,这是我唯一没有亲自做过的方式。这个例子直接来自 Node wiki (github.com/joyent/node/wiki/Installation)。显然,构建可能需要非常长的时间。在命令提示符中,输入以下内容:
C:\Users\ryan>tar -zxf node-v0.6.5.tar.gz
C:\Users\ryan>cd node-v0.6.5
C:\Users\ryan\node-v0.6.5>vcbuild.bat release
C:\Users\ryan\node-v0.6.5>Release\node.exe
> process.versions
{ node: '0.6.5',
v8: '3.6.6.11',
ares: '1.7.5-DEV',
uv: '0.6',
openssl: '0.9.8r' }
>
使用 CoffeeScript
由此看来。仅仅为了获取 CoffeeScript 就需要安装 Node.js 和 npm 可能看起来很费力,但你将体验到拥有一个出色的服务器端 JavaScript 框架和良好的命令行工具来编写 CoffeeScript 的强大之处。
现在你已经安装了 CoffeeScript,我们该如何使用它呢?你进入该语言的主要入口点是 coffee 命令。
coffee 命令
这个命令行工具就像是 CoffeeScript 的瑞士军刀。你可以用它以交互式的方式运行 CoffeeScript,将 CoffeeScript 文件编译成 JavaScript 文件,执行 .coffee 文件,监视文件或目录,当文件发生变化时进行编译,以及一些其他有用的功能。执行命令很简单,只需输入 coffee 并附带一些选项和参数即可。
要获取所有可用选项的帮助,请使用带有 -h 或 --help 选项的 coffee 命令。以下屏幕截图显示了有用的选项列表:

我们已经看到了 -v 选项,它将打印出 CoffeeScript 的当前版本。
REPL
使用不带参数或 -i 选项的 coffee 命令将进入 CoffeeScript 的 读取-评估-打印循环(REPL)。从这里,你可以输入 CoffeeScript 代码,它将即时执行并在控制台显示输出。这对于玩转语言、探索一些核心 JavaScript 和 Node.js 库,甚至拉入另一个外部库或 API 并能够交互式地探索它非常有用。
我敦促你运行 coffee REPL 并尝试我们在上一章中讨论的一些代码示例。注意每个表达式输入后输出的显示。解释器也足够聪明,可以处理多行和嵌套表达式,例如函数定义。

在之前的屏幕截图中,解释器展示了如何处理函数定义。
小贴士
要退出 REPL,请使用 Ctrl + D 或 Ctrl + C。
运行 .coffee 文件
在 REPL 中输入足够的代码后,你将到达一个想要开始存储和组织你的 CoffeeScript 到源文件的时刻。CoffeeScript 文件使用 .coffee 扩展名。你可以通过将 .coffee 文件作为参数传递给 coffee 命令来运行它。文件中的 CoffeeScript 将被编译成 JavaScript,然后使用 Node.js 作为其环境执行。
小贴士
你可以使用任何文本编辑器来编写你的 CoffeeScript。许多流行的编辑器都有插件或添加了对 CoffeeScript 的支持,具有语法高亮、代码补全等功能,甚至允许你直接从编辑器中运行代码。支持 CoffeeScript 的文本编辑器和插件的完整列表可以在 github.com/jashkenas/coffee-script/wiki/Text-editor-plugins 找到。
编译到 JavaScript
要将 CoffeeScript 编译成 JavaScript,我们需要传递 -c 或 --compile 选项。它可以接受一个文件名或文件夹名作为单个参数,或者多个文件和文件夹名。如果你指定了一个文件夹,它将编译该文件夹中的所有文件。默认情况下,JavaScript 输出文件将与源文件具有相同的名称,因此 foo.coffee 将编译成 foo.js。
如果我们想要控制输出 JavaScript 的位置,可以使用带有文件夹名的 -o 或 --output 选项。如果你指定了多个文件或文件夹,也可以传递带有文件名的 -j 或 --join 选项。这将把输出合并成一个单一的 JavaScript 文件。
观察
如果你正在开发一个 CoffeeScript 应用程序,不断地运行 --compile 可能会变得繁琐。另一个有用的选项是 -w 或 --watch。这告诉 CoffeeScript 编译器持续运行并监视特定文件或文件夹中的任何文件变化。当与 --compile 结合使用时,这会非常好,因为它会在文件每次变化时编译文件。
整合一切
coffee 命令的酷之处在于可以通过组合标志来创建一个非常有用的构建和开发环境。比如说,我有一个源文件夹中的多个 CoffeeScript 文件,每次文件发生变化时,我都想将它们编译成一个位于 js 文件夹中的单个 output.js 文件。
你应该能够使用以下类似的命令:
coffee –o js/ -j output.js –cw source/
这将监视源文件夹中 .coffee 文件的所有变化,并将它们编译并合并成一个名为 output.js 的文件,位于 js 文件夹内,如下面的截图所示:

摘要
在本章中,你或许已经学会了如何在你的选择开发环境中运行 CoffeeScript。你也学会了如何使用 coffee 命令来运行和编译 CoffeeScript。现在你已经有了这些工具,我们将转向编写一些代码,并了解 CoffeeScript 在实际应用中的情况。让我们从 JavaScript 的起点开始,看看如何在浏览器中编写 CoffeeScript。
第三章。CoffeeScript 和 jQuery
jQuery 是一个跨浏览器兼容的库,旨在简化 HTML 应用程序开发者的生活。它最初由 John Resig 在 2006 年发布,并从此成为世界上最受欢迎的 JavaScript 库,被数百万个网站使用。
为什么它会变得如此流行?嗯,jQuery 有一些很好的特性,如简单的 DOM 操作和查询、事件处理和动画,以及 AJAX 支持。所有这些结合在一起,使得针对 DOM 的编程和在 JavaScript 中的编程变得更好。
该库在跨浏览器兼容性和速度方面也进行了高度优化,因此使用 jQuery 的 DOM 遍历和操作函数不仅可以让你免于编写繁琐的代码,而且通常比你自己编写的代码要快得多。
事实上,jQuery 和 CoffeeScript 一起使用得很好,并且当结合使用时,提供了一套强大的工具集,可以以简洁和表达性的方式编写网络应用程序。
在本章中,我们将做以下事情:
-
探索 jQuery 的一些高级功能,并讨论它为你提供了什么
-
学习如何在浏览器中使用 CoffeeScript 和 jQuery
-
使用 jQuery 和 CoffeeScript 构建一个简单的待办事项列表应用
让我们先详细讨论 jQuery 库,并了解它为什么如此有用。
查找和更改元素
在网络浏览器中,DOM(文档对象模型)是用于程序化交互的 HTML 文档中元素的表示。
在 JavaScript 中,你会发现你需要进行大量的 DOM 遍历来找到你感兴趣的元素,然后对其进行操作。
要仅使用标准的 JavaScript 库来完成这项任务,你通常需要结合使用 document.getElementsByName、document.getElementById 和 document.getElementsById 方法。一旦你的 HTML 结构开始变得复杂,这通常意味着你不得不将这些方法组合在一个尴尬且繁琐的迭代代码中。
以这种方式编写的代码通常对 HTML 的结构有很多假设,这意味着如果 HTML 发生变化,它通常会出问题。
$ 函数
使用 jQuery,许多这种命令式风格的代码通过 $ 函数——jQuery 的工厂方法(一个创建 jQuery 类实例的方法)和进入库的主要入口点——变得更加简单。
这个函数通常需要一个 CSS 选择器字符串作为参数,它可以用来根据元素名称、ID、类属性或其他属性值选择一个或多个元素。此方法将返回一个包含一个或多个与选择器匹配的元素的 jQuery 对象。
在这里,我们将使用 $ 函数选择具有 address 类的文档中的所有 input 标签:
$('input .address')
您可以使用众多函数来操作或查询这些元素,这些函数通常被称为命令。以下是一些常见的 jQuery 命令及其用途:
-
addClass: 这给元素添加一个 CSS 类 -
removeClass: 这将从元素中移除一个 CSS 类 -
attr: 这从元素中获取一个属性 -
hasClass: 这检查元素上是否存在 CSS 类 -
html: 这获取或设置元素的 HTML 文本 -
val: 这获取或设置元素值 -
show: 这将显示一个元素 -
hide: 这将隐藏一个元素 -
parent: 这获取元素的父亲 -
appendTo: 这将子元素附加到父元素 -
fadeIn: 这使元素淡入 -
fadeout: 这使元素淡出
大多数命令都会返回一个 jQuery 对象,可以用来将其他命令链式附加到它上面。通过链式命令,您可以使用一个命令的输出作为下一个命令的输入。这种强大的技术让您可以对 HTML 文档的部分进行非常简短和简洁的转换。
假设我们想要突出显示并启用 HTML 表单中的所有address输入;jQuery 允许我们执行类似以下操作:
$('input .address').addClass('highlighted').removeAttr('disabled')
在这里,我们再次选择所有具有address类的input标签。我们使用addClass命令给每个标签添加highlighted类,然后通过链式调用removeAttr命令来移除disabled属性。
工具函数
jQuery 还附带了一系列工具函数,这些函数通常可以改善您的日常 JavaScript 编程体验。这些函数都是以全局 jQuery 对象上的方法的形式存在的,例如:$.methodName。例如,最广泛使用的工具之一是each方法,它可以用来遍历数组或对象,如下所示(在 CoffeeScript 中):
$.each [1, 2, 3, 4], (index, value) -> alert(index + ' is ' + value)
jQuery 的工具方法包括数组集合辅助方法、时间字符串操作,以及许多其他有用的 JavaScript 和浏览器相关函数。许多这些函数都源于许多 JavaScript 程序员的日常需求。
通常,您会发现一个函数适用于您在编写 JavaScript 或 CoffeeScript 时遇到的一个常见问题或模式。您可以在api.jquery.com/category/utilities/找到函数的详细列表。
Ajax 方法
jQuery 提供了$.ajax方法来执行跨浏览器的 Ajax 请求。传统上,这一直是一个痛点,因为浏览器都实现了不同的接口来处理 Ajax。jQuery 负责所有这些,并提供了一种更简单、基于回调的方式来构建和执行 Ajax 请求。这意味着你可以声明性地指定 Ajax 调用应该如何进行,然后提供 jQuery 在请求成功或失败时调用的函数。
使用 jQuery
在浏览器中使用 jQuery 非常简单;你只需要在 HTML 文件中包含 jQuery 库。你可以从他们的网站上下载 jQuery 的最新版本(docs.jquery.com/Downloading_jQuery)并引用它,或者你可以直接链接到库的 内容分发网络(CDN)版本。
下面是一个示例,说明你可能如何做到这一点。这个片段来自优秀的 HTML5 Boilerplate 项目(html5boilerplate.com/)。这里我们包括来自 Google CDN 的最新压缩版 jQuery,但如果从 CDN 包含失败,我们也会包括本地版本。
<script src="img/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="img/jquery-1.7.2.min.js"><\/script>')
</script>
在浏览器中使用 CoffeeScript 和 jQuery
在我们开始用 jQuery 和 CoffeeScript 玩耍之前,让我们谈谈如何编写在浏览器中运行的 CoffeeScript 代码。
编译 CoffeeScript
编译 CoffeeScript 用于 Web 应用的最常见方法是运行 coffee 命令来监视一个或多个 CoffeeScript 文件的变化,然后将它们编译成 JavaScript。然后,输出将包含在你的 Web 应用程序中。
例如,我们将组织我们的项目文件夹结构,使其看起来类似于以下文件夹结构:

'
src 文件夹是放置你的 CoffeeScript 文件的地方。然后我们可以启动一个 CoffeeScript 编译器来监视该文件夹,并将 JavaScript 编译到我们的 public/js 文件夹。
这就是 CoffeeScript 命令的样子:
coffee -co public/js -w src/
让这个命令在单独的终端窗口中后台运行,当你保存 CoffeeScript 文件时,它将重新编译你的 CoffeeScript 文件。
小贴士
CoffeeScript 标签
在浏览器中运行 CoffeeScript 的另一种方法是,在 <script type="text/coffeescript"> 标签中包含 CoffeeScript 代码,然后在你的文档中包含压缩的 CoffeeScript 编译器脚本(coffee-script.js)。这将编译并运行页面中的所有内联 CoffeeScript 代码。
这并不是为了严肃使用,因为每次页面加载时,你将为编译步骤付出严重的性能代价。然而,有时只是快速在浏览器中玩一些 CoffeeScript 而不设置完整的编译链,这可以相当有用。
jQuery 和 CoffeeScript
让我们在 CoffeeScript 文件中添加一些内容,看看我们是否能够成功将其与 jQuery 连接起来。在 src 文件夹中,创建一个名为 app.coffee 的文件,并包含以下代码:
$ -> alert "It works!"
这设置了 jQuery 的 $(document).ready() 函数,当应用程序初始化时将被调用。这里我们使用它的简写语法,只需将匿名函数传递给 $ 函数即可。
你现在应该在 public/js 文件夹中有一个 app.js 文件,其内容类似于以下内容:
// Generated by CoffeeScript 1.3.3
(function() {
alert('It works!');
}).call(this);
最后,我们还需要在我们的应用程序的 HTML 文件中包含此文件以及 jQuery。在 public/index.html 文件中,添加以下代码:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>jQuery and CoffeeScript Todo</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<script src="img/jquery.min.js"></script>
<script src="img/app.js"></script>
</body>
</html>
上述代码创建了我们 HTML 的骨架,并包含了 jQuery(使用 Google CDN)以及我们的应用程序代码。
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件,网址为 www.PacktPub.com。如果你在其他地方购买了这本书,你可以访问 www.PacktPub.com/ 支持并注册,以便将文件直接通过电子邮件发送给你。
测试所有内容
现在,我们应该能够通过在浏览器中打开我们的 index.html 文件来运行我们的应用程序。如果一切顺利,我们应该会看到如下截图所示的弹出窗口:

运行本地网络服务器
虽然我们现在可以从磁盘轻松测试我们的网络应用程序,但我们可能很快就会想要在本地网络服务器上托管它,特别是如果我们想要开始做 Ajax。由于我们已经安装了 Node.js,运行网络服务器应该非常简单,我们目前只需要提供静态内容。幸运的是,有一个 npm 包可以为我们做到这一点;它被称为 http-server,可以在 github.com/nodeapps/http-server 找到。
要安装它,只需运行以下命令:
npm install http-server -g
然后,我们通过导航到我们的应用程序文件夹并输入以下内容来执行它:
http-server
这将在端口 8080 上托管公共文件夹中的所有文件。现在,我们应该能够通过使用 URL http://localhost:8080/ 导航到我们的托管站点。
我们的应用程序
在本章的剩余部分,我们将使用 CoffeeScript 构建一个 jQuery 应用程序。该应用程序是一个待办事项列表应用程序,可以用来跟踪你的日常任务以及你如何完成它们。
TodoMVC
我已经将应用程序的大部分模型建立在一些 TodoMVC 项目的源代码上,这些源代码属于公共领域。该项目展示了不同的 JavaScript MVC 框架,它们都用于构建相同的应用程序,当评估框架时非常有用。如果你想查看它,可以在 addyosmani.github.com/todomvc/ 找到。
注意
MVC(模型-视图-控制器)是一种广泛使用的应用程序架构模式,旨在通过将应用程序关注点拆分为三个领域对象类型来简化代码并减少耦合。我们将在本书的后面部分更多地讨论 MVC。
我们将主要基于 TodoMVC 项目构建我们的应用程序,以获取它附带的一些出色的样式表以及精心设计的 HTML5 结构。然而,大部分客户端 JavaScript 将用 CoffeeScript 重新编写,并且为了说明目的,它将被简化并修改很多。
因此,无需多言,让我们开始吧!
我们最初的 HTML
首先,我们将添加一些 HTML,这将允许我们输入待办事项并查看现有条目的列表。在 index.html 中,在包含的 script 标签之前,向 body 标签添加以下代码:
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<ul id="todo-list"></ul>
</section>
<footer id="footer">
<button id="clear-completed">Clear completed</button>
</footer>
</section>
让我们简要地浏览一下前面标记的结构的结构。首先,我们有一个带有 todoapp ID 的部分,它将作为应用程序的主要部分。它由一个 header 标签组成,该标签将包含我们创建新条目的输入,一个 main 部分,其中将列出所有待办事项,以及一个 footer 部分,其中将包含 清除已完成 按钮。在我们打开浏览器中的此页面之前,让我们从我们的 app.coffee 文件中删除之前的警告行。
当你导航到这个页面时,它看起来不会太多。这是因为我们的 HTML 丝毫没有样式。下载本章的 styles.css 文件并将其复制到 public/css 文件夹。现在它应该看起来好多了。
初始化我们的应用程序
大多数 jQuery 应用程序,包括我们的,遵循类似的模式。我们创建一个 $(document).ready 处理程序,它反过来执行页面初始化,通常包括为用户操作设置事件处理程序。让我们在我们的 app.coffee 文件中这样做。
class TodoApp
constructor: ->
@bindEvents()
bindEvents: ->
alert 'binding events'
$ ->
app = new TodoApp()
在这里,在前面的代码片段中,我们创建了一个名为 TodoApp 的类,它将代表我们的应用程序。它有一个构造函数,该构造函数调用 bindEvents 方法,目前只是显示一个警告消息。
我们设置了 jQuery 的 $(document).ready 事件处理程序来创建我们的 TodoApp 实例。当你重新加载页面时,你应该看到 绑定事件 的弹出窗口。
小贴士
没有看到预期的输出?
记得关注在后台运行的咖啡编译器的输出。如果你有任何语法错误,编译器将输出错误消息。一旦你修复了它,编译器应该重新编译你的新 JavaScript 文件。记住,CoffeeScript 对空白字符敏感。如果你遇到任何你不理解的错误,请仔细检查你的缩进。
添加待办事项
现在我们可以添加事件处理程序,实际上将待办事项添加到列表中。在我们的 bindEvents 函数中,我们将选择 new-todo 输入并处理其 keyup 事件。我们将此绑定到调用我们的类上的 create 方法,我们也将定义它;这将在以下代码片段中显示:
bindEvents: ->
$('#new-todo').on('keyup', @create)
create: (e) ->
$input = $(this)
val = ($.trim $input.val())
return unless e.which == 13 and val
alert val
# We create the todo item
$('#new-todo') 函数使用 jQuery CSS 选择器语法获取具有 new-todo ID 的输入,on 方法将 create 方法绑定到其 'keyup' 事件,该事件在输入具有焦点时按下任何键时触发。
在 create 函数中,我们可以通过使用 $(this) 函数来获取输入的引用,该函数将始终返回生成事件的元素。我们将此分配给 $input 变量。使用以 $ 为前缀的变量名是在分配 jQuery 变量时的常见约定。然后我们可以使用 val() 函数获取输入的值并将其分配给一个本地的 val 变量。
我们可以通过检查keyup事件的which属性是否等于13来查看是否按下了Enter键。如果是这样,并且val变量不是null,我们就可以继续创建待办事项。目前,我们只是通过一个警告消息输出其值。
一旦我们创建了项目,我们应该将其放在哪里?在许多传统的 Web 应用中,这些数据通常会通过 Ajax 请求存储在服务器上。我们希望现在保持这个应用简单,所以暂时只将这些项目保留在客户端。HTML5 规范定义了一个名为localStorage的机制,正是为了实现这一点。
使用 localStorage
localStorage是新的 HTML5 规范的一部分,允许你在浏览器中的本地数据库中存储和检索对象。接口相当简单;在支持的浏览器中,将存在一个名为localStorage的全局变量。这个变量有以下三个重要的方法:
localStorage.setItem(key, value)
localStorage.getItem(key)
localStorage.removeItem(key)
key和value参数都是字符串。存储在localStorage变量中的字符串即使在页面刷新后也会保留。在大多数浏览器中,你可以在localStorage变量中存储多达 5MB 的数据。
因为我们希望将待办事项存储为一个复杂对象而不是字符串,所以在设置和从localStorage获取项目时,我们使用将对象转换为 JSON 对象以及从 JSON 对象转换的常用技术。为此,我们将向Storage类的原型添加两个方法,这样它们就会在全局的localStorage对象上可用。将以下代码片段添加到我们的app.coffee文件顶部:
Storage::setObj = (key, obj) ->
@setItem key, JSON.stringify(obj)
Storage::getObj = (key) ->
JSON.parse @getItem(key)
在这里,我们使用::运算符将setObj和getObj方法添加到Storage类。这些函数通过将对象转换为 JSON 以及从 JSON 转换来包装localStorage对象的getItem和setItem方法。
现在我们终于准备好创建待办事项并将其存储在localStorage中了。
下面是我们的create方法的其余部分:
create: (e)->
$input = $(this)
val = ($.trim $input.val())
return unless e.which == 13 and val
randomId = (Math.floor Math.random()*999999)
localStorage.setObj randomId,{
id: randomId
title: val
completed: false
}
$input.val ''
为了让我们能够唯一地识别任务,我们将使用最简单的方法,即生成一个大的随机数作为 ID。这不是识别文档最复杂的方法,你很可能不应该在生产环境中使用这种方法。然而,它实现起来相当简单,目前对我们的目的来说效果很好。
在生成 ID 之后,我们现在可以使用setObj方法将待办事项放入我们的本地数据库。我们传递一个从input标签值中获取的标题,并将项目默认设置为未完成。
最后,我们清除$input的值,以便用户能够看到create操作成功。
现在我们应该能够测试我们的小程序,看看待办事项是否被存储到localStorage中。Google Chrome 开发者工具允许你在资源选项卡中检查localStorage。添加几个任务后,你应该能够在这里看到它们,如下面的截图所示:

显示待办事项
现在我们能够存储待办事项列表,如果在屏幕上看到它们会很好。为此,我们将添加一个displayItems方法。这将遍历本地的待办事项列表并将它们显示出来。
将以下代码添加到我们的TodoApp中,在create方法之后:
displayItems: ->
alert 'displaying items'
现在我们应该能够从create方法中调用此方法,如下面的代码所示:
create: (e) ->
$input = $(this)
val = ($.trim $input.val())
return unless e.which == 13 and val
randomId = (Math.floor Math.random()*999999)
localStorage.setObj randomId,{
id: randomId
title: val
completed: false
}
$input.val ''
@displayItems()
让我们运行这段代码看看会发生什么。当我们这样做时,我们得到以下错误:
未捕获的类型错误:对象#
这里发生了什么?看起来@displayItems()的调用试图在一个HTMLInputElement的实例上调用方法,而不是在TodoApp实例上。
这是因为 jQuery 会将this的值设置为引用触发事件的元素。当我们将类方法绑定到事件处理器时,jQuery 实际上会“劫持”this,使其不指向类本身。这是当你使用 jQuery 和 CoffeeScript 中的类时应该知道的一个重要注意事项。
为了修复它,我们可以在设置keyup事件处理器时使用 CoffeeScript 的胖箭头,这将确保this的值保持不变。让我们修改我们的bindEvents方法,使其看起来类似于以下代码:
bindEvents: ->
$('#new-todo').on('keyup',(e) => @create(e))
但是还有一件事;在我们的createItem方法中,我们使用了$(this)来获取触发事件的input元素的价值。自从切换到胖箭头后,现在它将指向我们的TodoApp实例。幸运的是,传入的事件参数有一个目标属性,它也指向我们的输入。将create方法的第 1 行修改得类似于以下代码片段:
create: (e) ->
$input = $(e.target)
val = ($.trim $input.val())
现在我们创建一个项目时,我们应该看到“显示项目”的提示,这意味着displayItems方法已经正确连接。
我们可以做得更好。由于每次create方法被触发时都需要查找$input标签,我们可以将其存储在类变量中,以便可以重用。
最佳位置应该是在应用程序启动时。让我们创建一个cacheElements方法,它正好做这件事,并在构造函数中被调用——如下面的代码所示:
class TodoApp
constructor: ->
@cacheElements()
@bindEvents()
cacheElements: ->
@$input = $('#new-todo')
bindEvents: ->
@$input.on('keyup',(e) => @create(e))
create: (e) ->
val = ($.trim @$input.val())
return unless e.which == 13 and val
randomId = (Math.floor Math.random()*999999)
localStorage.setObj randomId,{
id: randomId
title: val
completed: false
}
@$input.val ''
@displayItems()
cacheElements调用分配了一个名为@$input的类变量,然后在整个类中使用。这种@$语法一开始可能看起来很奇怪,但它确实在几键中传达了大量的信息。
显示待办事项
我们现在应该能够显示项目。在displayItems方法中,我们将遍历所有的localStorage键,并使用它们来获取每个相应的待办事项。对于每个项目,我们将在具有todo-list ID 的ul元素中添加一个li子元素。在我们开始使用$('#todo-list')元素之前,让我们像对@$input所做的那样缓存其值:
cacheElements: ->
@$input = $('#new-todo')
@$todoList = $('#todo-list')
displayItems: ->
@clearItems()
@addItem(localStorage.getObj(id)) for id in Object.keys(localStorage)
clearItems: ->
@$todoList.empty()
addItem: (item) ->
html = """
<li #{if item.completed then 'class="completed"' else ''} data-id="#{item.id}">
<div class="view">
<input class="toggle" type="checkbox" #{if item.completed then 'checked' else ''}>
<label>#{item.title}</label>
<button class="destroy"></button>
</div>
</li>
"""
@$todoList.append(html)
在这里,我们对 displayItems 方法进行了一些修改。首先,我们从 $@todoList 中删除任何现有的子列表项,然后我们遍历 localStorage 中的每个键,获取具有该键的对象,并将其发送到 addItem 方法。
addItem 方法构建一个待办事项的 HTML 字符串表示形式,然后使用 jQuery 的 append 函数将子元素附加到 $@todoList 上。同时,我们还创建了一个用于设置任务为完成的复选框和一个用于删除任务的按钮。
注意 li 元素上的 data-id 属性。这是一个 HTML5 数据属性,它允许你向任何元素添加任意数据属性。我们将使用它将每个 li 与 localStorage 对象中的待办事项相关联。
注意
虽然 CoffeeScript 可以使构建此类 HTML 字符串变得容易一些,但定义客户端代码中的标记可能会很快变得繁琐。我们在这里主要为了说明目的这样做;可能更好的做法是使用 JavaScript 模板库,例如 Handlebars (handlebarsjs.com/)。
这类库允许你在标记中定义模板,然后使用特定上下文编译它们,这会给你一个格式良好的 HTML,你可以将其附加到元素上。
最后一点,现在我们可以在创建后显示项目,让我们将 displayItems 调用添加到构造函数中,这样我们就可以显示现有的待办事项;下面的代码中突出显示了此调用:
constructor: ->
@cacheElements()
@bindEvents()
@displayItems()
删除和完成项目
让我们连接删除任务按钮。我们为其添加了一个事件处理程序,如下所示:
bindEvents: ->
@$input.on('keyup',(e) => @create(e))
@$todoList.on('click', '.destroy', (e) => @destroy(e.target))
在这里,我们处理 @$todoList 上任何具有 .destroy 类的子元素的点击事件。
我们再次使用箭头函数创建处理程序,调用 @destroy 方法并传入目标,该目标应该是被点击的 删除 按钮。
我们现在需要创建 @destroy 方法,如下面的代码片段所示:
destroy: (elem) ->
id = $(elem).closest('li').data('id')
localStorage.removeItem(id)
@displayItems()
closest 函数将找到定义在按钮本身最近的 li 元素。我们使用 jQuery 的 data 函数检索其 data-id 属性,然后我们可以使用它从 localStorage 中删除待办事项。还调用了一次 @displayItems 来刷新视图。
完成一项任务将遵循一个非常相似的模式;也就是说,我们添加一个事件处理程序,如下面的代码所示:
bindEvents: ->
@$input.on('keyup',(e) => @create(e))
@$todoList.on('click', '.destroy', (e) => @destroy(e.target))
@$todoList.on('change', '.toggle', (e) => @toggle(e.target))
这次我们处理 'change' 事件,该事件将在完成的复选框被勾选或取消勾选时触发。这反过来会调用 @toggle 方法,其代码如下:
toggle: (elem) ->
id = $(elem).closest('li').data('id')
item = localStorage.getObj(id)
item.completed = !item.completed
localStorage.setObj(id, item)
此方法也使用 closest 函数获取待办事项的 ID。它从 localStorage 中加载对象,切换 completed 的值,然后使用 setObj 方法将其保存回 localStorage。
现在,轮到你了!
作为最后的练习,我会要求你使 清除已完成 按钮生效。
摘要
在本章中,我们学习了 jQuery 是什么,以及它的优势和好处。我们还学习了如何将 jQuery 的强大功能与 CoffeeScript 结合起来,以更少的努力和复杂性编写复杂的网络应用程序。jQuery 是一个非常庞大的库,我们只是刚刚触及了它所能提供的表面。我敦促你花更多的时间学习这个库本身,并且使用 CoffeeScript 来学习。
接下来,我们将首先看看如何使用 CoffeeScript 和 Rails 与服务器端代码进行交互。
第四章:CoffeeScript 和 Rails
Ruby on Rails 是一个在 2004 年出现的网络框架。它是由 David Heinemeier Hansson 编写的,并从他为他的公司37signals编写的 Ruby 项目管理网络应用Basecamp中提取出来的框架。它是一个诺贝尔物理奖获得者理查德·费曼。
Rails 通过如何轻松快速地编写网络应用程序给很多人留下了深刻印象,很快变得非常受欢迎。
在开发的时候,Ruby 是一种来自日本的神秘脚本语言,没有人真正听说过。Ruby 实际上是 Rails 之所以成功的关键。它已经证明是一种强大而简洁的编程语言,许多程序员都表示它让编程变得有趣起来。
Rails 有什么特别之处?
Rails 推动了网络开发者编写应用程序的方法。其核心哲学包括以下两个重要原则:
-
约定优于配置
-
不要重复自己,或者 DRY
约定优于配置
Rails 被设计成假设程序员会遵循某些已知约定,如果使用这些约定,将提供巨大的好处,并减少对框架配置的需求。它通常被称为有偏见的框架。这意味着框架对典型应用程序应该如何构建和结构做出了假设,并且它不会试图过于灵活和可配置。这有助于你花更少的时间在配置和连接应用程序架构等日常任务上,更多的时间用于实际构建你的应用程序。
例如,Rails 会使用对象来模拟你的数据库中的表,这些对象对应于它们的名称,因此Transactions数据库中的记录将自动映射到Transactions类实例,同样,people数据库表中的记录也会自动映射到Person类实例。
Rails 通常会使用约定为你做智能的事情。比如说,我们的people表也有一个名为created_at和updated_at的datetime字段。Rails 足够智能,现在可以自动更新这两个字段在记录创建或更新时的时间戳。
Rails 约定最重要的地方在于你应该了解它们,而不是与框架对抗,或者没有充分的理由试图过多地偏离 Rails 的方式。通常,这可能会抵消你从这些约定中获得的所有好处,甚至可能使你更难找到解决方案。
不要重复自己(DRY)
这个软件工程原则也可以表述如下:
每项知识必须在系统中有一个单一、明确和权威的表示。
这意味着 Rails 力求在可能的情况下消除重复和样板代码。
例如,一个Person类,它模拟people表中的记录,不需要定义其字段,因为它们已经在你的数据库表中定义为列。在这里,Rails 可以使用 Ruby 强大的元编程能力,神奇地给Person类添加与数据库列相对应的属性。
注意
元编程是编写作用于其他代码作为数据结构的代码的概念。换句话说,元编程就是编写编写代码的代码。它在 Ruby 社区,尤其是 Rails 源代码中得到了广泛的应用。
Ruby 语言具有非常强大的元编程能力,这与开放类和对象的概念紧密相关,这意味着你可以轻松地“打开”现有的类定义,并对其进行重新定义和添加成员。
Rails 和 JavaScript
很长一段时间里,Rails 都附带Prototype.js和Script.aculo.us JavaScript 库,用于 AJAX、页面动画和效果。
Rails 有视图辅助器的概念——这些是在视图中使用的 Ruby 方法,可以用来抽象常见的 HTML 结构。许多处理客户端代码和 AJAX 的视图辅助器都是建立在这些两个框架之上的,因此它们完全内置于框架中,没有使用替代方案的方法。
Prototype.js与 jQuery 共享许多相同的思想和目标,但随着时间的推移,jQuery 被许多程序员视为一个更优雅、更强大的库。
随着 jQuery 变得越来越流行,Rails 社区中的许多开发者开始尝试使用 jQuery 与 Rails 一起使用,而不是默认的 JavaScript 库。一套标准的库或gem出现了,用于用 jQuery 替换内置的 Prototype 库。
在 Rails 3.1 版本中,宣布 jQuery 将成为默认的 JavaScript 库。因为 jQuery 已经包含了Script.aculo.us的大部分动画和页面效果功能,所以这个库也不再需要了。
这项举措似乎已经酝酿良久,并且普遍得到了 Rails 社区大多数人的祝福。
Rails 和 CoffeeScript
Rails 3.1 的另一个重大新增功能是资产管道。其主要目标是使 JavaScript 和 CSS 等资产在 Rails 应用程序中成为一等公民变得容易。在此之前,JavaScript 和 CSS 只是作为静态内容被服务。它还提供了一个组织结构,帮助你组织 JavaScript 和 CSS,并提供了一个用于访问它们的领域特定语言(DSL)。
使用资产管道,你可以通过清单文件来组织和管理工作之间的依赖关系。Rails 还会使用管道来压缩和连接 JavaScript,以及应用指纹以实现缓存破坏。
资产管道还包含一个预处理链,允许你在文件被服务之前,通过一系列的输入-输出处理器来运行这些文件。它通过文件扩展名来识别应该运行哪些预处理程序。
在 Rails 3.1 发布之前,宣布了将支持 CoffeeScript 编译器,通过资产管道直接使用。这是一个重大的宣布,因为 CoffeeScript 仍然是一个非常年轻的语言,它在 Rails 社区中引起了很多争议,有些人哀叹他们不想学习或使用这种新语言。
尽管如此,Rails 维护者坚持了自己的立场,目前使用 CoffeeScript 在 Rails 中使用非常简单。CoffeeScript 作为编写客户端 JavaScript 代码的默认语言,对 CoffeeScript 来说是一个巨大的推动,许多 Rails 开发者因此开始了解并接受这种语言。
我们一直在谈论 Rails 的美妙之处以及它与 CoffeeScript 的良好配合,所以让我们安装 Rails,这样你就可以亲自看看所有这些喧嚣的原因。
安装 Rails
根据你的操作系统、你想要使用的 Ruby 版本、你是否使用版本管理器、从源代码构建以及许多其他选项,有无数种方式可以在你的开发机器上安装 Ruby 和 Rails。在这本书中,我们将简要介绍在 Windows、Mac 和 Linux 上安装它的最常见方法。请注意,在这本书中,我们将使用至少 3.2 版本的 Rails 和 1.9.2 版本的 Ruby 或更高版本。
使用 RailsInstaller 安装 Rails
在 Windows 上,或者可选地在 Mac 上,我推荐使用 RailsInstaller (railsinstaller.org/)。它包含了开始使用 Rails 所需的一切,包括 Ruby 的最新版本。下载设置程序后,安装过程非常简单;只需运行它并按照向导操作。安装完成后,你应该会看到一个打开的控制台命令提示符。尝试输入 rails -v。如果你看到一个版本号,你应该可以开始了。
使用 RVM 安装 Rails
在 Mac 和 Linux 上使用 RVM(Ruby 版本管理器),从 rvm.io/,可以非常容易地安装 Ruby 和 Rails。
在过去几年中,Ruby 语言变得越来越受欢迎,这导致了多种语言实现被编写出来,可以在不同的平台上运行。Matz 的 Ruby 解释器(MRI),Ruby 的标准实现,也已经经过了几个版本。RVM 对于管理和安装不同版本的 Ruby 来说非常出色。它包含一个一站式安装器 bash 脚本,可以安装最新的 Ruby 和 Rails。只需从终端运行以下命令:
curl -L https://get.rvm.io | bash -s stable --rails
这可能需要相当长的时间才能完成。一旦完成,你应该尝试在终端中输入 rails -v。如果你看到一个至少为 3.2 的版本号,你应该可以开始了。
Rails 已经安装好了吗?
现在我们已经安装了 Rails,让我们继续使用 CoffeeScript 来构建一个应用程序。
如果您遇到了任何问题或需要有关安装 Rails 的更多信息,最好的起点是 Ruby on Rails 网站的下载部分(rubyonrails.org/download)。
开发我们的 Rails 应用程序
我们将使用 Rails 在服务器端后端扩展我们现有的待办事项列表应用程序。如果您没有在前一章中跟随,那么您应该能够根据需要复制该章节的代码。
注意
本章的目的不是全面介绍 Ruby on Rails 或 Ruby 语言,而是希望专注于在如何使用 Rails 与 CoffeeScript 结合的背景下构建一个简单的 Rails 应用程序。
我们不会过于详细地介绍所有内容,并且我们将相信 Ruby 是一种相当简单且易于阅读的语言,Rails 代码也易于理解。即使您不熟悉这种语言和框架,也应该不会太难跟随。
首先,我们将使用rails命令创建一个空的 Rails 基础应用程序。导航到您想要创建应用程序的文件夹,然后运行此命令:
rails new todo
这将创建一个包含大量文件和文件夹的todo文件夹,用于您的 Web 应用程序。遵循 Rails 的约定精神,您的 Web 应用程序将以某种方式组织。
注意
rails命令除了用于生成新应用程序之外,还用于许多其他事情,并且是您进入许多常见日常 Rails 任务的入口点。本书将介绍其中的一些,如果您想查看它可以做什么的完整列表,可以运行rails -h。
让我们简要地谈谈 Rails 如何组织我们的应用程序。您的大部分应用程序代码可能都位于顶级app文件夹中。这个文件夹包含以下四个重要的子文件夹:
-
assets:这是资产管道操作的文件夹。这是所有 CoffeeScript(或 JavaScript)和 CSS 源代码以及我们的 Web 应用程序使用的图像所在的地方。 -
controllers: 这是您的控制器所在的位置。这些负责处理应用程序的路由请求,并与您的视图和模型进行交互。 -
models: 这是您将找到领域模型的地方。模型代表系统中的领域对象,并使用ActiveRecord基类对应于数据库表。 -
views: 这个文件夹包含用于渲染应用程序 HTML 的视图模板。默认情况下,Rails 使用 ERB 模板,这允许我们在 HTML 模板中包含 Ruby 代码片段,这些代码片段将被评估以生成最终的输出 HTML。
MVC
MVC,或模型-视图-控制器,是一种广泛使用的应用程序架构模式,旨在通过将应用程序关注点拆分为三个领域对象类型来简化代码并减少耦合。
Rails 非常遵循 MVC 模式,大多数 Rails 应用程序在模型、控制器和视图方面都会结构得非常严格。
过去几年中,许多 Rails 程序员提倡的一种 MVC 之上的模式是“胖模型,瘦控制器”。这个概念鼓励将大部分领域逻辑放在模型中,而控制器只应关注路由以及模型和视图之间的交互。
运行我们的应用程序
在这个阶段,我们 already 可以运行我们的 Rails 应用程序来查看是否一切正常。从终端输入:
cd todo
rails server
Rails 现在将在端口3000上为我们应用程序启动一个本地 Web 服务器。您可以通过浏览到http://localhost:3000/来测试它。如果一切顺利,那么您应该看到以下友好的欢迎信息:

小贴士
记得在另一个控制台窗口中保持这个服务器运行,因为我们测试我们的应用程序。您还可以检查这个过程的输出,以查找可能发生的任何错误。
我们的待办事项资源
因此,我们现在有一个正在运行的应用程序,但它除了显示欢迎页面之外并没有做什么。
为了达到跟踪待办任务的最终目标,我们将为我们的待办事项生成一个资源。在 Rails 术语中,资源由一个模型、具有一些操作的控制器以及这些操作的视图组成。
在终端,运行以下命令:
rails generate resource todo_item title:string completed:boolean
这是什么操作?这是一个 Rails 生成器语法的示例,它可以用来生成样板代码。在这里,我们告诉它创建一个名为TodoItemsController的“资源型”控制器和一个模型TodoItem,该模型有一个用于标题的string字段和一个标记为完成的boolean标志。
如您从命令输出中看到的,它还生成了一些文件,并在config/routes.rb中修改了一个现有的文件。让我们首先打开这个文件。
routes.rb
在routes.rb文件的顶部,您应该看到以下内容:
Todo::Application.routes.draw do
resources :todo_items
在 Rails 中,routes.rb定义了 HTTP 调用如何映射到可以处理它们的控制器操作。
在这里,生成器为我们添加了一行,使用了resources方法。这个方法为“资源型”控制器的最常见操作创建路由。这意味着它使用 HTTP 动词 GET、POST、PUT 和 DELETE 在您的应用程序中公开单个领域资源。
通常,这将创建七个不同的控制器操作的路由,即index、show、new、create、edit、update和destroy。您稍后将会看到,我们不需要为我们的控制器创建所有这些操作,因此我们将告诉resources方法只过滤出我们想要的。将文件修改如下代码片段:
Todo::Application.routes.draw do
resources :todo_items, only: [:index, :create, :update, :destroy]
控制器
在resources的调用中,Rails 使用:todo_items符号将resources方法传统地映射到为我们生成的TodoItemsController。
打开 app/controllers/todo_items_controller.rb 文件;这里是你将看到的内容:
class TodoItemsController < ApplicationController
end
如你所见,这里并没有太多内容。一个名为 TodoItemController 的类被声明,并且它继承自 ApplicationController 类。ApplicationController 类也是在创建应用时为我们生成的,并且它继承自 ActionController::Base,这给了它很多功能,并让它表现得像 Rails 控制器。
我们现在应该能够通过导航到 http://localhost:3000/todo_items URL 来测试我们的控制器。
你看到了什么?你应该会得到一个 未知动作 错误页面,指出 TodoItemsController 中找不到 index 动作。
这是因为控制器还没有定义 index 动作,正如我们在 routes.rb 文件中所指定的。让我们继续在我们的 TodoItemsController 类中添加一个方法来处理这个动作;这将在下面的代码片段中显示:
class TodoItemsController < ApplicationController
def index
end
end
如果我们刷新页面,我们会得到一个不同的错误消息:模板缺失。这是因为我们没有为 index 动作提供模板。默认情况下,Rails 总是会尝试返回与 index 动作名称相对应的已渲染模板。让我们现在就添加一个。
视图
Rails 视图保存在 app/views 文件夹中。每个控制器都会在这里有一个子文件夹,包含其视图。我们已经在上一章中有一个 index.html 文件,我们将在这里重新使用它。为此,我们需要将 body 标签内的一切内容复制到名为 app/views/todo_items/index.html.erb 的文件中,除了旧 index.html 文件中的最后两个 script 标签。
你应该得到以下标记:
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<ul id="todo-list">
</ul>
</section>
<footer id="footer">
<button id="clear-completed">Clear completed</button>
</footer>
</section>
看到这里,你可能想知道其余的 HTML,如包围的 html、head 和 body 标签去哪里了。
好吧,Rails 有一个布局文件的概念,它充当所有其他视图的包装器。这样,你可以有一个一致的网站骨架,你不需要为每个视图创建它。我们的视图将被嵌入到默认布局文件中:app/views/layouts/application.html.erb。让我们看看这个文件:
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<%= stylesheet_link_tag "application", :media => "all" %>
<%= javascript_include_tag "application" %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
stylesheet_link_tag 和 javascript_include_tag 方法将确保所有在 assets 文件夹中指定的文件都包含在 HTML 中。<%= yield %> 行是当前视图将被渲染的地方,在我们的例子中是 index.html.erb。
现在刷新页面后,我们会看到 index 页面。查看源代码以了解最终的 HTML 是如何输出的。
如你所见,我们的页面仍然没有样式,看起来相当单调。让我们看看我们能否让它再次看起来很漂亮。
CSS
默认情况下,资产管道会在 app/assets/stylesheets 文件夹中查找 CSS 文件。当我们浏览到这个文件夹时,我们会看到一个名为 todo_items.css.scss 的文件,这是在我们创建控制器时为我们生成的。
将上一章的 styles.css 文件内容复制到这个文件中。现在我们的 index 页面应该看起来又体面了。
注意
这个具有奇怪 .css.scss 扩展名的文件是一个 Sass 文件 (sass-lang.com/)。
与 CoffeeScript 一样,Sass 是正常 CSS 语言的扩展版本,具有许多使编写 CSS 更容易、更简洁的出色功能。
与 CoffeeScript 一样,它是 Rails 资产管道中的默认 CSS 编译器。我们使用的 Sass 版本是 CSS 的超集,这意味着我们可以在该文件中使用正常的 CSS 而不需要使用 Sass 的任何功能,并且它将正常工作。
我们的模型
因此,现在我们可以看到我们的待办事项列表,但没有任何条目显示出来。这次,我们不会将它们存储在本地,而是将它们存储在数据库中。幸运的是,我们已经有了一个数据库模型,它是我们在创建资源时自动生成的,即 TodoItem 模型,该模型定义在 app/models/todo_item.rb 中:
class TodoItem < ActiveRecord::Base
attr_accessible :completed, :title
end
在这里,就像控制器一样,你可以看到 Rails 模型通过从 ActiveRecord::Base 继承而获得大部分功能。attr_accessible 行告诉 ActiveRecord 哪些字段可以从用户输入中分配和赋值。
我们如何使用模型?在 todo_items_controller.rb 中添加以下高亮代码:
def index
@todo_items = TodoItem.all
end
这行代码使用了 TodoItem 类的 all 类方法,这也是由 ActiveRecord 提供的。这将返回数据库中每个记录的新 TodoItem 类实例,我们可以将其分配给名为 @todo_items 的实例变量(在 Ruby 中,所有实例变量都以 @ 符号开头)。
当 Rails 执行控制器动作时,它会自动将任何控制器实例变量提供给正在渲染的视图,这就是为什么我们在这里将其赋值。我们很快就会在视图中使用它。
让我们再次刷新页面以查看是否成功。然而,我们再次遇到了 找不到表 'todo_items' 的错误。
你可能已经猜到,我们应该在某个数据库中创建一个名为 todo_items 的表。幸运的是,Rails 已经为我们处理了这项艰巨的工作,使用了一种称为迁移的方法。
迁移
当我们生成资源时,Rails 不仅为我们创建了一个模型,还创建了一个用 Ruby 编写的数据库脚本,或称为 迁移。我们应该能够在 db/migrations 文件夹中打开它。实际的文件将以时间戳开头,并以 _create_todo_items.rb 结尾。它应该类似于以下代码片段:
class CreateTodoItems < ActiveRecord::Migration
def change
create_table :todo_items do |t|
t.string :title
t.boolean :completed
t.timestamps
end
end
end
此脚本将创建一个名为 todo_items 的表,其中包含我们在生成 todo_item 资源时指定的字段。它还使用 t.timestamps 方法创建了两个时间戳字段,名为 created_at 和 updated_at。Rails 将确保在创建或更新记录时,具有这些名称的字段会更新为适当的时间戳。
迁移脚本是一种自动化数据库更改的绝佳方式,甚至允许你回滚之前的更改。你也不必依赖于由资源或模型生成器创建的迁移。可以通过运行以下命令来生成自定义迁移:
rails generate migration migration_name
在生成你的自定义迁移之后,你只需实现 up 和 down 方法,这些方法将在你的迁移执行或回滚时被调用。
迁移是通过 rake 命令执行的。rake 是一个任务管理工具,它允许你将任务编写为 Ruby 脚本,然后通过 rake 命令行工具运行。Rails 随带了许多内置的 rake 任务,你可以通过以下方式查看它们的完整列表:
rake –T
目前我们感兴趣的任务是名为 db:migrate 的任务,让我们运行它看看会发生什么:
rake db:migrate
你应该看到以下输出:
== CreateTodoItems: 迁移中 ==================================================
-- create_table(:todo_items)
-> 0.0011s
== CreateTodoItems: 迁移完成 (0.0013s) =====================================
这意味着 Rails 已经成功地在数据库中为我们创建了一个 todo_items 表。当我们刷新应用程序页面时,我们应该看到错误已经消失,我们看到了我们的空白待办事项列表。
小贴士
数据库在哪里?
你可能想知道我们的实际数据库目前在哪里。Rails 默认使用嵌入式的 SQLite 数据库。SQLite (www.sqlite.org) 是一个自包含的、基于文件的数据库,不需要配置服务器即可运行。这使得在开发应用程序时快速启动和运行变得非常方便。
一旦你实际部署你的网络应用程序,你可能希望使用更传统的数据库服务器,例如 MySQL 或 PostgreSQL。你可以在 config/database.yml 文件中轻松更改你的数据库连接设置。
我们还没有将我们的视图与显示待办事项列表的实际功能连接起来。在我们这样做之前,让我们在数据库中手动创建几个待办事项。
Rails 控制台
Rails 通过使用 Rails 控制台以交互方式与你的代码进行玩耍,这是一种加载了所有 Rails 项目代码的交互式 Ruby 解释器,或称为 irb 会话。让我们通过以下命令启动它:
rails console
一旦你进入控制台,你可以输入任何有效的 Ruby 代码。你还可以访问你 Rails 应用程序中的所有模型。让我们用之前用过的 TodoItem.all 方法来试一试;这将在以下屏幕截图中展示:

目前它返回一个空数组,因为我们的表仍然是空的。注意,Rails 还输出了它为获取所有记录生成的 SQL 查询。
从这里我们也可以使用我们的模型创建一个新的待办事项。以下代码将完成这个任务:
TodoItem.create(title: "Hook up our index view", completed: false)
现在,我们应该在我们的表格中只有一个待办事项。你可以通过使用 TodoItem.first 来验证这一点,它将返回我们表格中的第一个项目。
我想要确保我们的模型始终有一个标题。ActiveRecord 有非常强大的内置验证功能,允许以非常声明性的方式指定模型属性的约束。让我们确保我们的模型在保存之前始终检查标题的存在;为此,添加以下突出显示的代码:
class TodoItem < ActiveRecord::Base
attr_accessible :completed, :title
validates :title, :presence => true
end
继续创建一些其他的待办事项。一旦完成,再次尝试运行 TodoItem.all。这次它返回一个 TodoItem 实例的数组。
注意
要退出 Rails 控制台,只需输入 exit。
使用 ERB 在我们的视图中显示项目
为了在我们的视图中显示待办事项,我们将使用我们在控制器操作中创建的 @todo_items 实例变量。让我们修改 app/views/todo_items.html.erb 文件,并使用 ERB 混合一些 Ruby;在以下代码片段中添加突出显示的代码:
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<ul id="todo-list">
<% @todo_items.each do |item| %>
<li class="<%= item.completed ? "completed" : "" %>" data-id="<%= item.id %>">
<div class="view">
<input class="toggle" type="checkbox" <%= "checked" if item.completed %>>
<label><%= item.title %></label>
<button class="destroy"></button>
</div>
</li>
<% end %>
</ul>
</section>
<footer id="footer">
<button id="clear-completed">Clear completed</button>
</footer>
</section>
ERB 模板很容易理解。基本思想是,你像平常一样编写你的 HTML,然后使用 ERB 标签混合 Ruby。以下三个标签很重要:
<% These tags will be just be executed %>
<%= These should contain a Ruby expression that will be evaluated and included in the document %>
<%# This is a comment and will be ignored %>
在我们的 index ERB 模板中,我们使用 Ruby 的 each 迭代器遍历 @todo_items 数组实例变量中的所有元素;each 接收一个 Ruby 块作为参数。块是一段可以被传递给方法作为数据的代码,类似于在 CoffeeScript 中函数可以作为参数传递的方式。
这个块将为数组中的每个项目执行,将其作为项目变量传递。对于每个项目,我们创建其标记,在 ERB 标签中使用项目的 title 和 completed 属性。
当我们刷新页面时,我们现在最终应该看到我们的待办事项列表!如果你好奇,查看文档的 HTML 源代码,并将其与 ERB 模板进行比较,这应该能给你一个很好的想法它是如何生成的。输出页面在以下屏幕截图中显示:

创建部分
目前,我们的视图代码开始变得有些杂乱,特别是待办事项列表。我们可以通过使用 视图部分 来清理它,这允许我们将视图的片段拉到一个单独的文件中。然后,我们可以在主视图中需要的地方渲染它。在你的文件中添加以下代码片段中突出显示的行:
<section id="main">
<ul id="todo-list">
<% @todo_items.each do |item| %>
<%= render partial: 'todo_item', locals: {item: item} %>
<% end %>
</ul>
</section>
我们将把待办事项标记移动到它自己的部分文件中。按照惯例,部分文件名以下划线开头,当渲染部分时,Rails 会寻找与指定的部分具有相同名称的文件,且文件名以一个下划线开头。继续创建一个文件:app/views/todo_items/_todo_item.html.erb,内容如下:
<li class="<%= item.completed ? "completed" : "" %>" data-id="<%= item.id %>">
<div class="view">
<input class="toggle" type="checkbox" <%= "checked" if item.completed %>>
<label><%= item.title %></label>
<button class="destroy"></button>
</div>
</li>
如果一切顺利,我们的视图应该仍然像以前一样工作,我们已经很好地清理了主要的视图代码。使用部分来简化视图也是为了提高可重用性,我们稍后会看到这一点。
我们的待办事项应用还需要做一些工作。目前,我们无法添加新任务,完成的任务和删除操作也不起作用。这需要一些客户端代码,这意味着我们终于可以开始使用一些 CoffeeScript 了。
添加新项目
要将新项目添加到我们的待办事项列表中,我们将使用一些 Rails 的原生 AJAX 功能。以下代码片段是我们在 index 视图上修改后的 todo 输入:
<header id="header">
<h1>todos</h1>
<%= form_for TodoItem.new, :method => :post, :remote => true do |f| %>
<%= f.text_field :title, id:'new-todo', placeholder: 'What needs to be done?', autofocus: true %>
<% end %>
</header>
那么,这里有什么变化?首先,你会注意到我们包含了 form_for 方法,并在其块内调用另一个 text_field。这些都是 Rails 的视图辅助方法,它们是 Ruby 方法,可以在视图中使用,提供构建 HTML 输出的方式。
form_for 方法将输出一个 HTML form 标签,而 text_field 方法将在表单内生成一个 input 标签,其类型为 text。
我们将 TodoItem 的新实例作为参数传递给 form_for 方法。Rails 足够聪明,可以从 TodoItem 实例中知道表单的 URL 应该指向 TodoItemController,并且将使用 TodoItem 模型的属性作为表单内输入的名称。
真正的魔法来自于发送给 form_for 方法的 :remote => true 参数。这告诉 Rails 你希望使用 AJAX 提交这个表单。Rails 将在后台处理所有这些。
所以我的表单将提交到哪个控制器动作?由于我们指定了它的动作为 post,它将映射到 TodoItemController 中的 create 动作。我们还没有一个,所以让我们去写一个:
def create
@todo_item = TodoItem.create(params[:todo_item])
end
在这里,我们使用 params 中的 :todo_item 键创建 TodoItem——params 是 Rails 创建的 Ruby 哈希。它包含一个具有 :todo_items 键的值,这是一个包含从表单提交的所有参数值的哈希。当我们把这个哈希传递给 TodoItem.create 方法时,Rails 将知道如何将它们映射到我们新模型上的属性并将它们保存到数据库中。
让我们尝试添加一个待办事项
在我们的输入框中输入一个新待办事项的标题并按 Enter。
然而,看起来什么都没发生。我们可以查看正在运行的 Rails 服务器会话的输出,看看是否能找到任何错误。如果你稍微滚动一下,你应该会看到一个类似于以下错误消息的错误:
ActionView::MissingTemplate (Missing template todo_items/create, application/create with {:locale=>[:en], :formats=>[:js, "application/
ecmascript", "application/x-ecmascript", :html, :text, :js, :css, :ics, :csv, :png, :jpeg, :gif, :bmp, :tiff, :mpeg, :xml, :rss, :atom,
:yaml, :multipart_form, :url_encoded_form, :json, :pdf, :zip], :handlers=>[:erb, :builder, :coffee]}. Searched in:
*** "/home/michael/dev/todo/app/views"**
)
添加 CoffeeScript 视图
因此,似乎我们还需要做一件事。所有控制器动作都会默认尝试渲染一个视图。当我们现在尝试添加待办事项时,我们会得到与之前相同的 模板缺失 错误。由于表单是使用 AJAX 提交的,所以可能不清楚应该发生什么。我们是否仍然需要渲染一个视图?它会是什么样子?
仔细查看错误信息可能会给我们一些线索。由于我们的动作是通过 AJAX 调用的,Rails 默认会寻找一个 CoffeeScript 视图来渲染成 JavaScript。
生成的 JavaScript 将作为 AJAX 调用的响应,并在完成后执行。这似乎也是更新我们在服务器上创建的待办事项列表的完美位置。
我们将在 app/views/todo_items/create.js.coffee 中为我们的 create 动作创建一个 CoffeeScript 视图模板。
$('#new-todo').val('')
html = "<%= escape_javascript(render partial: 'todo_item', locals: {item: @todo_item}) %>"
$("#todo-list").append(html)
在前面的代码片段中,我们获取了 #new-todo 输入并清除了其值。然后我们渲染了之前使用的相同的 todo_item 部分视图,传递了在控制器动作中创建的 @todo_item 实例变量。
我们将渲染调用包裹在 escape_javascript 辅助方法中,这将确保我们的字符串中的任何特殊 JavaScript 字符都会被转义。然后我们将新渲染的部分追加到我们的 #todo-list 元素中。
尝试一下。我们现在终于可以创建待办事项列表项了!
提示
jQuery 从何而来?
Rails 已经为我们包含了 jQuery。Rails 资产管道使用一个清单文件,app/assets/javascript/application.js,来包含所需的依赖项,例如 jQuery。
资产管道中的 CoffeeScript
注意这一切是多么的流畅?Rails 将 CoffeeScript 视为其堆栈中的第一公民,并确保在它们被使用之前,.coffee 文件被编译成 JavaScript。你还可以在服务器上使用 ERB 模板预先处理 CoffeeScript,这使得它更加强大。
完成待办事项
让我们连接这个功能。这次,我们将采取不同的方式来展示在 Rails 中编写 CoffeeScript 的不同风格。我们将遵循处理 AJAX 调用的更传统方法。
当我们创建控制器时,Rails 已经创建了一个文件,我们可以将其用于客户端代码。每个控制器都会得到自己的 CoffeeScript 文件,该文件将在页面上自动包含任何对该控制器动作的操作。
提示
还有一个 application.js.coffee 文件,可以在其中添加全局客户端代码。
我们感兴趣的文件将是 app/assets/views/javascripts/todo_items.js.coffee。我们可以用以下代码替换其内容,该代码将处理完成任务时的 AJAX 调用:
toggleItem = (elem) ->
$li = $(elem).closest('li').toggleClass("completed")
id = $li.data 'id'
data = "todo_item[completed]=#{elem.checked}"
url = "/todo_items/#{id}"
$.ajax
type: 'PUT'
url: url
data: data
$ ->
$("#todo-list").on 'change', '.toggle', (e) -> toggleItem e.target
首先,我们定义了一个名为 toggleItem 的函数,该函数在复选框值改变时被调用。在这个函数中,我们切换父 li 元素的 completed 类,并使用其 data 属性获取待办事项的 ID。然后,我们向 TodoItemController 发起一个 AJAX 调用,以更新复选框的当前选中值。
在我们能够运行此代码之前,我们需要在我们的控制器中添加一个 update 动作,如下面的代码片段所示:
def update
item = TodoItem.find params[:id]
item.update_attributes params[:todo_item]
render nothing: true
end
params[:id] 将是 URL 中 ID 的值。我们使用这个值来查找待办事项,然后调用 update_attributes 方法,它正是这样做的,更新我们的模型并将其保存到数据库中。注意,我们明确告诉 Rails 不要渲染视图,通过调用 render nothing: true。
将任务设置为已完成现在应该可以工作。注意,当你刷新页面时,任务保持已完成状态,因为它们已经被保存到数据库中。
移除任务
对于移除任务,我们将遵循一个非常相似的模式。
在 todo_items.js.coffee 文件中,添加以下代码:
destroyItem = (elem) ->
$li = $(elem).closest('li')
id = $li.data 'id'
url = "/todo_items/#{id}"
$.ajax
url: url
type: 'DELETE'
success: -> $li.remove()
$ ->
$("#todo-list").on 'change', '.toggle', (e) -> toggleItem e.target
$("#todo-list").on 'click', '.destroy', (e) -> destroyItem e.target
在我们的控制器中,添加以下代码:
def destroy
TodoItem.find(params[:id]).destroy
render nothing: true
end
那应该就是我们需要移除列表项的所有内容。注意,在这里我们只在 AJAX 调用成功后移除元素,通过处理 success 回调。
现在,轮到你了
作为最后的练习,我会要求你使 清除已完成 按钮工作。作为一个提示,你应该能够使用现有的 destroyItem 方法功能。
摘要
本章以对 Ruby on Rails 的快速浏览开始。你可能会逐渐欣赏 Rails 为网络开发者提供的魔法,以及开发 Rails 应用程序有多么有趣。我们还花了一些时间发现使用 CoffeeScript 在 Rails 应用程序中是多么容易,以及你通常会用到的不同方法和技巧来编写客户端代码。
如果你还没有这样做,我鼓励你花更多的时间学习 Rails 以及 Ruby,并沉浸在他们支持的精彩社区中。
在下一章中,我们将探索另一个使用 JavaScript 构建的新兴服务器框架,以及 CoffeeScript 与其的关系。
第五章. CoffeeScript 和 Node.js
Ryan Dahl 在 2009 年创建了 Node.js。他的目标是创建一个系统,使用户能够编写高性能的网络服务器应用程序,使用 JavaScript。当时,JavaScript 主要在浏览器中运行,因此服务器端框架需要某种方式来运行 JavaScript 而不依赖浏览器。Node 使用了 Google 的 V8 JavaScript 引擎,最初是为 Chrome 浏览器编写的,但由于它是一个独立的软件组件,它可以在任何地方运行 JavaScript 代码。Node.js 允许你编写可以在服务器上执行的 JavaScript 代码。它可以充分利用你的操作系统、数据库和其他外部网络资源。
让我们谈谈 Node.js 的一些特性。
Node 是事件驱动的
Node.js 框架只允许非阻塞、异步 I/O。这意味着任何访问外部资源(如操作系统、数据库或网络资源)的 I/O 操作都必须异步进行。这是通过使用事件或回调来实现的,一旦操作成功或失败,就会触发回调。
这种做法的好处是,你的应用程序会变得更加可扩展,因为请求不需要等待缓慢的 I/O 操作完成,而是可以处理更多的传入请求。
其他语言中也有类似的框架,例如 Python 中的 Twisted 和 Tornado,以及 Ruby 中的 EventMachine。这些框架的一个大问题是它们所使用的所有 I/O 库都必须是非阻塞的。常常会不小心使用到会阻塞 I/O 操作的代码。
Node.js 是从头开始构建的,具有事件驱动哲学,并且只允许非阻塞 I/O,从而避免了这个问题。
Node 是快速和可扩展的
Node.js 使用的 V8 JavaScript 引擎在性能上高度优化,因此使得 Node.js 应用程序非常快。Node 非阻塞的事实将确保你的应用程序能够在不使用大量系统资源的情况下处理许多并发客户端请求。
Node 不是 Rails
虽然 Node 和 Rails 经常被用来构建类似类型的应用程序,但它们实际上非常不同。Rails 努力成为构建 Web 应用程序的全栈解决方案,而 Node.js 则更像是编写任何类型快速和可扩展网络应用的底层系统。它几乎不对你的应用程序结构做出任何假设,除了你将使用基于事件的架构。
因此,Node 开发者通常会从各种基于 Node 构建的框架和模块中选择,用于编写 Web 应用程序,例如 Express 或 Flatiron。
Node 和 CoffeeScript
正如我们之前所看到的,CoffeeScript 可以作为 npm 模块使用。因此,使用 CoffeeScript 编写 Node.js 应用程序非常简单。事实上,我们之前讨论的coffee命令默认会使用 Node 运行.coffee脚本。要安装带有 CoffeeScript 的 Node,请参阅第二章,运行 CoffeeScript。
"Hello World"在 Node 中
让我们使用 CoffeeScript 编写最简单的 Node 应用程序。创建一个名为hello.coffee的文件,并在其中输入以下代码:
http = require('http')
server = http.createServer (req, res) ->
res.writeHead 200
res.end 'Hello World'
server.listen 8080
这使用了 Node.js 的http模块,它提供了构建 HTTP 服务器的功能。require('http')函数将返回一个http模块的实例,该实例导出createServer函数。此函数接受一个requestListener参数,该参数是一个将响应用户请求的函数。在这种情况下,我们以 HTTP 状态码200响应,并以Hello World作为请求体结束响应。最后,我们调用返回服务器的listen方法来启动它。当调用此方法时,服务器将监听和处理请求,直到我们停止它。
我们可以使用 coffee 命令运行此文件,如下所示:
coffee hello.coffee
我们可以通过浏览到http://localhost:8080/来测试我们的服务器。我们应该看到一个只有文本的简单页面,内容为Hello World。
Express
如您所见,Node 本身非常底层和简单。构建 Web 应用程序基本上意味着编写一个原始的 HTTP 服务器。幸运的是,在过去的几年中已经开发了许多库来帮助在 Node 上编写 Web 应用程序,并抽象出许多底层细节。
其中最受欢迎的可能是Express (expressjs.com/)。与 Rails 类似,它具有许多使执行常见 Web 应用程序任务(如路由、渲染视图和托管静态资源)更简单的优秀功能。
在本章中,我们将使用 CoffeeScript 在 Express 中编写一个 Web 应用程序。
WebSocket
由于我想展示一些 Node 的可扩展性功能以及它通常被用于的应用类型,我们将利用另一种有趣的现代网络技术,称为WebSocket。
WebSocket 协议是一个标准,允许通过标准 HTTP 端口80进行原始、双向和全双工(双向同时)的 TCP 连接。这允许客户端和服务器建立长期运行的 TCP 连接,服务器可以通过该连接执行推送操作,这在传统 HTTP 中是不可能的。它通常用于需要客户端和服务器之间大量低延迟交互的应用程序。
Jade
Jade 是一种轻量级的标记模板语言,它允许你使用与 CoffeeScript 非常相似的语法编写优雅且简短的 HTML。它使用了许多特性,如语法空白,以减少你编写 HTML 文档所需的按键次数。通常情况下,当你运行 Express 时,它会默认安装,我们将在本书中使用它。
我们的应
在本章中,我们将构建一个协作式待办事项列表应用。这意味着你将能够实时与其他人共享你的待办事项列表。一个人或多人将能够同时添加、完成或删除待办事项列表项。待办事项列表的更改将自动传播到所有用户。这正是 Node 完美适合的应用类型。
我们的 Node.js 代码将包括两个不同的部分,一个是提供静态 HTML、CSS 和 JavaScript 的常规网络应用,另一个是处理所有待办事项列表客户端实时更新的 WebSocket 服务器。与此相关,我们还将有一个由 jQuery 驱动的客户端,其外观将非常类似于我们的 第三章 应用。
我们将使用一些现有待办事项应用的资产(样式表和图片)。我们还将重用来自第三章的客户端 jQuery 代码,并对它进行调整以适应我们的应用。如果你没有跟随前面的章节,你应该能够根据需要从本章的代码中复制资产。
让我们开始吧
要开始,我们将执行以下步骤:
-
为我们的应用创建一个文件夹。
-
使用
package.json文件指定我们的应用依赖项。 -
安装我们的依赖项。
-
创建一个
app.coffee文件。 -
首次运行我们的应用。
package.json
在名为 todo 的新文件夹中创建一个名为 package.json 的文件。将以下代码添加到该文件中:
{
"name": "todo",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app"
},
"dependencies": {
"express": "3.0.0beta6",
"jade": "*",
"socket.io": "*",
"coffee-script": "*",
"connect-assets": "*"
}
}
这是一个简单的 JSON 文件,用作应用清单,并用于告诉 npm 你在应用中依赖哪些依赖项。在这里,我们使用 Express 作为我们的网络框架,Jade 作为我们的模板语言。由于我们将使用 WebSocket,我们将引入 socket.io。我们还可以通过将其添加到我们的文件中来确保 CoffeeScript 已安装。最后,我们将使用 connect-assets,这是一个模块,以与 Rails 资产管道类似的方式管理客户端资产。
在处理 Node.js 框架时,你会注意到应用通常是以这种方式由 npm 模块编织而成的。寻找 npm 模块的好地方是 Node 工具箱网站 (nodetoolbox.com)。
安装我们的模块
要安装 package.json 文件中的依赖项,在命令行工具中导航到项目文件夹,并运行以下命令:
npm install
如果一切顺利,那么我们现在应该已经安装了所有项目依赖项。为了验证这一点或只是查看 npm 做了什么,你可以运行以下命令:
npm ls
这将输出一个以树形格式显示的已安装模块及其依赖项的列表。
创建我们的应用程序
运行我们的应用程序,我们只需要创建一个主、入口点文件,该文件用于连接我们的 Express 应用程序并指定我们的路由。在根文件夹中,创建一个名为app.coffee的文件,并将以下代码添加到其中:
express = require 'express'
app = express()
app.get '/', (req, res) ->
res.send('Hello Express')
app.listen(3000)
console.log('Listening on port 3000')
这看起来与我们的“Hello World”示例非常相似。
首先,我们使用require函数加载 Express 模块。Node 模块很简单;每个模块对应一个文件。每个模块可以声明代码,当它被导入时将被导出。当你调用require,并且模块的名称既不是原生模块也不是文件路径时,Node 会自动在node_modules文件夹中查找该文件。这当然是 npm 安装模块的地方。
在下一行,我们通过调用express函数并分配给app变量来创建我们的 Express 应用程序。
然后,我们使用get方法为我们的应用程序创建一个索引路由。我们指定路径为'/',然后传递一个匿名函数来处理请求。它接受两个参数,即req和res参数。目前,我们只需将Hello Express写入响应并返回。
然后,我们使用listen方法启动我们的应用程序,并告诉它在端口3000上运行。最后,我们将写入标准输出,这样我们就会知道应用程序已启动。
如您所见,Express 的魔力在于声明性地设置路由。使用 Express,你可以通过指定 HTTP 方法、URL 路径和处理请求的函数来轻松创建路由。
运行我们的应用程序
让我们运行我们的应用程序,看看是否一切正常。在我们的应用程序文件夹中,在命令行工具中输入以下内容:
coffee app.coffee
你应该看到输出为监听端口 3000。
将您的浏览器指向http://localhost:3000/。你应该看到文本Hello Express。
要在命令行工具中停止 Node 进程,只需使用Ctrl + C。
创建视图
与 Rails 等其他 Web 框架类似,Express 有视图的概念,这允许你使用单独的文件将 UI 与应用程序分离。通常,这些是用 Jade 等模板语言编写的。让我们为我们的根操作创建一个视图。
要做到这一点,我们需要:
-
创建一个
views文件夹并添加一个 Jade 视图文件。 -
配置我们的 Express 应用程序以了解存储视图的文件夹以及我们正在使用的模板库。
-
将我们的索引路由更改为渲染我们的视图。
让我们在项目根目录中创建一个新的文件夹,命名为views。在这个文件夹中,我们创建一个名为index.jade的新文件。它应该看起来像这样:
doctype 5
html
head
title Our Jade view
body
p= message
如您所见,Jade 为普通 HTML 提供了非常干净和简洁的语法。您不需要在尖括号中包含结束标签。类似于 CoffeeScript,它也使用缩进来界定块,这样您就不需要输入结束标签。p= message行创建了一个<p>标签,其内容将被评估为message字段的值,这个值应该传递到我们的视图选项中。
在我们的app.coffee文件中,我们将添加以下代码:
express = require 'express'
path = require 'path'
app = express()
app.set 'views', path.join __dirname, 'views'
app.set 'view engine', 'jade'
app.get '/', (req, res) ->
res.render 'index', message: "Now we're cooking with gas!"
app.listen(3000)
console.log('Listening on port 3000')
这里,我们使用set函数设置views文件夹,并分配'views'键。我们使用文件顶部包含的path模块来创建和连接我们的当前文件夹名到views子文件夹。__dirname是一个全局变量,它指向当前工作文件夹。我们还设置了视图引擎为'jade'。
接下来,我们将我们的get '/'路由更改为渲染索引模板并传递一个包含消息的选项哈希。这是随后在视图中渲染的值。
一旦我们再次运行我们的应用程序并刷新页面,我们应该现在看到我们的页面已经更新为新的文本。
node-supervisor
到目前为止,你可能想知道每次我们更改代码时是否需要重新启动我们的 Node 应用程序。理想情况下,在开发中,我们希望每次我们更改代码时,代码都能自动重新加载,就像在 Rails 中那样工作。
幸运的是,有一个整洁的开源库我们可以使用,它正好能做这件事:node-supervisor (github.com/isaacs/node-supervisor)。我们像安装任何其他 npm 模块一样安装它,我们只是确保传递-g标志来全局安装,如下面的命令所示:
npm install supervisor -g
在终端中,你现在应该可以使用以下命令运行 supervisor:
supervisor app.coffee
在单独的窗口中保持此过程运行。为了检查是否成功,让我们编辑发送到视图的消息;编辑后的消息在下面的代码片段中被突出显示:
app.get '/', (req, res) ->
res.render 'index', message: "Now we're cooking with supervisor!"
如果我们现在刷新我们的页面,我们会看到它已经被更新。从现在开始,我们可以确保 supervisor 在运行,我们不需要重新启动 Node 进程来做出更改。
待办事项列表视图
现在让我们扩展我们的视图,使其看起来像我们的真实待办事项应用程序。编辑index.jade文件,使其看起来如下:
doctype 5
html
head
title Collaborative Todo
body
section#todoapp
header#header
h1 todos
input#new-todo(placeholder="What needs to be done?", autofocus=true)
section#main
ul#todo-list
footer#footer
button#clear-completed Clear completed
这里有一些我们之前没有见过的新的 Jade 语法。标签 ID 由#符号表示,所以header#header变为<header id="header">。标签属性在方括号内指定,如下所示:tag(name="value")。
由于我们不再在模板中使用message变量,我们将从app.coffee文件中的render调用中移除它,如下面的代码片段所示:
app.get '/', (req, res) ->
res.render 'index'
我们的页面现在将更新,但看起来可能不太好看。我们将使用在之前项目中使用的相同样式表来美化我们的页面。
小贴士
未按预期工作?
记得关注 supervisor 进程的输出,看看你的 CoffeeScript 或 Jade 模板中是否有语法错误,尤其是如果你没有看到预期的输出。
在我们使用样式表之前,我们需要设置 Express 为我们提供静态文件服务。修改app.coffee文件,使其看起来如下:
express = require 'express'
path = require 'path'
app = express()
app.set 'views', path.join __dirname, 'views'
app.set 'view engine', 'jade'
app.use(express.static(path.join __dirname, 'public'))
那么在之前的代码片段中发生了什么?我们在一行中添加了对静态文件服务的支持,但这是如何工作的?答案在于 Node 如何使用中间件。
中间件
Express 框架建立在名为Connect的底层框架之上(www.senchalabs.org/connect/)。Connect 的基本思想是提供用于 Web 请求的中间件。
中间件可以串联起来以生成一个 Web 应用程序堆栈。每个中间件部分只关心通过修改输出响应或请求的控制流来提供一小组功能。
在我们的例子中,我们告诉应用程序使用由express.static函数创建的中间件。这个函数将为提供的文件路径创建一个静态文件服务器。
我们的样式表
创建一个名为public的文件夹,并在其中创建一个名为css的子文件夹。将样式表保存为todo.css在这个文件夹中。我们仍然需要在index视图中包含这个样式表。将以下行——代码片段中突出显示的内容——添加到views文件夹中的index.jade文件中:
doctype 5
html
head
title Collaborative Todo
link(rel="stylesheet", href="css/todo.css")
body
一旦我们链接了样式表,我们应该能够刷新我们的视图。现在它看起来应该会好很多。
客户端
为了使我们的待办事项应用程序工作,我们将复制在第三章中创建的客户端 jQuery 代码。我们将把它放在一个名为todo.coffee的文件中。
我们接下来的决定是,我们应该把这个文件放在哪里?我们如何在应用程序中编译和使用它的输出?
我们在第三章中构建应用程序时做了同样的事情,即创建一个包含客户端 offeeScript 代码的src文件夹,然后使用带有--watch标志的coffee命令编译它。生成的 JavaScript 可以放入我们的public文件夹中,我们可以像通常一样包含它。但这意味着我们将有两个独立的后台任务在运行,一个是运行我们的服务器的 supervisor 任务,另一个是编译我们的客户端代码。
幸运的是,有更好的方法。你可能还记得我们在package.json文件中有一个对connect-assets模块的引用。它为我们提供了一个与 Rails 中得到的非常相似的资产管道。它将透明地处理编译和依赖关系管理。
我们需要在app.coffee文件中使用中间件,如下代码片段所示:
app.set 'views', path.join __dirname, 'views'
app.set 'view engine', 'jade'
app.use(express.static(path.join __dirname, 'public'))
app.use require('connect-assets')()
connect-assets模块默认将使用assets文件夹来管理和提供资产。让我们在根文件夹中创建一个名为assets/js的文件夹。我们将在该文件夹中创建一个名为todo.coffee的新文件,包含以下代码:
Storage::setObj = (key, obj) ->
localStorage.setItem key, JSON.stringify(obj)
Storage::getObj = (key) ->
JSON.parse this.getItem(key)
class TodoApp
constructor: ->
@cacheElements()
@bindEvents()
@displayItems()
cacheElements: ->
@$input = $('#new-todo')
@$todoList = $('#todo-list')
@$clearCompleted = $('#clear-completed')
bindEvents: ->
@$input.on 'keyup', (e) => @create e
@$todoList.on 'click', '.destroy', (e) => @destroy e.target
@$todoList.on 'change', '.toggle', (e) => @toggle e.target
@$clearCompleted.on 'click', (e) => @clearCompleted()
create: (e) ->
val = $.trim @$input.val()
return unless e.which == 13 and val
randomId = Math.floor Math.random()*999999
localStorage.setObj randomId,{
id: randomId
title: val
completed: false
}
@$input.val ''
@displayItems()
displayItems: ->
@clearItems()
@addItem(localStorage.getObj(id)) for id in Object.keys(localStorage)
clearItems: ->
@$todoList.empty()
addItem: (item) ->
html = """
<li #{if item.completed then 'class="completed"' else ''} data-id="#{item.id}">
<div class="view">
<input class="toggle" type="checkbox" #{if item.completed then 'checked' else ''}>
<label>#{item.title}</label>
<button class="destroy"></button>
</div>
</li>
"""
@$todoList.append html
destroy: (elem) ->
id = ($(elem).closest 'li').data('id')
localStorage.removeItem id
@displayItems()
toggle: (elem) ->
id = $(elem).closest('li').data('id')
item = localStorage.getObj(id)
item.completed = !item.completed
localStorage.setObj(id, item)
clearCompleted: ->
(localStorage.removeItem id for id in Object.keys(localStorage) \
when (localStorage.getObj id).completed)
@displayItems()
$ ->
app = new TodoApp()
如果您一直在跟随第三章,那么这段代码应该很熟悉。这是我们完整的客户端应用程序,它显示待办事项,并在localStorage中创建、更新和销毁项目。
要在我们的 HTML 中使用此文件,我们仍然需要包含一个script标签。由于我们使用 jQuery,我们还需要在 HTML 中包含库。
将以下代码添加到index.jade文件的底部:
script(src="img/jquery.min.js")
!= js('todo')
如您所见,我们使用 Google CDN 包含了 jQuery 的链接。然后我们使用由connect-assets提供的js辅助函数,创建一个指向我们编译的todo.js文件的script标签(connect-assets模块将透明地编译我们的 CoffeeScript)。!=符号是 Jade 的语法,用于运行 JavaScript 函数及其结果。
如果一切顺利,我们应该能够刷新页面,并拥有一个工作状态良好的客户端页面,用于我们的应用。尝试添加新项目,标记项目为完成,删除项目,以及清除已完成的项目。
添加协作
现在我们已经准备好将协作添加到我们的待办事项列表应用程序中。我们需要创建一个页面,让多个用户可以连接到同一个待办事项列表,并可以同时编辑它,实时看到结果。
我们希望支持命名列表的概念,您可以通过它与他人协作。
在我们深入功能之前,让我们稍微调整一下我们的 UI,以支持所有这些功能。
创建协作 UI
首先,我们将添加一个输入字段来指定列表名称,以及一个按钮来加入指定的列表。
对我们的index.jade文件进行以下更改(代码片段中突出显示),这将添加一个input元素和一个button元素来指定我们的列表名称并加入它:
footer#footer
| Join list:
input#join-list-name
button#join Join
button#clear-completed Clear completed
script(src="img/jquery.min.js")
!= js('todo')
我们的页面现在应该看起来像以下截图显示的页面:

客户端的 WebSocket
现在,让我们添加一个事件处理程序,当用户点击加入按钮时连接到房间。
在我们的todo.coffee文件中,我们将在cacheElements和bindEvents函数中添加以下代码:
cacheElements: ->
@$input = $('#new-todo')
@$todoList = $('#todo-list')
@$clearCompleted = $('#clear-completed')
@$joinListName = $("#join-list-name")
@$join = $('#join')
bindEvents: ->
@$input.on 'keyup', (e) => @create e
@$todoList.on 'click', '.destroy', (e) => @destroy e.target
@$todoList.on 'change', '.toggle', (e) => @toggle e.target
@$clearCompleted.on 'click', (e) => @clearCompleted()
@$join.on 'click', (e) => @joinList()
我们获取join-list-name输入和join按钮元素,并将它们存储在两个实例变量中。然后我们在@$join按钮上设置click处理程序,以调用一个名为joinList的新函数。让我们现在就定义这个函数。在bindEvents函数定义之后将其添加到类的末尾:
clearCompleted: ->
(localStorage.removeItem id for id in Object.keys(localStorage) \
when (localStorage.getObj id).completed)
@displayItems()
joinList: ->
@socket = io.connect('http://localhost:3000')
@socket.on 'connect', =>
@socket.emit 'joinList', @$joinListName.val()
这里是我们开始使用 Socket.IO 的地方。Socket.IO 库分为两部分:客户端库用于打开 WebSocket 连接,发送请求,并接收响应,以及服务器端 node 模块用于处理请求。
在前面的代码中,joinList 函数使用 io.connect 函数打开一个新的套接字,并传递 URL。然后它使用 on 函数传递一个处理函数,该函数将在 WebSocket 连接建立后运行。
成功连接处理函数将反过来使用 socket.emit 函数,这允许我们使用 joinList 作为标识符向服务器发送自定义消息。我们将 @joinListName 输入的值作为其值传递。
在我们开始实现服务器端代码之前,我们仍然需要包含一个 script 标签来使用 socket.io 客户端库。在 index.jade 文件的底部添加以下高亮的 script 标签:
script(src="img/jquery.min.js")
script(src="img/socket.io.js")
!= js('todo')
你可能想知道这个文件是从哪里来的。接下来,我们将在 app.coffee 文件中设置 Socket.IO 中间件。这将为我们托管客户端库。
服务器上的 WebSocket
我们已经准备好了客户端代码来发送 WebSocket 请求;现在我们可以继续我们的 Node 后端。首先,我们需要设置我们的 Socket.IO 中间件。这里有一个小的注意事项,我们不能直接将 Socket.IO 作为 Express 应用程序的中间件使用,因为 Socket.IO 期望一个 Node.js HTTP 服务器,并且没有直接支持 Express。相反,我们将使用内置的 Node.js HTTP 模块创建一个网络服务器,并将我们的 Express 应用程序作为 requestListener 传递。然后我们可以使用 Socket.IO 的 listen 函数连接到服务器。
以下是我们 app.coffee 文件中的代码外观:
express = require 'express'
path = require 'path'
app = express()
server = (require 'http').createServer app
io = (require 'socket.io').listen server
app.set 'views', path.join __dirname, 'views'
app.set 'view engine', 'jade'
app.use(express.static(path.join __dirname, 'public'))
app.use (require 'connect-assets')()
app.get '/', (req, res) ->
res.render 'index'
io.sockets.on 'connection', (socket) =>
console.log('connected')
socket.on 'joinList', (list) => console.log "Joining list #{list}"
server.listen(3000)
console.log('Listening on port 3000')
io.sockets.on 'connection' 函数处理客户端连接的事件。在这里,我们向控制台记录我们已连接的信息,然后设置 joinList 消息处理程序。目前,我们只是将客户端发送给我们的值记录到控制台。
我们现在应该能够测试连接到一个列表。刷新我们的待办事项列表主页,并输入一个要加入的列表名称。点击 加入 按钮后,转到我们的后台管理任务。你应该会看到以下类似的消息:
已连接
加入迈克尔列表
成功了!我们已经成功创建了一个双向 WebSocket 连接。到目前为止,我们还没有真正加入一个列表,所以让我们现在就加入。
加入列表
要加入一个列表,我们将使用 Socket.IO 的一个功能,称为 rooms。它允许 Socket.IO 服务器对客户端进行分段,并向所有已连接客户端的子集发送消息。在服务器上,我们将跟踪每个房间的待办事项列表,然后告诉客户端在连接时同步其本地列表。
我们将在 app.coffee 文件中更新高亮显示的代码,如下所示:
@todos = {}
io.sockets.on 'connection', (socket) =>
console.log('connected')
socket.on 'joinList', (list) =>
console.log "Joining list #{list}"
socket.list = list
socket.join(list)
@todos[list] ?= []
socket.emit 'syncItems', @todos[list]
我们初始化 @todos 实例变量为一个空哈希表。它将保存每个房间的待办事项列表,使用列表名称作为键。在 joinList 处理函数中,我们将 socket 变量的 list 属性设置为客户端传递进来的列表名称。
然后,我们使用socket.join函数将我们的列表加入到一个具有该名称的房间中。如果该房间尚不存在,它将被创建。然后,我们将一个空数组值分配给@todos中具有list键的项。?=运算符仅在左侧对象为null时将右侧的值分配给左侧对象。
最后,我们使用socket.emit函数向客户端发送消息。syncItems标识符将告诉它同步其本地数据与传递给它的待办事项列表项。
为了处理syncItems消息,我们需要更新todo.coffee文件,如下所示的高亮代码:
joinList: ->
@socket = io.connect('http://localhost:3000')
@socket.on 'connect', =>
@socket.emit 'joinList', @$joinListName.val()
@socket.on 'syncItems', (items) =>
@syncItems(items)
syncItems: (items) ->
console.log 'syncing items'
localStorage.clear()
localStorage.setObj item.id, item for item in items
@displayItems()
加入列表后,我们设置客户端连接以处理syncItems消息。我们期望接收我们刚刚加入的列表的所有待办事项。syncItems函数将清除localStorage中的所有当前项,添加所有新项,然后显示它们。
界面
最后,让我们更新我们的 UI,以便用户知道他们已经加入了一个列表,并允许他们离开它。我们将在index.jade文件中将以下修改应用于#footer div标签:
doctype 5
html
head
title Collaborative Todo
link(rel="stylesheet", href="css/todo.css")
body
section#todoapp
header#header
h1 todos
input#new-todo(placeholder="What needs to be done?", autofocus=true)
section#main
ul#todo-list
footer#footer
section#connect
| Join list:
input#join-list-name
button#join Join
button#clear-completed Clear completed
section#disconnect.hidden
| Joined list:  
span#connected-list List name
button#leave Leave
script(src="img/jquery.min.js")
script(src="img/socket.io.js")
!= js('todo')
在之前的标记中,我们向footer div标签添加了两个新的部分。每个部分将根据我们处于哪种状态(连接到列表的connected或disconnected)而隐藏或显示。connect部分与之前相同。disconnect部分将显示您当前连接到的列表,并有一个离开按钮。
现在,我们将向todo.coffee文件添加代码以在加入列表时更新界面。
首先,我们将在cacheElements函数中缓存新元素,如下代码片段所示:
cacheElements: ->
@$input = $('#new-todo')
@$todoList = $('#todo-list')
@$clearCompleted = $('#clear-completed')
@$joinListName = $("#join-list-name")
@$join = $('#join')
@$connect = $('#connect')
@$disconnect = $('#disconnect')
@$connectedList = $('#connected-list')
@$leave = $('#leave')
接下来,我们将更改 UI 以在调用syncItems(在成功加入列表后由服务器触发)时显示我们处于connected状态。我们使用@currentList函数,我们将在joinList函数中设置它;在以下代码片段中添加高亮代码:
joinList: ->
@socket = io.connect('http://localhost:3000')
@socket.on 'connect', =>
@currentList = @$joinListName.val()
@socket.emit 'joinList', @currentList
@socket.on 'syncItems', (items) => @syncItems(items)
syncItems: (items) ->
console.log 'syncing items'
localStorage.clear()
localStorage.setObj item.id, item for item in items
@displayItems()
@displayConnected(@currentList)
displayConnected: (listName) ->
@$disconnect.removeClass 'hidden'
@$connectedList.text listName
@$connect.addClass 'hidden'
displayConnected函数将仅隐藏connect部分并显示disconnect部分。
离开列表
离开列表应该相当简单。我们断开当前套接字连接,然后更新 UI。
为了处理按钮点击时的disconnect动作,我们在bindEvents函数中添加了一个处理程序,如下代码片段所示:
bindEvents: ->
@$input.on 'keyup', (e) => @create e
@$todoList.on 'click', '.destroy', (e) => @destroy e.target
@$todoList.on 'change', '.toggle', (e) => @toggle e.target
@$clearCompleted.on 'click', (e) => @clearCompleted()
@$join.on 'click', (e) => @joinList()
@$leave.on 'click', (e) => @leaveList()
如您所见,我们添加的处理程序将仅调用一个leaveList函数。我们还需要实现它。将以下两个函数添加到TodoApp类中最后一个函数定义之后:
leaveList: ->
@socket.disconnect() if @socket
@displayDisconnected()
displayDisconnected: () ->
@$disconnect.addClass 'hidden'
@$connect.removeClass 'hidden'
测试所有操作
现在让我们测试我们的列表加入和离开代码。要看到所有操作,请按照以下步骤进行:
-
在您的浏览器中打开
http://localhost:3000/。 -
在浏览器窗口中,输入列表名称并点击加入列表。界面应该按预期更新。
-
加入列表后,添加一些待办事项。
-
再次打开网站,这次使用第二个浏览器。由于
localStorage对每个浏览器都是唯一的,我们这样做是为了有一个干净的待办事项列表。 -
再次,输入与另一个浏览器中相同的列表名称,然后点击加入列表。
-
当列表同步时,您现在应该会看到之前添加的列表项出现在列表中。
-
最后,使用离开按钮从列表中断开连接。

来自不同浏览器的两个同步列表
这太棒了!我们现在可以看到 WebSocket 的实际力量。当不需要轮询服务器时,客户端会收到同步项的通知。
然而,一旦我们连接到列表,我们仍然不能添加新项目,以便它们在所有其他客户端中显示。让我们来实现这一点。
将待办事项添加到共享列表
首先,我们将在服务器上处理添加新项目。处理此操作的最佳位置是在创建待办事项的现有create函数中。我们不仅将它们添加到localStorage,还会向服务器发出消息,告诉它已创建新的待办事项,并将其作为参数传递。将create函数修改如下代码:
create: (e) ->
val = $.trim @$input.val()
return unless e.which == 13 and val
randomId = Math.floor Math.random()*999999
newItem =
id: randomId
title: val
completed: false
localStorage.setObj randomId, newItem
@socket.emit 'newItem', newItem if @socket
@$input.val ''
@displayItems()
我们需要在服务器上处理newItem消息。我们将在app.coffee中设置代码,以便在客户端加入列表时执行此操作。
让我们修改之前添加的joinList事件处理器;在以下代码片段中添加高亮代码:
io.sockets.on 'connection', (socket) =>
console.log("connected")
socket.on 'joinList', (list) =>
console.log "Joining list #{list}"
socket.list = list
socket.join(list)
@todos[list] ?= []
socket.emit 'syncItems', @todos[list]
socket.on 'newItem', (todo) =>
console.log "new todo #{todo.title}"
@todos[list].push todo
io.sockets.in(socket.list).emit('itemAdded', todo)
在此代码片段中,我们设置了一个新的socket事件,当用户加入列表时。在这种情况下,它是针对newItem事件的。我们使用push函数将新的待办事项添加到我们的@todos数组中。然后,我们向当前列表中的所有客户端发出新的itemAdded消息。
这个itemAdded消息会发生什么?您猜对了;它将在客户端再次被处理。这种来回的消息传递在 WebSocket 应用程序中很常见,并且确实需要一些习惯。不过别担心;一旦掌握了技巧,就会变得容易。
同时,让我们在客户端处理itemAdded事件。我们还在joinList方法中设置了此代码,通过在以下代码片段中添加高亮代码:
joinList: ->
@socket = io.connect('http://localhost:3000')
@socket.on 'connect', =>
@currentList = @$joinListName.val()
@socket.emit 'joinList', @currentList
@socket.on 'syncItems', (items) => @syncItems(items)
@socket.on 'itemAdded', (item) =>
localStorage.setObj item.id, item
@displayItems()
我们通过调用localStorage.setObject并传递项目 ID 和值来处理itemAdded事件。如果它不在localStorage中,这将创建一个新的待办事项;或者,它将更新现有的值。
就这样!我们现在应该能够向列表中的所有客户端添加项目。为了测试它,我们将遵循与之前类似的步骤:
-
在您的浏览器中打开
http://localhost:3000/。 -
在浏览器窗口中,输入一个列表名称并点击加入列表。UI 应该按预期更新。
-
现在再次打开网站,这次使用第二个浏览器。
-
再次,输入与另一个浏览器中相同的列表名称,然后点击加入列表。
-
在任一浏览器中添加新的待办事项。您将立即在另一个浏览器中看到待办事项的出现。
哇!这不是很令人印象深刻吗?
从共享列表中删除待办事项
要从共享列表中删除待办事项,我们将遵循与添加项类似的模式。在 todo.coffee 中的 destroy 函数中,我们将向我们的套接字发送一个 removeItem 消息,让服务器知道应该删除一个项,如下面的代码片段所示:
destroy: (elem) ->
id = ($(elem).closest 'li').data('id')
localStorage.removeItem id
@socket.emit 'removeItem', id if @socket
@displayItems()
再次设置服务器端代码来处理此消息,通过从内存中的共享列表中删除项,然后通知所有连接到列表的客户端该项已被删除:
io.sockets.on 'connection', (socket) =>
console.log("connected")
socket.on 'joinList', (list) =>
console.log "Joining list #{list}"
socket.list = list
socket.join(list)
@todos[list] ?= []
socket.emit 'syncItems', @todos[list]
socket.on 'newItem', (todo) =>
console.log "new todo #{todo.title}"
@todos[list].push todo
io.sockets.in(socket.list).emit('itemAdded', todo)
socket.on 'removeItem', (id) =>
@todos[list] = @todos[list].filter (item) -> item.id isnt id
io.sockets.in(socket.list).emit('itemRemoved', id)
removeItem 事件处理程序获取要删除的任务项的 ID。它通过将共享列表的当前值赋给一个使用 JavaScript 的数组 filter 函数创建的新值来从列表中删除待办事项。这将选择所有没有传递 ID 的项。然后,它对共享列表中所有客户端套接字连接调用 emit,并传递 itemRemoved 消息。
最后,我们需要在我们的客户端中处理 itemRemoved 消息。类似于添加项时,我们将在 todo.coffee 中的 joinList 函数中设置此操作,如下面的代码片段所示:
joinList: ->
@socket = io.connect('http://localhost:3000')
@socket.on 'connect', =>
@currentList = @$joinListName.val()
@socket.emit 'joinList', @currentList
@socket.on 'syncItems', (items) => @syncItems(items)
@socket.on 'itemAdded', (item) =>
localStorage.setObj item.id, item
@displayItems()
@socket.on 'itemRemoved', (id) =>
localStorage.removeItem id
@displayItems()
我们从 localStorage 中删除项并更新 UI。
要测试删除项,请按照以下步骤操作:
-
在您的浏览器中打开
http://localhost:3000/。 -
在浏览器窗口中,输入一个列表名称,然后点击 加入列表。UI 应按预期更新。
-
一旦您连接到共享列表,添加一些待办事项。
-
现在再次打开网站,这次使用第二个浏览器。
-
再次输入与另一个浏览器中相同的列表名称,然后点击 加入列表。您的待办事项列表将与共享列表同步,并包含您在另一个浏览器中添加的项。
-
点击删除图标以在任一浏览器中删除待办事项。您将立即看到被删除的待办事项在另一个浏览器中消失。
现在,轮到你了
作为最后的练习,我将要求您使 清除已完成 按钮工作。作为一个提示,您应该能够使用现有的 destroyItem 方法功能。
摘要
在本章中,我们通过探索 Node.js 作为快速、基于事件的平台,让您可以使用 JavaScript 或 CoffeeScript 编写服务器应用程序,完成了对 CoffeeScript 生态系统的全面游览。我希望您已经看到了使用 CoffeeScript 在服务器和浏览器上同时编写 Web 应用程序的乐趣。
我们还花了一些时间与为 Node.js 编写的许多出色的开源库和框架一起工作,如 expressjs、connect 和 Socket.IO,并看到了我们如何成功使用 npm 来管理应用程序中的依赖项和模块。
我们的示例应用程序正是您会用 Node.js 来实现的那种类型,我们看到了它的基于事件的模型如何适合编写客户端和服务器之间有大量持续交互的应用程序。
现在我们已经结束了这段旅程,我希望我已经在你心中种下了渴望和技能,让你能够走出并使用 CoffeeScript 改变世界。我们花了一些时间探索的不仅仅是语言,还有那些奇妙的工具、库和框架,它们使我们能够用更少的代码更快地开发出强大的应用程序。
CoffeeScript 和 JavaScript 生态系统的未来光明,希望你们能成为其中的一员!


浙公网安备 33010602011771号