gulpを利用したローカルサーバー立ち上げとsass+ejsのコンパイル環境構築 後編【gulp】

公開日:

最終更新日:

gulpを利用したローカルサーバー立ち上げとsass+ejsのコンパイル環境構築 後編【gulp】

前編ではgulpを使用する際の環境構築についてつらつらと書き出しました。

といってもこの辺りの情報は少し調べればいくらでも出てくる情報なので、最適な方法は他にもあるかと思いますし、人によって管理方法は様々なので、参考程度に見ていただければ問題ないと思います。
gulpを利用したローカルサーバー立ち上げとsass+ejsのコンパイル環境構築 前編【gulp】

今回は後編ということで、パッケージのインストールとgulpfile.jsの書き方について書いていきたいと思います。
現時点で私が利用しているパッケージの構成を元に書いていきますが、あくまでもgulpに依存したパッケージ構成になっているので、古いバージョンのパッケージを利用しているのが気になる場合は、適宜手動でバージョンアップを行うか、npm-scriptで書き直すという方法に挑戦することをおすすめします。

package.jsonの作成

まずはパッケージ情報を記録するpackage.jsonを作成します。
コマンドプロンプトで該当ディレクトリに移動して以下のコマンドを入力することで、package.jsonを生成をすることができます。

npm init -y

「-y」を入力する意味は、デフォルト値を自動で入力した状態で生成するためです。
gulpを利用する場合は、入力しなければいけない項目は特にないので、色々と設定したい方以外は特に必要ない作業かと思います。

package.jsonにはインストールしたパッケージの名称とバージョンが記録されていくので、このファイルとnpmを実行する環境さえ整っていれば、特に何も考えずnpm installとコマンドで入力するだけで好きなディレクトリに環境が再現できます。

複数人で並行して作業する際にバージョン管理システム(gitなど)にpackage.jsonさえあげておけば、依存しているパッケージに途中で変更が加わったとしても、不具合解決をできる人が1人いれば問題ないので、全員がバグや不具合などに振り回される心配がなくなるのではないかと思います。(知識として不具合解決の方法を共有することは必須だと思いますが)

npm installでパッケージをインストールする

パッケージのインストールは特に難しいことはありません。
私がやっている手順としては、必要なパッケージをnpmjsで検索してコマンドをコピーして貼り付けるだけです。

あまりないですが、最新のパッケージ情報では動作しなかったりするのでバージョン違いをインストールする必要もあります。
その場合は「versions」から過去バージョンをさかのぼってみるといいかもしれません。

私が利用しているパッケージ構成はこんな感じです。
sassはnode-sassからdart-sassを利用するように変更したのと、メディアクエリが散在しないようにするパッケージを追加しているので、一般的に紹介されている構成とは少し変わるかもしれません。

「devDependencies」に全て記述されていますが、npm installで引数なしの場合は「dependencies」に記述されます。
特に考えずにインストールして利用するだけであれば問題ありませんが、開発用と本番用でパッケージの切り分けができるので、もしパッケージを公開して利用してもらうようなケースがあれば注意しましょう。
https://docs.npmjs.com/specifying-dependencies-and-devdependencies-in-a-package-json-file

{
    "name": "gulp-test.testsite",
    "version": "1.0.3",
    "description": "",
    "main": "gulpfile.js",
    "dependencies": {},
    "devDependencies": {
      "@achingbrain/gulp-connect": "^5.6.1",
      "autoprefixer": "^10.3.1",
      "del": "^6.0.0",
      "gulp": "^4.0.2",
      "gulp-dart-sass": "^1.0.2",
      "gulp-data": "^1.3.1",
      "gulp-ejs": "^5.1.0",
      "gulp-plumber": "^1.2.1",
      "gulp-postcss": "^9.0.0",
      "gulp-rename": "^2.0.0",
      "postcss": "^8.3.6",
      "postcss-sort-media-queries": "^3.11.12",
      "sass": "^1.37.5"
    },
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
  }

