jQuery-Rails-和-Node-的-CoffeeScript-编程-全-

jQuery、Rails 和 Node 的 CoffeeScript 编程(全)

原文:zh.annas-archive.org/md5/0B0062B2422D4B29BA6F761E6D36A199

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

JavaScript 是由 Brendan Eich 在 1995 年左右在网景工作时编写的一种古怪的小语言。它是第一种基于浏览器的脚本语言,当时只在网景导航器中运行,但最终它找到了自己的位置,进入了大多数其他的 Web 浏览器。当时,网页几乎完全由静态标记组成。JavaScript(最初名为 LiveScript)的出现是因为需要使页面动态化,并为浏览器开发人员带来完整脚本语言的功能。

语言的许多设计决策都是出于简单和易用的需要,尽管当时有些决策纯粹是出于网景的营销原因。选择“JavaScript”这个名字是为了将它与 Sun Microsystems 的 Java 联系起来,尽管事实上 Sun 与之无关,而且它在概念上与 Java 有很大不同。

除了一种方式,即大部分语法都是从 Java、C 和 C++中借鉴而来,以便让那些熟悉这些语言的程序员感到熟悉。但尽管看起来相似,实际上它在内部是一个非常不同的东西,并且具有与更奇特的语言(如 Self、Scheme 和 Smalltalk)相似的特征。其中包括动态类型、原型继承、一级函数和闭包。

因此,我们最终得到了一种看起来很像当时的一些主流语言,并且可以被迫以与它们非常不同的中心思想行事的语言。这导致它在很多年里被人们误解。很多程序员从未将它视为一种“严肃”的编程语言,因此在编写浏览器代码时,他们没有应用几十年来积累的许多最佳开发实践。

那些深入研究这门语言的人肯定会发现很多奇怪之处。Eich 本人承认,这门语言的原型大约在 10 天内完成,尽管他的成果令人印象深刻,但 JavaScript 并非没有(很多)缺陷。这些缺陷并没有真正有助于提升它的声誉。

尽管存在所有这些问题,JavaScript 仍然成为世界上最广泛使用的编程语言之一,这不仅仅是因为互联网的爆炸和 Web 浏览器的普及。在众多浏览器上的支持似乎是一件好事,但它也因为在语言和 DOM 的实现上的差异而造成了混乱。

大约在 2005 年,AJAX 这个术语被创造出来,用来描述一种 JavaScript 编程风格,这种风格是由浏览器中XMLHTTPRequest对象的引入所可能的。这意味着开发人员可以编写客户端代码,直接使用 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 中。

Brendan Eich 也表达了对 CoffeeScript 的钦佩,并将其用作他希望在未来版本的 JavaScript 中看到的一些东西的例子。

本书既是对该语言的介绍,也是为什么您应该在尽可能的地方使用 CoffeeScript 而不是 JavaScript 的动机。它还探讨了在浏览器中使用 CoffeeScript 使用 jQuery 和 Ruby on Rails,以及在服务器上使用 Node.js。

本书涵盖的内容

第一章,为什么使用 CoffeeScript,介绍了 CoffeeScript 并深入探讨了它与 JavaScript 之间的区别,特别关注 CoffeeScript 旨在改进的 JavaScript 部分。

第二章,运行 CoffeeScript,简要介绍了 CoffeeScript 堆栈以及它通常是如何打包的。您将学习如何在 Windows、Mac 和 Linux 上使用 Node.js 和 npm 安装 CoffeeScript。您将了解 CoffeeScript 编译器(coffee)以及熟悉一些有用的工具和日常开发资源。

第三章,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 的计算机和一个基本的文本编辑器。在整本书中,我们将从互联网上下载一些我们需要的软件,所有这些软件都将是免费和开源的。

这本书适合谁

这本书适合现有的 JavaScript 程序员,他们想了解更多关于 CoffeeScript 的知识,或者有一些编程经验并想了解更多关于使用 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/

新术语重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会在文本中以这种方式出现:"页脚将有清除已完成按钮"。

注意

警告或重要提示会以这样的框出现。

提示

提示和技巧会以这种方式出现。

第一章:为什么使用 CoffeeScript?

CoffeeScript 编译成 JavaScript,并且紧密遵循其习惯用法。完全可以将任何 CoffeeScript 代码重写为 JavaScript,它们看起来并不会有很大的不同。那么为什么要使用 CoffeeScript 呢?

作为一名有经验的 JavaScript 程序员,你可能认为学习一个全新的语言根本不值得时间和精力。

但最终,代码是给程序员看的。编译器不在乎代码的外观或清晰的含义;它要么运行,要么不运行。我们的目标是作为程序员编写表达性强的代码,这样我们就可以阅读、引用、理解、修改和重写它。

如果代码过于复杂或充满了不必要的仪式,那么理解和维护将会更加困难。CoffeeScript 给了我们一个优势,可以澄清我们的想法并编写更易读的代码。

认为 CoffeeScript 与 JavaScript 非常不同是一种误解。可能在某些地方会有一些极端的语法差异,但本质上,CoffeeScript 旨在打磨 JavaScript 的粗糙边缘,揭示其中隐藏的美丽语言。它引导程序员走向 JavaScript 的所谓“好部分”,并对构成良好 JavaScript 的内容持有坚定的看法。

CoffeeScript 社区的口头禅之一是:“它只是 JavaScript”,我也发现真正理解这种语言的最佳方法是看它是如何生成输出的,实际上生成的代码相当可读和易懂。

在本章中,我们将重点介绍两种语言之间的一些差异,通常关注 JavaScript 中 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));

|

要运行本章中的代码示例,可以使用伟大的尝试 CoffeeScript在线工具,网址为coffeescript.org。它允许你输入 CoffeeScript 代码,然后在侧边栏显示相应的 JavaScript。你也可以直接从浏览器中运行代码(点击左上角的运行按钮)。如果你更喜欢先在计算机上运行 CoffeeScript 来运行示例,请跳到下一章,然后安装好 CoffeeScript 再回来。该工具如下截图所示:

CoffeeScript 语法

起初,这两种语言可能看起来截然不同,但希望随着我们对比差异,你会发现它们仍然是 JavaScript,只是进行了一些小的调整和大量的语法糖。

分号和大括号

