main-bower-files と gulp-inject と gulp-usemin を組み合わせる - Gulp で作る Web フロントエンド開発環境 #6

wakamsha
92

前置き - Bower は便利、だけど…

npm-logo

フロントエンド開発環境を用意するのに npm を利用するのはわりと一般的になっているかと思います。Grunt しかり Gulp しかり。Browserify をはじめとした依存関係解決や結合・圧縮といった開発そのものを補助するライブラリの多くは npm から取得することが出来ます。

bower-logo

では、jQuery や AngularJS といった開発時だけでなくブラウザから読み込んで使うライブラリに関してはどのように用意してますか?それらの類の幾つかは npm 経由でも取得できますが、全てではありません。そういったものに関しては別の方法で取得する必要があります。人によって方法は様々ですが、現在の僕は その類のライブラリに関しては Bower を使って取得するようにしています。Bower は GitHub 上で公開されている JS ライブラリ ( CSS ライブラリも含む ) の殆どを取得出来るのでとても便利です。

Bower から取得したライブラリは、デフォルトだと bower_components というディレクトリの配下に置かれます。以下は npm と Bower を用いたフロントエンド開発プロジェクトのディレクトリ構成の一例です。

sample_project/
├── bower_components/
│   ├── angularjs/
│   │   └── ...
│   ├── EaselJS/
│   │   └── ...
│   └── TweenJS/
│       └── ...
├── jade/
│   └── index.jade
├── node_modules/
├── public/
│   └── index.html
├── scss/
│   ├── ...
│   └── ...
├── bower.json
├── gulpfile.js
└── package.json

プロジェクトディレクトリ直下に bower.json という使用するライブラリの情報が記述されたファイルと bower_components ディレクトリがあります。あくまで一例ですが、多くの人はこのように配置するのではないでしょうか。

ここでひとつ疑問が生まれます。Bower 経由でライブラリを取得したはいいものの、どうやってHTMLファイルから読み込めばいいのでしょうか?上の例では Jade をテンプレートエンジンとして使っています。index.jadeから直接 bower_components 配下にあるライブラリファイルまでの相対パスを書きますか?さすがにそれではコードが冗長な上、お互いのディレクトリ階層が固定化されてしまって不便ですよね。であればライブラリファイルを直接取り出して別ディレクトリにコピーしますか?いやいや、それでは Bower を使うメリットが失われてしまいます。Bower に限らずリポジトリから取得したライブラリを直接触るのは避けたいところです。

ここから本題

前置きが長くなりました。今回はこの問題を解決するための Gulp タスクを紹介します。以下に紹介する Node パッケージを組み合わせることで、これらの問題を解決するだけでなく、ファイルの結合やミニファイ化も実現出来るようになります。

使用する Node パッケージ

  • main-bower-files
  • gulp-inject
  • gulp-usemin
  • gulp-uglify

main-bower-files

Bower コマンドでライブラリを取得する際に --save というオプションを付けると、bower.jsonファイルに取得したファイル情報 ( 依存関係 ) が追記されます。main-bower-files はこの bower.json ファイルに記載されているライブラリのコアファイルのパスを取得します。

Node パッケージをインストールします。

$ npm install --save-dev main-bower-files

以下は AngularJS, EaselJS, TweenJS を Bower 経由で取得した場合の例です。

{
  ⋮
  "dependencies": {
    "angularjs": "~1.4.1",
    "EaselJS": "~0.8.1",
    "TweenJS": "~0.6.1"
  }
}

main-bower-files はここから以下のようなファイルパスを取得することが出来ます。

[
  "bower_components/angularjs/angular.js",
  "bower_components/EaselJS/lib/easeljs-0.8.1.combined.js",
  "bower_components/TweenJS/lib/tweenjs-0.6.1.combined.js"
];

ファイルパスを配列形式で取得出来ました。この配列の順序は bower.json に書かれている順序と同じになります。つまり、AngularJS と ngResource といった依存関係がある場合は、bower.json に記述されている順序に気をつければ良いということになります。

さて、ファイルパスが取得できたら次はこのパスを HTML ファイルに注入していきます。

gulp-inject

gulp-inject は HTML 内の指定した箇所にコードを注入することができます。先ほどの main-bower-files が取得したライブラリのファイルパスを HTML に動的に埋め込んでみましょう。

Node パッケージをインストールします。

