pugで構造とデータを分離して管理した


はじめに

データとHTMLの元になるpugファイルを分割して、いくつかの書き方を試してみました。環境設定さえ出来てしまえば、HTMLとjavasriptの知識があれば、pugのドキュメントにあるサンプルを眺めながら書けるので非常に楽です。
pugそのものの説明はしません。(公式ドキュメントはコードの例示のみで理解可能)

テンプレートエンジンとしてのpugの話

pugは、HTMLをcoffeeやsassのようにインデントを使う簡易記法です。また、PHPのようにテンプレートエンジンとして利用することができます。さらに、データ部分はjavascriptで書きます。私にとっては、PHPよりjavascripのほうが馴染みがあるので、ありがたいです。

一点、テンプレートエンジンとしては、PHPと大きく違う点があります。それは、PHPはリクエストがあったタイミングで動的にHTMLを作成することを前提としている反面、pugは静的なHTMLを用意するためのツールだと言う点です。(原理的には動的なHTMLを作成することは可能ではないかと思います。)

しかし、ホームページやブログなど静的なwebページは多いので、pugをテンプレートエンジンとして利用するのはありかなと考えます。

何をしたかという話

今回、請求書を書くのに通常のアプリを使うのがなんとなくプログラマっぽくない。"怠惰"さに欠ける行為のような気がして、ブラウザからPDF出力する仕組みを作りました。請求書とセットで領収書も出力できるように、データを外だしして重複がない状態を作りました。

これを行うにあたり、gulpでpugがオートコンパイルされる様に環境設定しました。内容はこちらにまとめてあります。(バックエンドエンジニアがタグ書くのが面倒で組んだgulpfile晒します)

ソースコード

請求書のイメージ

sample_invoice.html

構成

pugファイルの構成

データ

json形式で表示用のデータを書きます。以下の2つのファイルがあります。

  • 振り込み先や連絡先など固定的なもの
  • 請求の中身に関する請求先・請求明細など個別のもの

テンプレート

通常のpugの記述の方法ですが、バインディング用の記法を使っています。
この2つは同じデータから作成されます。(それぞれでしか使わないデータもある)

  • 請求書
  • 領収書

構成

.
├── dist          ここにHTMLが出力される
│   └── css
└── src
    ├── data      データ(通常はここにデータを書く前提)  
    ├── includes  テンプレート
    ├── sample    データのサンプル
    └── sass      sassフォルダ(今回は解説しない)

コード解説

データ部分

固定的な情報をprofile.pugに記述しました。

src/sample/profile.pug
-
    var profile = {
        name: 'takaaki takaaki',
        adress: 'kumamoto city',
        email: '[email protected]',
        phone: '090-9999-9999',
        account: 'higo bank<br>9999999<br>takaaki iwamoto'
    } 

pugは-のあとインデントをつけると中をjavasriptとして記述します。コンパイル時に解釈され、コンパイル後には出力されません。

account: 'higo bank<br>9999999<br>takaaki iwamoto'

中にタグを書いてます。そのまま、だとエスケープ文字に変換されてしまいます。(よく出来ている)適用時にひと工夫をすると適用出来ます。
とはいえ、データ部分に表示に関わる部分を書くのは、あまり良くないと思います。

そして支払先の情報は毎回変わるので、別ファイルとして保管します。

src/sample/sample.pug
- 
    var invoice = {
        issue: '2020/05/07',
        name: 'john smith',
        term: '2020/04',
        due: '2020/06/30',
        paid: '2020/06/18',
        details: [
            {date: '2020/04/22', title: 'something (1)', price: 10000},
            {date: '2020/04/23', title: 'something (2)', price: 500},
        ]
    }
    invoice.total = invoice.details.map(x => x.price).reduce((acc, cur) => acc + cur)

termは月次で請求する場合に、オプションで使います。

detailsは請求明細で、配列が保持されています。

totaldetailsの金額を合計しています。javascriptが問題なく動作します。というわけでもっと複雑なシステムも可能です。

テンプレート部分

請求書

src/include/invoice.pug
doctype html
html(lang="jp")
  head
    title="請求書"
    link(rel="stylesheet" type="text/css" href="./css/style.css")
  body
    header
        h2 請求書
        .issue #{ invoice.issue } 作成
        .container
            .my-profile
                .my-name= profile.name
                .my-address 住所: #{ profile.adress }
                .my-email e-mail: #{ profile.email }
                .my-phone TEL: #{ profile.phone }
    section.bill-to
        h2 お請求先
        .container
            .your-name #{ invoice.name } 様
            if invoice.term
                .billing-term #{ invoice.term } 分
    section.details
        h2 請求明細
        .container
            .details-row.details-header
                .details-date 作業日
                .details-title 項目
                .details-price 金額
            for detail in invoice.details
                .details-row
                    .details-date #{ detail.date }
                    .details-title #{ detail.title }
                    .details-price= '¥' + detail.price.toLocaleString() + '-'
    section.total
        h2 請求合計
        .container
            .total-price= '¥' + invoice.total.toLocaleString() + '-'
    footer
        .container
            .billing-due 支払い期限: #{ invoice.due }
            .account-number !{ profile.account }