你可能已经注意到,CoffeeScript 取消了所有行末的分号。如果想要在一行上放两个表达式,仍然可以使用分号。它还取消了代码块的大括号(也称为花括号),比如if语句、switchtry..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关键字。默认情况下,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 借用了从参数创建数组的模式,这些参数由三个点(...)表示。这些在下面的代码片段中显示:

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对象代表一个打开的窗口。它对所有其他对象都是全局可用的,因此可以用作全局命名空间或其他对象的容器。

当我们谈到对象时,让我们谈谈 JavaScript 的另一个部分,CoffeeScript 使之更好:定义和使用对象。

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: () ->语法声明的。这些方法是在对象的原型上创建的。

注意

你注意到我们的警报字符串中的#{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 中作为数据的文字对象的键,比如classfor,然后您需要将其括在引号中。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 中,等号运算符==(及其反向!=)充满了危险,很多时候并不会做你期望的事情。这是因为它首先会尝试强制将不同类型的对象在比较之前变成相同的。

它也不是传递的,这意味着它可能根据操作符的左侧或右侧的类型返回不同的truefalse值。请参考以下代码片段:

'' == '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;

|

存在运算符

当你想要检查一个变量是否存在并且有值(不是nullundefined)时,你需要使用这种古怪的习惯用法:

typeof a !== "undefined" && a !== null 

CoffeeScript 为此提供了一个很好的快捷方式,即存在运算符?,它会在变量不是undefinednull时返回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;

|

如果链中的所有值都存在,你应该得到预期的结果。如果它们中的任何一个应该是nullundefined,你将得到一个未定义的值,而不是抛出TypeError

尽管这是一种强大的技术,但它也很容易被滥用,并且使代码难以理解。如果你有很长的方法链,可能很难知道nullundefined值究竟来自哪里。

迪米特法则,一个众所周知的面向对象设计原则,可以用来最小化这种复杂性,并改善代码中的解耦。它可以总结如下:

  • 你的方法可以直接调用其类中的其他方法

  • 你的方法可以直接调用自己字段上的方法(但不能调用字段的字段)

  • 当你的方法带有参数时,你的方法可以直接调用这些参数上的方法

  • 当你的方法创建本地对象时,该方法可以直接调用本地对象上的方法

注意

尽管这不是严格的法则,不应该被打破,但更类似于自然法则,使得遵循它的代码也更简单和更松散耦合。

既然我们已经花了一些时间来讨论 CoffeeScript 修复了 JavaScript 的一些不足和烦恼,让我们再来看看 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语句不像在 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名称执行的操作,可以通过调用带有它作为参数的方法、选择其上的属性或方法,或者赋值来实现。

whenby保护子句是可选的。它们描述了迭代应该如何被过滤(仅当后续的when条件为true时才会返回元素),或者使用by后跟一个数字来选择集合的哪些部分。例如,by 2将返回每个偶数编号的元素。

我们可以通过使用bywhen来重写我们的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]语法是 CoffeeScripts 的范围语法,它是从 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关键字的反义词;ifunless可以采用后缀形式,这意味着语句可以放在行的末尾。

CoffeeScript 还为一些逻辑运算符提供了纯英语别名。它们如下:

  • is 用于 ==

  • isnt 用于 !=

  • not 用于 !

  • 用于 &&

  • or 用于 ||

  • true也可以是yeson

  • false可以是nooff

将所有这些放在一起,让我们看一些代码来演示它:

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]将选择包括nm在内的所有元素,而[n…m]将仅选择nm之间的元素。

[..][…]都将选择整个数组。这些在以下代码中使用:

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 确实喜欢它的省略号。它们被用于 splat、范围和数组切片。以下是一些关于如何识别它们的快速提示:如果紧挨着函数定义或函数调用中的最后一个参数,那么它是 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];

当您运行此代码时,您将获得三个变量,citystatecountry,它们的值是从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 提供了=>,或者 fat 箭头,它将定义一个函数,但同时捕获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函数作为参数传递,以便在生日发生时调用。因为我们使用 fat 箭头传递这个函数,它的作用域将固定在Person对象上。这意味着我们仍然可以引用@name实例变量,即使它不存在于运行函数的Birthday对象上。

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,使用它来编写 Web 服务器应用程序,甚至将其用作正常的日常系统脚本语言。

注意

核心 CoffeeScript 编译器不依赖于 Node,从技术上讲,它可以在任何 JavaScript 环境上执行。但是,使用编译器的 coffee 命令行实用程序是一个 Node.js 包。

CoffeeScript 编译器的工作如下图所示:

CoffeeScript 堆栈

Node.js 和 npm

Node.js 有自己的包管理系统,称为 npm。它用于安装和管理在 Node.js 生态系统中运行的包、库及其依赖项。这也是安装 CoffeeScript 的最常见方式,CoffeeScript 本身也作为 npm 包可用。因此,在设置好 Node.js 和 npm 之后,安装 CoffeeScript 实际上非常容易。

根据您的操作系统以及是否需要编译源代码,有不同的安装 Node.js 和 npm 的方法。后续各节将介绍各个操作系统的说明。

提示

Node.js 维基包含大量关于在众多平台上安装和运行 Node 的信息。如果在本章中遇到任何问题,您可以查看它,因为它有很多有关故障排除问题的提示,并经常更新;链接是 https://github.com/joyent/node/wiki/Installation。

Windows 上的 Node.js、npm 和 CoffeeScript

Node.js 社区一直在努力提供良好的本地 Windows 支持,安装非常简单。

要这样做,首先转到 Node.js 网站(nodejs.org),然后单击“下载”按钮。您将看到几个可用的选项,但选择“Windows 安装程序”选项,如下截图所示:

Windows 上的 Node.js、npm 和 CoffeeScript

这将下载一个.msi文件。一旦下载完成,安装就变得非常简单;只需接受条款并单击“继续”。如果您看到以下屏幕,则已成功安装 Node:

Windows 上的 Node.js、npm 和 CoffeeScript

在这一点上,你可能需要注销 Windows 或重新启动,以便更改你的$PATH变量生效。完成后,你应该能够打开 DOS 命令提示符并运行以下命令:

node –v 

这应该会输出一个版本,这意味着你可以开始了。让我们也检查一下 npm 是否正常工作。同样在命令行工具中,输入以下内容:

npm

你应该会看到类似以下截图的内容:

