concat/minifyしたファイルへのリンクを grunt-usemin を使って自動で書き換える

最近、HTML,CSS,JavaScript を記述するのに、Jade,Sass,LESS,Stylus,CoffeeScript などのプリプロセッサが便利で使っていますが、CSSFrameworkを利用することもありファイルが複数に分かれていることが多くなってきました。

grunt に限らず自動化処理を実行して、本番環境用に結合( concat )・圧縮(minify) して複数あるファイルを1つにまとめてアップロードする方も多いと思います。

しかし、HTML内で CSS( meta link=~ ) JavaScript (script src=~) は 本番環境では minifyファイルを、開発環境では元々の複数にわかれたファイルのパスを設定することになります。

(Sassのinclude 等を使えば最終的には1ファイルに纏まりますが、CSSファイルのみの構成などもあるので・・・・)

意外とこれが面倒で、私は minify ファイルをリンクしたタグをコメントアウトした状態で挿入しておいて都度切り替えるということをしていました。

ここで登場するのが、grunt-usemin です。
grunt-usemin はYeomanで使用されているのですが、基本パック的な webapps もすでに大きくなり過ぎていてわかりづらいので、今回はシンプルなフォルダ構成での設定例を挙げてみます。
色々と設定があるので、後々拡張していくと良いかもしれません。

導入

サンプルは https://github.com/kamiyam/grunt-usemin-sample にあります。

git なら
git clone git@github.com:kamiyam/grunt-usemin-sample.git

もしくは GitHubからzip
ファイルでダウンロードしてください。

ダウンロードファイルのRootフォルダから
npm install コマンドで必要なモジュールをダウンロードします。

フォルダ構成

開発フォルダを src、本番配置用フォルダをdist フォルダとします。

ちなみに dist フォルダは設定により自動的に出力されるので今のところは気にしなくて構いません。

(Root)
├── Gruntfile.coffee
├── node_modules
├── package.json
├── dist # 本番配置用
│   ├── css
│   │   ├── layout.css
│   │   ├── reset.css
│   │   ├── style.css
│   │   └── style.min.css
│   ├── index.html
│   └── js
│       ├── apps.min.js
│       ├── main1.js
│       ├── main2.js
│       └── main3.js
└── src # 開発用フォルダ  
    ├── (less)
    ├── (sass)
    ├── css
    │   ├── layout.css
    │   ├── reset.css
    │   └── style.css
    ├── index.html
    └── js
        ├── main1.js
        ├── main2.js
        └── main3.js 

grunt-usemin(Prepare)を使って自動的に圧縮ファイルへのリンクに書き換える場合は、下記サンプルのように記述します。

linkscriptタグは任意の場所に配置します。

<!-- build:<type>(alternate search path) <path> -->
... HTML Markup, list of script / link tags.
<!-- endbuild -->

と設定します。

つまり、CSSであれば <type>css<path>には任意の圧縮ファイルの名前を設定します。

<!-- build:css /css/style.min.css -->
<link rel="stylesheet" href="/css/reset.css"/>
<link rel="stylesheet" href="/css/layout.css"/>
<link rel="stylesheet" href="/css/style.css"/>
<!-- endbuild -->

JavaScriptであれば、<type>js<path>には圧縮ファイルの名前を設定します。

  <!-- build:js /js/apps.min.js -->
  <script src="/js/main1.js"></script>
  <script src="/js/main2.js"></script>
  <script src="/js/main3.js"></script>
  <!-- endbuild -->

通常開発時は grunt server:devを実行します。

特に指定通りで変わったことは起こりません。

しかし、本番環境ファイルの出力では grunt server:distを実行します。

すると dist/index.html ファイルは次のように出力されています。

  <!-- (略) -->

  <!-- CSS 定義 -->
  <link rel="stylesheet" href="/css/style.min.css"/>

  <!-- Content Area -->

  <!-- JavaScript 定義 -->
  <script src="/js/apps.min.js"></script>

  <!-- (略) -->
  

コメントで囲われたファイル群が1つに結合され、コメント欄で指定したファイル名で圧縮、index.html のファイルが書き換えられていることがわかります。

