初步了解 CoffeeScript,第 4 部分: 在服务器端使用 CoffeeScript

系列 探讨了广受欢迎的 CoffeeScript 编程语言,该语言建立在 JavaScript 的基础之上。CoffeeScript 可以编译为效率较高且与许多最佳实践一致的 JavaScript。您可以在 Web 浏览器内运行这样的 JavaScript,也可以与 Node.js 等技术相结合,将它用于服务器应用程序。在本 系列 的之前几个部分中,您了解了 CoffeeScript 的优势、设置了开发环境、尝试了许多特性,还使用 CoffeeScript 为一个实际应用程序编写了客户端代码。在本系列的最后一篇文章中,我们将编写服务器端 CoffeeScript 代码。

Michael Galpin, 软件工程师, Google

/developerworks/i/p-mgalpin.jpgMichael Galpin 是 Google 的一名软件工程师。他是《Android in iPractice》一书的合著者,而且还频繁在 developerWorks 中发表文章。如果想对他即将发表的文章先睹为快,请访问他的 博客,在 Twitter 上关注@michaelg 或者在 Google+ 上关注 Michael Galpin



2012 年 10 月 10 日

简介

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

在本 系列文章第 1 部分 中,我们了解了使用 CoffeeScript 的优势。此外还设置了开发环境,运行了脚本。在 第 2 部分 中,我们在尝试解决数学问题的过程中尝试了许多 CoffeeScript 特性,探索了 CoffeeScript 编程语言。在 第 3 部分 中,为一个 Web 应用程序编写了客户端代码。

在最后的这篇文章中,您将编写服务器端组件,并完成应用程序 — 所有一切都是使用 CoffeeScript 完成的。

下载 本文中使用的源代码。


调用所有 Web 服务

第 3 部分 中的 Web 应用程序使用一个关键字执行了 Google 和 Twitter 搜索。对于应用程序的客户端,您模拟了来自服务器的结果。为了实际实现此类功能,您需要应用程序的服务器端调用 Google 和 Twitter 提供的 Web 服务。两家公司均提供了非常简单的搜索服务。您只需对搜索服务发出 HTTP GET 请求即可。清单 1 展示了发出 HTTP GET 请求的一般函数。

清单 1. 获取 Web 资源
http = require "http"
            
fetchPage = (host, port, path, callback) ->
    options = 
        host: host
        port: port
        path: path
    req = http.get options, (res) ->
        contents = ""
        res.on 'data', (chunk) ->
            contents += "#{chunk}"
        res.on 'end', () ->
            callback(contents)
    req.on "error", (e) ->
        console.log "Erorr: {e.message}"

require 语句是脚本的第一条语句,在本系列的 第 1 部分 中已经对此进行了简单的介绍。这是一种 Node.js 模块导入语法,或者至少应该说是这种语法的 CoffeeScript 版本。“原生” 版本应该是 var http = require("http");。在这篇文章中,您将使用多个 Node.js 核心模块。(这些模块的工作原理不在本文讨论范围之内。)如果您安装了 Node.js,那么就应该能使用本文中使用的所有模块(请参见 第 1 部分)。对于 清单 1 中的示例,您使用的是 http 模块,它为发出和接收 HTTP 请求提供了一些非常有用的类和函数。

清单 1 随后定义了一个 fetchPage 函数,可以接受以下四个参数:

  • 资源的 host 名称。
  • 资源的 port
  • 资源的 path
  • 一个 callback 函数。

    Node.js 中任何类型的 I/O 函数在本质上都是异步的,因此在完成时需要通过一个 callback 函数进行调用。fetchPage 函数接受一个 callback 函数作为第四个参数。随后使用前三个参数,通过 http 模块的 get 函数发出一条 HTTP GET 请求。

fetchPage 函数也获取一个 callback 函数,将有一个 ClientResponse 实例传递给后一个函数。ClientResponsehttp 模块中定义的一个对象,它实现了 ReadableStream 接口(Node.js 中的核心接口)。这是一个异步接口,接受两个事件:dataend。其惟一的函数用于为这些事件注册回调。在从您发出 HTTP GET 请求的资源接收到数据时,将发生数据事件。