Windows 上的 Node.js、npm 和 CoffeeScript

现在,为了继续安装 CoffeeScript,只需输入以下命令:

npm install coffee-script

如果一切顺利,你应该会看到类似以下截图的内容:

Windows 上的 Node.js、npm 和 CoffeeScript

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

Windows 上的 Node.js、npm 和 CoffeeScript

这是 CoffeeScript 解释器,正如你所看到的,你可以使用它来即时运行 CoffeeScript 代码。要退出,只需使用Ctrl + C

就是这样!在 Windows 上安装 Node.js 非常快速和简单。

在 Mac 上安装 CoffeeScript

在 Mac 上安装 Node.js 有两种方式,一种是从 Node.js 网站下载.pkg文件,然后使用苹果的安装程序应用进行安装,另一种是使用Homebrew命令行包管理器。

最简单的方法是只安装.pkg文件,所以我们先来看看这个。安装 Homebrew 可能需要更多的工作,但如果你喜欢在命令行工具上工作并且想要从源代码构建 CoffeeScript,那么这是值得的。

使用苹果安装程序

前往 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

从输出中可以看出,Homebrew 没有安装 npm,没有 npm 我们无法安装 CoffeeScript。要安装 npm,你只需在终端中复制并粘贴以下命令:

curl http://npmjs.org/install.sh |sh

安装 npm 后,你应该会看到类似以下屏幕的内容:

使用 Homebrew

使用 npm 安装 CoffeeScript

现在我们已经安装了 npm,我们应该能够安装 CoffeeScript。只需在终端中输入以下命令:

npm install –g coffee-script

-g标志让 npm 全局安装 CoffeeScript;一旦完成,您现在可以通过使用coffee命令来测试 CoffeeScript 是否正常工作,如下面的屏幕截图所示:

使用 npm 安装 CoffeeScript

就是这样!在 Mac 上安装 CoffeeScript 非常容易。

在 Linux 上安装 CoffeeScript

在 Linux 上安装 Node.js 与 CoffeeScript 的方式取决于您安装了哪个发行版。大多数流行的发行版都有软件包,如果没有,您也可以尝试从源代码构建 CoffeeScript,如下一节所述。

我只有使用基于 Debian 的发行版的软件包管理器的经验,并且已成功使用apt-get软件包管理器安装了 Node.js 和 CoffeeScript。但是,您应该能够按照其他发行版的说明进行操作。

在 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包含了在各种 Linux 和 Unix 发行版上安装的说明,包括 Fedora、openSUSE、Arch Linux 和 FreeDSB。

使用 npm 安装 CoffeeScript

在您的软件包管理器完成其任务后,您现在应该已经安装了 Node.js 和 npm。您可以使用 npm -v命令来验证这一点。您现在可以使用 npm 安装 CoffeeScript,方法是输入以下命令:

npm install –g coffee-script

-g标志告诉 npm 全局安装软件包。

以下屏幕截图显示了如何使用-v命令安装 CoffeeScript:

使用 npm 安装 CoffeeScript

就是这样!在 Linux 上安装 CoffeeScript 非常容易。

从源代码构建 Node.js

如果您不想使用软件包管理器或安装程序,或者您的操作系统没有可用的软件包管理器或者您想获取最新版本的 Node.js,那么您也可以从源代码构建 Node.js。不过要注意,这个过程通常充满了危险,因为源代码通常需要系统上的一些依赖项来构建。

在 Linux 或 Unix 上构建

要在 Linux 或 Unix 环境中构建,您需要确保已安装以下源依赖项:

  • Python–Version 2.6 或 Version 2.7:您可以通过在命令提示符中输入python --version来检查是否已安装 Python,并检查安装了哪个版本。

  • libssl-dev:这通常可以使用内置软件包管理器安装。它已经安装在 OS X 上。

我将向您展示如何使用最新的源代码构建 Node.js。该源代码是使用流行的 Git 版本控制系统进行管理,并托管在github.com的存储库中。要从 github 拉取最新的源代码,您需要确保已安装 Git。通过使用apt-get,您可以这样安装它:

apt-get install git-core

一旦您具备了这些先决条件,您应该能够构建节点。在命令行工具上输入以下命令:

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 维基(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。有关一些有用选项的列表显示在以下截图中:

coffee 命令

我们已经看到了-v选项,它将打印出 CoffeeScript 的当前版本。

REPL

执行coffee没有参数或使用-i选项将使您进入 CoffeeScript 的REPLRead Eval Print Loop)。从这里,您可以输入 CoffeeScript 代码,它将立即执行并在控制台中显示其输出。这对于玩转语言、探索一些核心 JavaScript 和 Node.js 库,甚至引入另一个外部库或 API 并能够进行交互式探索非常有用。

我建议你运行 coffee REPL,并尝试我们在上一章中讨论过的一些代码示例。注意每个表达式的输出是在输入后显示的。解释器还足够聪明,可以处理多行和嵌套表达式,比如函数定义。

REPL

在上一张截图中,显示了解释器处理函数定义。

提示

要退出 REPL,使用Ctrl + DCtrl + C

运行 .coffee 文件

在 REPL 中输入足够的代码后,您将会想要开始将您的 CoffeeScript 存储和组织在源文件中。CoffeeScript 文件使用.coffee扩展名。您可以通过将其作为参数传递给coffee命令来运行.coffee文件。文件中的 CoffeeScript 将被编译为 JavaScript,然后使用 Node.js 作为其环境执行。

提示

您可以使用任何文本编辑器来编写您的 CoffeeScript。许多流行的编辑器都具有插件或已经添加了对 CoffeeScript 的支持,包括语法高亮、代码补全,甚至允许您直接从编辑器中运行代码。在github.com/jashkenas/coffee-script/wiki/Text-editor-plugins上有一个支持 CoffeeScript 的文本编辑器和插件的全面列表。

编译为 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 非常搭配,结合起来提供了一个强大的工具集,以简洁和表达力的方式编写 Web 应用程序。

在本章中,我们将做以下事情:

  • 探索 jQuery 的一些高级功能,并讨论它给您带来了什么

  • 学习如何在浏览器中使用 CoffeeScript 和 jQuery

  • 使用 jQuery 和 CoffeeScript 构建一个简单的待办事项列表应用程序

让我们首先更详细地讨论 jQuery 库,并发现它的有用之处。