gulpfile.jsを記述する

大部分はどこでも紹介されているような一般的なものなので、調べて組み込んだもののみ後ほど説明をします。
変数の位置は基本的に利用する関数などの近くの方がいいと思いますが、関数自体を調整することはあまり無いので、どこで利用されているかわかりやすい変数名にしてまとめておくようにしてもいいかなと思っています。

ディレクトリ構成については変数に記載している通りに構成すれば問題はないですが、人によってわかりやすい構造というものは違うと思うので、自由に変更してください。

ただ、gulpfile.jsについてはルートディレクトリに設置して実行することを前提にしているので、別ディレクトリに設定ファイルをまとめたいといった場合は別途構成を考える必要があります。

'use strict';

const gulp = require('gulp'),
sass = require('gulp-dart-sass'),
autoprefixer = require('autoprefixer'),
postcss = require('gulp-postcss'),
sortmediaqueriese = require('postcss-sort-media-queries'),
plumber = require('gulp-plumber'),
ejs = require('gulp-ejs'),
rename = require('gulp-rename'),
data = require('gulp-data'),
webserver = require('@achingbrain/gulp-connect')
const del = require('del'),
fs = require('fs');

var outputDir = {
  dev: './dev',
  release: './release',
},
setOptions = {
  sass: {
    outputStyle: 'expanded'
  },
  postcss: [
    autoprefixer(
      {
        overrideBrowserslist: [
          "last 2 versions",
          "ie >= 11"
        ],
      }
    ),
    sortmediaqueriese()
  ],
  ejs: {
    srcroot: './ejs_template'
  },
  rename: {
    extname: '.html'
  },
  webserver: {
    port: 8080,
    root: outputDir.dev,
    livereload: true
  },
  sourcemaps: {
    sass: {
      bool: true,
      write: './maps'
    }
  },
},
paths = {
  sass: {
    watch: './assets/_scss/**/*.scss',
    src: './assets/_scss/**/*.scss',
    dest: outputDir.dev + '/assets/css',
  },
  ejs: {
    watch: setOptions.ejs.srcroot + '/**/*.ejs',
    src: [
      setOptions.ejs.srcroot + '/**/*.ejs',
      '!' + setOptions.ejs.srcroot + '/**/_*.ejs'
    ],
    data: setOptions.ejs.srcroot + '/_parts/data/site-conf.json',
    dest: outputDir.dev,
  },
  copy: {
    src: {
      files: [
        './assets/**/*.*',
        '!./assets/_scss/**/*.scss',
      ],
      options: {
        base: '',
      }
    },
    dest: outputDir.dev + '/assets',
  },
  del: [
    outputDir.dev + '/**/css/_**/*.css',
    outputDir.dev + '/**/css/_**/*.map',
    outputDir.dev + '/**/_*.html',
    outputDir.dev + '/**/* *.*',
  ],
  clean: [
    outputDir.dev + '/**',
  ],
  webserver: {
    src: outputDir.dev
  },
},
releasePaths = {
  sass: {
    dest: outputDir.release + '/assets/css',
  },
  ejs: {
    src: [
      setOptions.ejs.srcroot + '/**/*.ejs',
      '!' + setOptions.ejs.srcroot + '/**/_*.ejs',
      '!' + setOptions.ejs.srcroot + '/**/styleguide.ejs',
      '!' + setOptions.ejs.srcroot + '/**/_default/*.ejs',
    ],
    dest: outputDir.release,
  },
  copy: {
    src: {
      files: [
        outputDir.dev + '/**',
        '!' + outputDir.dev + '/assets/css/maps/**',
        '!' + outputDir.dev + '/assets/css/debug/**',
        '!' + outputDir.dev + '/assets/js/debug/**',
        '!' + outputDir.dev + '/styleguide.html',
        '!' + outputDir.dev + '/_default/**',
      ],
      options: {
        base: outputDir.dev,
      }
    },
    dest: outputDir.release,
  },
  del: [
    outputDir.release + '/**'
  ],
},
devFirstTask = true;