$ npm install --save-dev gulp-inject

次に HTML ( Jade ) の埋め込みたい箇所をコメントで指定します。

doctype html
html
    head
        meta(charset="UTF-8")
        title main-bower-files x gulp-inject x gulp-usemin

    body
        .container
            h1.page-header
            	|main-bower-files x gulp-inject x gulp-usemin

        // inject:js
        // endinject

// inject:js, // endinject の間がコードが注入される箇所です。コメントの記法は // {注入箇所の名前} : {注入するコードの形式} となり、名前を変えることで複数箇所を指定出来ます ( 名前のデフォルトは inject )。コードの形式を :js とすると が注入され、 :css とすると が注入されます。

Gulp タスクを定義します。

var gulp       = require('gulp');
var jade       = require('gulp-jade');
var inject     = require('gulp-inject');
var bowerFiles = require('main-bower-files');

var $DEST = './public/';
var $SRC = './';

var path = {
    $SRC: $SRC,
    $DEST: $DEST,
    src: {
        jade: [$SRC + "jade/**/*.jade", "!" + $SRC + "jade/**/_*.jade"],
        html: [$DEST + "*.html", "!" + $DEST + "/partials/*.html"]
    },
    dest: {
        html: $DEST
    }
};

gulp.task('jade', () => {
    return gulp.src(path.src.jade).pipe(jade({
        pretty: true
    })).pipe(gulp.dest(path.dest.html));
});

gulp.task('inject', () => {
    let bowerFiles = bowerFiles();
    let sources = gulp.src(bowerFiles, {
        read: false
    });
    return gulp.src(path.src.html).pipe(inject(sources, {
        relative: true
    })).pipe(gulp.dest(path.$DEST));
});

gulp.task('default', ['jade'], () => {
    return gulp.start(['inject']);
});

jade タスクと inject タスクを定義しました。inject タスクでは、まず最初に main-bower-files を実行して Bower から取得したライブラリのファイルパスを配列形式で取得します。次にそれを使って Gulp の Viniyl source オブジェクトを作成し、inject() の引数として渡します。この時 relative オプションに true を渡すと bower_components ディレクトリまでの相対パスを上手い具合に補完してくれます。

タスクを実行してみましょう。

$ gulp

すると以下の様な HTML ファイルが生成されます。inject コメントの箇所に script タグが埋め込まれているのが分かります。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>main-bower-files x gulp-inject x gulp-usemin</title>
  </head>
  <body>
    <div class="container">
      <h1 class="page-header">main-bower-files x gulp-inject x gulp-usemin</h1>
    </div>
    <!-- inject:js-->
    <script src="../bower_components/angularjs/angular.js"></script>
    <script src="../bower_components/EaselJS/lib/easeljs-0.8.1.combined.js"></script>
    <script src="../bower_components/TweenJS/lib/tweenjs-0.6.1.combined.js"></script>
    <script src="../bower_components/SoundJS/lib/soundjs-0.6.1.combined.js"></script>
    <script src="../bower_components/PreloadJS/lib/preloadjs-0.6.1.combined.js"></script>
    <!-- endinject-->
  </body>
</html>

この仕組みの良いところは、JS ライブラリを追加するたびに HTML 側に script タグを追記する必要がないことと、JS ライブラリの依存関係を bower.json で管理出来るところにあります。また HTML のディレクトリが変更になっても inject タスク側で上手い具合に補完してくれるので、些細な抜け漏れを防ぐことが出来ます。

gulp-usemin

gulp-usemin は、JS や CSS を結合して一つのファイルに置き換えることができます。これだけ聞くと gulp-concat や Browserify と同じモノのように思えるかもしれませんが、置き換えの際に HTML 内のコードまで書き換えてくれるという特徴があります。gulp-inject との相性が良さそうに思えたので今回採用してみることにしました。

パッケージをインストールします。

$ npm install --save-dev gulp-usemin

まずは HTML ( Jade ) のコードに gulp-usemin 用のコメントを追記します。

doctype html
html
    head
        meta(charset="UTF-8")
        title Hello, gulp-inject

    body
        .container
            h1.page-header Hello, gulp-inject

        // build:js_vendor ./assets/js/vendor.js
        // inject:js
        // endinject
        // endbuild