查找和更改元素

在 Web 浏览器中,DOM 或文档对象模型是用于与 HTML 文档中的元素进行编程交互的表示。

在 JavaScript 中,您会发现自己需要进行大量的 DOM 遍历,以查找您感兴趣的元素,然后对它们进行操作。

要使用标准的 JavaScript 库来实现这一点,通常需要使用document.getElementsByNamedocument.getElementByIddocument.getElementsById方法的组合。一旦您的 HTML 结构开始变得复杂,这通常意味着您将不得不在笨拙和繁琐的迭代代码中组合这些方法。

以这种方式编写的代码通常对 HTML 的结构做出了很多假设,这意味着如果 HTML 发生变化,它通常会中断。

$函数

使用$函数(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

为 Web 应用程序编译 CoffeeScript 的最常见方法是运行coffee命令,以监视一个或多个 CoffeeScript 文件的更改,然后将它们编译为 JavaScript。然后将输出包含在您的 Web 应用程序中。

例如,我们将组织我们的项目文件夹结构,看起来像以下文件夹结构:

编译 CoffeeScript

'

src文件夹是您的 CoffeeScript 文件所在的位置。然后,我们可以启动一个 CoffeeScript 编译器来监视该文件夹,并将 JavaScript 编译到我们的public/js文件夹中。

这是 CoffeeScript 命令的样子:

coffee -co public/js -w src/

在自己的终端窗口中保持此命令运行,并在保存文件时重新编译您的 CoffeeScript 文件。

提示

CoffeeScript 标签

在浏览器中运行 CoffeeScript 的另一种方法是在文档中包含内联的 CoffeeScript,包含在<script type="text/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)以及我们的应用程序代码。

提示

下载示例代码

您可以从您在www.PacktPub.com购买的所有 Packt 图书的帐户中下载示例代码文件。如果您在其他地方购买了本书,您可以访问www.PacktPub.com/支持并注册,以便直接通过电子邮件接收文件。

测试全部

我们现在应该能够通过在浏览器中打开我们的index.html文件来运行我们的应用程序。如果一切顺利,我们应该看到我们的警报弹出窗口,如下面的截图所示:

测试全部

运行本地 Web 服务器

虽然我们现在可以从磁盘轻松测试我们的 Web 应用程序,但是很快我们可能想要将其托管在本地 Web 服务器上,特别是如果我们想要开始进行 Ajax。由于我们已经安装了 Node.js,所以运行 Web 服务器应该非常容易,我们现在只需要为静态内容提供服务。幸运的是,有一个 npm 包可以为我们做到这一点;它名为http-server,可以在github.com/nodeapps/http-server找到。

要安装它,只需运行以下命令:

npm install http-server -g

然后,我们通过导航到我们的应用程序文件夹并输入以下内容来执行它:

http-server

这将在端口8080上托管 public 文件夹中的所有文件。现在,我们应该能够通过使用 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> 

让我们简要地浏览一下前面标记的结构。首先,我们有一个带有todoappID 的部分,它将作为应用程序的主要部分。它包括一个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-todoID 的输入,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)

keyvalue参数都是字符串。存储在localStorage变量中的字符串即使在页面刷新时也会保留。在大多数浏览器中,你可以在localStorage变量中存储多达 5MB 的数据。

因为我们想将待办事项存储为一个复杂的对象而不是一个字符串,所以在设置和从localStorage获取项目时,我们使用了常用的将对象转换为 JSON 对象的技术。为此,我们将在Storage类的原型中添加两个方法,然后这些方法将在全局localStorage对象上可用。在我们的app.coffee文件的顶部添加以下代码片段:

Storage::setObj = (key, obj) ->
  @setItem key, JSON.stringify(obj)

Storage::getObj = (key) ->
  JSON.parse @getItem(key)

在这里,我们使用::运算符将setObjgetObj方法添加到Storage类中。这些函数通过将对象转换为 JSON 来包装localStorage对象的getItemsetItem方法。

现在我们终于准备好创建我们的待办事项并将其存储在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中。谷歌 Chrome 开发者工具将允许您在资源选项卡中检查localStorage。添加几个任务后,您应该能够在这里看到它们,如下面的截图所示:

使用 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()

让我们运行这段代码看看会发生什么。当我们这样做时,我们会得到以下错误:

Uncaught TypeError: Object # has no method 'displayItems'

这里发生了什么?似乎对@displayItems()的调用试图在HTMLInputElement的实例上调用该方法,而不是在TodoApp上调用。

这是因为 jQuery 会将this的值设置为引发事件的元素。当我们将类方法绑定为事件处理程序时,jQuery 实际上会“劫持”this,使其不指向类本身。这是在使用 jQuery 和 CoffeeScript 中应该知道的一个重要注意事项。

为了修复它,我们可以在设置keyup事件处理程序时使用 CoffeeScript 的 fat 箭头,这将确保this的值保持不变。让我们修改我们的bindEvents方法,使其看起来类似于以下代码:

  bindEvents: ->
 $('#new-todo').on('keyup',(e) => @create(e))

只剩下一件事了;在我们的createItem方法中,我们使用$(this)来获取引发事件的input元素的值。由于切换到了 fat 箭头,现在这将指向我们的TodoApp实例。幸运的是,传递的事件参数有一个 target 属性,也指向我们的输入。将create方法的第一行更改为以下代码片段:

  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方法:

  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 结合起来,以更少的工作量和复杂性编写复杂的 Web 应用程序。jQuery 是一个非常庞大的库,我们只是触及了它所提供的一小部分。我建议您花一些时间学习库本身,并使用 CoffeeScript 进行学习。

接下来,我们将首先看一下如何使用 CoffeeScript 和 Rails 开始与服务器端代码进行交互。

第四章:CoffeeScript 和 Rails

Ruby on Rails 是一个于 2004 年出现的 Web 框架。它是由 David Heinemeier Hansson 编写的,并从Basecamp中提取出来,这是他为他的公司37signals用 Ruby 编写的项目管理 Web 应用程序。

Rails 立即给许多人留下了深刻的印象,因为他们可以轻松快速地编写 Web 应用程序,并很快变得非常受欢迎。

在开发时,Ruby 是一个来自日本的鲜为人知的脚本语言。Ruby 实际上是 Rails 如此成功的原因。它已被证明是一种强大而简洁的编程语言,许多程序员表示它让编程再次变得有趣。

