デモ環境
- Mac 10.11.x
- Yii 2.0.x
- Node.js 7.8.x
- Yarn 0.22.x
- webpack 2.3.x
想定しているサイト
- Qiita のような SPA ではないサイト
- CSS, JavaScript 関連のアセットをがっちゃんこしてリクエストを減らしつつ、ブラウザキャッシュを活かせるようなサイト
ディレクトリ構成
最終的には以下のような構成になります:
.
├── client
│ ├── config
│ │ ├── base.js
│ │ ├── dev.js
│ │ └── prod.js
│ ├── css
│ │ ├── app.less
│ │ └── site.css
│ ├── entries
│ │ └── app.js
│ ├── images
│ │ ├── apple-touch-icon.png
│ │ └── favicon.png
│ ├── js
│ ├── package.json
│ ├── webpack.config.js
│ └── yarn.lock
├── commands
├── composer.json
├── config
├── controllers
├── helpers
...
Yii 側の設定
Yii 2.0.x はアセット関連のパッケージを composer-asset-plugin 経由でインストールする形になっていますが、それをやめて、パッケージはすべて npm(Yarn) でインストールするようにします。詳しくは ここ らへんを参考にしてみてください。
ここからはデモとして yii2-app-basic を想定した説明になります。
プロジェクトのテンプレートを作成後、プロジェクトルートに client ディレクトリを作成:
(この client ディレクトリ以下にアセット関連のファイルを置いて管理する形になります)
cd /path/to/project
mkdir client
cd $_
{
"private": true,
"dependencies": {
"bootstrap": "3.3.7",
"jquery": "2.2.4",
"typeahead.js": "0.11.1",
"yii2-pjax": "2.0.6"
}
}
yarn
Yii 2 側で用意されているアセットバンドル機能を停止し、開発環境で使用するアセットのパスも書き換えておく:
$config = [
// ...
'components' => [
// ...
'assetManager' => [
'bundles' => false,
],
],
// ...
if (YII_ENV_DEV) {
// ...
$config['container']['definitions'] = [
yii\bootstrap\BootstrapAsset::class => [
'sourcePath' => '@app/client/node_modules/bootstrap/dist',
],
yii\bootstrap\BootstrapPluginAsset::class => [
'sourcePath' => '@app/client/node_modules/bootstrap/dist',
],
yii\gii\TypeAheadAsset::class => [
'sourcePath' => '@app/client/node_modules/typeahead.js/dist',
],
yii\web\JqueryAsset::class => [
'sourcePath' => '@app/client/node_modules/jquery/dist',
],
yii\widgets\PjaxAsset::class => [
'sourcePath' => '@app/client/node_modules/yii2-pjax',
],
];
}
webpack 側でアセット更新時のブラウザキャッシュを避けれるよう、最終的に app-fd07f98707077a0a0aee1.css|js みたいなファイル名にしたいので、それを上手く Yii 側でキャッチできるように、コアの Url ヘルパーにアセット専用のメソッドを追加します:
namespace yii\helpers;
class Url extends BaseUrl
{
/**
* @param string $file
* @return string
*/
public static function asset($file)
{
$path = \Yii::getAlias('@app/web/assets/manifest.json');
$manifest = file_exists($path)
? json_decode(file_get_contents($path))
: new \stdClass;
return property_exists($manifest, $file)
? '/assets/'.$manifest->$file
: '/assets/'.$file;
}
}
// ...
Yii::$classMap['yii\helpers\Url'] = '@app/helpers/Url.php';
$config = require(__DIR__ . '/../config/web.php');
// ...
最後にレイアウトファイルを修正:
<?php
use yii\helpers\Url;
// AppAsset::register($this); は使わない
?>
<?php $this->beginPage() ?>
<!DOCTYPE html>
<html lang="<?= Yii::$app->language ?>">
<head>
<meta charset="<?= Yii::$app->charset ?>">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?= Html::csrfMetaTags() ?>
<title><?= Html::encode($this->title) ?></title>
<link rel="icon" type="image/png" href="<?= Url::asset('favicon.png') ?>">
<link rel="apple-touch-icon" sizes="152x152" href="<?= Url::asset('apple-touch-icon.png') ?>">
<link rel="stylesheet" href="<?= Url::asset('app.css') ?>">
<?php $this->head() ?>
</head>
<body>
<?php $this->beginBody() ?>
<! -- ... -->
<script src="<?= Url::asset('app.js') ?>"></script>
<?php $this->endBody() ?>
</body>
</html>
<?php $this->endPage() ?>
これで Yii 側の準備は終了です :)
webpack 側の設定
yarn add webpack webpack-merge css-loader less-loader less file-loader expose-loader extract-text-webpack-plugin webpack-manifest-plugin compression-webpack-plugin --dev
-
webpack
アセットを良い感じにがっちゃんこするために使う -
webpack-merge
開発・本番・共通の設定を良い感じにマージするために使う -
css-loader, less-loader less
Less ファイルを CSS に良い感じにコンパイルするのに使う -
file-loader
Twitter Bootstrap のアイコン関連のファイルを良い感じに結びつけるために使う -
expose-loader
jQuery, $ などをグローバル空間で使えるようにするため (最終手段) -
extract-text-webpack-plugin
.js|css のアセットを分離させるために使う -
webpack-manifest-plugin
自動生成したランダムなファイル名と元のファイル名を結びつけるために使う (アセット更新時のブラウザキャッシュを避けるため) -
comporession-webpack-plugin
アセットを gzip 化するために使う
{
"scripts": {
"watch": "webpack -w --env=dev",
"build": "webpack --env=dev",
"build:prod": "webpack --env=prod"
}
}
設定ファイルの作成:
(上記のオプション(--env)の値によって読み込むファイルを設定している感じ)
function bundleConfig(env) {
return require('./config/' + env + '.js')(env)
}
module.exports = bundleConfig
環境ごとの設定ファイルを作成:
-
base.js
環境関係なく共通で使用する設定ファイル -
dev.js
開発環境でのみ使用する設定ファイル -
prod.js
本番環境でのみ使用する設定ファイル
このファイル構成や設定方法はオフィシャルのドキュメントを参考にしています。
webpack - Building for Production
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = function (env) {
return {
// project/client/entries/app.js に js 関連のアセットを羅列するように設定
entry: {
app: './entries/app.js'
},
// ビルド先は project/web/assets 以下、ファイル名は app-[hash].js で
output: {
path: path.resolve(__dirname, '../../web/assets'),
publicPath: '/assets/'
},
// Yii コアのアセットもモジュール検索してと webpack に伝える
resolve: {
modules: [
'node_modules',
path.resolve(__dirname, '../../vendor/yiisoft/yii2/assets')
]
},
module: {
rules: [
{
// Yii はコアのウィジェットなどを使うとインラインに js のコードを吐くので
// jQuery|$ をグローバルな空間でも使えるようにしておく
test: require.resolve('jquery'),
use: [
{ loader: 'expose-loader', options: 'jQuery' },
{ loader: 'expose-loader', options: '$' }
]
},
{
// .less ファイルを良い感じにコンパイル
test: /\.less$/,
use: ExtractTextPlugin.extract(['css-loader', 'less-loader'])
},
{
// Twitter Bootstrap のアイコン関連の設定
test: /\.(eot|woff2?|svg|ttf)$/,
use: 'file-loader'
},
{
// サイトのファビコン・アイコンの設定
test: /(favicon|apple-touch-icon)\.png$/,
use: {
loader: 'file-loader',
options: {
name: env === 'dev' ? '[name].[ext]' : '[name]-[hash].[ext]'
}
}
}
]
}
}
}
const baseConfig = require('./base')
const merge = require('webpack-merge')
const webpack = require('webpack')
module.exports = function (env) {
// project/client/config/base.js を読み込んで開発環境の設定とマージする
return merge.smart(baseConfig(env), {
output: {
filename: '[name].js'
},
plugins: [
// js と css を分離させる、ファイル名は app.css に
new ExtractTextPlugin({ filename: '[name].css' }),
]
})
}
const baseConfig = require('./base')
const webpack = require('webpack')
const merge = require('webpack-merge')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const ManifestPlugin = require('webpack-manifest-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
module.exports = function (env) {
// project/client/config/base.js を読み込んで本番環境の設定とマージする
return merge.smart(baseConfig(env), {
output: {
filename: '[name]-[hash].js'
},
plugins: [
// アセット関連を最小化してビルドする、デバッグはしない
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false // webpack 3 で削除予定
}),
// UglifyJS で .js ファイルの圧縮・最適化する
new webpack.optimize.UglifyJsPlugin(),
// js と css を分離させる、ファイル名は app-[hash].css に
new ExtractTextPlugin({ filename: '[name]-[contenthash].css' }),
// ランダムなファイル名と元のファイル名を結びつける manifest.json ファイルを project/web/assets 以下に生成
new ManifestPlugin(),
// .js|css ファイルを gzip 化する
new CompressionPlugin({ test: /\.(js|css)/ })
]
})
}
これで設定が完了したので、JavaScript, CSS 関連のファイルを取り込んでいきます。
cd /path/to/project/client
mkdir entries css js images
// app.js
require('jquery')
require('bootstrap/dist/js/bootstrap')
require('yii')
require('yii.activeForm')
require('yii.captcha')
require('yii.gridView')
require('yii.validation')
// app icon (サイトのファビコン類は適当に用意します)
require('../images/favicon.png')
require('../images/apple-touch-icon.png')
// app.css
require('../css/app.less')
mv /path/to/project/web/css/site.css /path/to/project/client/css
@import "~bootstrap/less/bootstrap";
@import "site.css";
これでとりあえずは OK :)
yarn run build
yarn run watch
yarn run build:prod
開発環境ではアセットファイルの最小化はされず、本番環境でのみ最小化・最適化と gzip 化がされます。
まとめ
シンプルな構成ですが、Yii + webpack の組み合わせでアセットをビルドする方法を書いていきました。おそらくパターンはいろいろあると思いますが(まだまだ最適化もできると思われる)、そこまで Yii 側をカスタマイズせずにがっちゃんこできるので、Yii の assetManager に不満を持っていたり、最近の gulp や gulp 関連のパッケージのコミット頻度に不安を感じていたりする方(自分)は試してみてはいかがでしょうか。
設定が分かりづらかったり、バージョンの差異で情報がごちゃっていたりで、泥沼にハマりがちな現状ですが、試行錯誤しながらやっていけば、そのうちなんとかなるかと思います。
自分が作成したサイトでもほぼ同じような感じで webpack を使用しているので、もしよければそれも参考にしてみてください。GitHub - jamband/plusarchive.com