Tumblr テーマを gulp でつくるタスクレシピ


はじめに

今回書くのは、 gulpfile.js の仕様と中身です。技術的な話はないし メモのようなものです。

Gulp で解決したいタスク

Tumblr をターゲットにした上のタスクですが、今回は react を使いたいので 純粋なものではないです悪しからず。

  • stylus を使う
  • browserify (babelify) を使う
  • minify したものを index.html に inline 展開する
  • watchify で差分監視する(react 周辺のコンパイルはくっそ時間かかるので)

なぜ Gulp ???

もともと私は assemble をしたくて grunt を使い続けていたんですが、

今朝方 http://mizchi.hatenablog.com/entry/2015/05/11/182118 に grunt は開発が止まっているという悲報が。。

となれば、 gulp するしかねえ!!(でも レポジトリのコミットみたら作者も頻繁ではないですがコミットはしている模様?ただ内容的にはもう保守的な感じなのでしょうか・・・)

そういう意味で。 gulp 初心者なのです。

フォルダ構造

node module も晒す。 まぁケースによっては増えたり減ったり。

notify とかは 完全に個人趣味。

.
├── build
│   ├── css
│   ├── index.html
│   └── js
├── gulpfile.js
├── inlined
│   └── index.html
├── node_modules
│   ├── autoprefixer-stylus
│   ├── babelify
│   ├── browserify
│   ├── del
│   ├── gulp
│   ├── gulp-inline-source
│   ├── gulp-notify
│   ├── gulp-plumber
│   ├── gulp-stylus
│   ├── normalize.css
│   ├── plumber
│   ├── run-sequence
│   ├── vinyl-source-stream
│   └── watchify
├── package.json
└── src
    ├── css
    ├── index.html
    └── js

流れとしては

  1. src/ から build/ に js/app.js css/style.css index.html をつくる
  2. build/ から tumblr にペーストできるように html を吐き出す。

ですね。

gulpfile.js

長い。汚い。とりあえず。

var gulp = require('gulp');
var source = require('vinyl-source-stream');

var stylus = require('gulp-stylus');
var autoprefixer = require('autoprefixer-stylus');

var inline = require('gulp-inline-source');

var browserify = require('browserify');
var watchify = require('watchify');
var babelify = require('babelify');

var del = require('del');

var plumber = require('gulp-plumber');
var notify = require('gulp-notify');

var sequence = require('run-sequence');

gulp.task('default', ['init'] );

// init
gulp.task('init', function() {
  return sequence(
    ['stylus', 'html'],
    ['watch', 'browserify'],
    'inline'
  );
});

//clean build, inlined Folder
gulp.task('clean', function() {
  del(['build', 'inlined'], function() {
    console.log('finished');
  });
});

// stylus
gulp.task('stylus', function() {
  return gulp.src('./src/css/style.styl')
    .pipe(plumber({
      errorHandler: notify.onError({
        title: "Stylus Error",
        message: "<%= error.message %>"
      })
    }))
    .pipe(
      stylus({
        use: [
          function() { autoprefixer({browsers: ['ie 9']}) }
        ]
      })
    )
    .pipe(gulp.dest('./build/css'));
});

// html copy
gulp.task('html', function() {
  gulp.src('./src/index.html')
    .pipe(gulp.dest('./build'));
});

// inline js, css
gulp.task('inline', function() {
  gulp.src('./build/index.html')
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(inline())
    .pipe(gulp.dest('./inlined/'));
});

// JS compile
gulp.task('browserify', function() {
  var bundler = browserify({
    entries: ['./src/js/app.js'],
    transform: [babelify],
    debug: true,
    cache: {}, packageCache: {}, fullPaths: true
  });

  var watcher  = watchify(bundler);

  return watcher
    .on('update', function() {
      var updateStart = Date.now();
      console.log('JS compile!');
      watcher
        .bundle()
        .on('error', notify.onError({
            title: "JS Error",
            message: "<%= error.message %>"
        }))
        .pipe(source('app.js'))
        .pipe(gulp.dest('./build/js'));
      console.log('JS compiled!', (Date.now() - updateStart) + 'ms');
    })
    .bundle() // Create the initial bundle when starting the task
    .on('error', notify.onError({
      title: "JS Error",
      message: "<%= error.message %>"
    }))
    .pipe(source('app.js'))
    .pipe(gulp.dest('./build/js'));
});

// watch html, css files
gulp.task('watch', function() {
  gulp.watch( ['./src/css/**/*.styl'], ['stylus'] );
  gulp.watch( ['./src/build/**/*'], ['inline'] );
});

タスク説明

default

gulp.task('default', ['init'] );

init を呼びます。 関数として直接定義しても良かったですが、後々変えようとしたときにダルいだろうと思い。こういう感じ