Rails 的特殊之处在哪里?

Rails 推动了 Web 开发人员编写应用程序的方式。其核心理念包括以下两个重要原则:

  • 约定优于配置

  • 不要重复自己,或者 DRY

约定优于配置

Rails 旨在假定程序员将遵循某些已知的约定,如果使用这些约定,将提供巨大的好处,并且几乎不需要配置框架。它通常被称为一种有主见的框架。这意味着框架对典型应用程序的构建和结构有假设,并且不试图过于灵活和可配置。这有助于您花费更少的时间在配置和连接应用程序架构等琐事上,而更多的时间实际构建您的应用程序。

例如,Rails 将使用与其名称对应的对象对数据库中的表进行建模,因此Transactions数据库中的记录将自动映射到Transactions类实例,people数据库表中的记录也将自动映射到Person类实例。

Rails 通常会使用约定来为您做一些聪明的事情。比如说,我们的people表还有一个名为created_atupdated_atdatetime字段。Rails 将聪明地在记录创建或更新时自动更新这两个字段的时间戳。

Rails 约定的最重要的事情是你应该了解它们,不要与框架对抗,或者试图过多地偏离 Rails 的方式,除非有充分的理由。通常,这可能会抵消您从这些约定中获得的任何好处,甚至使您更难以尝试解决问题。

不要重复自己(DRY)

这个软件工程原则也可以表述为:

系统中的每个知识都必须具有单一、明确和权威的表示。

这意味着 Rails 努力在任何可能的地方消除重复和样板。

例如,模拟people表中的记录的Person类将不需要定义其字段,因为它们已经在数据库表中定义为列。在这里,Rails 可以利用 Ruby 的强大的元编程能力,神奇地向Person类添加与数据库中的列对应的属性。

注意

元编程是编写对其他代码起作用的代码的概念。换句话说,元编程是编写编写代码的代码。它在 Ruby 社区和特别是 Rails 源代码中被广泛使用。

Ruby 语言具有非常强大的元编程能力,与开放类和对象的概念相关联,这意味着您可以轻松地“打开”现有的类定义并重新定义和添加成员。

Rails 和 JavaScript

很长一段时间,Rails 都使用Prototype.jsScript.aculo.us JavaScript 库进行 AJAX、页面动画和特效。

Rails 有视图助手的概念——这些是可以在视图中使用的 Ruby 方法,用于抽象出常见的 HTML 构造。许多处理客户端代码和 AJAX 的视图助手都是建立在这两个框架之上的,因此它们完全融入了框架,没有使用替代方案的简单方法。

Prototype.js与 jQuery 有许多相同的想法和目标,但随着时间的推移,jQuery 被许多程序员认为是一个更加优雅和强大的库。

随着 jQuery 变得越来越受欢迎,许多 Rails 社区的开发人员开始尝试使用 jQuery 代替默认的 JavaScript 库。一套标准的库或gems出现了,用于用 jQuery 替换内置的 Prototype 库。

在 Rails 3.1 版本中,宣布 jQuery 将成为默认的 JavaScript 库。因为 jQuery 已经具有大部分Script.aculo.us的动画和页面效果功能,所以这个库也不再需要了。

这一举措似乎已经等了很长时间,并且基本上得到了大多数 Rails 社区的祝福。

Rails 和 CoffeeScript

Rails 3.1 的另一个重要新增功能是资产管道。其主要目标是使在 Rails 应用中处理 JavaScript 和 CSS 等资产变得更加容易。在此之前,JavaScript 和 CSS 只是作为静态内容提供。它还提供了一个组织框架,帮助你组织 JavaScript 和 CSS,并提供了一个用于访问它们的 DSL。

使用资产管道,你可以使用清单文件组织和管理资产之间的依赖关系。Rails 还将使用管道来缩小和连接 JavaScript,并为缓存清除应用指纹。

资产管道还有一个预处理器链,可以让你在提供文件之前通过一系列的输入-输出处理器运行文件。它知道使用文件扩展名来运行哪些预处理器。

在发布 Rails 3.1 之前,宣布 CoffeeScript 编译器将通过资产管道进行支持。这是一个巨大的宣布,因为 CoffeeScript 仍然是一种相当年轻的语言,并且在 Rails 社区内引起了一些争议,一些人为他们不想学习或使用这种新语言而感到惋惜。

Rails 的维护者们一直坚持自己的立场,目前在 Rails 中使用 CoffeeScript 变得非常容易。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 上安装 Ruby 和 Rails 可能非常容易,使用RVMRuby Version Manager,从rvm.io/

在过去几年中,Ruby 语言已经变得非常流行,这导致编写了多个可以在不同平台上运行的语言实现。Matz's Ruby InterpreterMRI),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 语言进行完整介绍。在这里,我们想专注于在使用 CoffeeScript 的情况下构建简单的 Rails 应用程序。

我们不会详细介绍所有内容,并且我们相信 Ruby 是一种非常简单和可读的语言,Rails 代码也很容易理解。即使您不熟悉该语言和框架,也不应该太难跟上。

首先,我们将通过使用rails命令创建一个空的基本 Rails 应用程序。转到要创建应用程序的文件夹,然后运行此命令:

rails new todo

这将创建一个todo文件夹,其中包含用于 Web 应用程序的大量文件和文件夹。遵循惯例,Rails 将以一定的方式组织您的 Web 应用程序。

注意

rails命令用于许多事情,除了生成新应用程序之外,还作为您进入许多日常 Rails 任务的入口点。我们将在本书中涵盖其中的一些内容,如果您想查看它可以做什么的完整列表,可以运行rails -h

让我们简要谈谈 Rails 如何组织我们的应用程序。您的大部分应用程序代码可能都位于顶级app文件夹中。此文件夹包含以下四个重要的子文件夹:

  • 资产:这是资产管道操作的文件夹。这是您的 CoffeeScript(或 JavaScript)和 CSS 源代码,以及我们的 Web 应用程序使用的图像的位置。

  • 控制器:这是您的控制器所在的位置。它们负责处理应用程序的路由请求,并与视图和模型进行交互。

  • 模型:这是您将找到领域模型的位置。模型代表系统中的领域对象,并使用ActiveRecord基类对应数据库表。

  • 视图:此文件夹包含用于呈现应用程序 HTML 的视图模板。默认情况下,Rails 使用 ERB 模板,允许我们在 HTML 模板中包含 Ruby 代码片段,这些代码将被评估以生成最终输出的 HTML。

