pugでできることはVueでもだいたいできる件について

GA technologiesの澤田です。先日友人に「Vueとpugで何か記事を書きたい」って話をしたら

友人「そういやpugのテンプレート構文で書ける事はだいたいVueの記法で書けるよ。」

との言葉を貰いました。

今までVueにおけるpugは単なる構文的に用いていて、pug独自のテンプレート構文をVue内で利用したことはありませんでした。
そこでpugにはどのような構文があって、そしてそれをVueで使う意味があるのか・ないのかを個人的に調べた結果をまとめてみました!

はじめに

前提知識

  • Vue.jsの開発経験
  • pugのふわっとした理解

Vue.js普段書いていて、さらにpugを 使っている / 使おうとしている フロントエンドエンジニア向けの記事になります。

pugの構文をVueの構文に置き換えた上で、どっちを使うべきか?ということをつらつらと考察していきます。

そもそもpugとは

pugは短い記法でHTMLを記述出来るJST(JavaScript Template)の一つです。とてもスッキリ記述出来るため、全体の構造を理解しやすい・構造の変更が容易、などのメリットがあります。

.wrapper
  input#checkbox-id(
    type="checkbox"
    checked)
  label(for="checkbox-id") ラベル

<div class="wrapper">
  <input id="checkbox-id" type="checkbox" checked="checked">
  <label for="checkbox-id">ラベル</label>
</div>

Vueでpugを使うこと自体のメリット・デメリットも当然ありますが、ここでは割愛します。

それでは本題に入ります。以降はpugとVueの記述がないまぜになるため、区別のために語頭にpugと付けて pug変数 などと記述することがあります。(対してVueで使用するJavaScript変数はJS変数と記述します)

pug変数ではなくJS変数を使う

pugではpug変数を使用できますが、template内でのみ有効でscriptからは参照できません。順当にJS変数で記述するべきでしょう。

- var url = 'path/to/page'
a(href='/' + url) Link

<template lang="pug">
a(href="`/${url}`") Link
</template>

<script>
export default {
  data: () => ({ url: 'path/to/page' })
}
</script>

明確にtemplate内でしか使用しない文字列ならpug変数でもいいかな?とも思いましたが、例えばi18n対応をする際には結局JavaScriptになることなどを考えると最初から使用しない方が良いという結論です。

pugの&attributesではなくv-bindを使う

&attributesはオブジェクト形式のpug変数をattributesに展開してくれる構文です。
例えば type, pattern などのセットを変数で書いておいて、まとめてattributesに指定するといった使い方ができます。

便利な構文ですが、Vueの場合はv-bindで同様のことができます

div(data-bar="bar")&attributes({'data-foo': 'foo'})

<template lang="pug">
div(data-bar="bar" v-bind="attrs")
</template>

<script>
export default {
  data: () => ({ attrs: { 'data-foo': 'foo' } })
}
</script>

pugの制御構文ではなくディレクティブを使う

制御構文とはつまりfor, each, if/unless/else, case/whenのことです。
pugのこれらの制御構文で用いる条件式ですが、残念なことにJS変数を使用することはできません(当然といえば当然ですが)。
つまり静的なhtmlに展開される訳であって、決してVueのリアクティブな構文に変換される訳ではないのです。

制御構文はVueのディレクティブが用意されているのでこちらを使うべきでしょう。以下はpugにおけるcase/whenをVueに置けるv-if/v-else-if/v-elseに置き換える例です

- var status = 'hoge' //- あくまでpug変数
case status
  when 'hoge': p hoge!
  when 'fuga': p fuga!
  default: p default!

<template lang="pug">
template(v-if="status === 'hoge'"): p hoge!
template(v-else-if="status === 'fuga'"): p fuga!
template(v-else): p default!
</template>

<script>
export default {
  data: () => ({ status: "hoge" })
}
</script>

リアクティブにする必要のない繰り返しなども当然あるでしょうが、記述の統一という観点からもVueのディレクティブを用いるべきでしょう。

ところでVueにはswitch/caseのような制御ディレクティブはありません。過去にPullRequestが出たこともあるみたいですが、closeされています。
もしもwhen/caseを使いたいような複雑な分岐に直面した場合は、リンク先の議論にある通りv-if/v-else-if/v-elseをtemplateの中で何個も繋げるよりも、computedを用いるなどしてリファクタリングを行うべきです。