バインド(ヒゲこと中括弧で書く)

javasriptの値を表示する方法は2つあります。
1つは、#{ value }という形式で書く方法です。

.my-phone TEL: #{ profile.phone }

テンプレートエンジンっぽい書き方で、脳内での変換がしやすいです。
電話番号はこの様に変換されます。

<div class="my-phone">TEL: 090-9999-9999</div>

バインド(=でjavascriptで記述する)

もう一つは、= valueという書き方です。

.my-name= profile.name

括弧がいらないあたり、pugっぽい見た目です。多少のロジックが必要だったりした場合はこの書き方が良いかもしれません。
この場合はこの様に変換されます。

<div class="my-name">takaaki takaaki</div>

タグを含むバインド

振り込み先の部分はbrタグを書き込みました。

account: 'higo bank<br>9999999<br>takaaki iwamoto'

タグなどは、通常のバインドではエスケープされて文字として表示しようとするので、この様に指定します。

.account-number !{ profile.account }

するとエスケープされずに出力されます。

<div class="account-number">higo bank<br>9999999<br>takaaki iwamoto</div>

=を使う書き方だと、こうなります。

.account-number!= profile.account

条件式(if)

if invoice.term
  .billing-term #{ invoice.term } 分

if文の条件がtrue(この場合は値があること)場合にインデント以下が表示されます。

インデントが一つ下がって、読みづらいので多用は避けたいです。

繰り返し(for)

明細は配列の形で保持しているので繰り返しで出力する必要があります。

for detail in invoice.details
  .details-row
    .details-date #{ detail.date }
    .details-title #{ detail.title }
    .details-price= '¥' + detail.price.toLocaleString() + '-'

いわゆるeachで繰り返す方法です。

これもインデントがややこしくなるのであまり複雑なのは書きたくないですね。

表示の変換はこのファイルで行うのが正解ですが、記述がややこしくなったら別ファイルにヘルパー関数を用意しても良いかもです。

あと全然関係無いのですが、明細行のcssは初めてグリッドを使いました。cssっぽさが無いけど便利です。もう、tableタグは使いたくないし、bootstrapのグリッドもいらないです。flexboxとは使い分けが必要かなと思います。

領収書

請求書のほとんど同じなので割愛します。

結合ファイル

上記のファイルを結合して出力します。

/src/sample_invoice.pug
//- 請求・領収データ(json)
include sample/sample.pug
//- 私の情報(json)
include sample/profile.pug
//- 請求書の書式
include includes/invoice.pug

単純にincludeで展開するだけです。
請求データをこのファイルに含めてしまえば、良いのですが、領収書でも使っていますので、別ファイルになります。

引数でテンプレートにわたす形にできれば関係が見て取れるので可読性が

ちなみに、領収書を作成する場合はテンプレートを変更するだけです。

/src/sample_receipt.pug
//- 請求・領収データ(json)
include sample/sample.pug
//- 私の情報
include sample/profile.pug
//- 領収書の書式
include includes/receipt.pug

コンパイル

私の場合はgulpで自動コンパイルしています。
詳細は以前の投稿を見て下さい。
(バックエンドエンジニアがタグ書くのが面倒で組んだgulpfile晒します)

終わりに

感想

ものすごく長い文章になったけど、pugそのものが非常にわかりやすくどんどん改善できていったので書いていて何しろ気持ちが良いです。
クラスはセマンティックに出来るだけ1つだけにしたら、htmlぽさが無いほどスッキリ書けました。sass/scssを使うとクラスをいくつも足す必要がなくなるのでありがたいです。
書けるヒトだったらwordpressでブログやホームページを作るより、pugで組んだほうが幸せかもしれないとまで思えます。

改善案メモ

  • pugファイルの自動生成
    • データを追加したら自動で請求書/領収書が作成される。
    • データのフォルダから、生成されたHTMLのリンクを作成する。
  • brockを使ってみたい。サブのメニューがあったりとかに便利そう。
  • mixinを使ってみたい。forの中にifがあってとインデントが深くなったときに使いたい。
  • markdownを拡張で使えるみたい。