MVC

MVC,或Model-View-Controller,是一种广泛使用的应用程序架构模式,旨在通过将应用程序关注点分为三种领域对象类型来简化代码并减少耦合。

Rails 非常密切地遵循 MVC 模式,大多数 Rails 应用程序在模型、控制器和视图方面都会有很强的结构。

在 MVC 之上的另一个模式是“fat models, skinny controllers”,这是在过去几年中被许多 Rails 程序员所推崇的。这个概念鼓励将大部分领域逻辑放在模型中,并且控制器只关注路由和模型与视图之间的交互。

运行我们的应用程序

在这个阶段,我们已经可以运行我们的 Rails 应用程序,看看是否一切正常。从终端输入:

cd todo
rails server

Rails 现在将在端口3000上为我们的应用程序托管一个本地 Web 服务器。您可以通过浏览http://localhost:3000/来测试它。如果一切顺利,您应该会看到以下友好的欢迎消息:

Running our application

提示

记得在我们测试应用程序时,将此服务器保持在单独的控制台窗口中运行。您还可以检查此过程的输出,以查看运行时可能发生的任何错误。

我们的 todo_items 资源

因此,我们现在有一个正在运行的应用程序,但除了显示欢迎页面外,它并没有做太多事情。

为了实现跟踪待办任务的目标,我们将为待办事项生成一个资源。在 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 调用 URL 与可以处理它们的控制器操作之间的映射关系。

在这里,生成器为我们添加了一行,使用了resources方法。此方法使用 HTTP 动词 GET、POST、PUT 和 DELETE 为应用程序中的“资源”控制器创建路由。这意味着它使用 HTTP 动词在应用程序中公开单个域资源。

通常,这将为七个不同的控制器操作创建路由,indexshownewcreateeditupdatedestroy。正如您将在后面看到的,我们不需要为我们的控制器创建所有这些操作,因此我们将告诉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 文件,我们将在这里重用。为了做到这一点,我们需要将旧的 index.html 文件中 body 标签内的所有内容(不包括最后两个 script 标签)复制到一个名为 app/views/todo_items/index.html.erb 的文件中。

你应该最终得到以下标记:

<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 的部分,比如封闭的 htmlheadbody 标签去了哪里。

嗯,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_tagjavascript_include_tag 方法将确保在 HTML 中包含在 assets 文件夹中指定的所有文件。<%= yield %> 行是当前视图将被呈现的地方,这在我们的情况下是 index.html.erb

现在刷新页面,我们会看到 index 页面。查看源代码,了解最终的 HTML 输出。

正如你所看到的,我们的页面仍然没有样式,看起来相当沉闷。让我们看看是否可以再次让它看起来漂亮。

CSS

默认情况下,资产管道将在 app/assets/stylesheets 文件夹中查找 CSS 文件。当我们浏览到这个文件夹时,我们会看到一个名为 todo_items.css.scss 的文件,这是在我们创建控制器时为我们生成的。

将上一章的 styles.css 文件的内容复制到这个文件中。我们的 index 页面现在应该看起来还不错。

注意

这个带有奇怪 .css.scss 扩展名的文件是一个 Saas 文件(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_atupdated_at的时间戳字段。Rails 将确保这些名称的字段在记录创建或更新时得到适当的时间戳更新。

迁移脚本是自动化数据库更改的一种很好的方式,甚至可以让您回滚以前的更改。您也不必依赖于资源或模型生成器创建的迁移。可以通过运行以下命令生成自定义迁移:

rails generate migration migration_name

生成自定义迁移后,您只需实现updown方法,当您的迁移被执行或回滚时将调用这些方法。

迁移是使用rake命令执行的。rake是一个任务管理工具,允许您将任务编写为 Ruby 脚本,然后使用rake命令行实用程序运行这些任务。Rails 带有大量内置的rake任务,您可以使用以下命令查看它们的完整列表:

rake –T

我们目前感兴趣的任务叫做db:migrate,让我们运行它,看看会发生什么:

rake db:migrate

您应该看到以下输出:

== CreateTodoItems: migrating ================================================

-- create_table(:todo_items)

-> 0.0011s

== CreateTodoItems: migrated (0.0013s) =======================================

这意味着 Rails 已成功在数据库中为我们创建了一个todo_items表。当我们刷新应用程序页面时,应该看到错误已经消失,我们看到了空白的待办事项列表。

提示

数据库在哪里?

您可能想知道我们的实际数据库目前在哪里。Rails 默认使用嵌入式 SQLite 数据库。SQLite (www.sqlite.org)是一个自包含的基于文件的数据库,不需要配置服务器即可运行。这使得在开发应用程序时快速启动变得非常简单和方便。

一旦您实际部署您的 Web 应用程序,您可能希望使用更传统的数据库服务器,如 MySQL 或 PostgreSQL。您可以在config/database.yml文件中轻松更改数据库连接设置。

我们还没有将我们的视图连接起来,以实际显示待办事项列表。在这之前,让我们在数据库中手动创建一些待办事项。

Rails 控制台

Rails 有一种巧妙的方式可以通过使用 Rails 控制台与您的代码进行交互。这是一个交互式的 Ruby 解释器,或者irb,会话加载了所有 Rails 项目代码。让我们使用以下命令启动它:

rails console

一旦您进入控制台,您可以输入任何有效的 Ruby 代码。您还可以访问 Rails 应用程序中的所有模型。让我们尝试一下我们之前使用的TodoItem.all方法;这在以下截图中显示:

The Rails consoleRails consoleabout

目前它返回一个空数组,因为我们的表还是空的。请注意,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 中可以作为参数传递函数。

这个块将针对数组中的每个项目执行,将其作为 item 变量传递进来。对于每个项目,我们使用项目的titlecompleted属性在我们的 ERB 标记内部创建其标记。

当我们刷新页面时,现在应该终于看到我们的待办事项列表了!如果你好奇的话,可以查看文档的 HTML 源代码,并将其与 ERB 模板进行比较,这应该让你对它是如何生成的有一个很好的了解。输出页面如下截图所示:

使用 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方法将输出一个 HTMLform标签,而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键创建TodoItemparams是 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。然后,我们发起一个 AJAX 调用到TodoItemController,以更新复选框的当前选中值。

在我们可以运行这段代码之前,我们需要在我们的控制器中添加一个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 为 Web 开发人员提供的一些魔力,以及开发 Rails 应用程序可以有多么有趣。我们还花了一些时间发现在 Rails 应用程序中使用 CoffeeScript 是多么容易,以及你通常会使用哪些不同的方法和技术来编写客户端代码。

如果你还没有这样做,我鼓励你花一些时间学习 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 中的TwistedTornado,以及 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

Node 中的“Hello World”

让我们用 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 应用程序,并抽象掉许多低级细节。

可以说,其中最受欢迎的是Expressexpressjs.com/)。类似于 Rails,它具有许多很好的功能,使得执行常见的 Web 应用程序任务更容易,比如路由、渲染视图和托管静态资源。

