2018.10.09

Gulp 4 で SCSS + Stylelint + Webpack + ESLint + Edge.js + BrowserSync なフロントエンド環境構築


この記事では、Gulp 4でフロントエンドの開発環境を構築する方法を紹介します。Gulp 4をタスクランナーとして、CSSのトランスパイルに SCSS + Stylelint、JavaScript のトランスパイルに Webpack + ESLint、、テンプレートエンジンとして Edge.js、さらにローカルサーバーとして BrowserSync を組み合わせます。

私はどちらかというとサーバサイドな人でフロントエンド専門ではないのですが、モックアップ作ったりで HTML コーディングしたくなって、いまさらながら Gulp で自分なりの環境構築をしたので結果を共有します。

フロントエンドと言っても全面的に React や Vue でゴリゴリアプリケーションを書くというより HTML コーディングの範疇内でのユースケースを想定しています(フレームワークでゴリゴリしたいなら create-react-app とか vue-cli とか使うといいです)。

概要

サンプルコードも含めた設定の全体についてはこちらのリポジトリを参照してください。

依存パッケージ

https://github.com/MasahiroHarada/gulp-4-starter/blob/master/package.json

ちなみに開発環境は Node v10.7.0、npm v6.4.1 です。

ビルドコマンド

最終的なビルドコマンドは以下の通りです。

// 開発ビルド:監視モードで実行されます。
$ npm run dev

// 本番ビルド:JavaScript が圧縮されます。
$ npm run prod

ビルドタスク

gulpfile.babel.js

ES Modules(import/export)を使用したかったのでファイル名に .babel をつけて Babel のトランスパイルがかかるようにしました。

gulpfile.babel.js
import { series, parallel, watch } from 'gulp';

import { reload, serve } from './tasks/server';
import { styles } from './tasks/styles';
import { scripts } from './tasks/scripts';
import { templates } from './tasks/templates';
import { images } from './tasks/images';
import { clean } from './tasks/clean';

import {
  sass as sassConfig,
  scripts as jsConfig,
  images as imagesConfig,
  templates as templatesConfig
} from './tasks/config';

/**
 * ファイルの変更を監視
 */
function watchFiles() {
  // Sass
  watch(sassConfig.src, series(styles, reload));
  // Templates
  watch(
    [templatesConfig.edges, templatesConfig.data, templatesConfig.helper],
    series(templates, reload)
  );
  // JavaScript
  watch(jsConfig.src, series(scripts, reload));
  // Images
  watch(imagesConfig.src, series(images, reload));
}

/**
 * 開発用ビルド
 */
export const dev = series(
  clean,
  parallel(styles, templates, scripts, images),
  serve,
  watchFiles
);

/**
 * 本番用ビルド
 */
export const build = series(
  clean,
  parallel(styles, templates, scripts, images)
);

前述の通り gulpfile を ES2015 以降の構文で書くために Babel の設定ファイルも追加します。

.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": { "node": "current" }
      }
    ]
  ]
}

タスクを色々書くと長くなってしまうので、個別のタスクはそれぞれ別ファイルに分け、tasks ディレクトリに格納しています。

それぞれ見ていきましょう。

設定ファイル

まずは設定ファイルです。パスの設定が入っています。

tasks/config.js
const ASSET_ROOT = 'src';
const DEST_ROOT = 'public';

export const sass = {
  src: `${ASSET_ROOT}/sass/**/*.scss`,
  dest: `${DEST_ROOT}/styles`
};

export const scripts = {
  srcRoot: `${ASSET_ROOT}/js`,
  src: `${ASSET_ROOT}/js/**/*.js`,
  dest: `${DEST_ROOT}/js`,
  babelrc: {
    presets: [['@babel/env', { targets: '> 0.25%, not dead' }]]
  }
};

export const templates = {
  root: `${ASSET_ROOT}/templates`,
  edges: `${ASSET_ROOT}/templates/**/*.edge`,
  pages: `${ASSET_ROOT}/templates/pages/**/*.edge`,
  data: `${ASSET_ROOT}/templates/data.json`,
  helper: `${ASSET_ROOT}/templates/helper.js`,
  dest: DEST_ROOT
};

export const images = {
  src: `${ASSET_ROOT}/images/**/*.*`,
  dest: `${DEST_ROOT}/images`
};

export const isProd = process.env.NODE_ENV === 'production';

BrowserSync

ファイルの変更を検知して自動でブラウザをリロードさせるため、BrowserSync のタスクを作成します。設定方法はこちら(Minimal BrowserSync setup with Gulp 4)を参考にしました。

tasks/server.js
import browserSync from 'browser-sync';

const server = browserSync.create();

/**
 * 開発用サーバ再起動
 */
export function reload(cb) {
  server.reload();
  cb();
}

/**
 * 開発用サーバ起動
 */
export function serve(cb) {
  server.init({
    server: {
      baseDir: './public'
    }
  });
  cb();
}

Clean

ファイルの出力前に出力先の既存ファイルを削除しておくためのタスクです。

tasks/clean.js
import del from 'del';

/**
 * 出力先のディレクトリを空にする
 */
export function clean() {
  return del(['public']);
}

スタイル

スタイルに関しては2つのタスクを定義しています。

まず1つめはSCSS を CSS に変換するためのタスクです。ソースマップ生成(gulp-sourcemaps)とファイル圧縮(gulp-clean-css)も実行しています。

