MENU CLOSE

gulp4 + webpack4でつくるフロント開発環境

gulp+webpack

今回はフロントエンドの開発環境についてのメモ。
gulpwebpackなどの環境設定について。

まだgulp?

gulpってもう古くない?まだ使ってんの?
ってツッコミがありそうですが、僕の開発環境ではまだまだ現役です!

gulp.jsから卒業!や、もういらない!など、検索するといろいろな記事が出てきますが、個人的にはJSとCSSさえビルドできればなんでも一緒です。

そうなるとますますgulpじゃなくてもええやん。ってなりそうですが、
昔から特にgulpで消耗したこともないですし、他に変えるコストを考えるとバージョン上げて使い続ける方が楽だったので使い続けています。

とは言え、gulpを使っていてもフロントの開発環境はすぐに新しくなって変わっていくので、古くなったサイトの環境が今と合わなくてよく開発環境の立ち上げ方や使い方を忘れたりします。

そんなときのために今の開発環境を備忘録として残しておこうと思いました。

開発環境の構成

現状つかっている構成はこんな感じ.

gulpfile.babel.js
├── /tasks
     ├── browsersync.js
     ├── css.js
     ├── html.js
     ├── img.js
     ├── js.js
     ├── svg.js
     └── watch.js
├── config.js
├── index.js
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js

.babelrc
package.json

では詳しくみていきます。

package.json

こちらは扱うモジュールの依存関係を記載しています。
yarnnpmでインストールするとdevDependenciesに記載のあるパッケージがnode_moduleにインストールされます。

{
    //・・・基本設定部分省略

    "scripts": {
        "dev": "NODE_ENV=development gulp",
        "production": "NODE_ENV=production gulp",
        "build": "NODE_ENV=production gulp build:js build:css"
    },
    "devDependencies": {
        "@babel/core": "^7.4.4",
        "@babel/plugin-transform-runtime": "^7.4.4",
        "@babel/polyfill": "^7.4.4",
        "@babel/preset-env": "^7.4.4",
        "@babel/preset-react": "^7.0.0",
        "@babel/register": "^7.4.4",
        "@babel/runtime": "^7.4.4",
        "@babel/runtime-corejs2": "^7.4.4",
        "babel-loader": "^8.0.2",
        "browser-sync": "^2.26.5",
        "css-mqpacker": "^7.0.0",
        "exports-loader": "^0.7.0",
        "gulp": "^4.0.2",
        "gulp-base64": "^0.1.3",
        "gulp-cached": "^1.1.1",
        "gulp-csso": "^3.0.0",
        "gulp-diff-build": "^1.0.2",
        "gulp-if": "^2.0.2",
        "gulp-notify": "^3.0.0",
        "gulp-plumber": "^1.1.0",
        "gulp-postcss": "^8.0.0",
        "gulp-progeny": "^0.4.1",
        "gulp-sass": "^4.0.1",
        "gulp-sass-glob": "^1.0.9",
        "gulp-sourcemaps": "^2.6.0",
        "gulp-svgmin": "^2.0.0",
        "imports-loader": "^0.8.0",
        "node-sass": "^4.12.0",
        "postcss-assets": "^5.0.0",
        "postcss-cssnext": "^3.0.2",
        "postcss-flexbugs-fixes": "^4.1.0",
        "require-dir": "^1.0.0",
        "through2": "^3.0.1",
        "uglifyjs-webpack-plugin": "^2.0.0",
        "vinyl": "^2.1.0",
        "vinyl-named": "^1.1.0",
        "vinyl-source-stream": "^2.0.0",
        "webpack": "^4.19.0",
        "webpack-encoding-plugin": "^0.3.1",
        "webpack-merge": "^4.2.1",
        "webpack-stream": "^5.1.1"
    }
}

今回紹介するCSSとJSのビルドに必要なものだけをピックアップしました。
それでも多いですよね・・・まぁ多くても少なくても個人的にはあまり関係ないっちゃないのですが、依存しているプラグインが多いと、そのうちのどれかのプラグインの開発が終わってしまったときに問題が起こるかもしれません。

.babelrc

ES6で書きたいので.babelrc にbabelの設定を書いて gulpfile.jsgulpfile.babel.jsにしています。