在本章中,我们将使用 CoffeeScript 在 Express 中编写 Web 应用程序。

WebSocket

由于我想展示一些 Node 的可伸缩性特性以及它通常用于的应用程序类型,我们将利用另一种有趣的现代网络技术,称为WebSocket

WebSocket 协议是允许在标准 HTTP 端口80上进行原始、双向和全双工(同时双向)TCP 连接的标准。这允许客户端和服务器建立长时间运行的 TCP 连接,服务器可以执行推送操作,这在传统的 HTTP 中通常是不可能的。它经常用于需要在客户端和服务器之间进行大量低延迟交互的应用程序中。

Jade

Jade 是一种轻量级的标记模板语言,它让你以类似于 CoffeeScript 的语法编写优雅而简短的 HTML。它使用了许多功能,比如语法空白,以减少你编写 HTML 文档所需的按键次数。通常在运行 Express 时默认安装,我们将在本书中使用它。

我们的应用程序

在本章中,我们将构建一个协作待办事项列表应用程序。这意味着你将能够实时与其他人分享你的待办事项列表。一个或多个人将能够同时添加、完成或删除待办事项列表项目。待办事项列表的更改将自动传播到所有用户。这是 Node 非常适合的应用类型。

我们的 Node.js 代码将包括两个不同的部分,一个是正常的 Web 应用程序,将提供静态 HTML、CSS 和 JavaScript,另一个是处理实时更新所有待办事项列表客户端的 WebSocket 服务器。除此之外,我们还将有一个由 jQuery 驱动的客户端,看起来与我们在第三章中的应用程序非常相似,CoffeeScript 和 jQuery

我们将使用现有待办事项列表应用程序的一些资源(样式表和图像)。我们还将重用第三章中的客户端 jQuery 代码,并对其进行调整以适应我们的应用程序。如果你之前没有跟着前几章的内容,你应该可以根据需要从本章的代码中复制资源。

让我们开始吧

为了开始,我们将执行以下步骤:

  1. 为我们的应用程序创建一个文件夹。

  2. 使用package.json文件指定我们的应用程序依赖项。

  3. 安装我们的依赖项。

  4. 创建一个app.coffee文件。

  5. 第一次运行我们的应用程序。

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 用作我们的 Web 框架,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方法为我们的应用程序创建一个索引路由。我们指定路径为'/',然后传入一个匿名函数来处理请求。它接受两个参数,reqres参数。现在,我们只需向响应中写入Hello Express并返回。

然后,我们使用listen方法启动我们的应用程序,并告诉它在端口3000上运行。最后,我们将写入标准输出,以便我们知道应用程序已启动。

正如您所看到的,Express 的魔力在于声明性地设置路由。使用 Express,您可以通过指定 HTTP 方法、URL 路径和处理请求的函数轻松创建路由。

运行我们的应用程序

让我们运行我们的应用程序,看看是否一切正常。在我们的应用程序文件夹中,在命令行工具上键入以下内容:

coffee app.coffee

您应该会看到输出为Listening on port 3000

将浏览器指向http://localhost:3000/。您应该会看到文本Hello Express

要在命令行工具上停止 Node 进程,只需使用Ctrl + C

创建一个视图

与其他 Web 框架(如 Rails)类似,Express 具有视图的概念,它可以让您使用单独的文件将 UI 与应用程序分离开来。通常,这些是使用 Jade 等模板语言编写的。让我们为我们的根操作创建一个视图。

为此,我们需要:

  1. 创建一个views文件夹并添加一个 Jade 视图文件。

  2. 配置我们的 Express 应用程序以了解存储视图的文件夹,并使用的模板库。

  3. 更改我们的索引路由以呈现我们的视图。

让我们在项目根目录中创建一个名为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 '/'路由,渲染 index 模板并传递一个包含消息的哈希选项。这个值将在我们的视图中被渲染出来。

一旦我们再次运行我们的应用程序并刷新页面,我们应该能够看到我们的页面已经更新了新的文本。

node-supervisor

到目前为止,你可能会想知道每次更改代码时是否需要重新启动我们的 Node 应用程序。在开发中,我们希望我们的代码在每次更改时都能自动重新加载,类似于 Rails 的工作方式。

幸运的是,有一个整洁的开源库可以做到这一点:node-supervisorgithub.com/isaacs/node-supervisor)。我们像安装其他 npm 模块一样安装它,只是要确保传递-g标志来全局安装它,如下面的命令所示:

npm install supervisor -g

在终端中,你现在应该能够通过以下命令运行监督者:

supervisor app.coffee

在一个单独的窗口中保持这个过程运行。为了查看这是否起作用,让我们编辑发送到我们视图的消息;在下面的代码片段中,高亮显示了编辑后的消息:

app.get '/', (req, res) ->
 res.render 'index', message: "Now we're cooking with 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'

我们的页面现在将被更新,但看起来不太好。我们将使用在上一个项目中使用的相同样式表来为我们的页面设置样式。

提示

没有按预期工作?

记得要留意监督者进程的输出,看看你的 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 代码,CoffeeScript 和 jQuery。我们将把它放在一个名为todo.coffee的文件中。

我们接下来的决定是,我们应该把这个文件放在哪里?我们如何编译和在我们的应用程序中使用它的输出?

