どうも上かるびです。
以前、gulpの設定について下記の記事を書いたのですが、導入しようとしてくださった方から「上手くいきません」というご連絡をいただきました。

ごめんやで。
改めて見てみるとNode.jsのバージョンなど記載していなかったので、もっと分かりやすく&詳しく書き直そうと思い立ちました。

せっかくなのでテンプレートエンジンのEJSを導入してみました。お好みに合わせてお使いください。
今回の環境でgulpを走らせると構築されるサイト↓
本記事を読むのにおすすめの方
- gulpの導入は理解したけれど、どのようなセッティングにしようか迷っている方
- gulp使ってるけど、他人のgulpも見てみたい
- EJS対応のgulpを使いたい
逆に以下に当てはまる方は、本記事を読まずにもっと有益な記事を探すことをおすすめします。
- gulpを卒業して今はwebpackを使っている
- 普段のweb制作ではVueやReactなどのフレームワークを使っている
本記事のgulpでやること・やらないこと
やること
- ejsファイルのコンパイル・整形・コメントの削除
- scssファイルのコンパイル(ベンダープレフィックス付与、メディアクエリをまとめる)・圧縮
- jsの変換(Babel)・圧縮
- 画像(jpg,png,gif,svg)の圧縮
- Webp形式への変換
- ローカルサーバーで確認&自動反映
- コンパイル・圧縮しないファイルのコピー(外部ライブラリのスタイルシートやスクリプト、ローカルフォントなど)
- 出力先のフォルダの削除
やらないこと
- scss,cssのプロパティのソート(記述順序の変更)
- jsファイルのバンドル
- 出力先のフォルダ、ファイルを自動で削除
- FTPを使用した自動アップロード
- WordPress関連の処理
Gulp×EJS×Sass環境構築について
まずはディレクトリ構造やコードを一気に公開します。
そのあとに解説や注意事項を入れていきます。