资源将一次性返回所有数据,但更常见的做法是分块发送数据。接收到各块时,数据事件将被触发,回调将被调用。您创建了一个名为 contents 的变量;每次接收到另一个块时,都会将其附加到 contents。接收了所有数据之后,即触发 end 事件。现在,您获得了全部数据,因此可以将 contents 传递给传入 fetchPage 函数的 callback 函数。定义了这个多用途函数之后,下面我们将为 Google 和 Twitter 搜索 API 创建一些专用函数,如 清单 2 所示。

清单 2. Google 与 Twitter 搜索函数
googleSearch = (keyword, callback) ->
    host = "ajax.googleapis.com"
    path = "/ajax/services/search/web?v=1.0&q=#{encodeURI(keyword)}"
    fetchPage host, 80, path, callback

twitterSearch = (keyword, callback) ->
    host = "search.twitter.com"
    path = "/search.json?q=#{encodeURI(keyword)}"
    fetchPage host, 80, path, callback

清单 2 中定义了两个函数:

  • googleSearch,用于获取一个 keyword 和一个 callback 函数。它将固定主机,并使用 CoffeeScript 的字符串插值创建路径,随后使用 fetchPage
  • twitterSearch,该函数与 googleSearch 极为相似,但使用了不同的主机和路径值。

对于两个路径值,您都要使用字符串插值和 JavaScript 提供的便捷的 encodeURI 函数来处理任何空格或其他特殊字符。现在您已经拥有了这些搜索函数,下面即可为合并搜索场景创建特殊函数。


合并异步函数

您可以通过多种方法在 Google 和 Twitter 上执行合并搜索。您可以调用 googleSearch,随后在 callback 中调用 twitterSearch,或者相反。然而,Node.js 的异步/回调架构使您能够更优雅、更高效地完成任务。清单 3 展示了合并搜索。

清单 3. 同时搜索 Google 和 Twitter
combinedSearch = (keyword, callback) ->
    data = 
        google : ""
        twitter : ""
    googleSearch keyword, (contents) ->
        contents = JSON.parse contents
        data.google = contents.responseData.results
        if data.twitter != ""
            callback(data)
    twitterSearch keyword, (contents) ->
        contents = JSON.parse contents
        data.twitter = contents.results
        if data.google != ""
            callback(data)

combinedSearch 函数有一项现在已经广为人知的特征:接受一个关键字和一个回调。随后它为合并搜索结果创建一个数据结构,名为 datadata 对象拥有一个 google 字段和一个 twitter 字段,两者均初始化为空字符串。下一步是调用 googleSearch 函数。在回调中,您将使用标准 JSON.parse 函数解析来自 Google 的结果。Google 返回的 JSON 文本将解析为 JavaScript 对象。这种它来设置 data.google 字段的值。调用 googleSearch 之后,再调用 twitterSearch。其 callback 函数与 googleSearch 的回调函数极为相似。

有必要理解,在两个回调中,您都要检查是否有来自另一个回调的数据。您无法确知哪个回调先完成。因此需要查看是否有来自 Google 和 Twitter 的数据。确认之后,即可调用之前传入 combinedSearch 函数的 callback 函数。您现在得到了一个同时搜索 Google 和 Twitter 并提供合并结果的函数。下一个任务就是将这样的结果公开到您在本系列的 第 3 部分 中创建的网页上。您只需要编写一个 Web 服务器即可。


CoffeeScript Web 服务器

至此,您已经得到了:

  • 一个能够发送关键字、显示搜索结果的网页。
  • 一个能够接受关键字并生成 Google 和 Twitter 搜索结果的函数。

怎样将这一切关联起来?您可以将该服务器称为 Web 服务器、应用服务器,甚至是中间件。无论怎样称呼,在 CoffeeScript 为它编写代码都非常容易。

Web 服务器需要满足两个目的。显然,它需要接受合并搜索的请求。此外还需要提供您在 第 3 部分 中创建的静态资源。您要创建的是一个 Web 应用程序,因此必须密切注意同源策略。搜索调用必须发往生成网页的相同位置。我们首先来处理静态资源。清单 4 展示了一个处理静态资源的函数。