2つめは CSS(今回は SCSS)の構文チェックツール Stylelint を実行するためのタスクです。gulp-stylelint を使用します。またファイルの監視中に毎回すべてのファイルを lint せず変更があったファイルのみチェックするために gulp-changed-in-place というプラグインも使用します。

tasks/styles.js
import gulp from 'gulp';
import gulpSass from 'gulp-sass';
import sourcemaps from 'gulp-sourcemaps';
import cleancss from 'gulp-clean-css';
import plumber from 'gulp-plumber';
import gulpStylelint from 'gulp-stylelint';
import changed from 'gulp-changed-in-place';

import { sass as config, isProd } from './config';

/**
 * SCSS -> CSS
 */
export function sass() {
  return gulp
    .src(config.src)
    .pipe(plumber())
    .pipe(sourcemaps.init())
    .pipe(gulpSass().on('error', gulpSass.logError))
    .pipe(cleancss())
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest(config.dest));
}

/**
 * Stylelint
 */
export function stylelint() {
  return gulp
    .src(config.src)
    // firstPass は初回実行時に全ファイルを対象とするかどうかのオプション
    .pipe(changed({ firstPass: true }))
    .pipe(gulpStylelint({
      failAfterError: isProd,
      reporters: [{ formatter: 'verbose', console: true }],
      syntax: 'scss'
    }));
}

export const styles = gulp.series(stylelint, sass);

Stylelint の設定ファイルです。

.stylelintrc
{
  "extends": "stylelint-config-standard",
  "plugins": ["stylelint-order"],
  "rules": {
    "order/properties-alphabetical-order": true
  }
}

画像圧縮

gulp-imagemin を使って画像圧縮を行うタスクです。

オプションはお好みでどうぞ。

tasks/images.js
import gulp from 'gulp';
import imagemin from 'gulp-imagemin';

import { images as config } from './config';

export function images() {
  return gulp
    .src(config.src)
    .pipe(imagemin())
    .pipe(gulp.dest(config.dest));
}

Edge.js → HTML

テンプレートエンジン Edge.js のファイルを HTML に変換するためのタスクです。

Edge.js について詳しくはこちらの記事もご覧ください。

tasks/templates.js
import gulp from 'gulp';
import edge from 'edge.js';
import tap from 'gulp-tap';
import rename from 'gulp-rename';
import fs from 'fs';
import path from 'path';

import { templates as config } from './config';

/**
 * Edge.js -> HTML
 */
export function templates() {
  // テンプレートを読み込む
  edge.registerViews(path.join(__dirname, `../${config.root}`));

  // データファイルを読み込む
  const data = fs.existsSync(config.data)
    ? JSON.parse(fs.readFileSync(config.data, 'utf8'))
    : {};

  // ヘルパー関数を読み込む
  fs.existsSync(config.helpers) && require(`../${config.helper}`);

  return gulp
    .src(config.pages)
    .pipe(
      tap(file => {
        const contents = edge.renderString(String(file.contents), data);
        file.contents = new Buffer(contents);
      })
    )
    .pipe(rename({ extname: '.html' }))
    .pipe(gulp.dest(config.dest));
}

JavaScript

タスク esTranspileWebpack により JavaScript のトランスパイル〜バンドルを行います。

また esLint タスクではコードのチェックのために ESLint を実行します。gulp-eslint プラグインを使用します。

tasks/scripts.js
import gulp from 'gulp';
import gulpIf from 'gulp-if';
import plumber from 'gulp-plumber';
import gulpEslint from 'gulp-eslint';
import webpack from 'webpack';
import gulpWebpack from 'webpack-stream';
import changed from 'gulp-changed-in-place';

import { scripts as config, isProd } from './config';

export function esTranspile() {
  return gulp
    .src(config.src)
    .pipe(plumber())
    // gulp.watch と一緒に使う場合は第二引数が必須らしい。
    // https://www.npmjs.com/package/webpack-stream#usage-with-gulp-watch
    .pipe(gulpWebpack(require('../webpack.config.js'), webpack))
    .pipe(gulp.dest(config.dest));
}

export function esLint() {
  return gulp
    .src(config.src)
    .pipe(changed({ firstPass: true }))
    .pipe(gulpEslint())
    .pipe(gulpEslint.format())
    .pipe(gulpIf(isProd, gulpEslint.failAfterError()));
}

export const scripts = gulp.series(esLint, esTranspile);

Webpack の設定ファイルです。

webpack.config.js
import { scripts as config } from './tasks/config';

module.exports = {
  mode: process.env.NODE_ENV ? 'production' : 'development',
  entry: {
    app: `./${config.srcRoot}/app.js`
  },
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' }
    ]
  },
  output: {
    filename: '[name].js',
  },
  devtool: 'source-map'
}

複数のファイルを出力したい場合は、以下のように entry に追記すれば OK です。

entry: {
  app: `./${config.srcRoot}/app.js`,
  foo: `./${config.srcRoot}/foo.js`
},

こちらはESLint の設定ファイルです。

.eslintrc
{
  "extends": ["eslint:recommended"],
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module"
  },
  "env": {
    "browser": true,
    "node": true
  }
}

フォーマッタ

フォーマッタはオマケです。お好みの設定&エディタと連携していただければ。

EditorConfig

.editorconfig
root = true

[*]
end_of_line = lf
insert_final_newline = true

[*.{html,edge,scss,css,js,svg}]
indent_style = space
indent_size = 2

Prettier

.prettierrc
{
  "singleQuote": true
}

以上です。参考になれば幸いです 😇