結合するファイルが増えた場合は、(サンプルでは) /src/index.htmlの <!-- build --><!-- endbuild -->で囲われたファイルの指定を増やすだけです。

本番用にコードをわざわざ書き換えること無く、圧縮ファイルへのリンクが設定できます。

しかも Gruntile.js には、結合(concat)・圧縮(uglifyjs/css)のタスクの設定は記述していません。
useminのデフォルトの設定動作とhtml ファイルコメントの設定だけで完結出来てしまいます。

もう少し細かいことをしようと思うと設定を変えないといけないですが、難しい所は無いと思います。

詳細は grunt-usemin の README を参照ください。

以下、今回のサンプルの記述・設定例です。

(記述例) /src/index.html

<!DOCTYPE HTML>
<html lang="ja_JP">
<head>

  <!-- CSS 定義 -->
  <!-- build:css css/style.min.css -->
  <link rel="stylesheet" href="/css/reset.css"/>
  <link rel="stylesheet" href="/css/layout.css"/>
  <link rel="stylesheet" href="/css/style.css"/>
  <!-- endbuild -->

</head>
<body>
  
  <!-- Content Area -->

  <!-- JavaScript 定義 -->
  <!-- build:js js/apps.min.js -->
  <script src="/js/main1.js"></script>
  <script src="/js/main2.js"></script>
  <script src="/js/main3.js"></script>
  <!-- endbuild -->

</body>
</html>

(出力例) /dist/index.html

<!DOCTYPE HTML>
<html lang="ja_JP">
<head>

  <!-- CSS 定義 -->
  <link rel="stylesheet" href="/css/style.min.css"/>

</head>
<body>

  <!-- Content Area -->

  <!-- JavaScript 定義 -->
  <script src="/js/apps.min.js"></script>

</body>
</html>

(例)Gruntfile.js

"use strict";

var LIVERELOAD_PORT, folderMount, lrSnippet;

LIVERELOAD_PORT = 35729;

lrSnippet = require('connect-livereload')({
  port: LIVERELOAD_PORT
});

folderMount = function(connect, base) {
  return connect["static"](require("path").resolve(base));
};

module.exports = function(grunt) {
  grunt.initConfig({
    connect: {
      options: {
        port: 9000,
        hostname: "localhost"
      },
      dev: {
        options: {
          middleware: function(connect, options) {
            return [lrSnippet, folderMount(connect, "src")];
          }
        }
      },
      dist: {
        options: {
          middleware: function(connect, options) {
            return [lrSnippet, folderMount(connect, "dist")];
          }
        }
      }
    },
    watch: {
      options: {
        livereload: true
      },
      dev: {
        options: {
          cwd: "./src",
          livereload: LIVERELOAD_PORT
        },
        files: ["**/*.html", "css/**/*.css", "js/**/*.js"]
      },
      dist: {
        options: {
          cwd: "./src",
          livereload: LIVERELOAD_PORT
        },
        files: ["**/*.html", "css/**/*.css", "js/**/*.js"],
        tasks: ["clean", "copy", "useminPrepare", "concat", "uglify", "cssmin", "usemin"]
      }
    },
    clean: {
      dist: [".tmp", "dist"]
    },
    copy: {
      dist: {
        files: [
          {
            expand: true,
            dot: true,
            cwd: "src/",
            dest: "dist/",
            src: ["*", "css/**", "js/**"],
            filter: "isFile"
          }
        ]
      }
    },
    useminPrepare: {
      options: {
        root: "src",
        dest: "dist"
      },
      html: ["dist/**/*.html"]
    },
    usemin: {
      options: {
        dirs: ["dist/"]
      },
      html: ["dist/**/*.html"]
    }
  });
  require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
  grunt.registerTask("default", "server");
  return grunt.registerTask("server", function(target) {
    if (target !== "dist") {
      console.log("Development Server Mode...");
      return grunt.task.run(["connect:dev", "watch:dev"]);
    } else {
      console.log("Distroduction Server Mode...");
      return grunt.task.run(["clean", "copy", "useminPrepare", "concat", "uglify", "cssmin", "usemin", "connect:dist", "watch:dist"]);
    }
  });
};