UglifyJSのパーサを試してみる

前回の記事の続き。
asyncblockのソースを調べたところ、asyncblockのsource transformationは、内部でUglifyJSを使っていた。
UglifyJSは普通はソースをminifyする用途で使われるが、その一部としてjsの文をparseしており(当然か)、その部分だけ抜き出して使うこともできる。
ということで今回は、var foo = 12 + 34 * 56; という単純な文をパースしてみて、どのようになるか試してみる。

準備

npm install uglify-js

スクリプト

var parse = require('uglify-js').parse;
var code = 'var foo = 12 + 34 * 56;'
var result = parse(code);
// ネストが深かったので、今回はresult.bodyだけを出力。(配列だったけど中身1つだけだったので、body[0]を出力した)
console.log(JSON.stringify(result.body[0]));

結果(整形後)

結果を整形したところ、ものすごい長かったです。
何行目の何文字目等、直接文法に関係しないところを全部とっぱらった結果が以下

{
    // var
    "start": {
        "type": "keyword", 
        "value": "var"
    },

    // ;
    "end": {
        "type": "punc", 
        "value": ";"
    },

    // startが"var"の時は、その中身は"definitions"になるということだろう。
    // 代入文
    "definitions": [
        // definitionsの中身はname(変数名)とvalue(代入する値)

        // foo: 12 * 34 + 56
        {
            "start": {
                "type": "name", 
                "value": "foo"
            }, 
            "end": {
                "type": "num", 
                "value": 56
            }, 
            // 代入される変数名 foo
            "name": {
                "start": {
                    "type": "name", 
                    "value": "foo"
                }
                "end": {
                    "type": "name", 
                    "value": "foo"
                }, 
                "name": "foo", 
            },
            // 代入する値。値といってもまだ子がついてる部分木
            // 12 * 34 + 56
            "value": {
                "start": {
                    "type": "num", 
                    "value": 12
                }
                "end": {
                    "type": "num", 
                    "value": 56
                },
                // + 
                "operator": "+", 
                // +演算子の左がわ。12
                "left": {
                    // 最も小さい単位のトークンは、start・end・(value / name など?)が全て同じように定義される模様
                    "start": {
                        "type": "num", 
                        "value": 12
                    }, 
                    "end": {
                        "type": "num", 
                        "value": 12
                    }, 
                    "value": 12
                }, 
                
                // +演算子の右側。34 * 56
                "right": {
                    // ここも最も小さいトークン。それ以降はなし
                    "start": {
                        "type": "num", 
                        "value": 34
                    },
                    "end": {
                        "type": "num", 
                        "value": 56
                    }, 
                    "operator": "*", 
                    // *演算子の左側。34
                    "left": {
                        "start": {
                            "type": "num", 
                            "value": 34
                        }, 
                        "end": {
                            "type": "num", 
                            "value": 34
                        }, 
                        "value": 34
                    }, 
                    // *演算子の右側。56
                    "right": {
                        "end": {
                            "type": "num", 
                            "value": 56
                        }, 
                        "start": {
                            "type": "num", 
                            "value": 56
                        }, 
                        "value": 56
                    }, 
                }, 
            }
        }
    ], 
}

まとめ

構文解析木内のどのノードにも、start, end, 中身を表すキーがある。(葉ノードの時とかはstart, end, value全て同じようなデータになってるみたい。)
中身を表すキーは、

  • var宣言文… definitions
  • 変数への代入… name(変数名), value(代入する中身。今回は数式)
  • 数式… oprator(+とか*), left(演算子の左)、right(演算子の右)
  • 数値などの値… value

次はこれをどう操作しているのかを見ていこうと思います。…が、asyncblockはuglify-jsを自分でカスタマイズしたのを使っているようなので、その辺ちゃんと解読できるか心配。

記事を読んだ: JavaScript Source Transformation: Non-Destructive vs Regenerative

Node.jsの非同期処理を同期っぽく書けるようにするasyncblockというモジュールのreadmeを読んでいて、
その中に Source Transformationという言葉が出てきたので、なんじゃこれとググって次の記事を参照した。

http://ariya.ofilabs.com/2013/06/javascript-source-transformation-non-destructive-vs-regenerative.html

英語力に乏しいため全訳とかは無理だけれど、なんとか意味はつかめたと思うので残しておく。

source transformationとは

