[gulp4] ejs, sassの開発環境を作ったので覚書き

#はじめに
gulp4で開発環境を作ったので、全体の流れの覚書きです。
gulpfileのコードは載っけていますが、gulp.jsの詳しい説明は載せてません。

gulpfile設定の時のつまづきメモはこちら。
[[gulp4] gulpfileのつまずきアレコレ](https://qiita.com/nzzzz/items/a04057337301ff4379c1)

###作った環境
- ejsをhtmlに変換、圧縮
- sassをcssに変換、ベンダープレフィックス付与、圧縮、ソースマップ作成
- JSを圧縮、ソースマップ作成、
- 画像を圧縮
- 更新が内容保存されたら、ブラウザを自動でリロードする

ちなみに、作成時の環境はこちらでした。
node v12.16.1
npm v6.13.4

###導入の手順
1. node.jsをインストール
2. パッケージをインストール
3. gulpfile.jsを設定する


#手順１：node.jsをインストール
「作った環境」は、node.jsのパッケージをインストールすることで作れます。
その為にまずはnode.jsをインストールします。

##選択肢①： homebrew,nodebrewでインストール
こちらの記事が参考になりそうです！
[MacにNode.jsをインストール](https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09)

ただ、私はnode.jsはインストール済みでしたが、"nodenv"というものを知り使ってみたかったので「選択肢②」を今回試しました。



##選択肢②：nodenvでインストール
この機会に、プロジェクト毎にnode.jsのバージョンを簡単に切り替えられる**"nodenv"**を入れてみました。

こちらのサイトがわかりやすかったです！
[anyenvとnodenvでNode.jsのバージョン管理をしよう！](https://www.to-r.net/media/anyenv/)

ただ、私の場合、nodenvを使ってプロジェクト用のnode.jsをインストールする際に``nodenv: default-packages file not found``というエラーが出てしまいました。

yarnのインストールと、yarnでdefault-packagesファイルを作る必要があったようです。
``echo yarn >> $NODENV_ROOT/default-packages``を打ったらインストールできる様になりました。


このエラー時に参考にさせていただいたサイトはこちら。
[nodenv install時にdefault-packages file not foundが出た時のメモ](https://7me.oji.0j0.jp/2019/12/21/nodenv-default-packages-file-not-found/)





#手順２：パッケージをインストール
いよいよパッケージをインストールしていく訳なのですが、その前に、どんなものをインストールしたのかを記録するファイル package.json が必要になります。

##手順2-1：package.jsonを生成する
作業ディレクトリに移動して、ターミナルに以下を入力すると、package.jsonが生成されます。

```
npm init -y
```

``-y``オプションをつけると、ファイル名やファイル説明などの設定項目を全て "yes" で処理したこととなり、デフォルト設定になります。
最初にファイル設定をしたい場合は ``npm init`` と入れて、各項目を埋めていけばOKです。
設定は、package.jsonのファイル内容を書き換えることでいつでも編集できます。


##手順2-2：パッケージをインストールする
パッケージインストールのコマンドは以下の通りです。
ターミナル上で色々文字が動き出してちょっと怖いですが、アンインストールも簡単にできるので恐れず入力します。

```
//インストール
npm install --save-dev パッケージ名

//一括インストールしたい時
npm install --save-dev パッケージ名１ パッケージ名2 パッケージ名2

//アンインストール
npm uninstall --save-dev パッケージ名
```
今回使うパッケージは、全て開発環境でのみ使用するので、``--save-dev``オプションをつけます。
他にも、``npm install パッケージ名 --save-dev``、``npm i -D パッケージ名``といった書き方でもOKです。


###メモ： "--save-dev" オプションについて
``--save-dev`` や ``-D`` オプションを付けると、package.json内の **"devDependencies"** 項目に、つけずに ``npm install パッケージ名`` とすると **"dependencies"** 項目に登録されます。

"dependencies"とは、「依存関係」という意味です。
さっくりいうと、こういう感じ。

- devDependencies：開発にのみ使用するパッケージ（gulpなど）
- dependencies：本番環境でも使用するパッケージ （プラグインなど）

##今回インストールするパッケージ一覧
先ほど紹介したコマンドを使用して個別にインストールするか、こちら↓をコピペ。

```
npm install --save-dev gulp gulp-sass gulp-postcss autoprefixer gulp-clean-css gulp-ejs gulp-htmlmin gulp-uglify del gulp-imagemin imagemin-mozjpeg imagemin-svgo imagemin-pngquant imagemin-gifsicle gulp-watch browser-sync
```
パッケージの役割は次の通りです。（クリックで各提供元ページに遷移します）
他にも[gulpの公式サイト](https://gulpjs.com/plugins/)やググると、いろんな便利パッケージを探すことができます。

[gulp](https://www.npmjs.com/package/gulp) :タスクランナー
[gulp-rename](https://www.npmjs.com/package/gulp-rename)  ：ファイル名変更
[gulp-sass](https://www.npmjs.com/package/gulp-scss) :sassをcssに変換
[node-sass-package-importer](https://www.npmjs.com/package/node-sass-package-importer) ：.css拡張子のファイル（sanitize.cssなど）をscssファイルで読み込み可にする
[gulp-postcss](https://www.npmjs.com/package/gulp-postcss) :autoprefixerを使う為に必要
[autoprefixer](https://github.com/postcss/autoprefixer) :ベンダープレフィックスを自動付与する
[gulp-clean-css](https://www.npmjs.com/package/gulp-clean-css/) :cssを圧縮
[gulp-ejs](https://www.npmjs.com/package/gulp-ejs) ：ejsをhtmlに変換
[gulp-htmlmin](https://www.npmjs.com/package/gulp-htmlmin) ：htmlを圧縮
[gulp-babel](https://www.npmjs.com/package/gulp-babel) ：ES6をES5に変換する（IE11が非対応のES6に対応するため）
[gulp-uglify](https://www.npmjs.com/package/gulp-uglify/) ：jsを圧縮
[del](https://github.com/sindresorhus/del) ：ファイルを削除する
[gulp-imagemin](https://www.npmjs.com/package/gulp-imagemin) ：画像圧縮
[imagemin-mozjpeg](https://github.com/imagemin/imagemin-mozjpeg) ：jpg用エンコーダー
[imagemin-svgo](https://github.com/imagemin/imagemin-svgo) ：svg用エンコーダー
[imagemin-pngquant](https://www.npmjs.com/package/imagemin-pngquant) ：png用エンコーダー
[imagemin-gifsicle](https://github.com/imagemin/imagemin-gifsicle) ：gif用エンコーダー
[gulp-watch](https://www.npmjs.com/package/gulp-watch) ：タスクを監視する
[gulp-plumber](https://www.npmjs.com/package/gulp-plumber) ：エラー検知
[gulp-notify](https://www.npmjs.com/package/gulp-notify) ：検知したエラーをデスクトップに通知
[browser-sync](https://www.npmjs.com/package/browser-sync) ：ファイル保存時にブラウザを自動でリロード

インストールされたら、package.jsonの"devDependencies"項目に、パッケージ名とバージョンがずらっと並んでるはず。


#手順３：gulpfile.jsを設定する
インストールしたパッケージは、gulpfile.jsを設定することで使えるようになります。

##手順3-1： gulpfileを記載する
[gulpの公式サイト](https://gulpjs.com/docs/en/getting-started/quick-start)や、パッケージ提供元の説明を見ながら記載していきます。

出来上がったのはこんな感じ。

```JS:gulpfile.js
const gulp = require('gulp'); //gulp
const rename = require('gulp-rename'); //名前変更
const sass = require('gulp-sass'); //sassコンパイル
const packageImporter = require('node-sass-package-importer'); //cssパッケージのimport用
const postcss = require('gulp-postcss'); //autoprefixerを使うのに必要
const autoprefixer = require('autoprefixer'); //prefixをつける
const cleanCss = require('gulp-clean-css'); //css圧縮
const ejs = require('gulp-ejs'); //ejsコンパイル
const htmlmin = require('gulp-htmlmin'); //html圧縮と整理
const babel = require('gulp-babel'); //jsトランスパイル
const uglify = require('gulp-uglify'); //js圧縮
const del = require('del'); //フォルダやファイルを削除
const imagemin = require('gulp-imagemin'); //画像圧縮
const mozjpeg = require('imagemin-mozjpeg') //jpgエンコード
const imageminSvgo = require('imagemin-svgo'); //svgエンコード
const imageminOptipng = require('imagemin-pngquant'); //pngエンコード
const imageminGifsicle = require('imagemin-gifsicle'); //gifエンコード
const gulpWatch = require('gulp-watch'); //ファイル監視と実行
const plumber = require('gulp-plumber'); //エラーチェック
const notify = require('gulp-notify'); //エラーをデスクトップに通知
const browserSync = require('browser-sync').create(); //ブラウザシンク


//取得元、出力先のパス
const pathOrigin = {
  dest: './dest',
  dev: './src'
}
const paths = {
  html: {
    src: pathOrigin.dev + '/views/*.ejs',
    tempSrc: pathOrigin.dev + '/views/**/_*.ejs',
    dest:  pathOrigin.dest,
    map: '/map',
  },
  styles: {
    src: pathOrigin.dev + '/scss/*.scss',
    dest: pathOrigin.dest + '/css',
    tempSrc: pathOrigin.dev + '/scss/**/_*.scss',
    map: '/map',
    clearmap: pathOrigin.dest + '/css/map',
  },
  scripts: {
    src: pathOrigin.dev + '/js/*.js',
    dest: pathOrigin.dest + '/js',
    map: '/map',
    clearmap: pathOrigin.dest + '/js/map/',
  },
  images: {
    src: pathOrigin.dev + '/assets/*.{jpg,jpeg,png,svg,gif}',
    dest: pathOrigin.dest + '/assets',
  }
}
const srcpathAll = [paths.html.src, paths.html.tempSrc, paths.styles.src, paths.styles.tempSrc, paths.scripts.src, paths.images.src];


//画質コントロール
const imageQuality = {
  jpg: 60,
  png: [0.3, 0.5], //minimum, maximum
  svg: 60,
  gif: 1, //1 or 3, 1 is default
}

// sass
function stylesFunc() {
  return gulp
    .src(paths.styles.src, {sourcemaps: true, since: gulp.lastRun(stylesFunc)})
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(postcss([ autoprefixer({
      grid: 'autoplace', //gridレイアウトのプレフィックスを有効にする
    }) ]))
    .pipe(cleanCss())
    .pipe(gulp.dest(paths.styles.dest, { sourcemaps: paths.styles.map }));
}

// EJS
function htmlFunc() {
  return gulp
    .src([paths.html.src, '!' + paths.html.tempSrc], {since: gulp.lastRun(htmlFunc) })
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(ejs())
    .pipe(rename({extname:'.html'}))
    .pipe(htmlmin({
      collapseWhitespace : true, //余白を削除
      removeComments : true,　//コメントを削除
      removeRedundantAttributes: true //default値の属性を削除
    }))
    .pipe(gulp.dest(paths.html.dest))
}

// JS
function scriptFunc() {
  return gulp
    .src(paths.scripts.src, { sourcemaps: true })
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(babel({
      presets: ['@babel/env']
    }))
    .pipe(uglify())
    .pipe (gulp.dest(paths.scripts.dest, { sourcemaps: paths.styles.map, since: gulp.lastRun(scriptFunc)}));
}

//画像処理
function imageFunc() {
  return gulp
  .src([paths.images.src], {since: gulp.lastRun(imageFunc)})
    .pipe(imagemin([
      mozjpeg({quality: imageQuality.jpg}),
      imagemin.optipng({optimizationLevel: imageQuality.png}),
      imagemin.svgo({removeViewBox: false}),
      imagemin.gifsicle({optimizationLevel: imageQuality.gif}),
    ]))
    .pipe(gulp.dest(paths.images.dest));
}

// サーバーを立ち上げる
function destServer(done) {
  browserSync.init({
      server: {
          baseDir: pathOrigin.dest,
          index  : "index.html"
      },
      reloadOnRestart: true,
  });
  done();
}

// ブラウザのリロード
function browserReload(done) {
  browserSync.reload();
  done();
  console.log(('reload done'));
}

function removeFile(path, name) {
  return del(path.replace(/src\/views/, 'dest').replace('ejs', 'html'));
};

// タスクを監視
function watchFiles() {

  //ファイルやディレクトリが削除されたらdest内の同盟ファイルを削除、それ以外ならstylesFuncを実行
  gulp.watch(paths.styles.src)
    .on('all', (event, path) => {
      if (event === 'unlink' || event === 'unlinkDir') {
        return del(path.replace(/src\/scss/, paths.css.dest).replace('scss', 'css'));
      }
      return stylesFunc();
  });
  gulp.watch(paths.styles.tempSrc, gulp.series(stylesFunc));

  gulp.watch(paths.html.src)
    .on('all', (event, path) => {
      if (event === 'unlink' || event === 'unlinkDir') {  
        console.log(path);
        return del(path.replace(/src\/views/, paths.html.dest).replace('ejs', 'html'));
      }
      return htmlFunc();
    });
  gulp.watch(paths.html.tempSrc, gulp.series(htmlFunc));

    gulp.watch(paths.scripts.src)
      .on('all', (event, path) => {
        if (event === 'unlink' || event === 'unlinkDir') {
          return del(path.replace(/src\/js/, paths.scripts.dest));
        }
        return scriptFunc();
    });

    gulp.watch(paths.images.src)
      .on('all', (event, path) => {
        if (event === 'unlink' || event === 'unlinkDir') {
          return del(path.replace(/src\/assets/, paths.images.dest));
        }
        return imageFunc();
    });
    gulp.watch(srcpathAll, gulp.series(browserReload));
}


// mapfile削除
function clearMap() {
  return del([paths.styles.clearmap]);
}


// タスクの実行
exports.default = gulp.series(gulp.parallel(stylesFunc, htmlFunc, scriptFunc, imageFunc), gulp.series(destServer, watchFiles));
exports.clearmap = clearMap;
```

###メモ： ディレクトリ構造
このようにしていますが、変更したい場合は``const = paths``に記載のパスを書き換えてください。
<img width="192" alt="スクリーンショット 2020-04-02 17.52.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459055/1a34129b-2691-0706-f9cc-3a1d406f36e6.png">


##手順3-2：package.jsonにAutoprefixerの対応ブラウザ範囲を追記
package.jsonのdevDependencies{・・・},の後に、ベンダープレフィックスを対応するブラウザバージョンを指定します。
<img width="375" alt="スクリーンショット 2020-04-02 21.38.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459055/d9e99439-e3bd-1ce3-4129-b49d730aa75a.png">

```JS:package.json
  "browserslist": [
    "last 2 versions",
    "ie >= 11",
    "Android >= 4",
    "ios_saf >= 8"
  ]
```

##手順3-3：タスクを実行する
ターミナルに次のコマンドを入力することで、タスクが実行されます。
ソースマップは、本番環境には不要なので削除用のタスクを作りました。

```
//デフォルトタスクの実行
gulp

//ソースマップ削除を実行
gulp clearmap
```

##gulpfile設定にあたり参考にさせていただいたサイト
[gulp公式](https://gulpjs.com/docs/en/getting-started/quick-start)（英語）
[gulp4に移行するためにタスクを書き換えてみた](https://magnets.jp/web_design/10404/)
[gulp4の設定方法 - SassやAutoprefixer、ejs、画像の圧縮などを自動化する](https://yumegori.com/gulp4-setting20191025#chapter-4)


#おわりに
もし間違いや、「こっちの方がいいよ！」というのがあれば是非教えてください。
参考にさせていただいたサイトや記事の運営・執筆者さま、ありがとうございました。

----
2020/5/4追記：以下のパッケージを追加しました。
``node-sass-package-importer`` ``gulp-babel`` ``gulp-plumber`` 
 ``gulp-notify``