yutaponのブログ

javascript界隈の興味あるネタを備忘録的に残しておく場所

gulp.jsを使ってフロントエンドのビルドをする【webpack, stylus】

この記事はIwate Developers Advent Calendar 2014の6日目の記事です。

Iwate Developers Advent Calendar 2014 - Qiita

記事の内容はというと、岩手にまったく関係ありません。

学生の頃はフロントエンドを軽視してクソコードを量産していたので、今の学生の方にこんな風に作り始めるといいよって紹介する感じで書きたいと思います。

はじめに

昨今のフロントエンド開発はとにかく複雑になってきていて、学習コストが半端なくなってきてます。

angularとかbackbone.jsとか、最近ではreact+fluxなど、トレンドの移り変わりが激しいです。

またGruntやgulp.jsなどのタスクランナーの活用がフロントエンドでは欠かせなくなっていて、使ってないなら今すぐ導入すべき、とも言えます。

最近gulp.jsを使っていて、gulp.jsのタスクを書く時にこんな感じで書いていくとメンテしやすいかもと思えてきたので紹介してみようと思います。

この記事でやること

gulp.jsを使って

  • WebpackでJSをビルドする
  • Stylusをビルドする

最終的なコードはこちらに上げておきました。

sskyu/gulp-webpack-skeleton at blog-20141206 · GitHub

準備

前提として、node.jsが既にインストールされていることとします。

とりあえずプロジェクトのディレクトリを作ってpackage.json作ります。

$ cd path/to/project_dir
$ npm init
# とりあえずエンター連打

$ ls
package.json # できた

それとタスクランナーにgulp.jsを使うので、gulpコマンドが使えるようにしておきます。

$ npm install -g gulp
$ gulp -v
[gulp] CLI version 3.8.2
[gulp] Local version undefined

ソースを置くディレクトリとgulp関連のファイルを置く場所を作ってこんな風になりました。

$ tree
.
├── gulp      # gulp関連のファイル置き場
├── package.json
└── src       # ソースはここにまとめる
    ├── js    # js用
    ├── styl  # stylus用
    └── www   # 静的ファイル用

Webpackを使ってJSのビルドをする

いまどきのJSはCommonJS(またはAMD)スタイルでモジュール管理して、ビルドって工程を挟んで実際に動くJSを生成します。

CommonJSスタイルで書かれたJSをブラウザ向けにビルドするライブラリにBrowserifyがありますが、今回はWebpackを使ってみます。

Webpackの特徴はずばり、こんな感じ。

f:id:sskyu:20141206002741p:plain

Browserifyが単一のJSファイルを出力するのに対して、Webpackは複数のJSを出力することができるのですが、今回は1つのファイルだけ出力します。

jsファイルを置く

gulpメインにしたいので適当にJSファイルを作りました。

構成はこんな感じ。

$ tree src/js
src/js
├── app.js
└── components
    └── Hello.js

1 directory, 2 files
// @file app.js
(function () {
    'use strict';

    var Hello = require('./components/Hello');

    var hello = new Hello();

    document.write(hello.message);
})();
// @file Hello.js
function Hello() {
    'use strict';

    this.message = 'Hello!';
}

module.exports = Hello;

app.jsが処理のエントリーポイントとなるようにしています。

gulpfile.jsを置く

プロジェクトのルートに gulpfile.js を置きます。 普通に書いていくと gulpfile.js がどんどん肥大化していくので、gulp/tasksディレクトリを作成してその中にタスクごとにファイルを作成していきます。

とりあえずいろいろモジュールを入れます。

# モジュール入れる
$ npm install -D require-dir gulp gulp-if gulp-uglify gulp-webpack

gulpfile.jsを下記の内容で作成します。

// @file gulp.file
var requireDir = require('require-dir');

requireDir('./gulp/tasks', { recurse: true });

require-dirモジュールを使ってgulp/tasks以下を再帰的に読みこむようにします。

タスクの設定を別ファイルにしたいので、gulp/config.jsを作りその中にこんな感じの設定を書きます。

// @file config.js
var dest = './build'; // 出力先ディレクトリ
var src = './src';  // ソースディレクトリ

module.exports = {
  // 出力先の指定
  dest: dest,

  // jsのビルド設定
  js: {
    src: src + '/js/**',
    dest: dest + '/js',
    uglify: false
  },

  // webpackの設定
  webpack: {
    entry: src + '/js/app.js',
    output: {
      filename: 'bundle.js'
    },
    resolve: {
      extensions: ['', '.js']
    }
  }
}

webpackでJSのビルドをするので、gulp/tasks以下にwebpack.jsという名前でタスクファイルを作成します。

// @file webpack.js
var gulp = require('gulp');
var gulpif = require('gulp-if');
var uglify = require('gulp-uglify');
var webpack = require('gulp-webpack');
var config = require('../config');

