2018.05.13

フロントエンドでテンプレートエンジン Edge.js を使う


Edge.js の特徴

Edge.js とは

Edge.js は HTML を出力するテンプレートエンジンです。

同じテンプレートエンジンとして挙げられるのは…

  • JavaScript:EJS、Handlebars.js、Pug
  • Ruby:erb、Haml
  • PHP:Smarty、Blade
  • Python:Jinja2
  • Java:Thymeleaf

とまぁ各言語で色々ありますね。

HTML っぽいのに変数の埋め込みとか条件分岐とかループとか共通パーツの切り出しとかできて最終的に HTML が出てくる、アレです。

Web アプリケーションフレームワーク「Adonis.js」の一部として開発されていますが独立したライブラリですので、今回の記事では Edge.js だけをフロントエンドで利用する方法と構文を紹介します。

Edge.js は Node 環境上で動作するため、Gulp などで HTML への変換処理を行えばフロントエンドコーディングにおいて利用することができます。

良いところ

上でも挙げたように、JavaScript から使えるテンプレートエンジンはすでにたくさんあります。その中でも Edge.js の優れた特徴を挙げるとすると、デフォルトでレイアウト機能があることでしょう。レイアウト機能とは、外側の枠組みを共通化する仕組みです。

例えば EJS をフロントエンドコーディングの文脈で使用すると、パーツを切り出すことはできてもレイアウト機能がないので結局記述が重複してしまいます。

index.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>EJS Sample</title>
</head>
<body>
  <%- include('./_header') %>
  <main>
    <h1>Index page</h1>
  </main>
  <%- include('./_footer') %>
</body>
</html>
about.ejs
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>EJS Sample</title>
</head>
<body>
  ___ERB0___
  <main>
    <h1>About page</h1>
  </main>
  ___ERB1___
</body>
</html>

Edge.js はレイアウト機能を備えているため、記述の重複を避けられます。

layout.edge
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>EJS Sample</title>
</head>
<body>
  @include('header')
  <main>
    @!section('content')
  </main>
  @include('footer')
</body>
</html>
index.edge
@layout('layout.edge')

@section('content')
  <h1>Index page</h1>
@endsection
about.edge
@layout('layout.edge')

@section('content')
  <h1>About page</h1>
@endsection

include がパーツを共通化する、つまり子を共通化する仕組みであり、レイアウトは親を共通化する仕組みである、とも表現できるかもしれません。

とにかく、レイアウト機能はコーディングにおいてもとても便利だと思います 😇 💖

Gulp で開発環境構築

依存パッケージをインストール

Gulp はバージョン 4 を使用します。

$ npm init -y
$ npm install --save-dev edge.js gulp@next gulp-rename gulp-tap

ディレクトリ構成

以下のディレクトリ構成を前提に環境構築を進めます。

プロジェクトルート
├ public
├ src
│ └ templates
│   ├ components
│   │ └ 共通部品ファイル(Components)
│   ├ partials
│   │ └ 共通部品ファイル(Partials)
│   ├ layouts
│   │ └ レイアウトファイル
│   ├ pages
│   │ └ ページごとのテンプレート
│   └ data.json
├ gulpfile.js
└ package.json

gulpfile.js

gulpfile.js
const gulp = require('gulp')
const rename = require('gulp-rename')
const tap = require('gulp-tap')
const fs = require('fs')
const edge = require('edge.js')

// Edgeテンプレート -> HTML
function templates () {
  // テンプレートファイルを読み込む
  edge.registerViews('src/templates')
  // データファイルを読み込む
  const data = fs.existsSync('src/templates/data.json')
    ? JSON.parse(fs.readFileSync('src/templates/data.json', 'utf8'))
    : {}
  // ヘルパー関数を読み込む
  fs.existsSync('src/templates/helpers.js') && require('./src/templates/helpers.js')
  return gulp.src('src/templates/pages/**/*.edge')
    .pipe(tap(file => {
      const contents = edge.renderString(String(file.contents), data)
      file.contents = new Buffer(contents)
    }))
    .pipe(rename({ extname: '.html' }))
    .pipe(gulp.dest('public'))
}

// タスク登録
gulp.task('dev', templates)

npm スクリプト

package.json
"scripts": {
  "dev": "gulp dev"
},

これで、下記のコマンドで HTML を出力できます。

$ npm run dev

構文まとめ

レイアウト

テンプレート

src/templates/layouts/default.edge
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Hello, Edge.js</title>
</head>
<body>
  <header>Header</header>
  @!section('content')
  <footer>Footer</footer>
</body>
</html>
src/templates/pages/index.edge
@layout('layouts.default')

@section('content')
  <main>
    <h1>Hello, world!</h1>
  </main>
@endsection

出力

public/index.html
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Hello, Edge.js</title>
</head>
<body>
  <header>Header</header>
  <main>
    <h1>Hello, world!</h1>
  </main>
  <footer>Footer</footer>
</body>
</html>

データ埋め込み

データファイル

src/templates/data.json
{
  "message": "Hello"
}

テンプレート

src/templates/pages/index.edge(抜粋)
<h1>{{ message }}, world!</h1>

出力

public/index.html(抜粋)
<h1>Hello, world!</h1>

条件分岐

データファイル

src/templates/data.json
{
  "score": 92
}

テンプレート

src/templates/pages/index.edge(抜粋)
@if(score > 90)
  <h2>よくできました</h2>
@elseif(score >= 60)
  <h2>がんばったね</h2>
@else
  <h2>もっとがんばろう</h2>
