CoffeeScript の最初の一杯: 第 4 回 サーバーで CoffeeScript を使用する

この連載では、JavaScript をベースに作成された人気の高い CoffeeScript プログラミング言語について詳しく探ります。CoffeeScript は、多くのベスト・プラクティスに従った効率的な JavaScript コードにコンパイルされます。この JavaScript コードは Web ブラウザーで実行することも、Node.js などのサーバー・アプリケーション用の技術で使用することもできます。連載のこれまでの記事では、まず始めに CoffeeScript の魅力を紹介し、その開発環境をセットアップしました。続いて、この言語が持つさまざまな機能を実際に使ってみた後、実際のアプリケーションのクライアント・サイドのコードを作成しました。連載の最終回となる今回の記事では、サーバー・サイドの CoffeeScript を作成します。

Michael Galpin, Software Engineer, Google

Michael Galpin's photoMichael Galpin は、Google のソフトウェア・エンジニアです。彼は、『Android in iPractice』の共著者であり、developerWork にも頻繁に寄稿しています。彼が次に取り組もうとしている内容をこっそり覗くには、彼のブログを調べるか、Twitter の @michaelg または Google の +Michael Galpin で彼をフォローしてください。



2012年 3月 22日

はじめに

CoffeeScript は JavaScript をベースに作成された新しいプログラミング言語です。CoffeeScript が提供する簡潔な構文は、Python や Ruby のファンであれば誰でも魅力的に感じることでしょう。さらに CoffeeScript は、Haskell や Lisp などの言語から発想を得た数多くの関数型プログラミング機能も兼ね備えています。

この連載第 1 回では、CoffeeScript を使用することによってもたらされるメリットについて学び、CoffeeScript の開発環境をセットアップしてスクリプトを実行しました。第 2 回では、数学の問題を解くために CoffeeScript が持つさまざまな機能を使用しながら、このプログラミング言語の詳細を探りました。そして第 3 回では、Web アプリケーションのクライアント・サイドのコードを作成しました。

最終回となる今回の記事では、サーバー・サイドのコンポーネントを作成し、CoffeeScript のみを用いてコーディングしたサンプル Web アプリケーションを完成させます。

この記事で使用するソース・コードはここからダウンロードしてください。


すべての Web サービスを呼び出す

第 3 回で作成した Web アプリケーションは、キーワードを入力すると Google と Twitter の両方で検索を実行するというものでした。このアプリケーションのクライアント・サイドを作成するときには、サーバーから返される結果を模倣したモック・データを使用しましたが、実際にこのアプリケーションの機能を実装するためには、Google と Twitter から提供されている Web サービスをアプリケーションのサーバー・サイドから呼び出す必要があります。Google と Twitter は両方とも極めて単純な検索サービスを提供しているので、必要となる作業は、これらの検索サービスに対して 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 ではこのような表現になっています。Node.js のネイティブ・コードでは var http = require("http"); となります。この記事では、このような Node.js のコア・モジュールをいくつか使用することになります (モジュールが機能する仕組みの詳細については、この記事では説明しません)。Node.js がインストールされていれば (第 1 回を参照)、この記事で使用するすべてのモジュールを使用することができます。リスト 1 の例で使用している http モジュールは、HTTP リクエストを送信する場合と、受信する場合のどちらでも役に立つ複数のクラスと関数を提供するモジュールです。

続いて、リスト 1 では fetchPage 関数を定義しています。この関数が取る引数は以下の 4 つです。

  • リソースのホスト名を表す host
  • リソースのポートを表す port
  • リソースのパスを表す path
  • コールバック関数を表す callback

    Node.js では、あらゆるタイプの I/O 関数が本質的に非同期であるため、関数が処理を完了した時に呼び出すコールバック関数が必要です。fetchPage 関数はコールバック関数を 4 番目の引数として取り、最初の 3 つの引数を使用した HTTP GET リクエストを、http モジュールの get 関数を使用して実行します。

fetchPage 関数は、ClientResponse のインスタンスが渡されたコールバック関数を引数に取ることもあります。ClientResponsehttp モジュールに定義されているオブジェクトであり、dataend という 2 つのイベントを受け取る ReadableStream という非同期インターフェース (Node.js のコア・インターフェース) を実装します。このインターフェースには、コールバック関数を 2 つのイベントに登録するための関数しかありません。この 2 つのイベントのうち data イベントが発生するのは、HTTP GET リクエストの送信対象のリソースからデータを受信したときです。