すぐに使わしてくれぃ!禁断症状が出ちまうぜぃ!という方は目次より「ダウンロード・インストール」の項目に飛んでください。
ディレクトリ構造
ejsの動作確認のため、aboutという下層ページを設けております。
またその他CSSやJavaScriptのコンパイル確認のため、無理やりgridを使ったり、jQueryをCDNで読み込んでいたりします。実務に取り入れる際は、ある程度カスタマイズすることをおすすめします。
gulp.js
基本的にはコメントアウトで説明を入れています。
※ejsコンパイル前に出力先を削除する処理でエラーを見つけたので、その処理は一旦外しております。(2021年10月13日)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
// gulpコマンドの省略 const { src, dest, watch, lastRun, series, parallel } = require('gulp'); // EJS const fs = require('fs'); //Node.jsでファイルを操作するための公式モジュール const htmlMin = require('gulp-htmlmin'); const prettify = require('gulp-prettify'); const ejs = require('gulp-ejs'); const rename = require('gulp-rename'); const replace = require('gulp-replace'); // Sass const sass = require('gulp-dart-sass'); const notify = require('gulp-notify'); const plumber = require('gulp-plumber'); const postCss = require('gulp-postcss'); const autoprefixer = require('autoprefixer'); const gcmq = require('gulp-group-css-media-queries'); const cssNano = require('gulp-cssnano'); // JavaScript const babel = require('gulp-babel'); const terser = require('gulp-terser'); //ES6(ES2015)の圧縮に対応 // 画像圧縮 const imageMin = require('gulp-imagemin'); const pngQuant = require('imagemin-pngquant'); const mozJpeg = require('imagemin-mozjpeg'); const svgo = require('gulp-svgo'); const webp = require('gulp-webp'); //webpに変換 // ブラウザ同期 const browserSync = require('browser-sync').create(); // 削除 const clean = require('gulp-clean'); //パス設定 const paths = { ejs: { src: ['./src/ejs/**/*.ejs', '!./src/ejs/**/_*.ejs'], dist: './public/', watch: './src/ejs/**/*.ejs', }, styles: { src: './src/scss/**/*.scss', copy: './src/css/vendors/*.css', dist: './public/assets/css/', distCopy: './public/assets/css/vendors/', }, scripts: { src: ['./src/js/**/*.js', '!./src/js/**/vendors/*.js'], //外部のライブラリファイルはコンパイルしない copy: './src/js/**/vendors/*.js', dist: './public/assets/js/', }, images: { src: './src/images/**/*.{jpg,jpeg,png,gif,svg}', srcWebp: './src/images/**/*.{jpg,jpeg,png}', dist: './public/assets/images/', distWebp: './public/assets/images/webp/', }, fonts: { src: './src/fonts/**/*.{off,ttf,woff,woff2}', dist: './public/assets/fonts/', }, clean: { all: './public/', assets: ['./public/assets/css/', './public/assets/js/'], html: './public/!(assets)**', css:'./public/assets/css/', js:'./public/assets/js/', images: './public/assets/images/', fonts: './public/assets/fonts/', }, }; // ejsコンパイル const ejsCompile = () => { // ejsの設定を読み込む const data = JSON.parse(fs.readFileSync('./ejs-config.json')); return src(paths.ejs.src) .pipe( plumber({ // エラーがあっても処理を止めない errorHandler: notify.onError('Error: <%= error.message %>'), }) ) .pipe(ejs(data)) //ejsをまとめる .pipe( rename({ extname: '.html', }) ) .pipe( htmlMin({ //圧縮時のオプション removeComments: true, //コメントを削除 collapseWhitespace: true, //余白を詰める collapseInlineTagWhitespace: true, //inline要素のスペース削除(spanタグ同士の改行などを詰める preserveLineBreaks: true, //タグ間の余白を詰める /* *オプション参照:https://github.com/kangax/html-minifier */ }) ) .pipe( prettify({ //整形 indent_with_tabs: true, //スペースではなくタブを使用 indent_size: 2, }) ) .pipe(replace(/[\s\S]*?(<!DOCTYPE)/, '$1')) .pipe(dest(paths.ejs.dist)) .pipe(browserSync.stream()); //変更があった所のみコンパイル }; // Sassコンパイル const sassCompile = () => { return ( src(paths.styles.src, { // ソースマップの出力の有無 sourcemaps: true, }) .pipe( plumber({ // エラーがあっても処理を止めない errorHandler: notify.onError('Error: <%= error.message %>'), }) ) // scss→cssコンパイル .pipe( sass({ //出力時の形式(下記詳細) /* *https://utano.jp/entry/2018/02/hello-sass-output-style/ */ outputStyle: 'compressed', }).on('error', sass.logError) ) .pipe( postCss([ autoprefixer({ //ベンダープレフィックス追加※設定はpackage.jsonに記述 cascade: false, // プロパティのインデントを整形しない grid: 'autoplace', // IE11のgrid対応 }), ]) ) //メディアクエリをまとめる .pipe(gcmq()) //圧縮 .pipe(cssNano()) .pipe( dest(paths.styles.dist, { // ソースマップを出力する場合のパス sourcemaps: './map', }) ) //変更があった所のみコンパイル .pipe(browserSync.stream()) ); }; // JavaScriptコンパイル const jsCompile = () => { return src(paths.scripts.src) .pipe( plumber({ // エラーがあっても処理を止めない errorHandler: notify.onError('Error: <%= error.message %>'), }) ) .pipe( babel({ presets: ['@babel/preset-env'], }) ) .pipe(terser()) //圧縮 .pipe(dest(paths.scripts.dist)); }; // 画像圧縮 const imagesCompress = () => { return src(paths.images.src, { since: lastRun(imagesCompress), }) .pipe( plumber({ // エラーがあっても処理を止めない errorHandler: notify.onError('Error: <%= error.message %>'), }) ) .pipe( imageMin( [ mozJpeg({ quality: 80, //画質 }), pngQuant( [0.6, 0.8] //画質の最小,最大 ), ], { verbose: true, //メタ情報削除 } ) ) .pipe( svgo({ plugins: [ { removeViewbox: false, //フォトショやイラレで書きだされるviewboxを消すかどうか※表示崩れの原因になるのでfalse推奨。以降はお好みで。 }, { removeMetadata: false, //<metadata>を削除するかどうか }, { convertColors: false, //rgbをhexに変換、または#ffffffを#fffに変換するかどうか }, { removeUnknownsAndDefaults: false, //不明なコンテンツや属性を削除するかどうか }, { convertShapeToPath: false, //コードが短くなる場合だけ<path>に変換するかどうか }, { collapseGroups: false, //重複や不要な`<g>`タグを削除するかどうか }, { cleanupIDs: false, //SVG内に<style>や<script>がなければidを削除するかどうか }, // { // mergePaths: false,//複数のPathを一つに統合 // }, ], }) ) .pipe(dest(paths.images.dist)); }; // webp変換 // ※案件によってはIE対応が必要なので、混在しないように別フォルダに出力しています。 const webpConvert = () => { return src(paths.images.srcWebp, { since: lastRun(webpConvert), }) .pipe( plumber({ // エラーがあっても処理を止めない errorHandler: notify.onError('Error: <%= error.message %>'), }) ) .pipe(webp()) .pipe(dest(paths.images.distWebp)); }; // CSSファイルコピー(vendorsフォルダの中身はコンパイルしない const cssCopy = () => { return src(paths.styles.copy).pipe(dest(paths.styles.distCopy)); }; // JSファイルコピー(vendorsフォルダの中身はコンパイルしない const jsCopy = () => { return src(paths.scripts.copy).pipe(dest(paths.scripts.dist)); }; // fontコピー(何もせずにコピーする const fontsCopy = () => { return src(paths.fonts.src).pipe(dest(paths.fonts.dist)); }; // ローカルサーバー起動 const browserSyncFunc = (done) => { browserSync.init({ //デフォルトの connected のメッセージ非表示 notify: false, server: { baseDir: './', }, startPath: './public/index.html', reloadOnRestart: true, }); done(); }; // ブラウザ自動リロード const browserReloadFunc = (done) => { browserSync.reload(); done(); }; // ファイル削除 // ----------------------- // public 内をすべて削除 function cleanAll(done) { src(paths.clean.all, { read: false }).pipe(clean()); done(); } // HTML フォルダ、ファイルのみ削除( assets 以外削除) function cleanHtml(done) { src(paths.clean.html, { read: false }).pipe(clean()); done(); } //public 内の CSS と JS を削除 function cleanCssJs(done) { src(paths.clean.assets, { read: false }).pipe(clean()); done(); } //public 内の画像を削除 function cleanImages(done) { src(paths.clean.images, { read: false }).pipe(clean()); done(); } //public 内の fonts を削除 // function cleanFonts(done) { // src(paths.clean.fonts, { read: false }).pipe(clean()); // done(); // } // ファイル監視 const watchFiles = () => { watch(paths.ejs.watch, series(cleanHtml,ejsCompile, browserReloadFunc)); watch(paths.styles.src, series(sassCompile)); watch(paths.styles.copy, series(cssCopy)); watch(paths.scripts.src, series(jsCompile, browserReloadFunc)); watch(paths.scripts.copy, series(jsCopy, browserReloadFunc)); watch( paths.images.src, series(imagesCompress, webpConvert, browserReloadFunc) ); watch(paths.fonts.src, series(fontsCopy, browserReloadFunc)); }; // npx gulp のコマンドで実行される処理 exports.default = series( parallel( ejsCompile, sassCompile, cssCopy, jsCompile, jsCopy, imagesCompress, webpConvert, fontsCopy ), parallel(watchFiles, browserSyncFunc) ); // その他のコマンド 例: npx gulp cleanAll の形で入力 exports.cleanAll = series(cleanAll); exports.cleanExcludeHtml = series(cleanHtml);//assets以外削除 exports.cleanCssJs = series(cleanCssJs); exports.cleanImages = series(cleanImages); |
出力先のファイル削除について
gulpはsrcからファイルを削除しても、public(出力先)からは消えません。
アップするときや納品する際は余計なファイルを削除したいので、直前にcleanAllですべて削除してまたgulpを走らせる、みたいな処理を行います。
ただ今回、HTMLファイルに関しては、コンパイル前にHTMLのフォルダとファイル(assets以外)を削除する処理をしています。
cssやjs、imageやfontsに関しても同様にしたい場合は、HTMLの処理を参考にカスタマイズしてみてください。(もし分からなかったらご相談ください。)
EJSのディレクトリ構造について
今回のgulpではEJS初心者が分かりやすいように、共通パーツをすべて「includes」フォルダに入れております。
こちらはお好みで「components」や「laylouts」など、CSS設計やフレームワークチックにカスタマイズしてもよいと思います。
Sassのコンパイルについて
今回はコンパイルのオプションでcompressedを使っています。ただ納品後に他の誰かがCSSファイルを編集する場合は、expandedなど他のオプションを指定することをおすすめします。
またメディアクエリをまとめる際に、多くの方がcss-mqpackerを使っていますが、こちらは非推奨とされています。
ですので代わりにgulp-group-css-media-queriesを使ってます。
ただ残念ながらこちらのパッケージを使用すると、なぜか自動で整形されてしまいます。
私の場合、基本的に納品後に先方が修正することはない案件ばかりなので、最後にCSSを圧縮するcssNano()という処理を走らせています。
もしコンパイルオプションをexpandedなどにしている場合は、こちらのcssNano()の処理は削除またはコメントアウトすれば圧縮されません。
ダウンロード・インストール
①下記ファイルをダウンロード、解凍します。
②解凍したフォルダをVSCodeで開き、ターミナルで「npm i」のコマンド実行

今回の環境を試しに使ってみて、記述がおかしい、動作がおかしいなどありましたら、お気軽にコメントしていただければと思います。

レビューみたいな感じもドンと来い!
コメント
質問失礼いたします・・
dart-sass使用でパーシャルファイルに変更を加えた時にもコンパイルされるようにしたいのですが、gulpはどのように書く必要がありますでしょうか。
ご教示いただけますと幸いです。
記事を読んでいただきありがとうございます。
watchでsrcの中のscssフォルダ全体を監視していると思うので、特に何もせずパーシャルファイル変更時にもコンパイルが走るようにしています。
かえる様のディレクトリがどのような構造になっているのか分からないので何とも言えませんが、何らかの理由で上手くいかない場合はパスの部分を変更する必要があると思います。