我们可以做与我们在第三章中构建应用程序时一样的事情,也就是创建一个包含客户端 CoffeeScript 代码的src文件夹,然后使用coffee命令和--watch标志进行编译。输出的 JavaScript 然后可以放在我们的public文件夹中,我们可以像平常一样包含它。但这意味着我们将有两个独立的后台任务运行,一个是运行我们的服务器的监督任务,另一个是编译我们的客户端代码的任务。

幸运的是有更好的方法。您可能还记得我们在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()

如果您在第三章中跟着做,CoffeeScript 和 jQuery,那么这段代码应该很熟悉。这是我们完整的客户端应用程序,显示待办事项并在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')

我们的页面现在应该看起来像以下截图中显示的页面:

创建协作 UI

客户端上的 WebSocket

现在让我们为用户点击加入按钮时连接到一个房间添加一个事件处理程序。

在我们的todo.coffee文件中,我们将在cacheElementsbindEvents函数中添加以下代码:

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 连接、发出请求和接收响应的客户端库,以及用于处理请求的服务器端节点模块。

在上述代码中,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 模块创建一个 Web 服务器,将我们的 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

最后,让我们更新我们的 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: &nbsp
 span#connected-list List name
 button#leave Leave
    script(src="img/jquery.min.js")
    script(src="img/socket.io.js")
    != js('todo')

在先前的标记中,我们已经在footer div标记中添加了两个新部分。每个部分将根据我们所处的状态(connecteddisconnected)而隐藏或显示。connect部分与以前相同。disconnect部分将显示您当前连接到的列表,并有一个Leave按钮。

现在我们将在todo.coffee文件中添加代码,以便在加入列表时更新 UI。

首先,我们将在我们的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部分。

离开列表

离开列表应该很容易。我们断开当前的 socket 连接,然后更新 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'

测试全部

现在让我们测试我们的列表加入和离开代码。要看到所有操作,请按照以下步骤进行:

  1. 在浏览器中打开http://localhost:3000/

  2. 在浏览器窗口中,输入一个列表名称,然后点击Join List。UI 应该如预期般更新。

  3. 加入列表后,添加一些待办事项。

  4. 现在再次打开网站,这次使用第二个浏览器。由于localStorage是特定于浏览器的,我们这样做是为了拥有一个干净的待办事项列表。

  5. 再次在另一个浏览器中输入与之前相同的列表名称,然后点击Join List

  6. 当列表同步时,您现在应该看到之前添加的列表项显示出来。

  7. 最后,使用Leave按钮从列表中断开。

测试全部

从不同浏览器同步的两个列表

太棒了!我们现在可以看到 WebSockets 的威力。我们的客户端在无需轮询服务器的情况下,会在应该同步项目时收到通知。

然而,一旦我们连接到列表,我们仍然无法添加新项目以使其显示在房间中的所有其他客户端中。让我们实现这一点。

向共享列表添加待办事项

首先,我们将在服务器上处理添加新项目。处理这个的最佳位置是现有的用于创建待办事项的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处理itemAdded事件,其中包括项目 ID 和值。这将在localStorage中创建一个新的待办事项,如果它在localStorage中不存在,或者更新现有值。

就是这样!现在我们应该能够向列表中的所有客户端添加项目。要测试它,我们将按照之前的类似步骤进行:

  1. 在浏览器中打开http://localhost:3000/

  2. 在浏览器窗口中,输入一个列表名称,然后点击加入列表。UI 应该如预期般更新。

  3. 现在再次打开网站,这次使用第二个浏览器。

  4. 再次输入与另一个浏览器中相同的列表名称,然后点击加入列表

  5. 在任一浏览器中添加新的待办事项。你会立即看到待办事项出现在另一个浏览器中。

哇!这不是很令人印象深刻吗?

从共享列表中移除待办事项

要从共享列表中移除待办事项,我们将遵循与添加项目类似的模式。在todo.coffeedestroy函数中,我们将向我们的 socket 发出一个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 socket 事件处理程序获取要移除的任务的 ID。它通过使用 JavaScript 的数组filter函数将共享列表的当前值分配给我们创建的新值来从列表中移除待办事项。这将选择所有不具有传递 ID 的项目。然后,它通过共享列表中的所有客户端 socket 连接调用emit,发送itemRemoved消息。

最后,我们需要在客户端处理itemRemoved消息。与添加项目时类似,我们将在todo.coffeejoinList函数中设置这个消息,如下面的代码片段所示:

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。

要测试移除项目,请按照以下步骤操作:

  1. 在浏览器中打开http://localhost:3000/

  2. 在浏览器窗口中,输入一个列表名称,然后点击加入列表。UI 应该如预期般更新。

  3. 一旦连接到共享列表,添加一些待办事项。

  4. 现在再次打开网站,这次使用第二个浏览器。

  5. 再次输入与另一个浏览器中相同的列表名称,然后点击加入列表。您的待办事项列表将与共享列表同步,并包含您在另一个浏览器中添加的项目。

  6. 单击删除图标以删除浏览器中的待办事项。您将立即看到另一个浏览器中已删除的待办事项消失。

现在轮到你了

作为对您的最后一项练习,我将要求您使“清除已完成”按钮起作用。作为提示,您应该能够使用现有的destroyItem方法功能。

总结

在本章中,我们通过探索 Node.js 作为一个快速、事件驱动的平台,让您可以使用 JavaScript 或 CoffeeScript 来编写服务器应用程序,完成了对 CoffeeScript 生态系统的巡回。我希望您已经对能够同时在服务器和浏览器上使用 CoffeeScript 编写 Web 应用程序的乐趣有所了解。

我们还花了一些时间使用一些为 Node.js 编写的精彩开源库和框架,比如 expressjs、connect 和 Socket.IO,并看到了我们如何成功地使用 npm 来管理应用程序中的依赖项和模块。

我们的示例应用程序恰好是您可以使用 Node.js 的类型,我们看到它的事件驱动模型适用于编写客户端和服务器之间有大量常量交互的应用程序。

现在我们的旅程已经结束,我希望已经在您心中灌输了渴望和技能,让您走出去使用 CoffeeScript 改变世界。我们花了一些时间不仅探索语言,还有让我们能够更快速地开发强大应用程序的精彩工具、库和框架。

CoffeeScript 和 JavaScript 生态系统的未来是光明的,希望您能成为其中的一部分!

posted @ 2024-05-23 15:55  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报