JavaScript構文解析を行った後、その構文解析木に対して何か操作を行うことをSource Transformationと言う。(この時点で大分ギーク、、)
Source Transformationは、構文解析木へ操作する方法によって、Non-DestructiveとRegenerativeの2種類に分けられる。

Non-Destrucrive

例えば、ソースの" "でくくられている文字列をすべて' 'に直したいとき、変更したいのはそれだけで、それ以外のインデントやコメントや変数名などは全てそのままであってほしい。そのときは、構文解析木上の各ノードの位置+トークンの情報が必要になる??(原文ではwe use the location information of the syntax nodes and/or tokens to find out what needs to be tweaked と書いてあるんだけど、僕の英語力と(ソースの)構文解析に関する知識がないため、どこからどこまでandで比較されてて to find out..がどこにかかってるのか分からないorz)
※多分ですが、構文解析後のトークンの字句まで残して置く必要があるという意味だと思います。

Regenerative

元のソースの字句がどうだったかに興味がなければ、変数名やインデント等の情報は不要となるため、その辺気にする必要がない。その場合単にregenerateすれば良いから簡単とある。
(regenerateは構文解析木からソースを再生成することなんだと思うけど、Non-Destructiveでもソースの生成自体は必要だと思うのでその辺の違いは良く分からないので勉強してからまた。。)
例えば、ソースをminifyしたい時は、minify後は元のソースの構造だけ分かっていれば良く、変数名やインデント・スペース等は全て失われてしまってよい。
また、変換後にソースを出力する必要などなく、そのまま実行すればよいのであれば、なおさらである。
その例として、テストに使うIstanbulモジュールが挙げられている。

自分はIstanbulは使ったことがなかったが、shouldとexpectは使った事がある。
たとえばxが1であることをテストしたいとき、expectの方は

expect(x).equal(1)

と書く。これはexpect関数でxをラップしてExpectのオブジェクトを作っているわけだから、そのオブジェクトにequalメソッドが追加されていても何の不思議はない。
一方shouldの方は

x.should.equal(1)

と書く。これは普通の方法では達成できないはずだ。なぜならxはただのテストしたい値なのであって、shouldなんていうメンバを持っているわけはないからだ。したがって、source transformationの技術が必要である。この場合テストを実行したいだけで、変換後のソースを奇麗に保ったりする必要はないため、Regenerativeな方法でよいということになる。

2/20夜追記: 上の記述は間違えてました。Objectのプロトタイプにshouldを突っ込んでるという情報を頂きました。確かにそれで達成できますね…。全く気付きませんでした。
ソースを確認したところ確かにそうなっていました。lib/shoud.jsの107行目です。
なおnullやundefinedはObjectを継承しないため、xがnullのときはテストのエラーでなくjsの実行時エラーになります。

※一応expectの名誉のために追記すると、上の例では

expect(x).to.equal(1)

と、英語っぽく書くことができる。
Expectオブジェクトのtoは自分自身への参照ということだろうし、特に技術的な困難さはないと思う。

おわりに

ざっくりは理解したつもりだけど細かいところが分かっていないので、これから勉強していきます。
とりあえず、挙動を知っているshouldのソースから読んでいこうかと思います。興味もったきっかけのasyncblockを読むに変更。

なるべく怠惰にWebサービスを作りたい件[サーバ編1]

サーバ編から行きます。npm, Node.jsはインストール済とします。
とりあえずCoffeeScriptをインストールしましょう、

npm install -g coffee-script

フレームワーク

最初に、フレームワークExpress.ioを選定しました。

Node.jsのフレームワークといえば第一にExpressが来るのに加えて、リアルタイムWebをやりたいのでSocket.IOが必須です。
その上で面倒になる点が、セッション情報の共有です。Socket.IO自体はExpressと全く関係ないので、Expressのセッション情報をソケットにも紐づける必要があります。
これについてはJxckさんの解説記事が詳しく、これを正しくまねれば良いのですが、ちょっと大変です。

ですが、ここでExpress.ioを使えばそれだけでまるっと解決します。
Express.ioはExpressのラッパーですので、特に入れるのに苦労することはなく、
セッションの問題が解決します。
使い方はというと、Socket.IOのonメソッドがいい感じにラップされていて、

元はこんな感じ

socket.on 'fooEvent', (bar) ->
  console.log bar

だったのがこんな感じになる

app.io.route 'fooEvent', (req) ->
  # 何もしなくても req.session にセッションが入ってる(http通信のsessionと共通)
  # 送られてきたデータはreq.dataに入ってる
  bar = req.data
  console.log bar