// タスク名はファイル名と同じにしておくと見通しが良い
gulp.task('webpack', function () {
    gulp.src(config.webpack.entry)
        .pipe(webpack(config.webpack))
        .pipe(gulpif(config.js.uglify, uglify()))
        .pipe(gulp.dest(config.js.dest));
});

webpackタスクを実行します。

$ gulp webpack

すると、ルートディレクトリにbuildディレクトリが作成され、その中にjs/bundle.jsができます。

buildディレクトリは動的に生成されるものなので、.gitignoreでgit管理対象外にしておくとよいです。

index.htmlを置く

生成したbundle.jsの動作を確認するため、適当なHTMLファイルで読み込んでブラウザ実行してみましょう。

src/www 以下に下記の内容で index.html を作成します。

<!doctype html>
<html>
<head>
    <title>gulp test</title>
</head>
<body>
<script src="js/bundle.js"></script>
</body>
</html>

index.htmlをbuildディレクトリ以下に置くタスクを追加します。

やりたいことはただコピーするだけなので、gulp/tasks/copy.jsを作成します。
それとconfig.jsに若干の追記をします。

// @file copy.js
var gulp = require('gulp');
var config = require('../config').copy;

gulp.task('copy', function () {
    gulp.src(config.src)
        .pipe(gulp.dest(config.dest));
});
// @file config.js

  // 追記部分
  copy: {
    src: [   // 今後ただコピーするファイルが増えそうなので配列にしておく
      src + '/www/index.html'
    ],
    dest: dest
  }

copyタスクを実行してみます。

$ gulp copy
...

$ ls build
index.html js

生成されたindex.htmlをブラウザで開くとHello!と表示されます。

以上、JSをビルドする手順でした。

Stylusをビルドする

CSSプリプロセッサーにはsass, less, stylusの中から選ぶのが普通かと思いますが、sassの処理系がrubyなのに対して、lessとstylusはnode.jsが処理系に当たります。

node.jsでプロジェクトを進めるならlessかstylusを選ぶと依存が減ってやりやすいです。
今回は記法が柔軟なstylusを選びました。

stylを用意する

src/styl以下を次のような構成で作りました。

$ tree src/styl
src/styl
├── app.styl
├── base
│   ├── _core.styl
│   └── _reset.styl
└── modules
    ├── _header.styl
    └── _menu.styl

2 directories, 5 files

先頭に_が付いているファイルがありますが、これらは他のファイルから読み込まれる前提としてビルド対象にしないようにします。

// @file app.styl
@import 'base/_reset'
@import 'base/_core'
@import 'modules/_header'
@import 'modules/_menu'
// @file _reset.styl
*
    margin 0
    padding 0
// @file _core.styl
body
    padding 10px
// @file _header.styl
.header-title
    margin 10px
    font-size 3em
// @file _menu.styl
.menu
    .menu-list
        display inline-block
        width 100px
        margin 10px

        a
            text-decoration underline

index.htmlも若干修正をしておきます。

<!doctype html>
<html>
<head>
    <title>gulp test</title>
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
<header>
    <p class="header-title">Gulp test</p>
</header>
<nav>
    <ul class="menu">
        <li class="menu-list"><a href="http://gulpjs.com/" title="gulp.js">gulp.js</a></li>
        <li class="menu-list"><a href="http://learnboost.github.io/stylus/" title="stylus">stylus</a></li>
    </ul>
</nav>
<script src="js/bundle.js"></script>
</body>
</html>

stylusタスクを作る

stylusをビルドするタスクを作成するために、いろいろモジュールをいれます。

$ npm install -D gulp-stylus gulp-plumber gulp-concat gulp-autoprefixer gulp-minify-css

stylusをビルドするだけならgulp-stylusだけで十分です。

今回はautoprefixerやcss minifyまでやろうとしてます。

次の内容でgulp/tasks/stylus.jsを作成し、gulp/config.jsに追記します。

// @file stylus.js
var gulp = require('gulp');
var gulpif = require('gulp-if');
var plumber = require('gulp-plumber');
var stylus = require('gulp-stylus');
var concat = require('gulp-concat');
var autoprefixer = require('gulp-autoprefixer');
var minify = require('gulp-minify-css');
var config = require('../config').stylus;

gulp.task('stylus', function () {
    gulp.src(config.src)
        .pipe(plumber())              // エラー出ても止まらないようにする
        .pipe(stylus())               // 実際コンパイルしてるのはここ
        .pipe(concat(config.output))  // 1つのファイルに固める
        .pipe(autoprefixer(config.autoprefixer))  // vendor-prefixつける
        .pipe(gulpif(config.minify, minify()))    // 必要ならminifyする
        .pipe(gulp.dest(config.dest));            // 出力する
});
// @file config.js

  // 追記部分
  stylus: {
    src: [  // もし外部のcssフレームワーク使うなら配列の先頭で読み込むと良い
      src + '/styl/**/!(_)*'  // ファイル名の先頭がアンスコはビルド対象外にする
    ],
    dest: dest + '/css/',
    output: 'app.css',  // 出力ファイル名
    autoprefixer: {
      browsers: ['last 2 versions']
    },
    minify: false
  },

