了解知识

引导: 本系列探讨了广受欢迎的 CoffeeScript 编程语言,该语言建立在 JavaScript 的基础之上。CoffeeScript 可以编译为效率较高且与许多最佳实践一致的 JavaScript。您可以在 Web 浏览器内运行这样的 JavaScript,也可以与 Node.js 等技术相结合,将它用于服务器应用程序。本系列的 第 1 部分 介绍了如何开始使用 CoffeeScript,还解释了为开发人员提供的一些附加功能。第 2 部分 介绍了如何使用 CoffeeScript 解决一些编程问题。本文将介绍如何使用 CoffeeScript 创建一个完整的应用程序。

简介

CoffeeScript 是一种构建于 JavaScript 基础之上的全新编程语言。CoffeeScript 提供了能够吸引 Python 或 Ruby 爱好者的整洁的语法,此外还提供了受 Haskell 和 Lisp 等语言启发得出的许多函数式编程特性。

在本系列的 第 1 部分 中,您了解了使用 CoffeeScript 而不是仅使用 JavaScript 的优势。您设置了开发环境,运行了脚本。在 初步了解 CoffeeScript,第 2 部分:通过动手实践示例学习语言 中,您在尝试解决数学问题的过程中尝试了许多 CoffeeScript 特性,探索了 CoffeeScript 编程语言。

在这篇文章中,您将创建一个示例 CoffeeScript 应用程序。客户端和服务器端代码均将完全使用 CoffeeScript 编写。您还可以 下载 本文中使用的源代码。

 

CoffeeScript 支持的搜索

常用缩写

JSON:JavaScript Object Notation
UI:用户界面
您将要创建的示例应用程序允许用户输入一个搜索词,并同时在 Google 和 Twitter 上搜索,随后显示合并后的结果。应用程序的客户端将获取用户输入的数据,并将其发送给服务器,以获得结果。在服务器返回结果之后,客户端会为结果创建 UI 元素,并将其显示在屏幕上。目前而言,不必担心服务器端的工作方式。

首先定义应用程序的数据模型。应用程序将显示搜索结果,因此定义一个 SearchResult 类。清单 1 给出了定义。


清单 1. 基本 SearchResult 类

class SearchResult
constructor: (data) ->
@title = data.title
@link = data.link
@extras = data

toHtml: -> "<a href='#{@link}'>#{@title}</a>"
toJson: -> JSON.stringify @extras

SearchResult 类非常简单。它:

首先是一个构造函数,定义了两个成员变量:title 和 link。
在传递给构造函数的数据对象中查找这两个值。
将其余数据存储在一个名为 extras 的成员变量中。
这是一种便捷的做法,因为您的应用程序中拥有两种不同的搜索结果(Google 搜索结果和 Twitter 搜索结果)。

为 SearchResult 类定义两个方法:
toHtml,利用 CoffeeScript 的字符串插值,通过 SearchResult 实例创建一个简单的 HTML 字符串。
toJson,用于将 SearchResult 对象转为 JSON 字符串。
清单 1 中的类提供了基本的搜索结果特性。您将从 Google 和 Twitter 的搜索结果中获得更多数据。为各种类型的搜索结果创建子类,为此建模。清单 2 展示了 Google 搜索结果的类型。


清单 2. GoogleSearchResult 类

class GoogleSearchResult extends SearchResult
constructor: (data) ->
super data
@content = @extras.content
toHtml: ->
"#{super} <div class='snippet'>#{@content}</div>"

清单 2 展示了 CoffeeScript 中的面向对象编程有多么简单。GoogleSearchResult 扩展了 清单 1 中的基本 SearchResult 类。其构造函数将调用超类的构造函数。如果您拥有在 JavaScript 中实现类风格继承的经验,那么就能了解这有多困难。清单 3 展示了所生成的 JavaScript。


清单 3. GoogleSearchResult JavaScript

