Handlebars
gulp

gulp+Handlebars.jsで静的htmlを作る

はじめに。

IDOMアドベントカレンダー(12日目)に投稿しています。
https://qiita.com/advent-calendar/2017/idom-engineer
初心者向け、gulp+Handlebars.jsを使って効率的に静的なhtmlを作る方法を書きます。
序盤の初期設定などは、既にnpmをお使いの皆さんはご存知かと思いますので、
飛ばして読んだ方が良いかなと思います。(また、Macでの制作前提です。)

えっ?イマドキgulp…? WebpackもParcelも使わない理由は?

あまり設定ファイルゴリゴリかくようなタスクランナーは需要がないかもしれないですが、Webpack使わない理由はエントリーポイントがJSではないためです。静的htmlをただ組み立てるだけであればgulpで良いかなと。WEB制作でnode.jsを使いはじめる瞬間に「あっ、コレApacheじゃないからSSI使えないんだ、困ったな。」とか「テキストの情報は、htmlと分けてデータバインディングで作りたい」「最終的にhtmlで納品しなければならない(クライアントがサーバーの設定を公開してくれない)」みたいな人には、まだ使えると思います。SPAで作りたい?それはまた違う記事をご参照下さいませ。。

テンプレートエンジンに「handlebars.js」をあえて使う理由は?

Githubのstarsは「pug」の方が多いのですが、「npmtrends」では「handlebars」の方が多いです。handlebars.jsで日本語サイトを検索した感じでは「誰が使ってるのか分からない」的な超マイナーテンプレートエンジン的な扱いでしたが、グラフみるとメジャーなテンプレートエンジンだったようです。「gulp」だと特に追加のプラグインも不要のようです。
http://www.npmtrends.com/handlebars-vs-jade-vs-pug-vs-ejs-vs-hogan.js-vs-mustache-vs-jsrender
「ejs」よりもとっつきやすく、「pug」より書式を覚えるのが億劫でない、と個人的には思います。

(↓ここからしばらく初期設定が続きます。)

(スキップ可)Gitリポジトリを作る。

プロジェクト作成のやり方は人それぞれですが、Githubに「frontend」というリポジトリを作ったらクローンしてくる。または「git init」でlocalに「frontend」というリポジトリを作っておきます。ターミナルのコマンドがまったく分からない場合は「SOURCETREE」で作れば良いと思いますが、今回それについては省略。(※イマドキ「gitに保存しない」っていう選択肢も無いと思うので、記載しておきました。)

.gitignore
/node_modules
/dist

は.gitignoreしておいてください。

(スキップ可)Gulpを使う前の下準備

Homebrew(Mac:パッケージ管理)

terminal
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Nodebrew(node.js:バージョン管理)

terminal
$ brew install nodebrew

たまに無いと動かない人も居るためmkdirしておく。(8.3.0の箇所を打ちかえる)

terminal
$ mkdir .nodebrew/src/v8.3.0

node.jsのバージョンを指定してインストールするやり方(8.3.0の箇所を打ちかえる)

terminal
$ nodebrew install-binary 8.3.0

最新版node.jsインストールのやり方

terminal
$ nodebrew install-binary latest

インストールされたバージョンを一覧で見る

terminal
$ nodebrew list

使うバージョンをuse〇〇で選ぶ、(8.3.0の箇所を打ちかえる)

terminal
$ nodebrew use v8.3.0

もう一度、一覧をみる。current:に使うモノが選ばれている状態

terminal
$ nodebrew list

コレはパスを通す例のアレ。次からターミナル開くたびにやらなくて良くするやつ。

.bashrc
#!/bin/bash
export PATH=$HOME/.nodebrew/current/bin:$PATH
terminal
$ source ~/.bashrc

(スキップ可)プロジェクトに移動する

ターミナルを開いて

terminal
$ cd frontend
$ pwd

結果が

/Users/YourName/frontend

となっていれば良いです。

(スキップ可)npmの初期設定(プロジェクトを作る)

次に「$npm init」をしてください。これは「このプロジェクトの初期設定」的なものです。
「Package.json」という初期設定ファイルができていればOK。他人のリポジトリを落としてきたりして、既にこのファイルがある場合は「npm init」をしなくて良いです。「npm init」は対話形式なので、聞かれたとおり素直に、不明な所は適当に、答えていったら大丈夫です。

terminal
$ npm init
terminal
name: (hoge) hoge
version: (0.0.0) 
description: 
entry point: (index.html) 
test command:test
git repository:frontend
keywords:fuga
author: hoge
license: (BSD) MIT

