2012年7月25日水曜日

Webクライアントの実装(1)

はじめに

おはようございます。当ブログにアクセス頂き、ありがとうございます。
昨日美味しい生牡蠣を腹一杯食しご機嫌の、たなけんです。
本エントリでは、Clojurescriptを利用したWebクライアント開発について記載します。

Clojurescriptとは

Clojurescriptとは、ClojureでJavaScriptアプリケーションを開発するためのライブラリ群です。CoffeeScriptやJSX同様、書かれたプログラムをJavaScriptにコンパイルすることによって、アプリケーションをJavaScriptが動作する環境(webブラウザやnode.jsなどのサーバ上)で動作させることができます。

なぜClojurescriptなのか

JavaScriptの言語仕様(変数スコープ、プロトタイプ方式オブジェクト指向など)に馴染めないこと、Clojureスタイルでのプログラミングが私にとって最も効率が良いということからClojurescriptを利用することにしました。
また、ClojurescriptはGoogle Closure Toolsを利用しているため、Closureが提供するクラス方式のオブジェクトシステムや、多彩なUIコンポーネントが利用できる点も魅力でした。

Clojurescriptの導入

ひなたねこさんの記事を参考にlein-cljsbuildを導入しました。lein cljsbuild auto としておけば、core.cljsを更新したタイミングでJavaScriptにコンパイルされます。(他のツールは必要なく、lein-cljsbuildだけで完結している点が素晴らしいです)
また、emacsのClojurescript-modeはELPA経由でインストールしました。
Clojurescript用のreplもemacsから利用可能な様でしたが、今回は出力されたJavaScriptを見てデバッグを行いました。(時間を見つけて、Clojurescriptのreplも試そうと思います)

Webクライアントの実装(1)

プロジェクトファイル

(defproject cljstest "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.4.0"]]
:plugins[[lein-cljsbuild "0.2.4"]]
:cljsbuild {
:builds [{
:source-path "src"
:compiler {
:output-to "main.js"
:pretty-print true}}]})
view raw project.clj hosted with ❤ by GitHub

プラグイン部でlein-cljsbuildの利用を宣言します。また、closure compilerで用いるコンパイルオプションをcljsbuild部で指定します。今回は生成されるJavascriptの可読性を重視したため、最適化はしない方針としました。

index.html

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>cljstest</title>
<script type="text/javascript" src="out/goog/base.js"></script>
<script type="text/javascript" src="main.js"></script>
</head>
<body>
<input id="query-word" value="" type="text" />
<button id="dictionary-button" type="submit">send</button>
<div id="result"></div>
<script>cljstest.core.main();</script>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

ヘッダ部でJavaScriptファイルを指定し、ボディ部の最後でmain関数を呼び出しています。また検索語を入力するフォーム、確定ボタン、結果表示用のdiv要素を宣言しました。

nsマクロ

