この文章は`Clojure Advent Calendar 2013`_の20日目分です。
ClojureScriptの登場によって、私たちはWebアプリケーションのサーバサイドとクライアントサイドをClojureファミリーの言語で実装することができるようになった……ということは改めて触れるまでもなくご存知かと思います。しかし、私たちがWebを利用するときにはWebブラウザというコンポーネントを利用します。Clojurianがクライアントサイドで掌握したのはその一部分、ブラウザへ入力するデータです。Web系Clojurianの次のターゲットはブラウザ、となるのは自然な流れでしょう。[1]
実際、Webブラウザに限らずユーザエージェントまで範囲を広げれば、Clojureは既に活躍していると言えます。[2]しかし、普段Webを閲覧するときに多くの人々が使うのは様々な機能を満載した重量級のブラウザ、しかしそのようなものを実装するのは骨が折れる……といったところで、本題に入りましょう。ブラウザを新しく実装するよりも手軽にブラウザの機能を追加・変更・制限する手段があります。それがアドオンです。
アドオンの仕組みを実装したブラウザはいくつかありますが、ここではFirefoxのブラウザを対象とします。ChromeのアドオンをClojureScriptで実装したという話は検索するといくつか見つかるのでそちらを参照してください。
以前Mozillaのアプリケーション向けのアドオンを作ったことがある方であれば、install.rdfというファイル名に見覚えがあるでしょう。伝統的なアドオンはメタデータを格納したRDF形式のファイルを用意したり、XULというXMLベースの記述言語でUIを記述したりしていました。
しかし、最近はBuilderとAdd-on SDKによって、HTML・CSS・JavaScriptを使ってアドオンを書くこともできるようになりました。Add-on SDKのAPIはよく整理されていてわかりやすいですし、今回はこれを使うことにします。
まず、先に示したリンク先のページからAdd-on SDKをインストールし、適当な場所に展開してPATHを通し、アドオンのひな形をcfx initで作成します。
さらにLeiningenとlein-cljsbuildを使うためにproject.cljを用意します。例えば、このような感じです。
(defproject addon-in-cljs "0.1.0-SNAPSHOT"
:source-paths ["src/cljs"]
:dependencies [[org.clojure/clojurescript "0.0-2120"]]
:plugins [[lein-cljsbuild "1.0.1"]]
:cljsbuild {
:builds [{
:source-paths ["src/cljs"]
:compiler {
:output-to "lib/main.js"
:optimizations :whitespace
:pretty-print true}}]})
ソースコードを格納するディレクトリを掘ったところでさて何を書こうかと悩みましたが、今回はAdd-on SDKがどのようなものか知るためにチュートリアルのコードを移植することにしました。
チュートリアルの最初はツールバーにボタンを追加するものです。JavaScriptのコードを読んで、ClojureScriptで書きなおしてみます。
(ns addon-in-cljs.core)
(def widgets (js/require "sdk/widget"))
(def tabs (js/require "sdk/tabs"))
(def button (js-obj))
(set! (.-id button) "mozilla-link")
(set! (.-label button) "Mozilla website")
(set! (.-contentURL button) "http://www.mozilla.org/favicon.ico")
(set! (.-onClick button) (fn [] (.open tabs "http://www.mozilla.org/")))
(def widget (.Widget widgets button))
不恰好ですが、とりあえず動かすには十分です。lein cljsbuild once; cfx runでFirefoxが起動し、アドオンがテストできるでしょう。
ここで問題が発生します。ModuleNotFoundError: unable to satisfy: require(cljs.core) などといったエラーが発生し、ブラウザの起動に失敗します。
これはClojureScriptが利用するClosure LibraryのrequireとAdd-on SDKのrequireが衝突するためです。これを回避するために、project.cljの:optimizations :whitespaceを:optimizations :advancedに書き換えます。これで内部の識別子は短く置き換えられ、requireではなくなります。また、不要なコードが削除されて軽くなります。
そもそも、ClojureScriptはClosure Compilerの最適化に依存することで複雑な最適化パスを実装しないことを選択しました。そのため、ClojureScriptが出力するコードはそのままだとかなり冗長で無駄の多いものになっています。最適化レベルを上げると入力と出力の対応がわかりにくいので最初は最適化レベルを下げていましたが、必要がなければできるだけ上げておいた方が良いでしょう。
しかし、まだ問題は残っています。今度はA given cfx option has an inappropriate value: ZIP does not support timestamps before 1980というエラーが発生しました。
どうやらlein-cljsbuildは出力に非常に古いタイムスタンプを設定するようです。touch lib/main.jsしておきましょう。
まだ道のりは続いています。タイムスタンプを更新するとようやくアドオンテスト用にFirefoxのウインドウが開きますが、ボタンは表示されません。ターミナルの出力を読むと、Message: TypeError: a.a is not a functionとあります。どうやらClosure Compilerが置き換えてはいけない関数名まで置き換えたようです。
lib/main.jsは次のようになっています。
;(function(){
var a = require("sdk/widget"), b = require("sdk/tabs");
a.a({id:"mozilla-link", label:"Mozilla website", b:"http://www.mozilla.org/favicon.ico", c:function() {
return b.open("http://www.mozilla.org/");
}});
})();
最適化のおかげで、識別子以外は元のJavaScriptコードとほとんど同じコードが出力されています。見比べながら考えると、a.aはどうやらwidgets.Widgetにあたる部分のようです。また、その引数となっているオブジェクトのプロパティ名も2つほど置き換えられています。
この意図しない最適化を回避するようにClojureScriptのコードを書き換えるとこのようになりました。
(ns addon-in-cljs.core)
(def widgets (js/require "sdk/widget"))
(def tabs (js/require "sdk/tabs"))
(def button
(doto (js-obj)
(aset "id" "mozilla-link")
(aset "label" "Mozilla website")
(aset "contentURL" "http://www.mozilla.org/favicon.ico")
(aset "onClick" (fn [] (.open tabs "http://www.mozilla.org/")))))
(def widget ((aget widgets "Widget") button))
プロパティ名を文字列にして、asetやagetでアクセスしています。もっと良い方法があるとは思いますが、ClojureScriptの経験が浅いために思い至りませんでした。
出力はこのようになりました。
;(function(){
var a = require("sdk/widget"), b = require("sdk/tabs");
a.Widget.call(null, {id:"mozilla-link", label:"Mozilla website", contentURL:"http://www.mozilla.org/favicon.ico", onClick:function() {
return b.open("http://www.mozilla.org/");
}});
})();
若干無駄はありますが、正しく動くであろうコードが無事に出力されました。タイムスタンプを更新してからcfx runしてみると、確かにうまくいきました。
ClojureScriptで非常に単純な形のFirefoxのアドオンを作ることが可能だということがわかりました。しかし、その結果はあまり芳しくありません。つまらない落とし穴があり、その回避のために少し野暮ったい記述を導入したり、余計なステップを挟んだりする必要があります。
当然のことながら、私は{}より()の方が好きだからJavaScriptではなくClojureScriptで書こうとしているわけではありません。ClojureScriptで書くためにその利点と釣り合わないほどのコストを払わねばならないのであれば、たとえアドオンをClojureScriptで書くことが理屈の上では可能だとわかったとしてもこの試みは成功したとは言えません。
また、今回の範囲には含まれていませんがAdd-on SDKではlib/main.jsから別のJavaScriptファイルを読み込ませることが必要な場面があり、両方のJavaScriptファイルをClojureScriptで書いた上で両者を組み合わせるには識別子のリネームについて更なる工夫が必要となります。私はまだこの問題を解決できていません。[3]
すべての困難を乗り越えてアドオンを開発できたとして、Closure Compilerでリネームされた後のJavaScriptコードがhttp://addons.mozilla.org/のエディタによる事前検査を通過するかどうかという懸念もありますが、まずは実用的なアドオンの開発が可能であることを実証して初めて「ClojureScriptでFirefoxアドオンを作った」と胸を張って言えるようになるでしょう。
今回はあまり良い結果を得られたとは言えません。私がClojureScriptに習熟していなかったことが大きな原因の一つです。また、落とし穴をカバーするようなLeiningenのプラグインを用意したり、Add-on SDKのラッパーが存在すれば改善できる点もあったと思います。
アドベントカレンダーを読むと、シリアルポートを使ったデバイスの制御やiOSへの移植など、Clojureの適用範囲を広げるような投稿がいくつか見受けられます。2014年のClojureコミュニティがClojureで何を実現するのか楽しみにしつつ、もう少しAdd-on SDKと格闘しようと思います。
[1] | そういうことにしておきましょう。 |
[2] | このアドベントカレンダーの10日目、実用的なプログラムの話をするを参照。 |
[3] | この問題を解決しようと試行錯誤していたら、いつの間にか日付が変わっていました……。 |