LIFULL Creators Blog

LIFULL Creators Blogとは、株式会社LIFULLの社員が記事を共有するブログです。自分の役立つ経験や知識を広めることで世界をもっとFULLにしていきます。

jadeを知る

@nazomikan こんにちは、@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&sup2です
</p>

in jade:

p
  | この部屋の広さは#{space}m
  != &sup2
  | です

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={&quot;name&quot;:&quot;nazomikan&quot;}

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) {の式のvalue2undefinedになります

そのため、評価されない予定だった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/