// ファイル・ディレクトリの削除
function deleteFiles(){
  var dirCleanup = paths.del;
  // console.log(devFirstTask, dirCleanup);
  if( dirCleanup[0] !== releasePaths.del[0] ){
    (async function(){
      const deletedPaths = await del(paths.del, {dryRun: true});
      (deletedPaths.length) ? console.log('will be deleted:\n' + deletedPaths.join('\n')) : console.log('deleted');
    })();
    if( devFirstTask ){
      dirCleanup = paths.clean;
    }
  }
  return del(dirCleanup);
}

// コンパイル後のoutputDir.devからファイル抽出
function copyFiles(){
  // console.log('copy');
  if( !devFirstTask ){
    paths.copy.src.options.since = gulp.lastRun(copyFiles);
  }
  return gulp.src(
    paths.copy.src.files,
    paths.copy.src.options
  )
  .pipe(
    gulp.dest(paths.copy.dest)
  );
}



// sassコンパイル
function sassCompile(){
  // console.log('release: ' + releaseFlg);
  var diffCompile = (paths.ejs.dest === outputDir.dev && !devFirstTask) ?
  {
    since: gulp.lastRun(ejsCompile),
    sourcemaps: setOptions.sourcemaps.sass.bool
  }:
  {
    sourcemaps: setOptions.sourcemaps.sass.bool
  };
  return gulp.src(
    paths.sass.src,
    {
      sourcemaps: setOptions.sourcemaps.sass.bool
    }
  )
  .pipe(
    plumber()
  )
  .pipe(
    sass(setOptions.sass).on('error', sass.logError)
  )
  .pipe(
    postcss(setOptions.postcss)
  )
  .pipe(
    gulp.dest(
      paths.sass.dest,
      {
        sourcemaps: setOptions.sourcemaps.sass.write
      }
    )
  );
};

// ejsコンパイル
function ejsCompile(){
  var diffCompile = (paths.ejs.dest === outputDir.dev && !devFirstTask) ?
  {
    since: gulp.lastRun(ejsCompile)
  }:
  {};
  // console.log(diffCompile);
  return gulp.src(
    paths.ejs.src,
    diffCompile
  )
  .pipe(
    data(function(file){
      const ejsDir = setOptions.ejs.srcroot.replace('./', '') + '/';
      const filepathReplace = file.path.replace(/\\/g,'/')
      const absolute = '/' + filepathReplace.split(ejsDir)[filepathReplace.split(ejsDir).length - 1].replace('.ejs', '.html').replace(/index\.html$/, '');
      const relative = '../'.repeat([absolute.split('/').length - 2]);
      const siteConf = require(paths.ejs.data);
      // console.log(ejsDir);
      // console.log(siteConf);
      Object.assign(
        siteConf, 
        {
          'absolute': absolute,
          'relative': relative,  
        }
      );
      // console.log(siteConf);
      return siteConf;
    })
  )
  .pipe(
    ejs()
  )
  .pipe(
    rename(setOptions.rename)
  )
  .pipe(
    gulp.dest(paths.ejs.dest)
  );
};

// ローカルサーバー立ち上げ
function localserverInit(){
  return webserver.server(setOptions.webserver);
}

// ローカルサーバーリロード
function localserverReload(){
  return gulp.src(
    setOptions.webserver.root
  )
  .pipe(
    webserver.reload()
  );
}

// デフォルトタスクで開発用ファイルを出力
function develop(done){
  devFirstTask = false;
  gulp.watch(paths.sass.watch, gulp.series(sassCompile, localserverReload));
  gulp.watch(paths.ejs.watch, gulp.series(ejsCompile, localserverReload));
  gulp.watch(paths.del, deleteFiles);
  gulp.watch(paths.copy.src.files, copyFiles);
  localserverInit();
  // console.log(paths);
  // タスクはreturnしない場合コールバックを実行して完了を通知
  done();
}