init

gulp.task('init', function() {
  return sequence(
    ['stylus', 'html'],
    ['watch', 'browserify'],
    'inline'
  );
});

基本立ち上げ時に呼びたいタスクコール集。
inline の処理と絡めるとどうしてもシーケンス化する必要がありますね。
順序としては

  1. stylus 変換 と html のコピー
  2. watch 系のタスクを呼び出し
  3. inline 化する

clean

gulp.task('clean', function() {
  del(['build', 'inlined'], function() {
    console.log('finished');
  });
});

一旦フォルダー類を綺麗にしたいときに特別に呼び出します。
本当は init タスクの中に含めたかったんですが、どうやら、clean で使用している del は rm の実行コマンドを出した時点で next 処理が回って コールバックしているみたいで、、、他の処理(僕の場合は stylus )とかち合うと必ずエラーがでるという。。。(本当か?教えてエロい人)

stylus

gulp.task('stylus', function() {
  return gulp.src('./src/css/style.styl')
    .pipe(plumber({
      errorHandler: notify.onError({
        title: "Stylus Error",
        message: "<%= error.message %>"
      })
    }))
    .pipe(
      stylus({
        use: [
          function() { autoprefixer({browsers: ['ie 9']}) }
        ]
      })
    )
    .pipe(gulp.dest('./build/css'));
});

stylus には autoprefixer をつかう。 gulp-autoprefixer ってのもあるけど。
grunt 時代からの流れで、この使い方で。

html

gulp.task('html', function() {
  return gulp.src('./src/index.html')
    .pipe(gulp.dest('./build'));
});

html のコピー。何かしらのテンプレートエンジンを使うなら、ここに追記しよう。

inline

gulp.task('inline', function() {
  return gulp.src('./build/index.html')
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(inline())
    .pipe(gulp.dest('./inlined/'));
});

inline 化させる。一応 error に渡してるけど。ほぼ起こることないので外してもよし。

JS

gulp.task('browserify', function() {
  var bundler = browserify({
    entries: ['./src/js/app.js'],
    transform: [babelify],
    debug: true,
    cache: {}, packageCache: {}, fullPaths: true
  });

  var watcher  = watchify(bundler);

  return watcher
    .on('update', function() { // When any files update
      var updateStart = Date.now();
      console.log('JS compile!');
      watcher
        .bundle() // Create new bundle that uses the cache for high performance
        .on('error', notify.onError({
            title: "JS Error",
            message: "<%= error.message %>"
        }))
        .pipe(source('app.js'))
        .pipe(gulp.dest('./build/js'));
      console.log('JS compiled!', (Date.now() - updateStart) + 'ms');
    })
    .bundle()
    .on('error', notify.onError({
      title: "JS Error",
      message: "<%= error.message %>"
    }))
    .pipe(source('app.js'))
    .pipe(gulp.dest('./build/js'));
});

browserify は watchify を使用するので少し違う形態で書く。
この中で watch も同時に行うのがミソ。
ちなみにこのコードは参照元を大いに丸コピ。。。参考にしました。

plumber は browserify の中のエラーをハンドリングできないらしく有名。
そうなら、他でも使わずに書く選択肢もあるが、一応両方やろう。初めてだし。

ちなみに notify でなくてもよいが error をハンドリングして適切な処置(emit('end')だったかしら)してあげないと、error したあと タスク全体 が死ぬ。ので何かしら処理をかいてあげよう。

watch

gulp.task('watch', function() {
  gulp.watch( ['./src/css/**/*.styl'], ['stylus'] );
  gulp.watch( ['./src/build/**/*'], ['inline'] );
});

css と inline 化の watch タスク。
js 系は先に書いた通り、 watchify の処理系に任せる。

感想

  • 疲れた。これ作るのに 3 時間はかかった。慣れたいですね。
  • 初心者というか、node に慣れてない人にはとっつきにくいかも。でもやるしかない。私もできた。
  • notify は良い。 別のプロジェクトは beep させたりしてたけど、こっちでいいかな。
  • ソースが汚い。。。書き手の問題でもある。 普段使わないにしても こういうファイルは coffee で書くのがいいんだろう。
  • gulp の方が grunt に比べて柔軟ぽい!気がする。これから積極的にやっていく価値はありそうだと感じた。

XenobladeX について

にあるが、僕も GW の大半と有給をコンボにしてこのゲームをクリアした。
楽しかった。
初めてといっていいが オープンワールド ゲームをして、ほげえええとなったほど感じた。
ストーリーはブログにもあるような点を感じたが概ね満足である。
とにかくボリュームがあるゲームだなと思った。
私は任天堂とは全く関係のない人間だが、皆さん買って盛り上げて今後のxenoシリーズを応援しましょう!
というか xenogears が好きなんだな。