.babelrcの中身はこちら

{
  "presets": [
    "@babel/preset-react",
    ["@babel/preset-env", {
      "targets": {
        "node": "current",
        "browsers": ["last 2 versions"]
      }
    }]
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}

polyfillの適用など基本的なbabelの設定です。
こちらの設定内容については今回は省略します。

gulpfile.babel.jsをファイル分割

gulpfile.babel.jsはファイルではなくディレクトリにして直下にindex.jsを配置します。これは数年前にどこかで見た記事を参考にしたのですが、よく調べると公式ドキュメントに書いてありました。
この方法を使うとgulp実行する際にgulpfile.babel.js/index.jsを読み込んでくれるので、タスクごとにファイル分割して管理しやすくなります。

こちらがそのindex.jsの中身です。

import gulp          from 'gulp';
import requireDir    from 'require-dir';
//Tasks
requireDir('./tasks', {recurse: true});
//Default
gulp.task('default', gulp.task('watch'));

タスクはgulpfile.babel.js/tasks以下にそれぞれの役割ごとにファイル分割して設置。require-dirモジュールでディレクトリ内の全てのファイルを読み込みます。タスクを読み込んだらdefaultタスクを書いておきます。ここではwatchというタスクをgulp起動時に実行するようにしています。

設定ファイル(config.js)について

gulpfile.babel.js内にあるconfig.jsについて。

gulpfile.babel.js

├── /tasks
     ├── browsersync.js
     ├── css.js
     ├── html.js
     ├── img.js
     ├── js.js
     ├── svg.js
     └── watch.js
├── config.js <= こちらのファイル
├── index.js
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js

.babelrc
package.json

こちらには主にフォルダやファイルのパスや、glob(ワイルドカードでファイル名のセットを指定するパターン)を指定したものなどをまとめているファイルです。

中身はこんな感じです。

import path from 'path'
//各パス設定
export const paths = {
  serverDir       : 'notes.sharesl.net',
  themeDir        : path.join(__dirname, '../'),
  imageDir        : path.join(__dirname, '../assets/images'),
  imageminDir     : path.join(__dirname, '../assets/imagemin'),
  spriteDir       : path.join(__dirname, '../assets/sprite'),
  spriteminDir    : path.join(__dirname, '../assets/spritemin'),
  jsSrcDir        : path.join(__dirname, '../assets/js'),
  jsDistDir       : path.join(__dirname, '../assets/dist'),
  sassDir         : path.join(__dirname, '../assets/sass'),
  cssDir          : path.join(__dirname, '../assets/css'),
  svgDir          : path.join(__dirname, '../assets/svg'),
  svgminDir       : path.join(__dirname, '../assets/svg'),
  jsEntryFileName : 'entry.js'
}

//ファイルマッチパターン
export const globs = {
  html    : `${paths.themeDir}**/*.(html|php)`,
  svg     : `${paths.svgDir}/**/*.svg`,
  img     : `${paths.imageDir}/**/*.+(jpg|jpeg|png|gif|svg)`,
  sprite  : `${paths.spriteDir}/*.+(jpg|jpeg|png)`,
  sprites : `${paths.spriteDir}/**/*.+(jpg|jpeg|png)`,
  sass    : `${paths.sassDir}/**/*.scss`,
  js      : `${paths.jsSrcDir}/**/*.+(js|vue)`,
  entry   : `${paths.jsSrcDir}/**/entry.js`
}

//CSS対応ブラウザのバージョン
export const browsers = [
'> 5% in JP',
'last 2 versions',
'ie >= 11',
'Android >= 4',
'iOS >= 8'
]

const config = { paths, globs, browsers }
export default config

これを作っておけば、新しいプロジェクトを作るときにgulpfile.babel.jsをコピーしてきて、こちらのファイルをちょいちょいっと書き換えるだけですぐに環境が整います。globspathsは自由に追加・変更できますので新しいgulpタスクを作った場合にもすぐ対応できます。

gulpタスクの詳細

次にタスクの中身です。
自動でブラウザリロードできるBrowsersyncや画像・svgの圧縮
などタスクを切り分けていろいろ作っていますが、
今回はCSSとJSのみをメモしておきます。
また機会があれば画像圧縮のタスクは使えるので紹介したいと思います。

① CSSビルド(gulpfile.babel.js/tasks/css.js)

//Task:css
import gulp                     from 'gulp'
import sass                     from 'gulp-sass'
import nodesass                 from 'node-sass'
sass.compiler = nodesass;
//エラーでgulpが終了するのを止める
import plumber                  from 'gulp-plumber'
//デスクトップ通知
import notify                   from 'gulp-notify'
//小さい画像をbase64に変換
import base64                   from 'gulp-base64'
//PostCss
import postcss                  from 'gulp-postcss'
import cssnext                  from 'postcss-cssnext'
//flexboxのバグを自動修正
import flexBugsFixes            from 'postcss-flexbugs-fixes'
//ファイル名から画像パスやサイズを取得
import assets                   from 'postcss-assets'
//メディアクエリを整理する
import mqpacker                 from 'css-mqpacker'
//cssを圧縮する
import csso                     from 'gulp-csso'
//ソースマップの生成
import sourcemaps               from 'gulp-sourcemaps'
//config
import {paths, globs, browsers} from '../config'
import diff                     from 'gulp-diff-build'
import cache                    from 'gulp-cached'
import progeny                  from 'gulp-progeny'
import browserSync              from 'browser-sync'
//@importのglobを有効にする
import sassGlob                 from 'gulp-sass-glob'

gulp.task('build:css', () => {
  const processors = [
  assets({
    baseUrl     : `${paths.serverDir}/`,
    basePath    : paths.themeDir,
    loadPaths   : [
    'assets/images/',
    'assets/svg/',
    ],
    relative    : true,
    cachebuster : true,
  }),
  cssnext({
    browsers,
    features : {
      autoprefixer : {
        grid: true
      }
    }
  }),
  mqpacker({
    sort: true
  }),
  flexBugsFixes
  ];
  return gulp.src(globs.sass, { allowEmpty: true })
  .pipe(plumber({
    errorHandler: notify.onError('<%= error.message %>')
  }))
  .pipe(sassGlob())
  .pipe(diff())
  .pipe(cache('sass'))
  .pipe(progeny())
  .pipe(sass({
    outputStyle: 'expanded',
  }))
  .pipe(base64({
    baseDir      : paths.imageDir,
    extensions   : ['svg', 'png', /\.jpg#datauri$/i],
    exclude      : [/\.server\.(com|net)\/dynamic\//, '--live.jpg'],
    maxImageSize : 8 * 1024,
    debug        : true
  }))
  .pipe(postcss(processors))
  .pipe(sourcemaps.init())
  .pipe(csso({
    restructure : false,
    sourceMap   : false,
    debug       : true
  }))
  .pipe(sourcemaps.write('.'))
  .pipe(gulp.dest(paths.cssDir))
  .pipe(notify('css-build finished'))
  .pipe(browserSync.reload({
    stream: true
  }));
});

長年ちまちま更新してきたタスクなのでちょっと長いですね・・・笑
人によっては必要ない機能もあるかもしれませんが、30ファイルほどのSassをコンパイルしても1回目で0.6秒程度、2回目はキャッシュで早くなるので0.3秒程度となり、まぁ許容範囲だと思います。

モジュールに関しては結構たくさんあるのでさすがにひとつずつは説明できませんので、これは必須!というものだけピックアップしておきます。

gulp-postcss

こちらは生成するCSSを最適化・カスタマイズできるPostCSSが使えるプラグイン。
色々と便利なオプションが設定できますが、有名どころではcssnextAutoprefixerでしょうか。
僕はcss-mqpackerというメディアクエリを整理するプラグインを使いたくて使い始めたのですが、いろんなカスタマイズができることを知り手放せなくなりました。

gulp-cached + gulp-progeny

こちらはセットで使うのが普通かなと思いますのでまとめてご紹介。
gulp-cachedは文字通りキャッシュしてくれるので差分のみがビルドされます。
gulp-progenyはSassで@importなどを使用している場合にその依存関係を解決してコンパイルしてくれます。

詳しくはこちらの記事をどうぞ。

gulpでCSSの差分ビルド

https://qiita.com/73cha/items/270e2dc33c63292dd184
gulp-csso

こちらはCSSの圧縮プラグイン。
ファイルの軽量化には必須ですね。

他にも便利なプラグインを使っていますが、なくてもなんとかなるものばかりなので割愛させていただきます。プラグインの名前で検索したらすぐ出てくるものばかりです。

② JSビルド(gulpfile.babel.js/tasks/js.js)

//Task:build:js
import gulp            from 'gulp';
import {globs, paths}  from '../config';
//エラーでgulpが終了するのを止める
import plumber         from 'gulp-plumber';
//デスクトップ通知
import notify          from 'gulp-notify';
import path            from 'path';
import fs              from 'fs';
import through         from 'through2';
import diff            from 'gulp-diff-build';
//webpackでファイル結合時に名前変更
import named           from 'vinyl-named';
import gulpif          from 'gulp-if';
import webpack         from 'webpack';
import webpackStream   from 'webpack-stream';
import webpackConfig   from '../webpack.config';
import browserSync     from 'browser-sync';

//jsのエントリーポイントファイルかどうか
const isEntryFile = (file) => {
  let isEf = true;
  if(!isExistFile(file.path)){
    isEf = false;
  }
  else {
    isEf = path.basename(file.path) === paths.jsEntryFileName;
  }
  return isEf;
}

//ファイルの存在チェック
function isExistFile(file) {
  try {
    fs.statSync(file);
    return true
  } catch(err) {
    if(err.code === 'ENOENT') return false
  }
}

//bundleされたファイルのみを次の処理に通す
function passThroughBundled() {
  return through.obj(function (file, enc, cb) {
    if( file.isNull() ){
      cb(null,file);
    }
    else {
      const basename = path.basename(file.path);
      const isBundled = basename.indexOf('bundle.js') !== -1;
      if(isBundled){
        this.push(file);
      }
      cb();
    }
  });
}


gulp.task('build:js', () => {
  return gulp.src(globs.js, { allowEmpty: true })
  .pipe(plumber({
    errorHandler: notify.onError('<%= error.message %>')
  }))
  .pipe(diff())
  .pipe(named((file) => {
    return file.relative ? path.parse(file.relative).dir : 'code';
  }))
  .pipe(
    gulpif(isEntryFile, webpackStream(webpackConfig, webpack))
    )
  .pipe(passThroughBundled())
  .pipe(gulp.dest(paths.jsDistDir))
  .pipe(notify('js-build finished'))
  .pipe(browserSync.reload({
    stream: true
  }));
});

gulp.task('build:js-all', () => {
  return gulp.src(globs.entry, { allowEmpty: true })
  .pipe(plumber({
    errorHandler: notify.onError('<%= error.message %>')
  }))
  .pipe(named((file) => {
    return file.relative ? path.parse(file.relative).dir : 'code';
  }))
  .pipe(
    gulpif(isEntryFile, webpackStream(webpackConfig, webpack))
    )
  .pipe(gulp.dest(paths.jsDistDir))
  .pipe(notify('build:js-all finished'))
  .pipe(browserSync.reload({
    stream: true
  }));
});

こちらは基本的にはJSそのものは何も処理しません。することと言えば保存した時に差分のあった場合だけをwebpackに流したり、globで指定するentryファイルの存在チェックをしたりというフィルター処理でgulpの無駄な実行時間を減らす処理を書いています。ファイル結合や圧縮などはすべてwebpackに任せて処理を分けています。

ここでもいくつかモジュールを紹介しておきます。

gulp-diff-build

こちらはファイルを監視して変更があった場合のみ、ファイルをストリームに流します。つまり変更がある場合のみgulpタスクを実行するのでwatchした時に保存するたびに連続でタスクが実行されてしまうのを防止します。
これはかなり重宝しています!快適!

gulp-if

gulpのタスク処理で条件分岐したい場合にこちらを使います。
webpackにストリームを流す際に余計なファイルが含まれていないかチェックするのに使っています。

through2

これはgulpに独自の処理を入れたい時に使います。
プラグインの組み合わせだけではどうにもならないけど、ごく簡単な処理をはさみたい時があるのでこちらを使っています。JSビルド処理の中ではpassThroughBundledという独自の関数を作成し、webpackを通した後にbundle.jsという名前の入ったファイルのみをストリームに流すようにしています。こちらで意図しない余計なファイルがビルドされてしまうのを避けています。

webpack-stream

こちらはgulpwebpackを連携するためのプラグイン。webpackは絶対必要なのでこれが無いと始まりません。

③Webpack

続いてwebpackを見ていきます。
これも人によって書き方が分かれるところですが、僕は1ファイルでなく3ファイルで管理しています。

  • webpack.common.js:開発・本番モード共通
  • webpack.dev.js:開発モード(JSを圧縮しない)
  • webpack.prod.js:本番モード(JSを圧縮)

webpack4から開発モードと本番モードが出来たので、圧縮処理を省いたものを開発モード、圧縮処理を入れたものを本番モードにしています。
具体的には以下コードで見ていきます。

webpack.common.js
import webpack                from 'webpack'
import EncodingPlugin         from 'webpack-encoding-plugin'
import VueLoaderPlugin        from 'vue-loader/lib/plugin'
import {paths}                from './config.js'

module.exports = {
  cache    : true,
  output   : {
    filename : '[name].bundle.js',
  },
  optimization: {
    splitChunks: {
      name   : 'vendor',
      chunks : 'initial',
    }
  },
  plugins  : [
  new VueLoaderPlugin(),
  new webpack.optimize.OccurrenceOrderPlugin(),
  new webpack.optimize.AggressiveMergingPlugin(),
  new EncodingPlugin({
    encoding: 'utf-8'
  }),
  new webpack.ProvidePlugin({
    $                : 'jquery',
    jQuery           : 'jquery',
    objectFitImages  : 'object-fit-images',
    anime            : ['animejs/lib/anime.es.js', 'default'],
  })
  ],
  module: {
    rules: [
    {
      test: /\.js$/,
      exclude: [
      /(node_modules|bower_components)/
      ],
      use: [{
        loader: 'babel-loader'
      }]
    },
    {
      test: /\.vue$/,
      loader: 'vue-loader',
      options: {
        loaders: {
          js: 'babel-loader'
        }
      }
    }
    ]
  },
  resolve: {
    modules    : ["node_modules"],
    alias      : {
      '@js' : paths.jsSrcDir,
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
};

こちらは共通設定です。通常のJSと、Vue.jsを最近よく使うのでそれに対応したものです。Typescriptもそろそろやってかないとと思っていますが、まだ手が出せていません…

設定について少し見ていきます。

cache

trueに設定するとwebpackのキャッシュを有効にします。

output

これはエントリーファイルの親ディレクトリが名前になるようにgulpwebpackを使って調整しています。どういうことかというと、

js/
└──  /common
     ├── entry.js => エントリーファイル ①②をインポート
     ├── main.js  => モジュール化した処理①
     └── utils.js => モジュール化した処理②
└── /reserve
     ├── entry.js => エントリーファイル ③④をインポート
     ├── main.js => モジュール化した処理③
     └── utils.js => モジュール化した処理④

JSを設置する場合はこういうファイル構成にして使います。
この場合、common.bundle.jsreserve.bundle.jsという名前のファイルがビルドされるということです。

optimization.splitChunks

webpack4で追加された機能。以前はCommonsChunkPluginというものがあったらしいですが使っていませんでした。こちら最近かなり重宝しています!
具体的に何ができるかというと、複数ファイルにまたがって使われている共通モジュールをバンドルして別ファイルとして出力する、ということができます。

先ほどの例で言うと、

optimization: {
    splitChunks: {
      name   : 'vendor',
      chunks : 'initial',
    }
},

こういう設定がしてある場合、common.bundle.jsreserve.bundle.jsというファイルの他にvendor.bundle.jsという共通ファイルが出来上がります。読み込む時は以下のようになります。

<script type='text/javascript' src='vendor.bundle.js'></script>
<script type='text/javascript' src='common.bundle.js'></script>

これの何が良いかというと3点あります。

1点目は、webpackの処理が早くなること。jQueryVue.jsなどの大きいライブラリなどを共通ファイルとして出力してくれるのですが、そういったファイルは基本読み込むだけなので、保存するたびに書き換えることはまずないでしょう。そのためsplitChunksを使って一旦作成されたvendor.bundle.jsのような共通モジュールはキャッシュが有効になり、変更されたファイルだけがビルドされるようになるのでビルド処理が格段に早くなります。けっこう大きいアプリケーションの開発などをしている時はwebpackのビルドが遅いと開発にならなくなってきますのでこれだけでもかなり助かります。

続いて2点目。ファイルサイズが小さくなること。これは処理を共通化することで生まれるメリットです。ここで言えばcommon.bundle.jsreserve.bundle.jsvendor.bundle.jsを利用するので二重になっていた部分を削減できます。

最後に3点目。読み込む時にキャッシュが効く。
どのページでもvendor.bundle.jsを読み込むことになるので、ページごとにJSの内容を変える場合などでも共通化されていない部分だけを読み込めばいいので読み込み速度も早くなります。

plugins

ここには外部プラグインやwebpackにある機能などを追加できます。

  • VueLoaderPlugin:vue.js用
  • webpack.optimize.OccurrenceOrderPlugin:ファイルサイズを縮小
  • webpack.optimize.AggressiveMergingPlugin:コードを圧縮
  • EncodingPlugin:ファイル形式を指定
  • webpack.ProvidePlugin:あらかじめ読み込むモジュールを指定
module

ここではloaderの設定を書きます。babel-loaderでES6対応、vue-loaderで.vue拡張子のファイルに対応します。

resolve

aliasにパスを指定しておくと読み込みが楽になります。paths.jsSrcDirがわかりにくいのでちょっと置き換えます。

resolve: {
    modules    : ["node_modules"],
    alias      : {
      '@js' : path.join(__dirname, '../assets/js'),
      'vue$': 'vue/dist/vue.esm.js'
    }
}

設定しておくとJSファイル内でimport '../assets/js/common/main.js'みたいに書いているものをimport '@js/common/main.js'というようにファイルの相対位置などを気にせずに呼び出すことができるようになります。

webpack.dev.js

こちらは開発モードの設定ファイルです。

import merge  from 'webpack-merge'
import common from './webpack.common.js'

module.exports = merge(common, {
  mode     : 'development',
  devtool  : 'cheap-module-eval-source-map',
});

webpack-mergeというプラグインを使ってwebpack.common.jsと設定をマージします。ここではソースマップだけ設定しておきます。

webpack.prod.js

こちらは本番モードの設定ファイルです。

import merge           from 'webpack-merge'
import common          from './webpack.common.js'
import UglifyJSPlugin  from 'uglifyjs-webpack-plugin'

module.exports = merge(common, {
  mode     : 'production',
  plugins  : [
  new UglifyJSPlugin({
    uglifyOptions: {compress: {drop_console: true}},
  })
  ]
});

開発モードとの変更点は以下です。
・ソースマップを出力しない
・JSを圧縮する

JSの圧縮はけっこう時間がかかりますし、Chrome DevToolsツールなどからソースを見る時にわかりにくくなるので、開発モードでは使用しない方が良いです。

最後に

僕が個人的に使っている環境なのでかなり偏ったものになっているかもしれません。このままコピペで他のプロジェクトに使うことはできないと思います。ただ全体ではなく部分でもだれかの参考になればと思います。

今回はCSSとJSのビルドタスクだけを紹介しましたが、htmlの圧縮や画像圧縮など細々したタスクがたくさん増えて複雑化してきました。

今後、TypeScriptを使うとなった時にはもっとシンプル構成で新しい環境にしたいなぁ〜。

記事一覧

Related

JS
inoue
inoue
TIPS

Intersection Observerを使って画面内に要素が入ったらアニメーションさせる

wordpress
inoue
inoue
TIPS

普通のレンタルサーバーでできるWebページ高速化

wordpress
inoue
inoue
TIPS

【WordPress】便利なアクションフック「template_redirect」

wordpress
inoue
inoue
TIPS

【WordPress】REST APIで独自エンドポイントの作り方

wordpress
inoue
inoue
TIPS

【WordPress】画像に自動付与されるsrcset属性を削除する

wordpress
inoue
inoue
TIPS

WordPressでよく使うプラグイン15選!

ブログ記事一覧
  • HOME
  • TIPS
  • gulp4 + webpack4でつくるフロント開発環境