清单 4. 处理静态资源
path = require "path"
fs = require "fs"
serveStatic = (uri, response) ->
    fileName = path.join process.cwd(), uri
    path.exists fileName, (exists) ->
        if not exists
            response.writeHead 404, 'Content-Type': 'text/plain'
            response.end "404 Not Found #{uri}!\n"
            return
        fs.readFile fileName, "binary", (err,file) ->
            if err
                response.writeHead 500, 
                            'Content-Type': 'text/plain'
                response.end "Error #{uri}: #{err} \n"
                return
            response.writeHead 200
            response.write file, "binary"
            response.end()

serveStatic 函数处理 Web 应用程序中对静态资源的请求。请注意,您还需要使用两个 Node.js 模块:

  • path 是一个处理文件路径的实用工具库。
  • 文件系统或 fs 提供了 Node.js 中的所有文件 I/O,大体上就是基于标准 POSIX 函数的一个包装器。

serveStatic 函数接受两个参数:

  • uri 实际上是 Web 浏览器所请求的静态文件的相对路径。
  • ServerResponse 对象,是 http 模块中定义的另外一种类型。它的功能之一就是使您能够写入 HTTP GET 向资源请求的数据。

serveStatic 中,使用 process.cwd 将文件的相对路径转为绝对路径。process 对象是一个全局对象,指示正在运行 Node.js 的系统进程。它的 cwd 方法提供了当前工作目录。使用 path 模块,整合当前工作目录和您需要的文件的相对目录,结果将得到一个绝对路径。有了绝对路径,您就可以再次使用 path 模块,检查文件是否存在。检查一个文件是否存在时,需要涉及到 I/O,因此这是一个异步函数。为其传递 fileName 和回调函数。回调函数将提供一个布尔值,使您了解文件是否存在。如果不存在,那么您就就需要写出一条 HTTP 404 “文件未找到”消息。

如果文件确实存在,那么就需要使用 fs 模块和它的 readFile 方法(异步方法)来读取文件的内容。该方法将获取 fileName、一个类型和一个回调函数。回调函数获取两个参数:

  • 一个表明在从文件系统中读取资源时遇到的任何问题的错误参数。如果存在问题,系统会向客户端返回一个 HTTP 500 错误消息。
  • 如果没有问题,则会显示 HTTP 200 OK 消息,并将文件的内容回发给客户端。

此函数能对静态文件进行相对较为简单的处理。下一部分将讨论您希望动态响应一个搜索请求的更为困难的场景。


动态响应与服务器

示例 Web 服务器主要处理对静态资源的请求和动态搜索请求。我们的战略是使用特定 URL 来处理搜索请求,随后将其他请求分载到 serveStatic 函数。为搜索请求使用 /doSearch 的相对 URL。清单 5 展示了 Web 服务器代码。

清单 5. CoffeeScript Web 服务器
url = require "url"
server = http.createServer (request, response) ->
    uri = url.parse(request.url)
    if uri.pathname is "/doSearch"
        doSearch uri, response
    else
        serveStatic uri.pathname, response    
server.listen 8080
console.log "Server running at http://127.0.0.1:8080"

这个脚本同样从载入一个 Node.js 模块开始。url 模块是解析 URL 时的一个有用的库。下一步是使用 清单 1 中加载的 http 模块创建 Web 服务器。使用该模块的 createServer 方法,该方法将获取一个回调函数,每次对 Web 服务器发出一条请求时,都会调用这个回调函数。该回调函数接受两个参数:一个 ServerRequest 实例和一个 ServerResponse 实例。两种类型都是在 http 模块中定义的。在回调函数中,使用 url 模块的 parse 方法,解析对服务器发出的请求的 URL。这将为您提供一个 URL 对象,您可以使用它的 pathname 属性获取相对路径。如果 pathname/doSearch,则应调用 doSearch 函数(详见下文讨论)。否则就应该调用 清单 5 中的 serveStatic 函数。清单 6 展示了 doSearch 的工作方式。

清单 6. 处理搜索请求
 doSearch = (uri, response) ->
    query = uri.query.split "&"
    params = {}
    query.forEach (nv) ->
        nvp = nv.split "="
        params[nvp[0]] = nvp[1]
    keyword = params["q"]
    combinedSearch keyword, (results) ->
        response.writeHead 200, 'Content-Type': 'text/plain'
        response.end JSON.stringify results