@endif

条件を表すカッコ内では JavaScript の比較演算子を使うことができます。

出力

public/index.html(抜粋)
<h2>よくできました</h2>

ループ

データファイル

src/templates/data.json
{
  "songs": [
    { "title": "Watching the Wheels", "artist": "John Lennon" },
    { "title": "Maybe I'm Amazed", "artist": "Paul McCartney" }
  ]
}

テンプレート

src/templates/pages/index.edge(抜粋)
<ul>
  @each(song in songs)
    <li>
      <h2>{{ song.title }}</h2>
      <p>artist: {{ song.artist }}</p>
    </li>
  @endeach
</ul>

出力

public/index.html(抜粋)
<ul>
  <li>
    <h2>Watching the Wheels</h2>
    <p>artist: John Lennon</p>
  </li>
  <li>
    <h2>Maybe I'm Amazed</h2>
    <p>artist: Paul McCartney</p>
  </li>
</ul>

Partials

共通パーツを切り出す機能です。

今回、pages ディレクトリに入っている .edge ファイルを HTML に変換するように gulpfile.js を記述しているので、パーツのファイルは pages ディレクトリ以外に置いてください。

データファイル

src/templates/data.json
{
  "header": {
    "message": "I'm a header."
  }
}

テンプレート

src/templates/layouts/default.edge(抜粋)
@include('partials.header')
<main>
  <h1>Main content.</h1>
</main>
src/templates/partials/header.edge
<header>
  {{ header.message }}
</header>

出力

public/index.html(抜粋)
<header>
  I'm a header.
</header>
<main>
  <h1>Main content.</h1>
</main>

Components

Components は Partials と同様に共通パーツを切り出す機能です。

Partials との違いは、パーツにデータを渡せる点です。そのため、ヘッダーやフッターなど HTML 中で一度しか使わなかったり文脈によって中身が変わらない場合は Partials を使い、中身を変えて使い回したい場合は Components を使うという使い分けが良いでしょう。

また注意点としては、Partials と異なり明示的に引数で渡された変数しか参照できません

データファイル

src/templates/data.json
{
  "songs": [
    { "title": "Ziggy Stardust", "artist": "David Bowie" },
    { "title": "Welcome to the Machine", "artist": "Pink Floyd" }
  ]
}

テンプレート

src/templates/pages/index.edge(抜粋)
<ul>
  @each(song in songs)
    <li>
      @!component('components.card', title = song.title, body = 'artist: ' + song.artist)
    </li>
  @endeach
</ul>
src/templates/components/card.edge
<div class="card">
  <h2 class="card-title">{{ title }}</h2>
  <p class="card-body">{{ body }}</p>
</div>

出力

public/index.html(抜粋)
<ul>
  <li>
    <div class="card">
      <h2 class="card-title">Ziggy Stardust</h2>
      <p class="card-body">artist: David Bowie</p>
    </div>
  </li>
  <li>
    <div class="card">
      <h2 class="card-title">Welcome to the Machine</h2>
      <p class="card-body">artist: Pink Floyd</p>
    </div>
  </li>
</ul>

Components その 2 - slot を使う

Components の機能 slot を紹介します。

slot は Components に渡したいコンテンツが HTML である場合に役立ちます。

テンプレート

src/templates/pages/index.edge(抜粋)
@component('components.modal', title = 'Wuthering Heights')
  @slot('body')
    <h3>Kate Bush</h3>
    <p>Album: The Kick Inside</p>
  @endslot
@endcomponent
src/templates/components/modal.edge
<div class="modal">
  <h2 class="modal-title">{{ title }}</h2>
  <div class="modal-body">
    @!yield($slot.body)
  </div>
</div>

出力

public/index.html(抜粋)
<div class="modal">
  <h2 class="modal-title">Wuthering Heights</h2>
  <div class="modal-body">
    <h3>Kate Bush</h3>
    <p>Album: The Kick Inside</p>
  </div>
</div>

@!yield の部分が @slot の内容に置き換わっていますね。

Raw Output

@raw で囲まれた領域はデータの埋め込みがされなくなります。{{ }} をそのまま表示できるということです。

普通に {{ }} を表示したいことはあまりないと思いますが例えば Vue などでも {{ }} は使われるので、HTML を出力した後に他のツールに {{ }} を処理させたい場合に使用します。

テンプレート

src/templates/pages/index.edge(抜粋)
@raw
  <p>Lorem {{ ipsum }} dolor</p>
@endraw

出力

public/index.html(抜粋)
<p>Lorem {{ ipsum }} dolor</p>

ヘルパー関数

JavaScript でヘルパー関数を定義することもできます。

定義済み関数もいくつかありますが、独自で関数を定義する方法を紹介します。

データファイル

src/templates/helpers.js
const edge = require('edge.js')

edge.global('scoreEmoji', function (score) {
  if (score > 90) {
    emoji = '😄'
  } else if (score >= 60) {
    emoji = '🙂'
  } else {
    emoji = '😓'
  }
  return this.safe('<span>' + emoji + '</span>')
})

テンプレート

src/templates/pages/index.edge(抜粋)
<p>{{ scoreEmoji(99) }}</p>

出力

public/index.html(抜粋)
<p><span>😄</span></p>

ポイントは、HTML を返したい場合は this.safe() メソッドで囲ってあげることです。これによりエスケープされなくなります。


以上、テンプレートエンジン Edge.js を紹介しました。

EJS より便利ではないかと思いますのでぜひ HTML コーディングの場で使ってみてください。