コメントは // build:{結合するブロックの名前} {結合して生成したファイルのパス} // endbuild という書式になっています。先ほどの inject 用コメントを囲うことで、そこに注入された script タグを usemin が結合するというわけです。

次に gulpfile.js に usemin タスクを追記します。

var gulp       = require('gulp');
var jade       = require('gulp-jade');
var inject     = require('gulp-inject');
var bowerFiles = require('main-bower-files');
var usemin     = require('gulp-usemin');

var $DEST = './public/';
var $SRC = './';
var path = {
    $SRC: $SRC,
    $DEST: $DEST,
    src: {
        jade: [$SRC + "jade/**/*.jade", "!" + $SRC + "jade/**/_*.jade"],
        html: [$DEST + "*.html", "!" + $DEST + "/partials/*.html"]
    },
    dest: {
        html: $DEST
    }
};

gulp.task('jade', () => {
    return gulp.src(path.src.jade)
        .pipe(jade({
            pretty: true
        }))
        .pipe(gulp.dest(path.dest.html));
});

gulp.task('inject', () => {
    let bowerFiles = bowerFiles();
    let sources = gulp.src(bowerFiles, {
        read: false
    });
    return gulp.src(path.src.html)
        .pipe(inject(sources, {
            relative: true
        })).
        pipe(gulp.dest(path.$DEST));
});

gulp.task('build', () => {
    return gulp.src(path.src.html)
        .pipe(usemin())
        .pipe(gulp.dest(path.$DEST));
});

gulp.task('default', ['jade'], () => {
    return gulp.start(['inject']);
});

また、usemin() 実行時に各コメントブロックに対してミニファイといった処理を加える事も出来ます。先ほど追記した gulp タスクを以下のように書き換えます。

var usemin = require('gulp-usemin');
var uglify = require('gulp-uglify');

gulp.task('build', () => {
    return gulp.src(path.src.html)
        .pipe(usemin({
            js_vendor: [uglify()]
        }))
        .pipe(gulp.dest(path.$DEST));
});

js_vendor というコメントブロックに対して uglify() を指定しました。こうすることで結合されたファイルに対してミニファイ化処理を加える事ができます。

npm install --save-dev gulp-uglify を予め実行して パッケージをインストールしておくのを忘れずに。

実行してみよう

ひとまず各タスクを個別に実行してみて、最終的にどのようになるかを見てみましょう。

$ gulp jade
$ gulp inject
$ gulp build

生成された HTML はこのようになります。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>main-bower-files x gulp-inject x gulp-usemin</title>
  </head>
  <body>
    <div class="container">
      <h1 class="page-header">main-bower-files x gulp-inject x gulp-usemin</h1>
    </div>
    <script src="./assets/js/vendor.js"></script>
  </body>
</html>

Jade コンパイル > JS ライブラリ注入 > それらを結合してミニファイ化 という処理が正常に行われました。開発中は Jade コンパイルから JS ライブラリ注入までを行い、本番環境デプロイ時に usemin を使って結合とミニファイ化すると良いかと思います。

この例では Gulp タスクを個別に実行していますが、もちろん実際の開発においては default タスクを実行するだけで全ての処理が行われるようにすべきです。しかしこれらのタスクには依存関係があり、必ず前のタスクが完了してから次のタスクが実行される必要があります。Gulp は Grunt と違って複数のタスクが平行して走るため、普通に書いたのでは依存関係を解決しながら実行してくれません。

その場合は run-sequence という Node パッケージを使うと良いです。run-sequence はその名の通り、前のタスクが終了してから次のタスクをさせることができます。Gulp で依存関係を解決しながらタスクを実行したいときに重宝します。

これらの要件って Debowerify でも満たせるのでは?

はい、似たような処理は Debowerify でも満たせると思います。しかし個人的な意見ですが、ブラウザから読み込むファイルに対して require() を使うというがどうにもしっくりきません。また、本番環境にデプロイするときならまだしも開発中はなるべくファイルの結合をせずバラバラに読み込んだ方が可読性が保てるため、差分チェックもしやすくなります。なにより今回紹介した方法は、Bower から取得したライブラリを単に結合しているだけ、と思想がシンプルなので理解しやすいです。

今回ご紹介した仕組みは、こちらの yeoman テンプレートを参考にしています。

このあたりは僕自身まだまだ手探り中なので、他にもっとこういうのがあるよという方がいましたら、是非ともご意見を頂けると幸いです。