リソースから一度にすべてのデータが返される場合もありますが、それよりも、データがいくつかに分割されたチャンクで送信されるほうが一般的です。data イベントはチャンクを受信するたびに起動され、それによってコールバック関数が呼び出されます。上記の例では、contents という変数を作成してあるので、チャンクを受信するたびに行う処理は、そのチャンクを contents に追加することのみです。すべてのデータを受信した時点で、end イベントが起動されます。データはすべて揃っているため、fetchPage 関数に渡されたコールバック関数に、contents を渡すことができます。この多目的関数を定義したところで、次は 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 には、次の 2 つの関数が定義されています。

  • googleSearch: キーワードコールバック関数を引数に取ります。host に固定値を設定し、CoffeeScript の文字列補間機能 (文字列内での式展開) を使用して動的に path の値を設定してから、fetchPage を呼び出します。
  • twitterSearch: googleSearch とよく似ていますが、ホストとパスの値が異なります。

両方の関数の path の値には、文字列補間機能と、スペースやその他の特殊文字を処理する JavaScript の便利な encodeURI 関数を使用します。これで、それぞれの検索関数が用意できました。次は、複合検索のシナリオ専用の関数の作成に取り掛かります。


非同期関数を結合する

Google と Twitter での複合検索を行うには、いくつかの方法があります。例えば、googleSearch 関数を呼び出した後、コールバック関数の中で 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 関数は、今やすっかりお馴染みのシグニチャーである、キーワードとコールバック関数を引数に取ります。そして、複合検索結果を格納するための data というデータ構造を作成します。data オブジェクトには google フィールドと twitter フィールドがあり、この両方のフィールドが空のストリングとして初期化されます。初期化に続くステップでは、googleSearch 関数を呼び出します。この関数のコールバック関数の中では、Google から返される結果が標準的な JSON.parse 関数によって構文解析されます。Google から返された JSON テキストが JavaScript オブジェクトに構文解析されると、このオブジェクトを使用して、data.google フィールドの値が設定されます。googleSearch 関数を呼び出した後、今度は twitterSearch 関数を呼び出します。twitterSearch 関数のコールバック関数も googleSearch 関数の場合と同様です。

ここで理解しておくべき重要な点は、googleSearch 関数のコールバック関数においても twitterSearch 関数のコールバック関数においても、他方のコールバック関数によってデータが格納されているかどうかを調べていることです。どちらの検索が先に完了するかはわかりません。したがって、Google と Twitter の両方からのデータがあることを確認した上で、combinedSearch 関数に渡された callback 関数が呼び出されます。以上で、Google と Twitter の両方を検索し、検索結果を結合して返す関数が完成しました。次のタスクは、連載の第 3 回で作成した Web ページに、この複合検索関数を公開することです。そのために必要な作業は、Web サーバーを作成することのみです。


CoffeeScript Web サーバー

この時点で、以下のものが用意できています。

  • キーワードを送信して、検索結果を表示できる Web ページ
  • キーワードを引数に取り、Google と Twitter からの検索結果を生成できる関数

では、この 2 つを結び付けるものは何でしょう?それは、Web サーバーと呼ばれる場合もあれば、アプリケーション・サーバー、さらにはミドルウェアと呼ばれる場合もあります。どのように呼ぶにしても、CoffeeScript では簡単に作成することができます。

この Web サーバーは、2 つの役目を果たす必要があります。もちろんその 1 つは、複合検索のリクエストを受けることです。そしてもう 1 つは、第 3 回で作成した静的リソースを提供することです。ここで作成しているのは Web アプリケーションなので、同一生成元ポリシーに留意してください。つまり、検索の呼び出し先は、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 アプリケーションの静的リソースに対するリクエストを処理します。そのためには、さらに 2 つの Node.js モジュールを使用する必要があります。

  • path: これは、ファイル・パスを処理するための単なるユーティリティー・ライブラリーです。
  • fs: Node.js でのファイル I/O 関連のすべての関数を提供するファイルシステムで、基本的には標準 POSIX 関数に対するラッパーです。

serveStatic 関数は、次の 2 つの引数を取ります。

  • uri: 基本的に Web ブラウザーからリクエストされている静的ファイルの相対パスです。
  • ServerResponse オブジェクト: http モジュールに定義されている、もう 1 つのタイプのオブジェクトです。このオブジェクトは、リソースを求めて送信された HTTP GET リクエストの送信元に返すデータの書き込み先となるストリームを提供します。

serveStatic の中では、まず process.cwd を使用して、ファイルの相対パスを絶対パスに変換します。process オブジェクトは、Node.js が実行されているシステム・プロセスを表すグローバル・オブジェクトです。このオブジェクトの cwd メソッドが、カレント作業ディレクトリーを返します。path モジュールを使用して、カレント作業ディレクトリーと目的のファイルの相対パスを結合した結果が、絶対パスです。この絶対パスで path モジュールをもう一度使用すれば、該当するファイルの有無を確認することができます。ファイルの有無を確認するには I/O が伴うことから、これは非同期関数です。この非同期関数に、fileName とコールバック関数を渡します。コールバック関数によってブール値が取得され、その値でファイルが存在するかどうかが通知されます。ファイルが存在しなければ、当然、「ファイル未検出」を伝える HTTP 404 メッセージを書き込みます。

