どうも、Tommy(@SitommyBlog)です。
Webサイトを制作するにあたって、テンプレートエンジンを使用することがあります。
そのときに、私自身が使用しているテンプレートエンジンはEJSを採用しております。
EJS以外にも、テンプレートエンジンはありますがマークアップエンジン上がりの私にはマークダウン方式を採用しているPugよりhtmlの書き方と同じように記述できるEJSの方が楽かなっと思っています。
案件で、Pugを使ったものが出てきた際にはそちらも勉強しようかなっとは思っております。
決して、EJSの方が優れているとか競った考えはしておりません。
話を戻すと、私がWebサイトを構築する際に以下のものを自動化してくれるようにしてます。
- EJSからHTMLに
- SCSSからCSSに
- JSをmin化
- imageの圧縮
- JSONファイルの使用
これらを自動化するために、タスクランナーGulpを使用しています。
その設定(gulpfile.js)を紹介したいと思います。
gulpfile.js設定
const { src, dest, watch, lastRun, parallel } = require('gulp');
const sass = require("gulp-sass"); // Sassを使う
const glob = require("gulp-sass-glob"); // sassのimportを楽にする
const postcss = require("gulp-postcss"); // Autoprefixと一緒に使うもの
const autoprefixer = require("autoprefixer"); // Autoprefix
const plumber = require("gulp-plumber"); // エラーでも強制終了させない
const notify = require("gulp-notify"); // エラーのときはデスクトップに通知
const replace = require('gulp-replace'); // 文字列置換をする
const cmq = require('gulp-combine-media-queries'); // メディアクエリー
const cleanCSS = require("gulp-clean-css"); // cssの圧縮
const del = require("del"); // ファイル、ディレクトリの削除
const minifyHTML = require('gulp-minify-html'); // htmlの圧縮
const ejs = require("gulp-ejs"); // EJS(htmlのパーツ化)
const rename = require("gulp-rename"); // ejsの拡張子を変更
const concat = require("gulp-concat"); // ファイルの結合
const order = require("gulp-order"); // 指定した順番で並べる
const uglify = require("gulp-uglify"); // script圧縮用
const pngquant = require("imagemin-pngquant"); // 画像圧縮
const imagemin = require("gulp-imagemin"); // 上と同じ
const mozjpeg = require("imagemin-mozjpeg"); // 上と同じ
const browserSync = require("browser-sync"); // ブザウザ読み込み・反映
const fs = require('fs');//jsonファイルを使用するときに必要
const mode = require('gulp-mode')({
modes: ["production", "development"],
default: "development",
verbose: false
});
// 読み込むパスと出力するパスを指定
const srcPath = {
html: {
src: ["./src/ejs/**/*.ejs", "!" + "./src/ejs/**/_*.ejs"],
dist: "./dist/"
},
json: {
src: ["./src/**/*.json"]
},
styles: {
src: "./src/scss/**/*.scss",
dist: "./dist/css/",
map: "./dist/css/map"
},
scripts: {
src: "./src/js/**/*.js",
dist: "./dist/js/",
map: "./dist/js/map",
core: "src/js/core/**/*.js",
app: "src/js/app/**/*.js"
},
images: {
src: "./src/img/**/*.{jpg,jpeg,png,gif,svg}",
dist: "./dist/img/"
}
};
/// htmlの処理自動化 ////////////////////////////////////////////
const htmlFunc = () => {
const json01 = JSON.parse(fs.readFileSync('./src/json/pages.json', 'utf8')),
json_arr = { pages: json01 };
return src(srcPath.html.src)
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(ejs(json_arr))
.pipe(rename({ extname: ".html" }))
.pipe(
mode.production(
minifyHTML({ empty: true })
)
)
.pipe(replace(">\n\n", ">\n"))
.pipe(replace(/[\s\S]*?(<!DOCTYPE)/, "$1"))
.pipe(dest(srcPath.html.dist))
.pipe(browserSync.reload({ stream: true }));
};
/// Scssの処理自動化 ////////////////////////////////////////////
// 開発用
const stylesFunc = () => {
return src(srcPath.styles.src, { sourcemaps: true })
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(glob())
.pipe(sass({ outputStyle: "expanded" }).on("error", sass.logError))
.pipe(
postcss([
autoprefixer({
cascade: false,
grid: true
})
])
)
.pipe(cmq())
.pipe(replace("}\n\n", "}\n"))
.pipe(dest(srcPath.styles.dist, { sourcemaps: "./map" }))
.pipe(browserSync.reload({ stream: true }));
};
// 本番用
const stylesCompress = () => {
return src(srcPath.styles.src)
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(glob())
.pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError))
.pipe(
postcss([
autoprefixer({
cascade: false,
grid: true
})
])
)
.pipe(cmq())
.pipe(cleanCSS())
.pipe(dest(srcPath.styles.dist))
.pipe(browserSync.reload({ stream: true }));
};
/// scriptの処理自動化 ////////////////////////////////////////////
const scriptFunc = () => {
return src(srcPath.scripts.src, { sourcemaps: true })
.pipe(order([srcPath.scripts.core, srcPath.scripts.app], { base: "./" }))
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(concat("init.js"))
.pipe(uglify({ output: { comments: /^!/ } }))
.pipe(
rename({
suffix: ".min"
})
)
.pipe(dest(srcPath.scripts.dist, { sourcemaps: "./map" }))
.pipe(browserSync.reload({ stream: true }));
};
/// 画像圧縮の処理自動化 ////////////////////////////////////////////
const imagesBase = [
pngquant({
quality: [0.7, 0.85]
}),
mozjpeg({
quality: 80
}),
imagemin.gifsicle(),
imagemin.mozjpeg(),
imagemin.optipng(),
imagemin.svgo({
removeViewBox: false
})
];
const imagesFunc = () => {
return src(srcPath.images.src, { since: lastRun(imagesFunc) })
.pipe(plumber({ errorHandler: notify.onError("<%= error.message %>") }))
.pipe(imagemin(imagesBase))
.pipe(dest(srcPath.images.dist));
};
/// マップファイル除去 ////////////////////////////////////////////
const cleanMap = () => {
return del([srcPath.styles.map, srcPath.scripts.map]);
};
/// ブラウザの読み込み処理 ////////////////////////////////////////////
const browserSyncFunc = () => {
browserSync({
port: 8080,
server: {
baseDir: "./dist/",
index: "index.html"
},
reloadOnRestart: true
});
};
/// 監視フォルダ ////////////////////////////////////////////
const watchFiles = () => {
watch(srcPath.html.src, htmlFunc);
watch(srcPath.json.src, htmlFunc);
watch(srcPath.styles.src, stylesFunc);
watch(srcPath.scripts.src, scriptFunc);
watch(srcPath.images.src, imagesFunc);
};
exports.default = parallel(imagesFunc, watchFiles, browserSyncFunc);
exports.build = parallel(htmlFunc, stylesFunc, scriptFunc, imagesFunc);
exports.scsscompress = stylesCompress;
exports.cleanmap = cleanMap;
2年弱ほどgulpを使っていろいろ試行錯誤してきましたが、これが今の私の2020年版ベストgulpfile.jsかなっと思っております。
と言いつつすぐにバージョンアップする可能性もありますが今回はこちらを紹介できればと思います。
上から順に説明していきます。
変数エリア
const { src, dest, watch, lastRun, parallel } = require('gulp');
const sass = require("gulp-sass"); // Sassを使う
const glob = require("gulp-sass-glob"); // sassのimportを楽にする
const postcss = require("gulp-postcss"); // Autoprefixと一緒に使うもの
const autoprefixer = require("autoprefixer"); // Autoprefix
const plumber = require("gulp-plumber"); // エラーでも強制終了させない
const notify = require("gulp-notify"); // エラーのときはデスクトップに通知
const replace = require('gulp-replace'); // 文字列置換をする
const cmq = require('gulp-combine-media-queries'); // メディアクエリー
const cleanCSS = require("gulp-clean-css"); // cssの圧縮
const del = require("del"); // ファイル、ディレクトリの削除
const minifyHTML = require('gulp-minify-html'); // htmlの圧縮
const ejs = require("gulp-ejs"); // EJS(htmlのパーツ化)
const rename = require("gulp-rename"); // ejsの拡張子を変更
const concat = require("gulp-concat"); // ファイルの結合
const order = require("gulp-order"); // 指定した順番で並べる
const uglify = require("gulp-uglify"); // script圧縮用
const pngquant = require("imagemin-pngquant"); // 画像圧縮
const imagemin = require("gulp-imagemin"); // 上と同じ
const mozjpeg = require("imagemin-mozjpeg"); // 上と同じ
const browserSync = require("browser-sync"); // ブザウザ読み込み・反映
const fs = require('fs');//jsonファイルを使用するときに必要
const mode = require('gulp-mode')({
modes: ["production", "development"],
default: "development",
verbose: false
});
上記は、Gulpプラグインをインストールしたものを変数に置いています。
const srcPath = {
html: {
src: ["./src/ejs/**/*.ejs", "!" + "./src/ejs/**/_*.ejs"],
dist: "./dist/"
},
json: {
src: ["./src/**/*.json"]
},
styles: {
src: "./src/scss/**/*.scss",
dist: "./dist/css/",
map: "./dist/css/map"
},
scripts: {
src: "./src/js/**/*.js",
dist: "./dist/js/",
map: "./dist/js/map",
core: "src/js/core/**/*.js",
app: "src/js/app/**/*.js"
},
images: {
src: "./src/img/**/*.{jpg,jpeg,png,gif,svg}",
dist: "./dist/img/"
}
};
こちらは読み込むパスと出力するパスを事前に指定しています。
srcが読み込みをするパスで、distが出力するパスとしています。
mapは、sourcemapを出力するパスです。
coreやappに関しては、後ほどJSの部分で説明しますがそれぞれ役割をもっとフォルダーを用意して管理しやすいようにしています。
HTMLの処理自動化(JSONファイル読み込み)
const htmlFunc = () => {
const json01 = JSON.parse(fs.readFileSync('./src/json/pages.json', 'utf8')),
json_arr = { pages: json01 };
return src(srcPath.html.src)
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(ejs(json_arr))
.pipe(rename({ extname: ".html" }))
.pipe(
mode.production(
minifyHTML({ empty: true })
)
)
.pipe(replace(">\n\n", ">\n"))
.pipe(replace(/[\s\S]*?(<!DOCTYPE)/, "$1"))
.pipe(dest(srcPath.html.dist))
.pipe(browserSync.reload({ stream: true }));
};
こちらがEJSをHTMLにコンパイルして、なおかつJSONファイルを使ってEJSと紐付けています。
そして、開発用と本番用で出力方法を分けています。
本番時の書き出しはHTMLをmin化にさせる用に設定しています。
min化することでいろいろ不都合が起きることもあります。
例えば、クライアント側で更新などを目的としている場合はmin化にすると更新が難しくなったりやりにくくなったりしますのでmin化はしません。
JSONファイルをなぜ使用するのかと言うと、metaデータなどページごとに設定する必要があるものページごとに管理すると修正や更新がとてもめんどくさくなります。
でも、JSONファイルで一括管理すればJSONファイルだけ開いて作業することで効率がいいのです。
ここでは、詳しい説明は省かせてもらいますがJSONファイルを使用するのとしないのとでは作業が全然違います。
使用するためには、EJSの方でも書き方はありますがこちらは別途記事にしたいと思います。
Scssの処理自動化
こちらも、開発用と本番用があります。
// 開発用
const stylesFunc = () => {
return src(srcPath.styles.src, { sourcemaps: true })
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(glob())
.pipe(sass({ outputStyle: "expanded" }).on("error", sass.logError))
.pipe(
postcss([
autoprefixer({
cascade: false,
grid: true
})
])
)
.pipe(cmq())
.pipe(replace("}\n\n", "}\n"))
.pipe(dest(srcPath.styles.dist, { sourcemaps: "./map" }))
.pipe(browserSync.reload({ stream: true }));
};
// 本番用
const stylesCompress = () => {
return src(srcPath.styles.src)
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(glob())
.pipe(sass({ outputStyle: "compressed" }).on("error", sass.logError))
.pipe(
postcss([
autoprefixer({
cascade: false,
grid: true
})
])
)
.pipe(cmq())
.pipe(cleanCSS())
.pipe(dest(srcPath.styles.dist))
.pipe(browserSync.reload({ stream: true }));
};
html同様に出し分けをしています。
本番用は開発用と違って、コンパイルされたCSSをmin化することです。
こちらも状況によってはmin化せず納品する場合はあります。
scriptの処理自動化
const scriptFunc = () => {
return src(srcPath.scripts.src, { sourcemaps: true })
.pipe(order([srcPath.scripts.core, srcPath.scripts.app], { base: "./" }))
.pipe(
plumber({ errorHandler: notify.onError("Error: <%= error.message %>") })
)
.pipe(concat("init.js"))
.pipe(uglify({ output: { comments: /^!/ } }))
.pipe(
rename({
suffix: ".min"
})
)
.pipe(dest(srcPath.scripts.dist, { sourcemaps: "./map" }))
.pipe(browserSync.reload({ stream: true }));
};
こちらはJSファイルの自動化に当たります。
上記で少しお伝えしたcoreとappについてですが、coreに関してはjQuery本体を入れる場所、appはプラグインを入れる場所として管理しています。
順番もjQuery本体・プラグイン・自身で書くJSファイルの順にまとめてmin化してくれるようになります。
画像圧縮の処理自動化
const imagesBase = [
pngquant({
quality: [0.7, 0.85]
}),
mozjpeg({
quality: 80
}),
imagemin.gifsicle(),
imagemin.mozjpeg(),
imagemin.optipng(),
imagemin.svgo({
removeViewBox: false
})
];
const imagesFunc = () => {
return src(srcPath.images.src, { since: lastRun(imagesFunc) })
.pipe(plumber({ errorHandler: notify.onError("<%= error.message %>") }))
.pipe(imagemin(imagesBase))
.pipe(dest(srcPath.images.dist));
};
PSDデータなどで書き出した画像をそのまま使用すると結構重いことがあるので、できる限り圧縮して使用したほうがいいです。
フリーソフトなどで画像圧縮もありますが、自動でやってくれるなら楽なので採用しています。
ただ、デメリットとしてはプロジェクトファイルに画像データが倍の量格納されるということなので、ファイル全体としては重くなります。
マップファイル除去
const cleanMap = () => {
return del([srcPath.styles.map, srcPath.scripts.map]);
};
ソースマップを削除させるために使用しますが、こちらは本番時にのみ処理をします。
ブラウザの読み込み処理
const browserSyncFunc = () => {
browserSync({
port: 8080,
server: {
baseDir: "./dist/",
index: "index.html"
},
reloadOnRestart: true
});
};
gulpを起動した際に、自動でブラウザを立ち上げます。
localhost:8080で立ち上がり、なおかつIPアドレス + 8080と出力できるようになるので開発者としてはとても重宝しています。
IPアドレス + 8080をSPの実機で入力することで、開発の時点から実機確認ができます。
監視フォルダ
const watchFiles = () => {
watch(srcPath.html.src, htmlFunc);
watch(srcPath.json.src, htmlFunc);
watch(srcPath.styles.src, stylesFunc);
watch(srcPath.scripts.src, scriptFunc);
watch(srcPath.images.src, imagesFunc);
};
このwatchで修正・更新等をチェックし都度反映してくれます。
実行するためには
exports.default = parallel(imagesFunc, watchFiles, browserSyncFunc);
exports.build = parallel(htmlFunc, stylesFunc, scriptFunc, imagesFunc);
exports.scsscompress = stylesCompress;
exports.cleanmap = cleanMap;
基本的には開発スタートなので、デフォルトのタスクランナーを走らせます。
gulp
HTMLをmin化して本番用として切り替えるときはこちら。
gulp --prodution
CSSをmin化して本番用として切り替えるときはこちら。
gulp scsscompress
ただ、ファイル更新するのみの場合はこちら。
gulp build
ソースマップを削除する場合はこちら。
本番用として出力する際にこちらをしておきましょう。
gulp cleanmap
Gulpについて
上記のgulpfile設定は、Gulp4の設定構築です。
まだ、Gulp3で作業されている方もいるかと思いますので、事前確認はお願いいたします。
ちなみに確認方法はこちら。
gulp -v
まとめ
プロジェクトによっては、設定を変更していただければとおもいます。
今後は、この設定にJavaScriptの部分をTypeScriptに変更して行ければなっとは思っております。
また、この設定構築をブラッシュアップしていければとも思っております。