(ns my-dictionary-client.core
(:require [clojure.browser.event :as event]
[clojure.browser.dom :as dom]
[goog.net.XhrIo])
view raw core.cljs hosted with ❤ by GitHub

通常のClojureとの違いとして、ClojureからJavaのクラスを利用する場合は:requireではなく:importを使用するのですが、ClojurescriptからClosure Libraryのクラスを利用する場合もClojureのライブラリ同様:requireで良いという点があります。
nsマクロでは、以下のライブラリの利用を宣言しました。
  • clojure.browser.event: ブラウザのイベントを操作するClosure Libraryのクラス群の薄いラップしたライブラリ
  • clojure.browser.dom: ブラウザのDOMを操作するClosure Libraryのクラス群の薄いラップしたライブラリ
  • goog.net.XhrIo: Closure Libraryが提供するJacascriptのhttpXmlRequestオブジェクトを操作するクラス


main関数

(defn main
[]
(event/listen (dom/get-element :dictionary-button) "click" dictionary-request))
view raw core.cljs hosted with ❤ by GitHub

確定ボタンのクリックイベントをトリガーにdictionary-request関数を呼びます。

  • clojure.browser.event/listen関数: 引数に指定された要素、イベントによって第3引数で指定された関数が呼び出されるよう設定
  • clojure.browser.dom/get-element関数: 引数に指定されたシンボルのidに合致する要素を取得

生成されたJavaScript
cljstest.core.main = function main() {
return clojure.browser.event.listen.call(null, clojure.browser.dom.get_element.call(null, "\ufdd0'dictionary-button"), "click", cljstest.core.dictionary_request)
};
view raw main.js hosted with ❤ by GitHub


dictionary-request関数

(defn- dictionary-request
[e]
(goog.net.XhrIo/send
(str "http://localhost:3000/dictionary/" (dom/get-value (dom/get-element :query-word)))
dictionary-callback))
view raw core.cljs hosted with ❤ by GitHub

httpのGETメソッドによるリクエストを送信し、結果を引数にコールバック関数を呼び出します。
  • goog.net.XhrIo/sendメソッド: 引数のurlにhttpのGETメソッドによるリクエストを送信
  • clojure.browser.dom/get-value関数: 引数に指定された要素の値(今回の場合、フォームquery-wordに入力された文字列)を取得
生成されたJavaScript
cljstest.core.dictionary_request = function dictionary_request(e) {
return goog.net.XhrIo.send([cljs.core.str("http://localhost:3000/dictionary/"), cljs.core.str(clojure.browser.dom.get_value.call(null, clojure.browser.dom.get_element.call(null, "\ufdd0'query-word")))].join(""), cljstest.core.dictionary_callback)
};
view raw main.js hosted with ❤ by GitHub


dictionary-callback関数

(defn- dictionary-callback
[e]
(let [entries (.-entries (.getResponseJson e/target))]
(dom/append (dom/get-element :result) (create-list entries))))
view raw core.cljs hosted with ❤ by GitHub

httpレスポンスからJSONオブジェクトを取得し、create-list関数を用いて結果を変換し、変換された結果をdiv要素の子要素として追加します。
  • .-プロパティ名 特殊形式: JavaScriptオブジェクトのプロパティにアクセス(今回の場合entriesプロパティ)
  • goog.net.XhrIo/getResponseJsonメソッド: 取得されたレスポンス内のデータをJSONオブジェクトとして取得
  • clojure.browser.dom/append関数: 第1引数に指定された要素に、第2引数に指定された要素を子要素として追加
生成されたJavaScript
cljstest.core.dictionary_callback = function dictionary_callback(e) {
var entries__212463 = e.target.getResponseJson().entries;
return clojure.browser.dom.append.call(null, clojure.browser.dom.get_element.call(null, "\ufdd0'result"), cljstest.core.create_list.call(null, entries__212463))
};
view raw main.js hosted with ❤ by GitHub

create-list関数

(defn- create-list
[entries]
(dom/html->dom
(str "<ul>"
(apply str (for [entry entries] (str "<li>" (.-text entry) "</li>")))
"</ul>")))
view raw core.cljs hosted with ❤ by GitHub

JSONオブジェクトを引数に取り、textプロパティから値を取得し、<ul><li>textプロパティの値</li> ... </ul>形式のdom要素を作成します。

  • clojure.browser.dom/html->dom関数: html形式の文字列からDOMツリーを生成


生成されたJavaScript
cljstest.core.create_list = function create_list(entries) {
return clojure.browser.dom.html__GT_dom.call(null, [cljs.core.str("<ul>"), cljs.core.str(cljs.core.apply.call(null, cljs.core.str, function() {
var iter__2482__auto____212461 = function iter__212455(s__212456) {
return new cljs.core.LazySeq(null, false, function() {
var s__212456__212459 = s__212456;
while(true) {
if(cljs.core.seq.call(null, s__212456__212459)) {
var entry__212460 = cljs.core.first.call(null, s__212456__212459);
return cljs.core.cons.call(null, [cljs.core.str("<li>"), cljs.core.str(entry__212460.text), cljs.core.str("</li>")].join(""), iter__212455.call(null, cljs.core.rest.call(null, s__212456__212459)))
}else {
return null
}
break
}
}, null)
};
return iter__2482__auto____212461.call(null, entries)
}())), cljs.core.str("</ul>")].join(""))
};
view raw core.cljs hosted with ❤ by GitHub

意外な落とし穴

実装を終えて早速クライアントからサーバへの疎通を確認しようと、ブラウザからアクセスしたところ、Safariでは問題なくレスポンスが処理されるが、Firefoxではレスポンスのボディ部が0バイト(サーバ側で設定したはずの値が、ボディ部に含まれていない)様な挙動を見せ、想定した通りの処理をすることができませんでした。
ブラウザ毎に異なる挙動はClosureライブラリで吸収されているはず、と思いながらも、ここから問題解決まで思ったよりも時間が掛かってしまいました。。。(次回に続く)


今回の作業は以上。最後までお読み頂きありがとうございました。
たなけん (作業時間90分)

0 件のコメント:

コメントを投稿