ファイルが存在する場合には、ファイルの内容を読み取るために、fs モジュールとその readFile メソッドを使用します。この非同期メソッドは、fileName、タイプ、コールバック関数を引数に取ります。コールバック関数に渡される引数には、次の 2 つがあります。

  • エラー・パラメーター。ファイルシステムからリソースを読み取る際に発生した問題を示すためのパラメーターです。問題が発生した場合は、HTTP 500 エラー・メッセージがクライアントに書き込まれます。
  • 問題が発生しなければ、HTTP 200 OK メッセージがクライアントに書き込まれ、ファイルの内容がクライアントに返されます。

この関数が対処するのは、静的ファイルを提供する比較的簡単なケースです。次のセクションでは、それよりも難しいシナリオとして、検索リクエストに動的に応答しなければならない場合について説明します。


動的レスポンスとサーバー

サンプル Web サーバーは、主として静的リソースに対するリクエストと動的な検索リクエストを扱います。このような 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 サーバーに対してリクエストが行われるたびに呼び出されます。コールバック関数は、2 つの引数を取ります。1 つは ServerRequest のインスタンス、もう 1 つは ServerResponse のインスタンスです。どちらのタイプも、http モジュールに定義されています。コールバック関数の中でサーバーに対するリクエストの URL を構文解析するには、url モジュールの parse メソッドを使用します。このメソッドによって 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 オブジェクトの query プロパティーに格納されているので、まずストリングをアンパーサンドのところでサブストリングへと分割します。続いて、サブストリングのそれぞれを等号のところで分割することで、クエリー・ストリングから名前と値のペアを取得できるので、これらのペアのそれぞれを 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 検索メソッドと同じで、キーワードとコールバック関数を引数に取ります。この関数の中では、以下の処理が行われます。

  • すべての Web 開発者にとってお馴染みの XMLHttpRequest を使用して、サーバーに HTTP リクエストを送信します。リクエストには、/doSearch パスと、関数に渡されたキーワードを使用します。
  • レスポンスを受け取ったら、それを JSON.parse によって構文解析します。
  • google フィールドと twitter フィールドを持つ results オブジェクトを作成します。これらのフィールドは、第 3 回GoogleSearchResult クラスと TwitterSearchResult クラスを使って作成します。
  • results オブジェクトをそのまま callback 関数に渡します。

後は、Web ページの doSearch メソッドで、MockSearch に代わってこの CombinedSearch クラスを使用すればよいのです。リスト 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 と比べると、違いはほとんど見つからないでしょう。唯一の違いは、7 行目にあります。それは、MockSearch をインスタンス化する代わりに、CombinedSearch をインスタンス化していることです。この点を除けばすべて同じで、Web ページからキーワードを取得し、検索を呼び出し、その結果を各 SearchResult オブジェクトの toHtml メソッドを呼び出して追加します。図 1 に、サーバーから「実際の」検索結果を取得して表示する Web アプリケーションを示します。

図 1. サンプル Web アプリケーションを実行する
サンプル Web アプリケーションを実行する

クライアント・コードに加えた変更を反映するには、coffee -c search.coffee でコードを再コンパイルする必要があります。アプリケーションを実行するためのコマンドは、coffee search-server.coffee です。このコマンドを実行すれば、ブラウザーで http://127.0.0.1:8080 を開いて、さまざまなクエリーを試してみることができます。


まとめ

この記事では、第 3 回で作成したクライアント・コードを補完するサーバー・サイドのコンポーネントを作成して、サンプル Web アプリケーションを完成させました。この連載の最終回で完成させたアプリケーションは、すべて CoffeeScript で作成されています。CoffeeScript をサーバー・サイドの技術として使用するために、Node.js の数多くの機能を使用しました。

Node.js に対してよく聞かれる批判は、そのノンブロッキング・スタイルによってコールバック関数が何層にも重なってしまうことです。これらのコールバック関数を頭で整理するのは難しく、しかも JavaScript の冗長な構文がさらに事態を悪化させます。CoffeeScript を使うことで、これらのコールバック関数を使用する必要がなくなるわけではありませんが、その簡潔な構文がそのようなコードを作成し、理解するのを楽にしてくれることは確かです。


ダウンロード

内容ファイル名サイズ
Article source codecs4.zip7KB

参考文献

学ぶために

製品や技術を入手するために

  • Node.js: Node.js をダウンロードしてください。スケーラブルなネットワーク・プログラムを容易に構築できるようになります。
  • IBM 製品の評価版: DB2、Lotus、Rational、Tivoli、および WebSphere のアプリケーション開発ツールとミドルウェア製品を体験するには、評価版をダウンロードするか、IBM SOA Sandbox のオンライン試用版を試してみてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=802011
ArticleTitle=CoffeeScript の最初の一杯: 第 4 回 サーバーで CoffeeScript を使用する
publish-date=03222012