GoogleSearchResult = (function() {
__extends(GoogleSearchResult, SearchResult);
function GoogleSearchResult(data) {
GoogleSearchResult.__super__.constructor.call(this, data);
this.content = this.extras.content;
}
GoogleSearchResult.prototype.toHtml = function() {
return "" + GoogleSearchResult.__super__.toHtml.apply(this, arguments)
+ " <div class='snippet'>" + this.content + "</div>";
};
return GoogleSearchResult;
})();

为了调用超类的构造函数,您必须在 __super__ 变量中保留超类的一个实例(名称可能有所不同),随后显式调用构造函数。回到 清单 2,您可以看到这在 CoffeeScript 中有多么轻松。请注意,示例在 GoogleSearchResult 类中定义了一个名为 content 的新实例变量。这实际上主要是搜索结果指向的网页中的 HTML 片段。GoogleSearchResult 拥有这种功能,而基类并未提供这种功能,这应该并不出人意料。最后,请注意 toHtml 方法的覆盖。示例使用了超类的 toHtml 方法,但也附加了一个额外的 div,用它来包含内容片段。再次观察 清单 3,查看这种对超类 toHtml 方法的调用是如何完成的。您有一个 GoogleSearchResult 超类,您还需要一个 TwitterSearchResult 超类,如清单 4 所示。


清单 4. TwitterSearchResult 类


class TwitterSearchResult extends SearchResult
constructor: (data) ->
super data
@source = @extras.from_user
@link = "http://twitter.com/#{@source}/status/#{@extras.id_str}"
@title = @extras.text
toHtml: ->
"<a href='http://twitter.com/#{@source}'>@#{@source}</a>: #{super}"

TwitterSearchResult 类沿用了与 清单 2 中的 GoogleSearchResult 类相同的模式。其构造函数将利用超类的构造函数。此外还:

定义了自己的成员变量,将其命名为 source。
使用字符串模板及其成员变量构造 link 成员变量。
将 title 成员变量重置为输入数据中的另外一个字段。
覆盖超类的 toHtml 方法,附加一个写微博的用户的链接。
同样,CoffeeScript 的字符串插值使您在创建新方法时能够轻松利用超类的 toHtml 方法。要调用超类的 toHtml 方法,只需调用 super 即可。您可能希望转而调用 super.toHtml,实际上并不需要这样做,这样做也会导致错误。CoffeeScript 暗示着您希望调用超类的相同方法,使您的工作更加轻松。

您现在拥有了应用程序需要的数据结构,下面即可开始编写一些客户端逻辑。使用正常工作的后端对代码进行测试是更简单的一种做法。由于目前尚无后端,因此我们将使用下一个出色的特性:模拟数据 (mock data)。

回页首

使用模拟数据

构建现代 Web 应用程序这种客户端服务器应用程序时,有必要创建一种共享界面,使应用程序的两个部分能够同时汇合并创建模拟数据。这使您能够并行开发应用程序的客户端和服务器部分。使用 CoffeeScript 时,这种方法尤为有效,因为您可以在客户端和服务器使用相同的编程语言。清单 5 展示了 Google 的模拟搜索结果。


清单 5. 模拟 Google 搜索结果