正確に答えておいた方がもちろん良いと思います。
こんな適当でも「動かないこたぁない」って事で。。

(↑ここまでが初期設定でした。)

handlebars.js

htmlの元となるファイル(テンプレート)を、「handlebars.js」形式で用意します。ファイル名は「.hbs」とします。ディレクトリを作りたい構造にファイルを配置します。下記の例では「src」フォルダでは「pc」と「sp」のソースをウェブサーバーで表示する際のディレクトリ構造と一緒の構造で入れておき、書き出した時に「/dist/pc/」と「/dist/sp/」に仕分けされて保存されるようにしたものです。最初に作っているうちから「/pc/」と「/sp/」に分けて作り始めると、後で修正する際に、あちこち行ったり来たりして修正をしなければならず、クライアントによってはすごく面倒だったりするためです。

【ファイル&ディレクトリ構成】

frontend
 ├ .git
 ├ .gitignore
 ├ package.json
 ├ package-lock.json
 ├ gulpfile.js
 ├ gulptask
 │ ├ assemble_pc.js
 │ ├ assemble_sp.js
 │ ├ copy.js
 │ ├ watch.js
 │ └ webserver.js
 │
 ├ src
 │ ├ asset
 │ │ ├css
 │ │ ├images
 │ │ └js
 │ │ 
 │ ├ data
 │ │ └data.json
 │ │
 │ ├ doc
 │ │ ├pages1
 │ │ │├index.pc.hbs
 │ │ │└index.sp.hbs
 │ │ ├pages2
 │ │ │├index.pc.hbs
 │ │ │└index.sp.hbs
 │ │ └pages3
 │ │  ├index.pc.hbs
 │ │  └index.sp.hbs
 │ │ 
 │ ├ layouts
 │ │ ├pc.hbs
 │ │ └sp.hbs
 │ │ 
 │ └ partials
 │   ├header.hbs
 │   ├footer.hbs
 │   └common_meta.hbs
 │ 
 ├ dist
 │ ├ pc
 │ └ sp
 └ node_modules

/src/layouts/pc.hbs」

まず「レイアウトファイル」を用意します。コレはhtmlで言うと、「bodyの外側」を作る(作り分ける)時に使います。下記の例では「pc」という名前にしています。これは最後に出来上がるファイルもPC用とSP用で分けて作ろうと思っているためです。対応ブラウザの関係で、スマホでしか使えないタグを使えたり「jQuery」のバージョンをスマホだけは最新にしたりできるためです。レスポンシブで作る場合は作り分ける必要はないので、このレイアウトファイルは例えば「default.hbs」などにすれば良いです。

{% body %}の中がそれぞれ置き換わります。

pc.hbs
<!DOCTYPE html>
<html lang="ja" dir="ltr">
 <head>
  {{>common_meta}}
  <title>{{title}}</title>
  <link rel="canonical" href="https://sample.jp/{{dir}}/" />
 </head>
 <body>
  <div id="container">
   {% body %}
  </div>
 </body>
</html>

htmlに毎回使うようなパーツを読み込みたい場合は{{>パーツ名}}と書きます。そして「/src/partials/」(パーシャルズと読む)以下に「パーツ名.hbs」と言った感じに用意しておくと、読み込まれます。「GoogleTagManager」「header」「footer」も、このやり方で埋め込む事ができます。これで「Apache」がなくてもローカルでパーツ読み込みができるようになります。Layoutsのhbsファイルでも、個別hbsファイルの中でも、下記のような書式で読み込みできます。

partial
{{>common_meta}}
{{>gtm}}
{{>header}}
{{>footer}}

↓また、下記のようなデータバインディングを「<title></title>」にはさみ込んでいます。ココは各htmlにより変わりますが、テキストが可変になる部分をレイアウト側に書いて良いのかな?と言う疑問が湧くかと思いますが、ココに書いて良いようです。この「title」や「dir」にどうやってページ事に変わるテキストを入れるのか?という事は後で書きます。

pc.hbs
{{title}}
{{dir}}

/src/partials/common_〇〇.hbs」

いわゆるSSIやらPHPでいうrequire()的なパーツの読み込みには、Partialファイルを用意します。
common_meta.hbsの中身はこんな感じ。

common_meta.hbs
{{>common_meta}}
 ↓↓↓
htmlに変換すると
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="keywords""/>