doSearch 函数将解析 URL 的查询字符串,可以在 uri 对象的查询属性中找到这个字符串。根据 “&” 字符拆分字符串。随后根据等号字符拆分各子字符串,获得查询字符串中的名称值对。将各名称值对存储在 params 对象中。获取 "q" 参数,以便获得您希望搜索的关键字。将此传递给 清单 3 中的 combinedSearch 函数。您必须为其传递一个回调函数。示例回调函数直接写出一条 HTTP 200 OK 消息,并使用标准函数 JSON.stringify 将结果转为字符串。

这就是服务器所需的一切。在下一节中,我们将介绍如何将这样的服务器代码与本系列 第 3 部分 中的客户端代码挂接起来。


调用搜索服务器

第 3 部分 中,您编写了一个使用模拟数据提供搜索结果的 MockSearch 类。现在,您将定义一个新类,调用搜索服务器来执行真正的搜索。清单 7 显示了新的搜索类。

清单 7. 实际搜索类
class CombinedSearch
    search: (keyword, callback) ->
        xhr = new XMLHttpRequest
        xhr.open "GET", "/doSearch?q=#{encodeURI(keyword)}", true
        xhr.onreadystatechange = ->
            if xhr.readyState is 4
                if xhr.status is 200
                    response = JSON.parse xhr.responseText
                    results = 
                        google: response.google.map (result) -> 
                            new GoogleSearchResult result
                        twitter: response.twitter.map (result) -> 
                            new TwitterSearchResult result
                    callback results
        xhr.send null

CombinedSearch 类拥有单独一个方法,即 search 方法,它与 MockSearch 的 search 方法具有相同的特征。也就是接受一个关键字和一个回调函数。在函数内:

  • 使用 XMLHttpRequest(所有 Web 开发人员的 “老朋友”),通过传递到函数中的 /doSearch 路径和关键字向服务器发出 HTTP 请求。
  • 获得响应之后,使用 JSON.parse 来解析它。
  • 创建一个包含 googletwitter 字段的结果对象。使用 第 3 部分 中的 GoogleSearchResultTwitterSearchResult 类来创建这些字段。
  • 将结果传递回 callback 函数。

现在,您需要的只是在 Web 页面的 doSearch 方法中使用这些类,而不是在 MockSearch 中使用。清单 8 展示了如何使用 CombinedSearch 类。

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

将 清单 8 与 第 3 部分 中的 doSearch 对比,您不会发现很多的差异。惟一不同的就是第七行。这里实例化的不再是 MockSearch 实例,而是一个 CombinedSearch 实例。其他所有部分都是完全相同的。您从网页获取关键字,调用搜索,随后通过调用各 SearchResult 对象的 toHtml 方法来附加结果。图 1 展示了包含来自服务器的 “实时” 搜索结果的 Web 应用程序。

图 1. 运行示例 Web 应用程序
运行我们的 Web 应用程序

为了实现客户端代码的更改,您需要使用 coffee -c search.coffee 进行重新编译。如需运行应用程序,请使用 coffee search-server.coffee。随后即可打开浏览器,转到 http://127.0.0.1:8080,并尝试执行各种查询。


结束语

在这篇文章中,您完成了 Web 应用程序,构建了服务器端组件来补充 第 3 部分 中的客户端代码。现在,在本 系列 结束时,您获得了一个完全在 CoffeeScript 中编写的完整应用程序。您使用了 Node.js 中的许多特性,这使您能够将 CoffeeScript 用作服务器端技术。

人们对 Node.js 的普遍异议就是其非阻塞式风格会导致多层回调函数。这可能导致您难以理清头绪,而 JavaScript 繁冗的语法进一步加大了复杂度。CoffeeScript 并未改变使用所有这些回调的需求,但其优雅的语法确实使您能够更加轻松地编写和理解此类代码。


下载

描述名字大小
文章源代码cs4.zip7KB

参考资料

学习

获得产品和技术

讨论

  • developerWorks 中文社区:与其他 developerWorks 用户联系,浏览开发人员推动的博客、论坛、小组和 wiki。
  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=840014
ArticleTitle=初步了解 CoffeeScript,第 4 部分: 在服务器端使用 CoffeeScript
publish-date=10102012