前回の記事 Yii2のAssetsでNPMやBowerパッケージを読み込むには でYii2のAssetシステムの使い方についてまとめましたが、今回は、Yii2のAssetを使わずに、JavascriptやCSSをGruntで管理する方法について調べた内容をまとめてみました。

前半は、Yii2 CookbookのAsset processing with Gruntの内容を試したメモで、その後簡単なサンプルを作成してみます。

解説の前提として、Yiiプロジェクトが作成されているところからになります。

Gruntのセットアップ

ますは、Node.jsの環境を準備します。

Ubuntuの場合は以下のコマンドでインストールできます。

$ curl -sL https://deb.nodesource.com/setup_5.x | sudo -E bash -
$ sudo apt-get install -y nodejs

他のプラットフォームのパッケージインストールについては Installing Node.js via package manager を参照してください。

OS Xの場合は、nodebrewがおすすめです。

Gruntをグローバルインストールします。

$ sudo npm install -g grunt-cli

プロジェクトディレクトリに移動して、npm initしてpackage.xmlを作成した後、必要なGruntプラグインをインストールしていきます

$ cd project_dir
$ npm init
$ npm install grunt --save-dev
$ npm install grunt-contrib-copy --save-dev
$ npm install grunt-contrib-less --save-dev
$ npm install grunt-contrib-uglify --save-dev
$ npm install grunt-contrib-watch --save-dev
$ npm install grunt-concat-sourcemap --save-dev
$ npm install grunt-typescript --save-dev

デフォルトのassetManagerを無効にするようにconfig/web.phpを設定します。