// releaseタスクで本番用のコンパイルセッティングに差し替え
function releasePathset(done){
  setOptions.sourcemaps.sass.bool = false;
  Object.assign(paths.sass, releasePaths.sass);
  Object.assign(paths.ejs, releasePaths.ejs);
  Object.assign(paths.copy, releasePaths.copy);
  Object.assign(paths.del, releasePaths.del);
  // console.log(setOptions, paths);
  // タスクはreturnしない場合コールバックを実行して完了を通知
  done();
}

// 本番用ファイル出力
function releaseCompile(done){
  // console.log(setOptions, paths);
  sassCompile();
  ejsCompile();
  // タスクはreturnしない場合コールバックを実行して完了を通知
  done();
}

// developのタスクを「npx gulp」で実行(gulpのdefaultコマンドを上書き)
exports.default = gulp.series(
  deleteFiles,
  sassCompile,
  ejsCompile,
  copyFiles,
  develop
);
// releaseCompileのタスクを「npx gulp release」で実行(gulpにreleaseコマンドを追加)
// 削除タスク→除外ファイル以外を開発からコピー→sassとejsをコンパイル
exports.release = gulp.series(
  releasePathset,
  deleteFiles,
  copyFiles,
  releaseCompile
);

postcss-sort-media-queriesについて

sassのコンパイルはvscodeの拡張機能でも対応できるので、どうしてもgulpで・・・というわけでもないのですが、メディアクエリをまとめる機能がないので、指定が多くなるとファイルサイズが大きくなりやすいです。

メディアクエリごとにファイルを分割して読み込んでもいいのですが、それだとファイル数が増えて管理が少々めんどくさくなるので、コンパイル時にpostcss-sort-media-queriesを通して重複している記述はまとめるように処理しています。

copyFiles()とdeleteFiles()について

指定したパスにあるファイルやディレクトリを複製または削除します。
除外パス(パスの先頭に「!」)を設定することで該当のファイルを除外することができます。

defaultタスクとreleaseタスクの違い

いちいちコンパイルを通すのは非常に面倒なので、ローカルで作成していく段階ではdefaultタスクを実行するという想定です。
内容としては、gulp.watchでファイルの変更を監視して、gulp.lastRunで差分のみコンパイルというようにしています。

変数pathsの各箇所に出力パスなどをまとめていますが、環境に合わせて設定を変更してください。

作成が完了したらreleaseタスクを実行して、本番環境へアップするファイルをコンパイルするような仕組みにしています。
defaultタスクでは差分をコンパイルするようにしていましたが、releasePathset()をコンパイル前に実行することでオプション等を上書きし、全ファイルを本番アップ用のディレクトリに出力するようにしています。

構築した環境のルートディレクトリでgulpを実行する

あとは記述したgulpfile.jsのタスクを実行するだけです。

ターミナル(またはコマンドプロンプト)から該当のディレクトリに移動して、「npx gulp」を実行することで、「http://localhost:8080」がローカルサーバーとして機能し、ejsまたはsassを更新することでコンパイルとリロードが自動化されていると思います。
作業を終える際は「ctrl+C」でタスクを終了します。

本番反映用のファイルを出力する場合は、「npx gulp release」を実行することで、不要ファイルなどを除外した状態で新たにファイルが出力されていると思います。
releaseタスクについてはファイル監視をしていないので、ファイルの出力が終われば自動的にタスクは終了します。

まとめ

ひとまずgulpについての知見は一通り書き出しました。
基本的には便利そうなものを選んでJSを少しだけ書いて、どのように組み合わせるかを考えるだけなので、そこまで難しくはないと思います。

gulpはパッケージの更新が止まってから長いので、これから採用するには少し考えなければいけないところはあります。
ただ、それでも得られるメリットは多いので、新規の技術開拓についてはもう少し動向を探りつつ、手っ取り早く取り扱えるようなものが出てきたら随時取り入れてみようかと思っています。

関連記事