mockGoogleData = [
GsearchResultClass:"GwebSearch",
link:"http://jashkenas.github.com/coffee-script/",
url:"http://jashkenas.github.com/coffee-script/",
visibleUrl:"jashkenas.github.com",
cacheUrl:"http://www.google.com/search?q\u003dcache:nuWrlCK4-v4J:jashkenas
.github.com",
title:"\u003cb\u003eCoffeeScript\u003c/b\u003e",
titleNoFormatting:"CoffeeScript",
content:"\u003cb\u003eCoffeeScript\u003c/b\u003e is a little language that
compiles into JavaScript. Underneath all of those embarrassing braces and
semicolons, JavaScript has always had a \u003cb\u003e...\u003c/b\u003e"
,
GsearchResultClass:"GwebSearch",
link:"http://en.wikipedia.org/wiki/CoffeeScript",
url:"http://en.wikipedia.org/wiki/CoffeeScript",
visibleUrl:"en.wikipedia.org",
cacheUrl:"http://www.google.com/search?q\u003dcache:wshlXQEIrhIJ
:en.wikipedia.org",
title:"\u003cb\u003eCoffeeScript\u003c/b\u003e - Wikipedia, the free
encyclopedia",
titleNoFormatting:"CoffeeScript - Wikipedia, the free encyclopedia",
content:"\u003cb\u003eCoffeeScript\u003c/b\u003e is a programming language
that transcompiles to JavaScript. The language adds syntactic sugar inspired by
Ruby, Python and Haskell to enhance \u003cb\u003e...\u003c/b\u003e"
,
GsearchResultClass:"GwebSearch",
link:"http://codelikebozo.com/why-im-switching-to-coffeescript",
url:"http://codelikebozo.com/why-im-switching-to-coffeescript",
visibleUrl:"codelikebozo.com",
cacheUrl:"http://www.google.com/search?q\u003dcache:VDKirttkw30J:
codelikebozo.com",
title:"Why I\u0026#39;m (Finally) Switching to \u003cb\u003eCoffeeScript
\u003c/b\u003e - Code Like Bozo",
titleNoFormatting:"Why I\u0026#39;m (Finally) Switching to CoffeeScript -
Code Like Bozo",
content:"Sep 5, 2011 \u003cb\u003e...\u003c/b\u003e You may have already heard
about \u003cb\u003eCoffeeScript\u003c/b\u003e and some of the hype surrounding it
but you still have found several reasons to not make the \u003cb\u003e...
\u003c/b\u003e"
]

显而易见,利用 CoffeeScript 简洁的语法,即便是模拟数据也能轻松创建。示例展示了 CoffeeScript 中的对象表达方式。清单 5 是一个数组。额外的缩进用于指示一个对象,此外该对象的各属性进一步缩排。这样的代码比 JSON 更出色。空格取代了花括号。这就像是 JavaScript 的表达方式,属性不放在引号中。而 JSON 必须为这些属性加引号。

清单 6 展示了用于 Twitter 搜索结果的类似模拟数据。


清单 6. 模拟 Twitter 搜索结果

mockTwitterData = [
created_at:"Wed, 09 Nov 2011 04:18:49 +0000",
from_user:"jashkenas",
from_user_id:123323498,
from_user_id_str:"123323498",
geo:null,
id:134122748057370625,
id_str:"134122748057370625",
iso_language_code:"en",
metadata:
recent_retweets:4,
result_type:"popular"
profile_image_url:"http://a3.twimg.com/profile_images/1185870726/gravatar
_normal.jpg",
source:"<a href="http://itunes.apple.com/us/app/twitter/id409789998?mt
=12&quot; rel="nofollow">Twitter for Mac</a>",
text:""CoffeeScript [is] the closest I felt to the power I had twenty
years ago in Smalltalk" - Ward Cunningham (http://t.co/2Wve2V4l) Nice.",
to_user_id:null,
to_user_id_str:null
]

清单 6 中的模拟数据与清单 5 中的模拟数据相似,但包含特定于 Twitter 的字段。现在,您只需创建一个返回这些模拟数据的界面即可。清单 7 展示的类用于完成这个任务。


清单 7. 模拟搜索引擎类


class MockSearch
search: (query, callback) ->
results =
google: (new GoogleSearchResult obj for obj in mockGoogleData)
twitter: (new TwitterSearchResult obj for obj in mockTwitterData)
callback results

MockSearch 类只有一个方法,名为 search。它获取两个参数:query 用于搜索,还有一个 callback 函数。 MockSearch 快速返回结果,但实际搜索将需要通过网络与服务器对话。为了在 JavaScript 中处理这项任务,为了确保您不会导致 UI 冻结,通常需要使用回调函数。

