あすたぴのブログ

astap(あすたぴ)のブログ

webpack でそれっぽい構成を作る

それっぽいとは

webpackは、bundleツールである。 bundleとは、まとめること。 なので、 webpackの役目は、javascript等の依存を理解しその依存関係が崩れないように1つのファイルにまとめること。 である。

しかしそれで嬉しいのは主にSPAの時であり、普通のwebサイトであれば全ページで使う javascriptcss をすべて1つにまとめられるのは困ったりする。

今回目指しているのは普通のwebサイトを作る際に必要なことを満たすこと。

具体的には以下になる。

  • bundle単位を制御できること。
  • 共通の依存は各bundleに含めないこと。
  • CSSは別ファイルにbundleすること。
  • CSS内の画像ファイル参照は画像ファイルとして別にすること。
  • CSSはSASSで書けること。
  • ソースマップが作成されてデバッグできること。
  • JSは、es6で書けること。

結果ファイル

const path = require('path');
var webpack = require('webpack')
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  context: path.resolve(__dirname, 'assets/js/'),
  entry: {
    home: './home.js',
    second: './second.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'js/[name].js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['env']
          }
        }
      },
      {
        test: /\.sass$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader'],
          publicPath: '../'
        })
      },
      {
        test: /\.(jpg|jpeg|png)$/,
        use: {
          loader: 'file-loader?name=images/[hash].[ext]'
        }
      }
    ]
  },
  devtool: 'cheap-module-eval-source-map',
  plugins: [
    new ExtractTextPlugin({
      filename: 'css/[name].css',
      allChunks: true
    }),
    new CopyWebpackPlugin([{
      from: '../static_images',
      to: 'images'
    }]),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'js/commons-[hash].js',
    })
  ]
};

ディレクトリ構成

assets
├── css
│   ├── app.sass
│   ├── hoge.sass
│   └── vendor
│       └── huga.sass
├── images
│   └── ganbaruzoi.jpg
├── js
│   ├── home.js
│   ├── second.js
│   └── vendor.js
└── static_images
    └── moga.jpg

public
├── css
│   ├── home.css
│   └── second.css
├── images
│   ├── 66ad830e621b1b231887708a3c8b4b52.jpg
│   └── moga.jpg
└── js
    ├── commons-ad736945b12c5cd3ca17.js
    ├── home.js
    └── second.js

解説

bundle単位を制御できること。

ちと、面倒なのだが、entryで分けたい単位ごとに記述し、 outputで filename: '[name].js と記述することで出来る。

  entry: {
    home: './home.js',
    second: './second.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'js/[name].js'
  },

共通の依存は各bundleに含めないこと。

webpackにbuildinされているpluginのCommonsChunkPluginを使うと出来る。

    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'js/commons-[hash].js',
    })

home.js, second.js両方で以下のように記述シてある場合。 これら共通の依存は別ファイルにbundleしてくれる。

import './vendor.js'

CSSは別ファイルにbundleすること。

      {
        test: /\.sass$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader'],
          publicPath: '../'
        })
      },
      {
        test: /\.(jpg|jpeg|png)$/,
        use: {
          loader: 'file-loader?name=images/[hash].[ext]'
        }
      }
~~~~

    new ExtractTextPlugin({
      filename: 'css/[name].css',
      allChunks: true
    }),

以下2つも一緒に解説する。

  • CSS内の画像ファイル参照は画像ファイルとして別にすること。
  • CSSはSASSで書けること。

webpackが理解するのは、javascriptのみである。 その為、webpack経由でcssを扱うためにはjs内でcssをimportする必要がある。

import '../css/app.sass'

moduleのrulesでは、entryポイントから読み込まれている依存をルールにマッチングしたものを指定した方法で解決する。

test: /\.sass$/, では、jsから読み込まれているファイルの拡張子が.sassだった場合に、 ExtractTextPluginを使って、sass-loader,css-loaderの順番に適用する。 sass-loaderでcssの構文に分解し、css-loaderでcssとして処理する。 ExtractTextPluginでjsにbundleされた後にbundleファイルから引き抜く。

引き抜いたあとに保存先は、 filename: 'css/[name].css', で指定している。

css内で background-image: url('../images/ganbaruzoi.jpg') のように、画像参照を行っている場合は、file-loaderで処理を行う。 url-loaderと併用してもいい。url-loaderは画像パスを解決し、画像をbase64エンコードしてCSSファイルを書き換える。 file-loaderはパスを解決して、書き換える。

注意点として、ExtractTextPluginはbundle後のファイルからCSS部分を引き抜くため、 デフォルトではその時点でのパスで file-loaderが適用されている。 その後、 public/css ディレクトリに引き抜いたCSSをファイルとして配置するため、パスがずれる。 以下のようにpublicPathを適用することでこれは解決するが、必ずしも良い解決策とは言えない。

publicPath: '../'

develop環境であればこれで問題ないが、productionではassetsをCDNに配置したりする。 その際は、相対パスではなく http://cdn.com/images/ みたいなURLになってほしい。

その時はproduction用の設定でCDNのURLをpublicPathに記述する必要がある。

ソースマップが作成されてデバッグできること。

devtool: 'cheap-module-eval-source-map',

これは develop 用なので、 production用はまた別になる。 詳しくは公式のdocumentに書いてある。

JSは、es6で書けること。

      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['env']
          }
        }
      },

excludeが必要なのかどうかは、まだよくわかっていない。 これは、testを満たしたファイルのうち、node_modules, bower_componets配下のものは対象としない。という意味になる。

まとめ

今回の設定は develop 向けになる。 そのため、production時には [hash] だったり、 [id] のプレースホルダーを使用して、ブラウザのキャッシュ対策を施す必要がある。

一般的には、各ファイルに [name]-[hash].js みたいに hash を付ける。

普段、一切関わりのない frontend 周りだった為、この構成を作るには結構時間がかかった。 といっても、4,5時間?

productionを見据えてたり、css,image周り含めてまでしっかりした構成が取られている情報は少なかった印象だった。 公式のドキュメントを全部読んで、あたりを付けてからぐぐると情報は出てきた。

また、しっかりとした開発を進めているわけでないのでこれでもまた問題はでてくると思う。