はじめに
はじめまして、フロントエンドエンジニアをやっているゆーひと言います!
Qiitaには初めて投稿するので至らぬ点などあればご指摘いただけたらと思います。
現在SESでとある現場に常駐しているんですが、そこでちょっと珍しい形でHTMLを運用することとなったので備忘録として残したいと思います(世間的にこの手法は一般的なのかもしれませんが笑)
要件
- jadeやectのように『_layout.ejs』なるものを作って全ぺージ共通コンテンツ等をextendしたい
- title, description, keywordなどのぺージ固有のメタ情報等はejsごとに定義してextendされてきた_layout.ejsの中に値を渡してあげてHTMLを生成する
- お知らせ一覧などの更新頻度が高いものはJSONやYAMLなどを使って管理する
今回やりたかったことはこんなかんじです。
Front-matterってなんぞ?
表題にあるFront-matter(フロントマター)というものですが、これはYAMLの中でサイトのメタ情報などを変数の値として定義して静的サイトを生成する際に使用するものです。
YAMLとは、JSONやXMLと同じようにデータを管理する為のデータフォーマットと思っていただければ問題ないです。
実際に例にすると下記のようなかんじです!
{
"name": "ゆーひ",
"work": "フロントエンドエンジニア",
"hobby": [
{
"anime": "ラブライブ!"
},
{
"illust": "たまにイラスト描いてます"
}
]
}
これが、
name: ゆーひ
work: フロントエンドエンジニア
hobby:
- anime: ラブライブ!
- illust: たまにイラスト描いてます
こういうかんじになります!
JSON形式と比べるとシンプルで見やすいですね。
で、Front-matterというのはこのYAML形式の上下に---
と置いてあげるだけでejsなどのテンプレートエンジンの中でローカル変数として活躍してくれるのです!
---
name: ゆーひ
work: フロントエンドエンジニア
hobby:
- anime: ラブライブ!はいいぞ
- illust: たまにイラスト描いてます
---
<p>私の名前は<%= name %>です。</p>
nameには『ゆーひ』という値が入ってきます。
しかし、こう書くだけではただエラーが出て使えないので、gulpを用いてこのFront-matterを使えるようにしてみましょう!
gulpfile.js
まず、使わなければいけないnode_modulesがいくつかあるのでそれをインストールします。
※gulpfile.jsの説明はES2015で書いています。詳細はkosuke_nishayaさんのgulpfileをES2015(ES6)で書くをご覧いただけたらと思います。
npm i -D gulp gulp-plumber gulp-ejs gulp-front-matter layout-wrapper
ツール | ざっくり説明 |
---|---|
gulp | タスクランナー |
gulp-ejs | gulp製のejs。ejs と特に違いはない |
gulp-plumber | エラーが出てもタスクを止めないようにしてくれる |
gulp-front-matter | テンプレートエンジンの中でFront-matterを使えるようにする為のモジュール |
layout-wrapper | 静的サイトジェネレータ |
さて、インストールが終わったところで実際にgulpfileに書き込んでいきます!
説明しやすいように_layout.ejsとindex.ejsも公開します。
import gulp from 'gulp';
import plumber from 'gulp-plumber';
import ejs from 'gulp-ejs';
import frontMatter from 'gulp-front-matter';
import wrapper from 'layout-wrapper';
const path = {
ejs: {
layoutDir: `${__dirname}/src/ejs/layouts`,
src: [
'./src/ejs/**/*.ejs',
'!./src/ejs/**/_*.ejs'
],
dist: './htdocs/'
},
json: {
package: './package.json',
newsList: './src/data/newsList.json'
}
};
global.jsonData = {};
jsonData.newsListJson = require(path.json.newsList);
gulp.task('ejs', () => {
var extName = require(path.json.package);
gulp.src(path.ejs.src)
.pipe(plumber())
.pipe(frontMatter({
property: 'data'
}))
.pipe(ejs())
.pipe(wrapper({
layout: path.ejs.layoutDir,
data: {
name: 'ホゲのサイト',
layoutsDir: path.ejs.layoutDir
},
engine: 'ejs',
frontMatterProp: 'data'
}))
.pipe(ejs(extName, { 'ext': '.html' }))
.pipe(gulp.dest(path.ejs.dist));
});
gulp.task('default', ['ejs']);
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- meta -->
<meta charset="UTF-8">
<meta name="description" content="<%= file.data.header.description %>">
<meta name="keywords" content="<%= file.data.header.keywords %>">
<title><%= file.data.header.title %> - <%= name %></title>
<!-- link -->
<% file.data.css.forEach(cssItem => { %>
<link rel="stylesheet" href="<%= cssItem %>">
<% }); %>
</head>
<body>
<%- contents %>
<% file.data.js.forEach(jsItem => { %>
<script src="<%= jsItem %>"></script>
<% }); %>
</body>
</html>
---
layout: _layout
header:
title: Top
description: これはホゲのサイトです。
keyword: ホゲ, イケてるサイト, いいかんじのサイト
css: []
js: []
---
<p><%= header.title %></p>
何やってんのかサッパリわからんって人の為に自分の中での落とし込みも兼ねてgulpfileを主軸に上から順番に流れを見ていきましょう!
gulpタスク内は同期処理なので上から流れていくので順番を追って読めるのが素敵なところですね!
1. plumber()
ここでまずタスク内でエラーが出ていないかのチェックをしてくれています。
通常タスク実行中にエラーが出てしまうとパイプラインの途中で処理が止まってしまいますが、plumber(プラマー)を挟んであげることで普通は出ないようなエラーを細かく出してくれたり、エラーが出ても処理を止めずに最後まで実行してくれます。
余談ですがpipe
は配管、plumber
は配管工という意味だそうです。
2. frontMatter()
ejsの中にあるFront-matterを切り取って、data
という変数にオブジェクトを格納しています。
試しに_layout.ejsの中で<% console.log(file.data); %>
と入れてあげるとindex.ejs
で定義したFront-matterの変数オブジェクトがターミナル上で確認できるかと思います!
※ここで注意しておきたいのは、_layout.ejsとぺージ固有のejsとではFront-matterの取得方法が違うのです。
例えばの上記の'ホゲ, イケてるサイト, いいかんじのサイト'
を取得したいってなった時、_layout.ejsからhfile.data.header.keyword
で取得できるんですが、index.ejsのようなぺージ固有のejsからはheader.keyword
でないとアクセスできないんです。
3. ejs()
frontMatterで生成した変数のオブジェクトは各ejsのローカル変数として定義されているので、フラグに使ったりテキストを埋め込んだりと好きなように使用することができます。
4. wrapper()
最後は一番重要な所ですね。
ここでは今まで流れてきたパイプラインのデータを文字通りラッピングしてくれます!
オプションもいくつかあるんで細かく見ていきましょう!
① layout
ここでは_layout.ejsが格納されているディレクトリを選択します。
直接_layout.ejsを指定しないのは今後別パターンのlayoutを使うという話が出てきた時に柔軟に対応できるようにする為なのかなと思っています。
そしてindex.ejs側でどのlayoutを使うかを指定してあげればOKです。
② data
ここでもFront-matterと同様変数を定義できます。
ただ役割がFront-matterと被っていることと、_layout.ejsにしか向けられない変数なんであまりアクティブに使うことはないかなぁなんて思ってます。
以下の場合name
に値が入っていきます。
data: {
name: 'ホゲのサイト'
},
<title><%= file.data.header.title %> - <%= name %></title>
③ engine
ここでは何のテンプレートエンジンを使うかを指定してあげます。
デフォルトではloadshが入ってるのでお好みのテンプレートエンジンで試してみましょう!
④ frontMatterProp
やってる事としては先述の2. frontMatterと同じでejsのFront-matterを取得しています。
なんで同じ事やってるのかというと2. front-matterではlayoutではないejsにしか変数が入っていきません。
_layout.ejsにFront-matterを読み込ませるにはここでdata
という変数にして読み込ませてあげる必要があるのです。
ここまでで何も問題がなければうまく./htdocs
にindex.htmlが生成されているはずです!
一見重たい処理のように感じますが、自分の扱っているプロダクトでは約60P分を一度に処理したりもしてますが、ストレスフリーで仕事できてます笑
お知らせ用のJSONをejsに組み込む
まずお知らせ用のjsonを用意します。
{
"newsList": [
{
"date": "19XX年",
"text": "世界は核の炎に包まれた",
"url": "http://www.hokuto-no-ken.jp/series/hokutonoken.html"
}
]
}
そしたらこのJSONをejsの中に読み込ませてあげる必要があります。
最初はejsの中でJSONをrequireできないかなーなんて目論んでたんですけれど結論無理という事が判明したんで別の方法で試してみました!
gulpfile.jsからrequireする作戦
なんでgulpfileからなんだと思いましたがどうもこのgulpfile.jsと_layout.ejsはglobalオブジェクトで繋がってるそうです!(JSの見識が非常に広い方から教わりました🙏)
globalオブジェクトとは所謂JSで言うところのwindowオブジェクトです。(globalオブジェクトはNode.js)
なのでgulpfile.jsからrequireしてきて、同じNode.js空間で繋がっている_layout.ejsに値を渡してあげることができるんです!すごい!
const path = {
ejs: {
src: [
'./src/ejs/**/*.ejs',
'!./src/ejs/**/_*.ejs'
],
dist: './htdocs/'
},
json; {
newsList: './src/data/newsList.json'
}
};
global.jsonData = {};
jsondata.newsList = require(path.newsList);
こういうかんじで名前空間にJSONを渡してあげれば_layout.ejsからでもindex.ejsからでも取得できます!
あとはejs側でforEachとかで回してあげれば見事お知らせが入ったHTMLが生成されます。
つまづいた所
最初は爆速テンプレートエンジンで噂のectを使って運用してみようと思ったんですけれど、中々うまくできなかったので使い慣れているejsでやってみました。
気になる点
_layout.ejsとindex.ejsでのFront-matterの取得方法が違う
例えばfile.data.〜
ってかんじでどっちも取得できるならごちゃごちゃせずに分かりやすいなぁなんて思いました。
globalオブジェクトにJSONをくっつけるのがあまり正しい気がしない
現状これしか方法がないと思ってるんですが、やはりどうもしっくりこない…
ajaxでJSON読み込んでJSでHTML吐き出せばいいじゃないかって話なんでしょうけど今回はEJSからHTMLを生成した時点でJSONの中身がHTMLにある事にこだわってみました。
何かいい案があったら教えていただきたいです。
さいごに
今回この作業するにあたって結構色んな人に助けていただきました!
おかげ様で自分の中での知見が広がったと実感しています。
ありがとうございました!
ソースはGitHubにあげてあるので試したい方はクローンしてみてくださいー!^^