下一步是创建名为 results 的对象。您同样要使用 CoffeeScript 的对象表达语法。results 对象有两个字段:google 和 twitter。各字段的值均使用数组推导式来表示。表达式将创建恰当类型的 SearchResult 数组(对于 Google 是 GoogleSearchResult,对于 Twitter 是 TwitterSearchResult)。最后调用 callback 函数,并向其传递 results 对象。

模拟搜索可以正常工作,下面即可编写与 UI 相关的客户端代码。

回页首

在浏览器中整合一切

在将 UI 与应用程序代码结合之前,首先来观察一下您要使用的 UI。清单 8 给出了一个非常简单的网页。


清单 8. CoffeeSearch 网页

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">

<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>CoffeeSearch</title>
<script type="text/javascript" src="search.js"></script>
</head>
<body>
<div>
<label for="searchQuery">Keyword:</label>
<input type="text" name="searchQuery" id="searchQuery"></input>
<input type="button" value="Search" onclick="doSearch()"/>
</div>
<div class="goog" id="gr"/>
<div class="twit" id="tr"/>
</body>
</html>

上面的网页采用了一种非常基本的形式,即输入一个关键字并将其传递到搜索引擎。它定义了两个部分,可将搜索结果添加到其中。网页中未定义任何 JavaScript 内容。只有一个名为 search.js 的文件,这将是 CoffeeScript 的编译版。请注意,在单击搜索按钮时,将调用一个名为 doSearch 的函数。此函数必须处于 search.js 文件中,也是您尚未看到的 search.js 文件中的惟一内容。清单 9 展示了 CoffeeScript 中的定义。


清单 9. 网页的 doSearch 函数


@doSearch = ->
$ = (id) -> document.getElementById(id)
kw = $("searchQuery").value
appender = (id, data) ->
data.forEach (x) ->
$(id).innerHTML += "<p>#{x.toHtml()}</p>"
ms = new MockSearch
ms.search kw, (results) ->
appender("gr", results.google)
appender("tr", results.twitter)

您可能会注意到,函数附加了一个 @ 符号,这是 this 的缩写。如果在脚本的最顶层定义,this 将成为一个全局对象。如果脚本是网页中的脚本,则全局对象就是窗口对象,允许在 清单 8 所示的网页中引用。

doSearch 函数通过短短几行代码完成了大量工作。其代码:

定义了一个名为 $ 的本地函数,这基本上就是非常有用的 document.getElementById 函数的快捷方式。使用这个函数获取在 清单 8 的搜索表单中输入的关键字。
定义另外一个名为 appender 的本地函数,它将获取 DOM 中的一个元素的 ID 以及一个数组。随后它将遍历数组,创建一个 HTML 字符串,并附加到具有给定 ID 的元素中。
创建一个 MockSearch 对象,并调用其 search 方法。
传递表单中的关键字,创建一个回调函数。
这个回调函数将使用 appender,将来自 Google 的搜索结果附加到一个 div 中,并将来自 Twitter 的搜索结果附加到另一个对象。

现在,您就可以轻松编译和部署所有代码。图 1 展示了包含模拟数据的网页。


图 1. 包含模拟数据的搜索页面
包含模拟数据的搜索页面
这个示例看起来或许不是那么激动人心,但它确实演示了客户端需要的素有函数。尽管您尚未进行服务器端编码,但您一定有信心,只要服务器端代码能生成结构类似于模拟数据的数据,那么应用程序就能正常工作。

回页首

结束语

在这篇文章中,我讨论了 CoffeeScript 的实际应用,使用它构建了一些实实在在的内容。通过与 node.js 相结合,CoffeeScript 将使您有机会利用同样优雅的编程语言编写包括客户端和服务器在内的完整应用程序。现在我们已经做好了准备,在本系列的最后一篇文章中,我们将重新使用部分完全相同的代码,构建应用程序的服务器端。请继续保持关注。 

标签: Coffeescript
扩展知识