$config = [
    ...
    'components' => [
        ...
        'assetManager' => [
            'bundles' => false,
        ],
    ],
    ...

GruntでビルドしたCSSとJSを読み込むように、レイアウトviews/layouts/main.phpに追加していきます。

CSSは、<?= Html::csrfMetaTags() ?>の後ろにHtml::cssFileの呼び出しを追加します。

...
<head>
    <meta charset="<?= Yii::$app->charset ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <?= Html::csrfMetaTags() ?>
    <?= Html::cssFile(YII_DEBUG ? '@web/css/all.css' : '@web/css/all.min.css?v=' . filemtime(Yii::getAlias('@webroot/css/all.min.css'))) ?>
    <title><?= Html::encode($this->title) ?></title>
    <?php $this->head() ?>
</head>
...

http://example.com/css/all.cssがあれば開発用のall.cssを、なければプロダクションの圧縮したall.min.cssのリンクを追加するようにしています。

JSの読み込みは、<?php $this->endBody() ?>の前に追加します。

...
<body>
<?php $this->beginBody() ?>

...

<?= Html::jsFile(YII_DEBUG ? '@web/js/lib.js' : '@web/js/lib.min.js?v=' . filemtime(Yii::getAlias('@webroot/js/lib.min.js'))) ?>
<?= Html::jsFile(YII_DEBUG ? '@web/js/all.js' : '@web/js/all.min.js?v=' . filemtime(Yii::getAlias('@webroot/js/all.min.js'))) ?>
<?php $this->endBody() ?>
</body>
...

JSもCSSと同じく開発用とプロダクション用のJSの読み込みを切り替えています。

Gruntfile.jsは以下のようになります。

module.exports = function (grunt) {
    grunt.initConfig({
        less: {
            dev: {
                options: {
                    compress: false,
                    sourceMap: true,
                    outputSourceFiles: true
                },
                files: {
                    "web/css/all.css": "assets/less/all.less"
                }
            },
            prod: {
                options: {
                    compress: true
                },
                files: {
                    "web/css/all.min.css": "assets/less/all.less"
                }
            }
        },
        typescript: {
            base: {
                src: ['assets/ts/*.ts'],
                dest: 'web/js/all.js',
                options: {
                    module: 'amd',
                    sourceMap: true,
                    target: 'es5'
                }
            }
        },
        concat_sourcemap: {
            options: {
                sourcesContent: true
            },
            all: {
                files: {
                    'web/js/all.js': grunt.file.readJSON('assets/js/all.json')
                }
            }
        },
        copy: {
            main: {
                files: [
                    {expand: true, flatten: true, src: ['vendor/bower/bootstrap/fonts/*'], dest: 'web/fonts/', filter: 'isFile'}
                ]
            }
        },
        uglify: {
            options: {
                mangle: false
            },
            lib: {
                files: {
                    'web/js/lib.min.js': 'web/js/lib.js'
                }
            },
            all: {
                files: {
                    'web/js/all.min.js': 'web/js/all.js'
                }
            }
        },
        watch: {
            typescript: {
                files: ['assets/ts/*.ts'],
                tasks: ['typescript', 'uglify:all'],
                options: {
                    livereload: true
                }
            },
            js: {
                files: ['assets/js/**/*.js', 'assets/js/all.json'],
                tasks: ['concat_sourcemap', 'uglify:lib'],
                options: {
                    livereload: true
                }
            },
            less: {
                files: ['assets/less/**/*.less'],
                tasks: ['less'],
                options: {
                    livereload: true
                }
            },
            fonts: {
                files: [
                    'vendor/bower/bootstrap/fonts/*'
                ],
                tasks: ['copy'],
                options: {
                    livereload: true
                }
            }
        }
    });

    // Plugin loading
    grunt.loadNpmTasks('grunt-typescript');
    grunt.loadNpmTasks('grunt-concat-sourcemap');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-copy');

    // Task definition
    grunt.registerTask('build', ['less', 'typescript', 'copy', 'concat_sourcemap', 'uglify']);
    grunt.registerTask('default', ['watch']);
};

lib.jsに含めるYii標準のassetを指定するassets/js/all.jsonを作成します。

[
    "vendor/bower/jquery/dist/jquery.js",
    "vendor/bower/bootstrap/dist/js/bootstrap.js",
    "vendor/yiisoft/yii2/assets/yii.js",
    "vendor/yiisoft/yii2/assets/yii.validation.js",
    "vendor/yiisoft/yii2/assets/yii.activeForm.js"
]

次にassets/less/all.lessを作成します。

@import "../../vendor/bower/bootstrap/less/bootstrap.less";
@import "site.less";

site.lessは、web/css/site.cssの内容をコピーします。

$ cat web/css/site.css > assets/less/site.less

ここまでで、Gruntでアセットをビルドする環境が整いました。
grunt buildでビルドします。

$ grunt build

トップページにアクセスすると以下のように、デフォルトのAssetシステムを使っている場合と同じ表示になります。
yii2-start-page-built-with-grunt

AngularJSを利用したサンプル

GruntでAssetをビルドする環境が整いましたので、サンプルとしてAngularJSのアプリを作成してみます。

サンプルは、Yeoman AngularJS Generatorで作成したプロジェクト利用してみます。

実装の流れは、typescriptを指定してGeneratorでAngularJSのプロジェクトを作成して、必要なコードをYii2プロジェクトにコピーしていきます。

まずは、以下のコマンドでAngularJSプロジェクトを作成します。

$ sudo npm install -g yo generator-angular
$ yo angular testApp --typescript

Angularのモジュールは、angular-routeのみ追加してみました。
yii2-grunt-angular-create-project-with-yo

プロジェクトの動作を確認する場合は、以下のコマンドを実行します。

$ npm install && bower install
$ grunt serve

ブラウザで動作を確認するとトップページが以下のように表示されます。
yii2-grunt-angular-project-index

生成された、AngularJSプロジェクトのファイルをYii2プロジェクトに組み込んで行きます。

以下のディレクトリ・ファイルをコピーしてください。

app/scripts -> assets/ts
yii2-grunt-angular-app-js

app/images -> web
app/views -> web
yii2-grunt-angular-app-web

typings -> プロジェクトルート
yii2-grunt-angular-typings

コピーするファイルは以上になります。

次に、angularとangular-routeをComposertでインストールします。

$ composer require bower-asset/angular
$ composer require bower-asset/angular-route

all.jsonにangular.jsを追加します。

[
    "vendor/bower/jquery/dist/jquery.js",
    "vendor/bower/bootstrap/dist/js/bootstrap.js",
    "vendor/bower/angular/angular.js",
    "vendor/bower/angular-route/angular-route.js",
    "vendor/yiisoft/yii2/assets/yii.js",
    "vendor/bower/bootstrap/dist/js/bootstrap.js",
    "vendor/yiisoft/yii2/assets/yii.validation.js",
    "vendor/yiisoft/yii2/assets/yii.activeForm.js"
]

トップページのindexをAngularJS用に書き換えます。

<?php

/* @var $this yii\web\View */

$this->title = 'My Yii Application';
?>
<div ng-app="testAppApp">
    <div ng-view=""></div>
</div>

再度、grunt buildして、ブラウザでアクセスすると以下のように、Yiiのレイアウトの中に、AnguarJSでレンダリングしたコンテンツが表示されます。

yii2-grunt-angular-project-index-in-yii2-layout