pugのincludeではなく単一ファイルコンポーネントを使う

指定した.pugファイルの内容をインライン展開します。使う目的としては2つ考えられます

  1. 複数のファイルから利用するため別ファイルに記述する
  2. header/body/footerなど構造的な意味で分割する

どちらの場合もVueのコンポーネントを使うべきでしょう。
というのも、残念なことに私の知る限りのテキストエディタでは.pugファイルにおけるVueのシンタックスハイライトが効きません。
もしscriptやstyleを使う必要がない場合は関数型コンポーネントにすることもできます。

<template lang="pug">
.modal
  include path/to/modal-header
  include path/to/modal-body
  include path/to/modal-footer
</template>

<template lang="pug">
.modal
  modal-header(v-bind="someProps")
  modal-body(...)
  modal-footer(...)
</template>

<script>
import ModalHeader from 'path/to/ModalHeader'
import ModalBody from 'path/to/ModalBody'
import ModalFooter from 'path/to/ModalFooter'

export default {
  components: { ModalHeader, ModalBody, ModalFooter }
}
</script>

propsの受け渡しに関する記述を書くのは多少煩わしいかもしれませんが、各部分のコンポーネント単体での見通しが良くなるためこれは必要なことと考えます。

例えば上の2.のようにheader/body/footerなどの構造的なファイル分割を行ったとすると、分割先の.pugファイルではそのファイル内では定義されていない親コンポーネントのコンテキストを記述することになります。これはpugの利点でもあり、同時にコードがわかりずらくなる原因でもあります。あまり使わない方が良いでしょう。

pugのTemplate Inheritanceではなくslotを使う

Vueで言うslotのようなことがpugでもできます。逆に言えば、slotで出来ることをわざわざpugでする必要も無いでしょう。

<template lang="pug">
extends path/to/modal.pug

block header
  h1 タイトル
block body
  div ここはbodyです
</template>

<template lang="pug">
modal
  template(v-slot:header)
    h1 タイトル
  template(v-slot:body)
    div ここはbodyです
</template>

<script>
import modal from 'path/to/Modal'

export default {
  components: { Modal }
}
</script>

pugのmixinをVueで使うべきか否か

最後になりますがpugのmixinです。実を言うとこの構文だけは上手く使えば記述が楽になるのではないかと考えています。

と言うのも、Vueにおいてコンポーネントにpropsを渡すのはpugと同様にできますが、ディレクティブは別コンポーネントのelementに紐付けることは通常できないからです。以下は「引数に渡したプロパティをv-modelに引き渡すmixin」をVueのコンポーネントに置き換える例です。

<template lang="pug">
mixin input-number(prop)
    input(type="number" v-model.number=prop)&attributes(attributes)

div
  +input-number("hoge")
  +input-number("fuga")(required)
</template>

<script>
export default {
  data: () => ({ hoge: 100, fuga: 200 })
}
</script>

<template lang="pug">
div
  input-number(v-model="hoge")
  input-number(v-model="fuga" required)
</template>

<script>
import InputNumber from 'path/to/InputNumber'

export default {
  components: { InputNumber },
  data: () => ({ hoge: 100, fuga: 200 })
}
</script>

InputNumber.vue

<template lang="pug">
input(type="number" v-bind="$attrs" :value="value" @input="v => $emit('input', +v)")
</template>

<script>
export default {
  props: { value: { type: [String, Number], required: true } }
}
</script>

v-model ディレクティブは実際には :value propと @input ハンドラーを追加するシンタックスシュガーです。そのためそれを受け取るコンポーネント側では単なる「変数名の置き換え」では済まない程度の記述が求められます。

さらに言うとv-modelは上記のようにすれば一応解決できますが、全てのディレクティブの操作を別コンポーネントのelementに上手く割り当てることは不可能です。

ただ、includeの時にも書きましたがシンタックスハイライトが効かないなどの問題があるため、やはりオススメはできません。

おわりに

もし正確じゃない記述があったり、もっといい方法があるよ!って時はコメントいただけると助かります。

あれこれ否定的な考察を述べましたが私はpugが大好きです。できることならpugの便利な構文がうまい形でVueで利用できるようになれば嬉しいですね。