はい、特に努力せずに大変怠惰になりました。ありがとうございます。
Express.ioのコードもCoffeeScriptで書かれてますが、ちょうど良いですね。
偉そうに書いといて、まだクライアントから呼んでみてすらいないので、本当にreq.sessionに入ってるかどうかは確認していませんが、
githubを見る限りスターが550ついてますから、信頼できると思います(?

1点付け加えるとすれば、expressのレスポンス返却はres.send(200, 'おk') や res.json(500, { msg: 'err!' })等色々用意されているのに対し、
socket.ioでクライアントに送るメソッドはemitメソッド1つしかありません(express.ioも基本ラップしてるだけですので特にありません)。
実際に使っていると次のように、エラーをemitするコードを書くことが増えると思うので

app.io.route 'fooEvent', (req) ->
  # 'error'イベントは予約されてるので使えない。ここでは'err'にしておく 
  if not req.data? then req.io.emit 'err', new Error('えらーです')
  anAsyncMethod req.data, (err) ->
    if err? then req.io.emit 'err', err
    # 以下略...

シンタックスシュガー(って言って良いのかな・・?)を用意すると少しだけスッキリするかなと。

# req.io.emit 'err', data のシンタックスシュガーとして
req.io.error data

$.ajax('get', foo)と$.get(foo)が同じように使える、みたいなイメージです。

しかし、express.io本体に手を入れなければいけないので、もし他にも色々手をいれたければforkするなりラップしたフレームワークを自作するなりでしょうが、
そうでなければ正直このためだけにいじるのは面倒です。

ルーティング (コントローラー)

express.ioの利用により、

# httpのルーティング (expressと同じ)
app.post '/url', callback
# socketのイベントハンドラ
app.io.route 'eventName', callback

と書けるようになり、統一感が出てよい感じになりました。
ここの統一感というのが結構大事だと思っていて、
このメソッドajax通信のつもりだったけどやっぱsocketにしようかな・・・みたいなときに、すんなりと変更できますよね。socket通信とajax通信を、同列に近く扱いたいのです。

しかし、ルーティングを全部app.coffeeに書くわけにはいかないので、うまい具合にばらけさせつつ、メンテが楽ですむ方法を探求したいところです。

expressの(http)ルーティングは、デフォルトだと以下のような感じです

# ## app.coffee (プログラムの起点)
routes = require('./routes')  # routes/index.coffeeがrequireされる
# urlマッピングはここで全部行う
app.get '/', routes.index
app.get '/foo', routes.foo.hoge

# ## routes/index.coffee
# routes以下のファイルでは、urlに対応する関数のみを記述する
exports.index = (req, res) ->
  # 略
exports.foo = require('./foo')

# ## routes/foo.coffee
exports.hoge = (req, res) ->
  # 略

これだとちょっと分かりにくいのですが、簡単にまとめると、

  • routes/以下に、コントローラーメソッドを書く
  • urlとコントローラーメソッドのマッピングは、一カ所で行う

という感じです。ルーティングを設定ファイルに切り出してるのに近いと思います。

この方法の良いところは、

  • 1. routes/以下に自由にファイルを作ってコントローラーメソッドを定義できる
  • 2. URLから、どのコントローラーメソッドが叩かれるのかを調べるときに確実に探せる

が上げられると思いますが、一方で、欠点は

  • 3. コントローラーメソッドを追加するときに、別ファイルでルーティングを設定しなければいけない
  • 4. コントローラーメソッドから、それがなんと言うURLで叩かれるのかを調べる(2の逆)のが面倒

というのが挙げられます。

なので、3や4を解決するために、コントローラーメソッドにannotationをくっつけられたりしたら個人的には最高だと思うわけです。

# ## app.coffeeにはurlとのマッピングを書く必要なし
# ## 以下のように、routes配下の各ファイルのみで完結する

# ## routes/foo.coffee

# @method GET
# @uri '/foo'
exports.hoge = (req, res) ->
  # 略

Javaなら言語にアノテーションがありますから、出来そうですね。jsでも試す価値があると思います。
ただ、これをするには構文解析をしなければいけないわけで、
このような言語仕様外のことをやってしまうのはさすがに危険であると判断しました。
CoffeeScriptはjsにコンパイルして実行されるので、コンパイル後のjsの状態から構文解析する、とかはさすがに厳しいので・・・。

というわけで、それに近い方法を考えます。
新しくurlとそれに対応するコントローラメソッドを追加するとき、ルーティングの1ファイルに追記することで完結すればOKです(その上である程度書き方がきれいでラクなのが望ましい)。
自分からルーティングに登録してもらいに行けば良いのではないでしょうか。

ということで、全体を以下のようにしました。

# ## app.coffee
# routing設定のトリガーを引く
require('./routing.coffee').route(app)
# ## routing.coffee

# ルーティングを行う関数 を返す関数 (appを保持する必要があるのでクロージャに)
getRouteFunc = (app) ->
    (method, uri, func) ->
        console.log 'route!', arguments
        switch method
            # ソケットの場合
            when 'socket', 'SOCKET'
                app.io.route uri, func

            # 以下、httpの場合
            when 'get', 'GET'
                app.get uri, func
            when 'post', 'POST'
                app.post uri, func

            else
                throw new Error("サポートしていないメソッドです: #{method}")

# 各routesファイルを全部呼び出す(その際、上で定義したルーティング関数を渡す)
module.exports = (app) ->
    # ルーティング関数を(appを包んで)取得
    routeFunc = getRouteFunc(app)

    # 各ルーティングを行うファイルを呼び出す
    filenames = fs.readdirSync './routes'
    for filename in filenames
        extname = path.extname filename # 拡張子の手前まで
        basename = path.basename filename # 拡張子
        continue if extname isnt '.coffee' or basename is 'index'
        
        require('./routes/' + basename)(routeFunc)
    return

ここまでが基盤。
あとは、新しくコントローラを実装する際、
route関数にHTTPメソッド(または'socket')・URI(またはイベント名)・コントローラーメソッドをセットで渡すだけとなります。

# ## 各ルーティングファイル
# ## routes/foo.coffee
# 引数でルーティング関数をもらう
module.exports = (route) ->
    # getだとこんな感じ
    route 'GET', '/foo', (req, res) ->
        res.send '200', 'おkkkk'

    # socketだと
    route 'socket', 'fooEvent', (req) ->
        req.io.emit 'afterFoo', { msg: 'foooooooooo!' }

※コードの流れが追いにくいのはひとえに僕の非力さによるもので、もっと分かりやすい方法は絶対にあると思うのですが、、

さて、こうした結果どうなったか見てみます。元の状態と比較すると、
まず長所だった次の2点に関して、

  • 1. routes/以下に自由にファイルを作ってコントローラーメソッドを定義できる
  • 2. URLから、どのコントローラーメソッドが叩かれるのかを調べるときに確実に探せる

1は問題ない。2に関しては、URLの定義が各ファイルに散らばるので分かりにくいという意見もあるかと思います。
私個人的には、探す時は

find ./routes -type f |xargs grep "/foo"

とでもすればすぐ見つかるはずなので、大して気になりません。
一方、短所だった

  • 3. コントローラーメソッドを追加するときに、別ファイルでルーティングを設定しなければいけない
  • 4. コントローラーメソッドから、それがなんと言うURLで叩かれるのかを調べる(2の逆)のが面倒

に関しては完全に解決されており、総合として満足という感触を受けます。

まとめ

個人で作るレベルでそこまで大きくないけど、Socket.ioを使ったWebサービスを怠惰に作りたいという希望のもと、
サーバサイドはexpress.ioを選定し、セッションの共有で詰まる必要がなくなりました。
さらにルーティングを少し工夫することで、メンテをらくちんにしました。

怠惰というかはわかりませんが、書きたいときに気分よく書けるのが大事ですから、基盤部分の設計は丁寧にやりたい。あれこれ考えて頭を使うしね。
結局、30分の単純作業を1時間で自動化した例のように、余計に時間がかかってるのは間違いないけど、それはそれなのです!

サーバはまた工夫しどころがあったら書きます。
次回はクライアント編です。明らかにこっちのが難易度高い。
四苦八苦してますが、もうちょっと頑張れば良いとこいけそうです。1週間以内に続き書きたい。

なるべく怠惰にWebサービスを作りたい件[前編]

一人で趣味でWebサービスを作る時、Node.jsを選択すればサーバもクライアントもJavaScriptで書けるわけですが、
JavaScriptコンパイルできる言語はいくつかあり、
特に静的型付けを必要としなければ、CoffeeScriptは第一候補に入ってくるでしょう。
CoffeeScriptは言語仕様にpythonとかrubyの流れを取り入れており、個人的にはすっきり書けて好みです。
素のjsをある程度きちんと知っていれば、困ったらコンパイル後のjsのコードを読めばそう嵌ることもないです。

そこまで大きくないプロダクトを一人で作るとき、次のような事柄を重視したいと僕は考えています。

  • DRY (Don't Repeat Yourself)
  • CoC (Convention over Configuration, 設定より規約)
  • Laziness, 怠惰

1つめと2つめについては、僕は語れるほどの実力を持ち合わせておりませんので、3つめについて注釈を付け加えると、
僕のプログラマー観は怠惰であることを美徳であるとしていて、(アルバイトでお世話になった方の考えの影響が大きいのですが)
なるべくプログラムは短く書きたいし、不必要にタイプを打ちたくない。

ただし矛盾しているようですが、怠惰であるために努力するのはOKだと思っています。
要は、プログラム全体で書く行数を減らすために、基幹部分のソースを頑張って書く。

まあ、CoffeeScriptを使おうとしている時点でそうです。単にjsで書いた方が、正直いろいろめんどくさくないです。
そこをあえてCoffeeScriptを選択し、jsへのコンパイルだとか何だとかの自動化を頑張る。
今作っているものがどっちが早く作れるかだったら、jsで書いた方が早いでしょうが、それはそれです。

前どこかで見た話で、30分かかる単純作業があるとき、それの自動化に1時間かかるなら迷わず行うべきだ、というのがありましたが、この考えと似ています。
30分の単純作業からは何も生まないが、自動化に費やした1時間は確実にプログラマーとして血肉になっているという発想です。

さて、CoffeeScriptを利用してサーバ・クライアントのコードを両方書いているうちに、ある程度形になりそうなものもあるので、
熱いうちに文書化して、冷めた頃に見返してみたいと思います。 (以上前置き)

※偉そうなことを書いていますが中の人は新卒1年目のへっぽこですので、いろいろと指摘して頂けると嬉しいです。
なおNode.jsを選択する理由はjsだからでもイベントループモデルだからでもなく、
WebSocketを利用したリアルタイムWebがやりたいからです。

RequireJSで循環参照の回避

Backbone.jsの各ファイルをRequireJSでまとめて使ってるます。

今回生じた問題は、
「viewからrouterをRequireJSを使って読もうとしたとき、なぜか読み込まれない」現象

  • views/index.js
define(['router', 'backbone'], function(router, Backbone) {
  var IndexView = Backbone.View.extend({
    events: {
      'click .hoge': 'enterRoom'
    },
    enterRoom: function() {
      // ここでrouterよんでる。クリックされたら他のページに飛ぶ的な
      router.navigate('/room', {trigger: true});
    },
  });
  return IndexView;
});
  • router.js
define(['views/index', 'backbone'], function(IndexView, Backbone) {
  var MainRouter = Backbone.Router.extend({
    routes: {
      '': 'index',
      'room/:roomName': 'roomShow'
    },
    index: function() {
      // ここでIndexViewよんでる。
      var indexView = new IndexView();
      // 表示する処理。略
    }
    // roomShow 略
  });
  var router = new MainRouter();
  return router;
});

こんなかんじにした時、routerからIndexViewが読めなくなっている(ログをはくとundefined)現象がおきていた。

調べてみると、routerがIndexViewを参照し、IndexViewがrouterを参照しているため、循環参照が起きていて、
そのせいでうまくいかなくなっているらしい。

試してみた結果、次のようにすることで割と楽に回避できた。
(RequireJS本家のドキュメント通りだと、無駄がある気がして、次のようにしても問題なく動きました)

  1. routerは変えない。
  2. IndexViewの中で、routerを使うときに、'require'を利用する。
  • views/index (訂正版)

// routerを呼ぶかわりにrequireを呼ぶ

define(['require', 'backbone'], function(require, Backbone) {
  var IndexView = Backbone.View.extend({
    events: {
      'click .hoge': 'enterRoom'
    },
    enterRoom: function() {
      // routerを使う時に、requireする
      var router = require('router');
      router.navigate('/room', {trigger: true});
    },
  });
  return IndexView;
});

ここでポイントとなるのは、defineの中でrouterを呼ぶのではなく、
使う箇所でrequire('router')として呼んでいること。
requireを使うことで、IndexViewを最初から呼ぶのではなく、使う時に呼ぶようにしていることで、きちんと読み込まれるようにしている。

だから、requireを使うのは(defineのさらに1つ下階層の)関数の中でなければならない。requireが関数の中じゃないと、最初にdefine関数が実行されるときにrequireで読みにいって、やっぱりundefinedになってしまう。
(上の例では使う場所がenterRoom関数の中なので、問題無し。)

まとめると、RequireJSで循環参照になってしまうときは多分、

  • 主呼び出しと思われる方はそのままに、
  • 副呼び出しのと思われる方で、(関数内で)requireを使うことで動的に呼び出す

ことで回避できる。

なお本家ドキュメントでは両方からrequire使って、さらにdefineでも定義しているので、もしかしたら本当はこうしないといけない理由があるのかもしれない。

さらに今回の話で言えば、使ってるのがBackbone.jsですし、
Viewがクリックしたらイベントを発火して、それをrouterが検知してルーティングを行うのが正解かもしれない。

毎度ながら日本語がへたくそでまとまりがない。。とりあえず参考url
RequireJS API
backbone.js - Require.js module not seeing Backbone Router.js - Stack Overflow

.bash_profileでnvm useしてもなぜかnodeやnpmコマンドが打てない?

nvm use するとそのままnodeやnpmコマンドが使えるようになるのだが、~/.bash_profileに次のように書いても、

source ~/.nvm/nvm.sh
nvm use "0.10"
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin

なぜかnodeやnpmコマンドが効かない。もう一度

nvm use 0.10

すると効くようになる。

なんだろうと思ったら、export PATHの行が最後になってたのが悪かったっぽい。

export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
source ~/.nvm/nvm.sh
nvm use "0.10"

当たり前か。。一番上にして解決。

jade(テンプレートエンジン)をクライアントサイドで動かすの巻

2014/02/24 追記:
jade.compile(jadeText, {client: true})は使えなくなったようです。かわりにjade.compileClient(jadeText)を使えとのこと。
あと、下の文中のanonymousはtemplateに変わってました。

Node.jsのフレームワークExpressの標準テンプレートエンジンである、jade
https://github.com/visionmedia/jade
を、クライアントサイドで動かしたくなったので、色々調べながらなんとかゴリ押しでできるようにした。
公式読んだ限り普通に出来そうな事を書いてあるんだけど、
僕が英語弱すぎてかつ色々初心者すぎて分からなかったので、色々試してやってみました。

まずjadeは、jade->js->htmlという流れでコンパイルされる。
jadeをjsに変換することを、公式に倣い、プリコンパイルということにする。
プリコンパイル時にclient: trueオプションをつけると、次のような感じのjsの関数に変換される。

コンパイル元(jade)

p= hello

コンパイル後(js)

function anonymous (locals) {
  var buf = [];
  var locals_ = (locals || {}),hello = locals_.hello;jade.indent = [];
  buf.push("\n<p>" + (jade.escape(null == (jade.interp = hello) ? "" : jade.interp)) + "</p>");;return buf.join("");
}

例えばNodeで動かすなら、

var jade = require('jade');
var fn = jade.compile('p= hello', {client: true, pretty: true, compileDebug: false});
console.log(fn.toString());

とかすると上のような関数を文字列として書き出せる。

で、htmlを生成するには、この関数の第一引数localsに、テンプレートで埋めたいモノを含んでるオブジェクトを渡せばよいので、

var html = fn({hello: 'Hi'});
console.log(html);

とかすると、

<p>Hi</p>

というhtmlが生成されるという理屈。
プリコンパイルされた状態はただのjsなので、クライアントサイドでも普通に評価できる。

ということで、私が至った結論としては、

  1. 1箇所(例えばclient/views/)にjadeファイルを置く
  2. シェルで全jadeファイルを回し、
  3. jadeファイルを開き、その文字列をjade.compile()の引数に渡す(Nodeならfsモジュールを使う)
  4. できた関数を1ファイルに書き出していく。
  5. public/以下に置き、公開する。
  6. クライアント側で、その関数を呼び出す。

あとは、些細な点ですが、
上のようにプリコンパイルした関数をtoString()するとanonymousという名前の関数になってしまうので、この名前を変えてやる必要があります。
anonymousをjadeのファイル名に変えようと思ったのですが、それだとファイル名にjsで使えない文字が入っていた時困りますので、
書き出す1ファイルを

var pre = {
  'jadeのファイル名': function (locals) {...},
  'jadeのファイル名2': function (locals) {...},
};

というように連想配列にしました。これで、呼ぶときはpre.ファイル名(obj)でOK。