上記は普通のincludeです。何か変数を持って来る場合は下記のような感じです。例えば「thymeleaf」という変数を持ってgulpが処理する場合はif文の上が入る感じです。後々、動的な処理が入る場所に関しては予めif文で分岐させておき、「dist」で確認する時は「static」に表示させ、正式に書き出す場合に「thymeleaf」を「true」にして書き出すといいです。

trueにするやり方は色々あるので省略。

common_meta.hbs
{{#if thymeleaf}}
<meta th:substituteby="/pc/common/inc/head :: common_meta" />
{{else}}
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="keywords""/>
{{/if}}

/src/doc/sample/index.pc.hbs」

各Pageのファイルを用意します。Layoutファイル内の{% body %}の中が置き換わるイメージです。
Layoutにある{{title}}や{{dir}}を置き換える場合はhbsの先頭にこのような変数とその中身を書いておくと適用されます。
(この変数がうまく出てこないと思ったら「:」の後ろに半角スペースが入っているかを確認してください。なぜか:の後ろのスペースを詰めて記述すると、テキストが上手く表示されないようです。ハマりました。)

index.pc.hbs
---
title: 「ここでLayoutsの変数をかきかえる」
dir: directory
---
<h1>{{obj.pagename.h1}}</h1>
<h2>{{obj.pagename.h2}}</h2>
<div>{{obj.pagename.description}}</div>

{{obj.pagename.h1}}や{{obj.pagename.h2}}はどうするの?という話ですが「/src/data/data.json」を読みこんでおきます。ちょっとしたサイトであれば、配列をコピペで書けますが、中規模以上のサイトでは手作業でJSONを更新するのはすごく大変です。そうなってきたらサーバーサイドのエンジニアの方に「こんな感じで」、とjsonを出してもらうと良いです。最初からJSONを読み込む前提で作っておけば、後から本格的なデータバインディングに移行するのもラクですし。Staticに書いちゃうと後でメンテナンスや量産が雪だるま式に大変になっていきます。。

data.json
{
   pagename:{
    h1:"h1にこれが入ります",
    h2:"h2にこれが入ります",
    description:"descriptionにコレが入ります。"
   }
}

gulp

色々インストールしていく。「--save-dev」は必ず書くようにする。
必要なプラグインが分かるようにするため。gulp-assembleは古いので使わない。

terminal
$ npm install --save-dev gulp
$ npm install --save-dev gulp-htmlmin
$ npm install --save-dev gulp-rename
$ npm install --save-dev gulp-extname
$ npm install --save-dev assemble
$ npm install --save-dev gulp-plumber
$ npm install --save-dev path
$ npm install --save-dev gulp-webserver
$ npm install --save-dev gulp-server-livereload

【2018.03 追記】「fs」はNode.jsに含まれているので、要らないです。

$ npm install --save-dev fs  ← コレ不要

gulptask.js

プロジェクト直下に「gulptask.js」というファイルを作る。
中身は「/gulptask」以下に収納しようと思うので、こんな書き方にする。
(設定ファイルが長くなってくると、メンテがしづらくなってgulp疲れが発生するため)
PCとSPでhtmlを別々にしたいので、タスクを別々に書きました。
(これも中で条件分岐するより2回書いた方が設定ファイル的にラクなため。)

gulptask.js
var gulp = require("gulp");
var requireDir = require('require-dir');

requireDir('./gulptask',{recurse: true});

gulp.task('default',
  ['assemble_pc','assemble_sp','copy','watch']
);

まず1行ずつ丁寧説明していきますと・・・
↓このファイルを実行するのには、「gulp」が必要です。

gulptask.js
var gulp = require("gulp");

↓「require-dir」っていうプラグインを使います。

gulptask.js
var requireDir = require('require-dir');

↓「requireDir();」を使って「/gulptask」以下にタスクを小分けにして書けるようにする。
↓「{recurse:true}」はオプションで、さらにサブディレクトリを使えるようにするヤツです。

gulptask.js
requireDir('./gulptask',{recurse: true});

↓下記は「gulp」の「task」です。
↓タスクの名前は「default」です。
↓タスクの内容は「assemble_pc」と「assemble_sp」と「copy」と「watch」です。

gulptask.js
gulp.task('default',['assemble_pc','assemble_sp','copy','watch']);

実行する順番を先に書きました。次に中身を書いて行きます。

/gulptask/assemble_pc.js

gulptasksの下に「assemble_pc.js」というファイルを作ります。
中身はこんな感じで書きました。

assemble_pc.js
var gulp = require('gulp');
var htmlmin = require('gulp-htmlmin');
var rename = require('gulp-rename');
var extname = require('gulp-extname');
var assemble = require('assemble');
var plumber = require('gulp-plumber');
var webserver = require('gulp-webserver');
var path    = require('path');
var fs = require("fs");
var app = assemble();
var obj = JSON.parse(fs.readFileSync("./src/data/data.json", { encoding:"utf8" }));
gulp.task('load', function(cb) {
  app.partials('src/partials/*.hbs');
  app.layouts('src/layouts/pc.hbs');
  app.pages('src/doc/**/*.pc.hbs');
  cb();
});
gulp.task('assemble',['load'],function() {
  app.toStream('pages')
    .pipe(plumber())
    .pipe(app.renderFile({layout:'pc',obj:obj}))
    .pipe(htmlmin({collapseWhitespace: true}))
    .pipe(rename({basename:'index'}))
    .pipe(extname('.html'))
    .pipe(app.dest('dist/pc/'))
});

数行ずつまとめて説明します。
下記の部分は必要なプラグインを記した箇所です。(varは今風にカンマでつなげて書いても大丈夫です。)

objにはJSONをパースしたものを格納しておきます。objにJSONを格納しておけば後で呼び出せるので簡易的なデータバインディングができます。JSONファイルではなく、API叩いてJSONを返すヤツをココに組み込んでも良い、と思いますが、それはサーバーサイドのエンジニアさんに手伝ってもらって下さい。

忘れがちですが、分割されたタスクには、もう一度「gulp」を読み込む必要があります。

assemble_pc.js
var gulp = require('gulp');
var htmlmin = require('gulp-htmlmin');
var rename = require('gulp-rename');
var extname = require('gulp-extname');
var assemble = require('assemble');
var plumber = require('gulp-plumber');
var webserver = require('gulp-webserver');
var path    = require('path');
var fs = require("fs");
var obj = JSON.parse(fs.readFileSync("./src/data/data.json", { encoding:"utf8" }));

assembleを格納しておきます。

assemble_pc.js
var app = assemble();

1つ目のタスク「load」は下処理です。色んなものを読み込ませます。
「partials」とは「パーツ」の事です。(headerとかfooterなど)
「layouts」とは「レイアウトファイル」の事です(bodyの外側)
「pages」は、今から組み立てるhtmlファイルの事です。(bodyの内側)
/**/*.pc.hbs」は、その階層以下の末尾に.pcと付いてる拡張子hbsのファイル全部、という指定の仕方です。「index.pc.hbs」的な名前の付いたファイルのみ変換されます。「index.sp.hbs」と同じフォルダに置いて作業できるので、作業する時にあちこち行き来せずに済むためラクです。
「cb」は「callback」です。「load」が次に呼び出された場所に値を返す感じです。

assemble_pc.js
gulp.task('load', function(cb) {
  app.partials('src/partials/*.hbs');
  app.layouts('src/layouts/pc.hbs');
  app.pages('src/doc/**/*.pc.hbs');
  cb();
});

下記はいよいよ組み立てるタスクです。このタスクは['load']が完了した後に実行されます。
gulpは並列でタスクを処理するので、何も意識しないと読み込みと組み立てが同時に動いてしまいます。タスクの順番がおかしくなる場合は、必ずタスクの名前の後ろに['事前に行なうタスク']を入れておくと順番がバラバラにならずに助かります。

assemble_pc.js
gulp.task('assemble_pc',['load'],function() {
  app.toStream('pages')
    .pipe(plumber())
    .pipe(app.renderFile({layout:'pc',obj:obj}))
    .pipe(htmlmin({collapseWhitespace: true}))
    .pipe(rename({basename:'index'}))
    .pipe(extname('.html'))
    .pipe(app.dest('dist/pc/'));
});

1個ずつ説明していきますと・・・
コレが「assemble_pc」の外側です。名前の後ろのオプション的な箇所は['load1','load2','load3']とつなげて書く事ができます。

assemble_pc.js
gulp.task('assemble_pc',['load'],function() {○○○});

コレはstream()の新しい書き方?です。
「assemble-stream」というモジュールを使って処理するようです。pagesに格納されたファイルに対して、以下の処理を行ないます。

assemble_pc.js
app.toStream('pages')

gulpのお作法は「.pipe」でタスクを繋いでいきます。
gulp.watchしている時にエラーで処理が止まらないようにする「gulp-plumber」を使っています。watchの使い方に付いては、省略します。

assemble_pc.js
.pipe(plumber())

レイアウトは「pc」を使ってhtmlをrenderingします。「obj:obj」はパースしたJSONをrenderfile内に持って入るために利用します。

assemble_pc.js
.pipe(app.renderFile({layout:'pc',obj:obj}))

htmlをミニファイします。

assemble_pc.js
.pipe(htmlmin({collapseWhitespace: true}))

ファイル名を「index」に変更します。

assemble_pc.js
.pipe(rename({basename:'index'}))

ファイルの拡張子を「.html」に変更します。

assemble_pc.js
.pipe(extname('.html'))

ファイルを書き出す場所は「/dist/pc/」以下を指定します。

assemble_pc.js
.pipe(app.dest('dist/pc/'))

livereloadで更新するようにします。

assemble_pc.js

同様のファイルをspにも作成します。(省略)

このタスクだけを実行するには?

terminal
$ gulp assemble_pc

/gulptask/copy.js

gulptasksの下に「copy.js」というファイルを作ります。

copy.js
var gulp = require('gulp');
gulp.task('copy', function() {
  gulp.src('./src/asset/**')
    .pipe(gulp.dest('./dist/'));
});

/src/asset/以下にある画像などのファイルをコピーします。
画像はdistにコピーしてあげないと表示が確認できないので、必ずセットで実行して下さい。
scssやAltJSは別のタスクで処理すると思いますが、もしもトランスパイルしない感じであれば
/src/asset/内に置いて/dist/にコピーして下さい。

このタスクだけを実行するには?

terminal
$ gulp copy

上記、全部のタスクを実行するには?

terminal
$ gulp

または

terminal
$ gulp default

サーバーの立て方(gulp-webserver)

node.jsの簡易的なサーバーを立てる方法は色々ありますが、gulpなら、gulp-webserverを使います。特に「livereload」は最近のモダンなコーディング事情的には、ほぼ必須の仕組みです。(ファイルの変更を監視して自動的にブラウザが自動リロードを書けるものです。ホットリロードとも言います。)これでhtmlやCSSを書き直した時に「F5」や「Ctrl+R」をポチポチ毎回叩かなくて良いです。もっと良いやり方があるかもしれませんが、自分は「webserver」が「livereload」のイベントを拾うのと、「watch」イベントが「ファイルの更新」を監視するイベントを一つにできなかったので、「webserver」のターミナルは、「gulp default」とはターミナル別窓で開いております。(追記:「gulp-connect」はもう古いらしいので「gulp-webserver」に書き換えました。)

webserver.js
var gulp = require('gulp')
var webserver = require('gulp-webserver');

gulp.task('webserver', function() {
  gulp.src('dist')
   .pipe(webserver({
    host: 'localhost',
    port: 8888,
    livereload: true
  });
});

監視の説明がなかったので、追記しました。「gulp-webserver」は「dist」を監視しているので
「src」を更新したら「dist」へ書き出し、「dist」の更新を「livereload」で拾う、という感じでできます。

watch.js
var gulp = require("gulp");
gulp.task("watch",['assemble_pc','assemble_sp','copy'], function () {
     var watcher = gulp.watch("./src/doc/**",['assemble_pc','assemble_sp','copy']);
     watcher.on('change', function(event) {
       console.log(event.path + 'が変更されました。');
     });
});

サーバーを立てると「http://localhost:8888」をブラウザで見れるようになり、/dist/以下に書き出されたファイルを表示します。

terminal
$ gulp webserver

npmでタスクを実行するには?

最近はタスクランナー的な抽象化をしないで「\$ npm run」で何でもやることになった(らしい)ので、ターミナルに「\$ gulp」なんて打つの恥ずかしいよ〜npmコマンドでやりたいよ〜という場合は、次のようにする。「package.json」の「scripts」に、下記のような記述をする。(下記、webpackやgruntは説明用に入ってるだけです。削って下さい。)

package.json
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "npm-run-all build:*",
    "build:webpack": "webpack",
    "build:gulp": "gulp default",
    "build:grunt": "grunt default"
  },

このモジュール(npm-run-all)をインストールしておくと
複数のbuild以下のスクリプトを実行できるようになります。

terminal
$ npm install --save-dev npm-run-all

そして

terminal
$ npm run build

とすれば良いです。

いかがでしたでしょうか?「非エンジニア」でも「タスクランナー」を導入することでWeb制作を効率化ができるんじゃないかなと思います。以上です。