タスクを実行してbuild/css以下にapp.cssが生成されていることを確認します。

$ gulp stylus  # タスク実行
...

$ tree build/css  # 確認
build/css
└── app.css

0 directories, 1 file

index.htmlも編集したので$ gulp copyも実行しておきます。

生成されたbuild/index.htmlをブラウザで表示するとこんな感じです。

f:id:sskyu:20141206015728p:plain

雑なスタイルが当たっていますが、コンパイルできていることは確認できました。

細かいタスクを統合して便利にする

buildタスク

今のところ、js, css, htmlがよく編集するものになりますが、最新の動くものをビルドするには $ gulp webpack stylus copy を実行しなければなりません。

これでは面倒なのでbuildタスクを作ります。

gulp/tasks/build.jsを作成します。

// @file build.js
var gulp = require('gulp');

gulp.task('build', ['webpack', 'stylus', 'copy']);

これで $ gulp buildで動くソースが出来上がるようになりました。

gulp.task()の第二引数の配列はタスク名の配列ですが、これらは並列に実行されるので、何かのタスクの後にあるタスクを実行したい場合はコールバックスタイルで書いたりすることが必要です。

詳しくはこちらを見てみてください。

タスクランナーgulp.js最速入門 - id:anatooのブログ

watchタスク

ソースを変更したら自動でビルドが走るようにしないとやってられないです。

gulp.watch()だとwatch中に新規ファイルを追加しても反応しないので、gulp-watchモジュールを使います。

gulp-watchをインストールして、gulp/tasks/watch.jsを作成します。

$ npm install -D gulp-watch
// @file watch.js
var gulp = require('gulp');
var watch = require('gulp-watch');
var config = require('../config').watch;

gulp.task('watch', function () {
    // js
    watch(config.js, function () {
        gulp.start(['webpack']);
    });

    // styl
    watch(config.styl, function () {
        gulp.start(['stylus']);
    });

    // www
    watch(config.www, function () {
        gulp.start(['copy']);
    });
});

watch用の設定をgulp/config.jsに追記します。

// @file config.js
var path = require('path'); // 追記

var dest = './build';
var src = './src';
var relativeSrcPath = path.relative('.', src);  // 追記

module.exports = {

  // 中略

  // 追記
  watch: {
    js: relativeSrcPath + '/js/**',
    styl: relativeSrcPath + '/styl/**',
    www: relativeSrcPath + '/www/index.html'
  }
};

gulp-watchの第一引数にはglobで監視対象のパスを指定するのですが、パスの先頭を./みたいに.から始めると正常に動作しません。

それで./src/js/**みたいなパスをpath.relative()を使ってsrc/js/**に直す必要があります。

これで監視対象以下でファイルの追加や削除があってもwatchしてくれるようになりました。

$ gulp watchを実行するとファイルの監視が始まることが確認できます。

defaultタスク

defaultタスクには$ gulpを実行した時に動作させたいタスクを指定できます。
$ gulpと打ったらビルドとwatchを開始してほしいので、この2つを登録しておきます。

gulp/tasks/default.jsとして作成します。

// @file default.js
var gulp = require('gulp');

gulp.task('default', ['build', 'watch']);

これである程度フロント開発が楽になるはずです。

まとめ

何が言いたかったかというと、gulpfile.jsにはrequire-dirだけ書いて、タスク名と同じ名前のJSファイルで管理していくと捗るということ。

gulpfile.jsにどんどんタスクを書いていくと、どんなタスクが登録されているのか中身を見ないとわからないけど、ファイル名と対応付けすると$ lsするだけでわかる。

$ ls gulp/tasks
build.js   default.js watch.js
copy.js    stylus.js  webpack.js

この書き方は material-ui のリポジトリ見ていいなと思って真似してみました。

material-ui/docs at master · callemall/material-ui · GitHub

おわりに

まだまだgulpは紹介しきれないことがあるんですが、それはまたの機会ということで。

Webpackはさわり程度のことしか書けなかったけど、実際あまり触れてないので知見たまるまで時間かかりそう。

明日は @7kaji さんです。期待!!

【追記】
今回Webpackについて深く踏み込まなかったのですが、Webpackの設定についてthujikunさんが詳細にまとめてくださっています。(この記事へのリンクが..!!)

webpackを使い倒す - Thujikun blog

私も現在勉強中なので参考にさせていただきます。