こんにちは、@nazomikanです
(この記事はNode.js Advent Calendar 2013の10日めの記事です)
nodeでテンプレートエンジンといえばjade
その一方で公式ドキュメントで書かれていることだけではだいたい痒い所に手が届かないのでissueから拾い集めたノウハウとか将来的な話とかを書きます
属性の存在が条件によって分かれるケースの書き方
url
が存在するときdata-url属性をつける
//truthy: <p data-url="xxx"> //falsy: <p>
in jade:
p(data-url=(url ? url : false))
属性の値が条件によって分かれるケースの書き方
bool
がtruthyの時はclass="is-show"
を、そうでない時はclass="is-hide"
をつける
//truthy: <p class="is-show"> //falsy: <p class="is-hide">
in jade:
p(data-url="is-#{bool ? 'show' : 'hide'}")
子要素にテキストとDOM要素が並列に存在しているケースの書き方
<p> この商品は<span class="price">10,000</span>円です </p>
in jade:
p | この商品は span.price 10,000 | 円です
エスケープしたい変数とエスケープしたくない文字列が混在してる時の書き方
<p> この部屋の広さは#{space}m²です </p>
in jade:
p | この部屋の広さは#{space}m != ² | です
if/elseで出し分けた要素の子要素を記述したい時の書き方
// bool is true <p> <a href="#"> <img src="./hoge.jpg"> </a> </p> // bool is false <p> <span> <img src="./hoge.jpg"> </span> </p>
in jade(うまくいかないケース1):
p if bool a(href="#") else span img(src="./hoge.jpg")
-> elseの時だけしかでない
in jade(うまくいかないケース2):
p if bool a(href="#") else span img(src="./hoge.jpg")
-> pの直接の子要素になる
in jade(うまくいかないケース3):
p if bool a(href="#") else span img(src="./hoge.jpg")
-> elseの時にspanの兄弟要素として出る
end
とかないのでこの辺はmixin
を使うしかない
in jade(正攻法):
mixin wrap(bool) if bool a(href="#") else span p +wrap(bool) img(src="./hoge.jpg")
こんな荒業もある
p #{bool ? 'a': 'span'} img
でもこれは属性かくのうまくできないので辛い
また、|
で続く文字列がプレーンなテキストになるのを利用して単純に開始タグと閉じタグで挟むという方法もあるにはある
if bool | <a href="#"> else | <span> #inner-div if bool | </a> else | </span>
個人的にはラップしたいタグの階層が同じ場合は#{bool ?'a(href="#")' : 'span'}
で済ませて、階層がごっそり違う場合はmixinを使うようにしてる
動的にインクルードしたい時の書き方
inclide ./#{foo}.jade
みたいなことをしたいと誰しもが夢を見るが現在のところカスタマイズ無しでは無理
caseを用いて泥臭くやるしかない
in jade:
case foo when 'a' include a.jade when 'b' include b.jade //- ...
checked=checkedとかdisabled=disabledとかを条件に応じて出し分けたい時の書き方
// bool is true <p checked="checked"> // bool is false <p>
in jade:
p(checked=bool)
data-*にjsonを流し込みたい
<p data-person={"name":"nazomikan"}
in jade:
- var person = JSON.stringify({name: "nazomikan"}) p(data-person="#{person}")
inlineのブロックを使いたい時
インラインのブロックを使いたいこともあるでしょう
タグの宣言後に:
を使えばネストしたタグがかけるのでそれを利用してblockも書きましょう
parent.jade
h1: block article_title
child.jade
include parent block artiicle_title | awesome article
親ブロックの中身を継承したい時の書き方
parent.jade
block contents p parent
child.jade
include parent block contents p child
とあった時、評価結果は以下のようになる
<p>child</p>
これを
<p>parent</p> <p>child</p>
といった感じで親のブロックの中身を継承したいときはblock append xxx
を利用する
parent.jade
block contents p parent
child.jade
include parent block append contents p child
とすると
<p>parent</p> </p>child</p>
が得られる
また、前に挿入したいときはblock prepend xxx
で実現できる
実はyield
が使える
langage referenceには記載されてなかったけど平然とyield
キーワードをサポートしている
includeと組み合わせることで使える
main.jade
div include partial li main side
partial.jade
ul yeild li partial side1 li partial side2
とすることで以下のような結果を得ることができる
<div> <ul> <li>main side</li> <li>partial side1</li> <li>partial side2</li> </ul> </div>
タグの圧縮を解除したい場合
基本的にjadeではデフォルトでタグが下記のように圧縮されるようになっている
<!DOCTYPE html><html><head><title>Express</title><link rel="stylesheet" href="/stylesheets/style.css"></head><body><p>Hello World</p></body></html>
これをインデントのついた状態に表示したいときは以下のようにする
生のjade
var html = jade.render('jade string', { pretty: true });
in Express(v3.x)
app.locals.pretty = true
in Express(v2.x)
app.configure(function(){ app.set('views', dirname + '/views'); app.set('view engine', 'jade'); app.set('view options', { pretty: true }); // <- here app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(app.router); app.use(express.static(dirname + '/public')); });
パフォーマンスを上げる
jadeは現時点のデフォルト設定で十分に高速なテンプレートエンジンですが設定と実装次第ではさらに高速化させることができます
namespaceを導入する
jadeは通常以下の手順でコンパイルされます
var fn = jade.compile("p #{title}", {}); fn({title: 'Title'});
template文字列の中でtitle
という変数でアクセスできてますが、オブジェクト渡ししてるのに、なぜxxx.title
という形じゃないのか不思議ですね
それは内部的にwith
構文を利用してオブジェクトプロパティの変数展開をおこなっているからです
ある程度jsに精通している方ならわかると思いますがwith
は構文内で与えられたオブジェクトのプロパティを変数に展開できるという機能があり、それまでの処理内容によって展開される変数に変化がおこるという動的スコープの性質をもっています
これは実行前最適化に対して非常に厄介な問題でwith
構文が処理中に含まれているとその部分に対して最適化が行えなくなります
この問題を解決するためのオプションが self: true
です
このオプションを設定するとテンプレート内でself.title
という形で変数にアクセスできるようになります
常にself
という名前空間を通して変数にアクセスすることによってwith
構文を回避できるというものです
これを踏まえて先ほどの処理を書き換えると以下のようになります
var fn = jade.compile("p #{self.title}", {self: true}); fn({title: 'Title'});
結構中の人的には絶賛されてる機能みたいです
tj:
I like self muuuuch
danielbeardsley:
This is a crazy performance increase: 490ms -> 27ms
Running the benchmarks shows jade-self takes 27ms, where jade takes 490ms.
The fastest non-jade templating engine was ejs and it ran the test in 89ms. Go Jade!
このベンチのことだと思います
かなり高速化されてますね
ejsと比べても数倍早いみたいですね
compileDebugを本番時に切る
jadeにはデバッグ用にcompileDebug
というオプションがあります
これを切ると単純にデバッグ用にとってた情報をとらなくなるので早くなります
といっても最低限の最低限の情報は出るので安心ください
エラーログを見比べてみましょう
適当なファイルで参照エラーを出してみたときのエラーです
わざと間違えたコード
- var title = self.title h1= title p Welcome to #{sel.title} //- ここが間違い sel -> self
compileDebug: true
500 ReferenceError: /home/nazomikan/node_workspace/jade_test/views/index.jade:10<br/> 8| - var title = self.title<br/> 9| h1= title<br/> > 10| p Welcome to #{sel.title}<br/> 11| <br/> 12| <br/><br/>sel is not defined at ....(以下コールスタック)
compileDebug: false
500 ReferenceError: sel is not defined at ....(以下コールスタック)
compileDebug: true
の時は中のテンプレートも文字列まで表示してくれてるようになってます
開発時はtrue
, 本番時にはfalse
にして切ってしまうのが推奨されています
将来のことを考える
jadeはこんなにも普及してる一方でまだメジャーバージョンではないという反面もあります。(2013/11時点でv0.35.0)
まだ用意されているsyntaxの中で廃止や追加がかなりの数、検討されています。
以降の項目を確認しましょう
!!!
はv1.0.0で消滅する
html2jadeとかで
<!DOCTYPE html> <html> </html>
と打ち込むと
!!! 5
と変換されますがこのsyntaxは廃止されますので注意が必要です
書きやすい反面、jade初学者には大変読みづらいという意見がでて廃止に至りました 参考
もともと!!!
はdoctype
のエイリアスなのでこう書くのが将来的にも生き残る書き方になります
doctype 5
代入構文(-
無し)が削除される
今まで、
(1)
- var a = 1 - var b = 2 a = b span #{a}
みたいな感じで書くと
<span>2</span>
って感じに評価されていたのですがこのa = b
という代入演算子が使えなくなります
下記のように-
を前につけてあげるとこれからも使えます
(2)
- var a = 1 - var b = 2 - a = b span #{a}
これはjadeからjsにコンパイルされたときの状態に問題があったため削除されることになりました
-
ありであれば式はそのまま評価されます
(1)を評価すると以下のようにjsに変換されます
function anonymous(locals) { var buf = []; var a = 1; var b = 2; var a = b; { buf.push("<span>" + jade.escape((jade.interp = a) == null ? "" : jade.interp) + "</span>"); } return buf.join(""); }
var a = b
となって二重定義されていることに注目してください
(2)を評価するとvar a = b
の部分がa = b
と評価されます
これは以下のようにスコープのネストが発生した場合に問題を起こします
mixin parent() div block mixin check(value1, value2) +parent unless value2 value2 = value1 | value1 = #{value1}, value2 = #{value2} +check(1, 2)
これは value1 = 1, value2 = 2
を期待した処理ですが実際にはvalue1 = 1, value2 = 1
と解釈されます
なぜならこの処理は以下のようにjsに評価されるからです
function anonymous(locals) { var buf = []; var parent_mixin = function() { var block = this.block, attributes = this.attributes || {}, escaped = this.escaped || {}; buf.push("<div>"); block && block(); buf.push("</div>"); }; var check_mixin = function(value1, value2) { var block = this.block, attributes = this.attributes || {}, escaped = this.escaped || {}; parent_mixin.call({ block: function() { if (!value2) { var value2 = value1; // ここに注目 } buf.push("value1 = " + jade.escape((jade.interp = value1) == null ? "" : jade.interp) + ", value2 = " + jade.escape((jade.interp = value2) == null ? "" : jade.interp) + " "); } }); }; check_mixin(1, 2); return buf.join(""); }
check_mixin
内のparent_mixin
呼び出し時のblock
のスコープの中でvar value2 = value1
と評価された式があります
このblock
のスコープ内ではvalue2
はスコープチェインを通してcheck_mixin
の引数のvalue2
を参照したいのにここでvar
宣言されたことによって、このスコープ内でhoistingが発生し、一つ上のif (!value2) {
の式のvalue2
がundefined
になります
そのため、評価されない予定だったvar value2 = value1
が評価されてしまう結果になります
ちなみに -
をつけて以下のように書くと
mixin parent() div block mixin check(value1, value2) +parent unless value2 - value2 = value1 | value1 = #{value1}, value2 = #{value2} +check(1, 2)
jsに以下のように変換されます
function anonymous(locals) { var buf = []; var parent_mixin = function() { var block = this.block, attributes = this.attributes || {}, escaped = this.escaped || {}; buf.push("<div>"); block && block(); buf.push("</div>"); }; var check_mixin = function(value1, value2) { var block = this.block, attributes = this.attributes || {}, escaped = this.escaped || {}; parent_mixin.call({ block: function() { if (!value2) { value2 = value1; //ここに注目 } buf.push("value1 = " + jade.escape((jade.interp = value1) == null ? "" : jade.interp) + ", value2 = " + jade.escape((jade.interp = value2) == null ? "" : jade.interp) + " "); } }); }; check_mixin(1, 2); return buf.join(""); }
この場合はvalue2 = value1;
にvar
がついていないためhoistingが発生せず意図したvalue1 = 1, value2 = 2
という値が得られます
こういった複雑な問題から-
無しで記述される代入構文が削除されます
クライアントサイドでのjadeの利用においてIE8以下のサポートがなくなる
クライアントサイドでjadeを利用してますっていう声はあまり聞きませんが、サポートがなくなるそうです
古いブラウザに対する狂気じみた対応のせいで全ブラウザでのパフォーマンスが劣化するのにはもうやめだ
モダンブラウザこそが真にサポートされるべきなんだ!
みたいな感じのようです
株式会社ネクストでは、一緒に世の中をもっと便利に楽しくしたい仲間を募集中です。 主体的に楽しみながら仕事できる方からのエントリーお待ちしています! http://recruit.next-group.jp/