1. Qiita
  2. 投稿
  3. react.js

WebpackってCSS周りのLoaderがいっぱいあって分かりにくいので整理してみる

  • 10
    いいね
  • 0
    コメント

Webpackを使ってCSSを処理する際に使うLoaderの種類が多すぎる問題について簡単に整理してみたいと思います。まず以下のコンフィグを見て、実際にCSSファイルに何が起こるのか、すぐに理解できますか?

{
    test: /\.css$/,
    loader: "style-loader!css-loader?modules&importLoaders=1&camelCase!postcss-loader",
},

WebpackではEntryにあるファイルから順番にImportやRequireされたファイルをたどっていき、拡張子ごとに違うローダーを通して、最終的なBundleファイルを作成する*という処理をします。

*) WebpackではChunkという言い方をしていますが。。。

なのでこの設定は、Entryパス上のファイルからCSSファイルがRequireされていた場合、postcss-loadercss-loader、そしてstyle-loaderの3つのLoaderを通しましょうという意味になります。ここで重要なのは、処理される順番で最後のものから順番に処理されるという点です。

👉 WHAT ARE LOADERS?

基本的にはpostcss-loaderの出力がcss-loaderの入力になり、css-loaderの出力がstyle-loaderの入力になり、その出力が最終的なBundleファイルに書き出されると理解すればよいでしょう。このあたりはUNIXのパイプ処理を逆にしたようなイメージがわかりやすいかもしれません。

# ローダの処理を無理やりシェル風に書いてみた
$ cat file.css | postcss-loader | css-loader --modules --importLoaders=1 | style-loader > bundle.js

ではそれぞれのLoaderは何をしているのでしょうか。呪文のようにStyle-LoaderとCSS-Loaderを組み合わせるのはなぜなのでしょうか。この記事では個人的に調べた内容を簡単にまとめてみたいと思います。

なぜStyle-LoaderとCSS-Loaderがあるのか

この2つがReactの例でよく出で来るのは、CSSファイルの内容を、読み込み時にJSから<style>タグとしてDOMに出力するという処理をしたいという流行から来ています。つまりCSSファイルを読み込まなくてもBundle.jsを読み込むと、CSSに指定した内容がStyleタグとして実行時に書き出されるということがやりたいわけです。

◆ Style-Loaderの仕事はSTYLEタグの出力

なのでStyle-Loaderの仕事は渡されたCSSの内容をStyleタグの形に変換して、さらにそれをHTML上にInjectするJSコードとしっしょにBundleに渡すという処理になります。

たとえば以下のようなCSSファイルとJSファイルがあったとします。CSSをimportしているのはWebpackに教えるため。

/* main.css */
.myInput { margin: 10px; }
// Bundle前のJS
import CSS from "./main.css"

これをWebpackを通して生成されたJSを見ると、実行時に(デフォルトでは)HEADタグ内に、Styleタグを挿入するコードが埋め込まれていることがわかります。

// 生成されたBundled JSの抜粋
var styles = listToStyles(list);
addStylesToDom(styles, options);

return function update(newList) {
  var mayRemove = [];
  for(var i = 0; i < styles.length; i++) {
    var item = styles[i];
    var domStyle = stylesInDom[item.id];
    domStyle.refs--;
    mayRemove.push(domStyle);
  }
  if(newList) {
    var newStyles = listToStyles(newList);
    addStylesToDom(newStyles, options);
  }
  for(var i = 0; i < mayRemove.length; i++) {
    var domStyle = mayRemove[i];
    if(domStyle.refs === 0) {
      for(var j = 0; j < domStyle.parts.length; j++)
        domStyle.parts[j]();
      delete stylesInDom[domStyle.id];
    }
  }
}

◆ CSS-LoaderがCSSファイル間の依存関係の解決を行う

これに対してCSS-Loaderの役割はそのものズバリ、CSSファイルの依存関係の解決を行う処理を行います。例えば、CSSファイル上で@importなどで外部のCSSファイルをインポートしている場合、これらのファイルもまとめてバンドルしたいという処理や、クラス名のローカル・スコープ化などを行ってくれます。

  • @import記法による外部CSSファイルの読み込み
  • URL記法による画像やフォントなど外部リソースの読み込み
  • クラス名やID名などのローカル・スコープ化

などが主な仕事です。詳しい処理はオフィシャルのGithubレポジトリを見るとよく分かるのですが、生成されたJSを見ると何が起こるのかよくわかると思います。

// 生成されたBundled JSファイル。デフォルトではGlobal Scopeとの衝突を避けるため、
// それぞれのクラス名がランダムな名前に変更されている。
exports.locals = {
  "myInput": "_1LSFDF7AGSjr4tqAAn21qj",
  "myInput": "_1LSFDF7AGSjr4tqAAn21qj"
};

◆ Less-Loader, SASS-Loader, Postcss-Loaderとか

さらに問題を複雑にするのはLess、Sass、CSSNextなどを使いたい場合です。以下のような書き方を見たことがあるんじゃないでしょうか。

{
    test: /\.less$/,
    loader: "style-loader!css-loader!less-loader",
}

これはかんたんで、最初にless-loaderを通して、LESSで書かれたスタイルシートを一般的なCSS記法に変換して、さらにcss-loaderでCSS特有の依存関係の解決や、識別子のローカル化を行い、style-loaderでJSコード内に書き込むという処理をしているわけです。

まとめ

ここまでで、CSSファイルになにが起こるのかだいぶ分かってきたのではないでしょうか。もしもCSSの背景画像などのPNGファイルを、Base64記法でCSSファイルに組み込んでしまいたい場合は、URL-Loaderを組み合わせればいい(WebpackはCSSの中でbackground: url("./file.png")を見つけると、PNGファイルに対応するLoaderを呼び出す)ですし、そもそもStyleタグとして動的に生成するのではなくて、別のCSSファイルとして出力したい場合は、style-loaderの代わりにextract-text-webpack-pluginを使えばよいわけです。

最後にそれぞれの場合の設定例などを簡単に紹介しておきます。

要件ごとの設定例

例1 CSSファイルをバンドルしてひとつのファイルに書き出す

Style-Loaderの代わりにExtractTextPluginを使います。

npm i -D extract-text-webpack-plugin

でプラグインをインストールしたあと、それをコンフィグから呼び出すだけです。このプラグイン、若干くせがあるので、使用前にはマニュアルを読むことをおすすめします。

👉  EMBEDDED STYLESHEETS

また合わせて、HtmlWebpackPluginを使うと、生成されたCSSファイルへのLinkタグを動的に挿入してくれます。(便利!)

// webpack.config.js
var ExtractTextPlugin = require("extract-text-webpack-plugin");
var HtmlWebpackPlugin = require("html-webpack-plugin");
var config = {
    entry: { main: "./src/main.js" },
    output: {
        path: "./build/",
        filename: "[name].[chunkhash].js",
        sourceMapFilename: "[name].[chunkhash].map",
    },
    plugins: [
        // 1) Extract Pluginを読み込む
        new ExtractTextPlugin("[name].[chunkhash].css"),

        // 2) あわせてHtmlWebpackPluginを使うとHTML上にCSSへのリンクを書き出してくれる
        // Add => <link href="<name.chunkhash>.css" rel="stylesheet"></head>
        new HtmlWebpackPlugin({
            template: "./src/client/index.html",
            inject: "body",
        }),
    ]
    module: {
        loaders: [
            // Extract css files
            {
                // 3)実際にExtractTextPluginでCSSを処理する
                test: /\.css$/,
                loader: ExtractTextPlugin.extract("css-loader")
            },
        ]
    },
}
module.exports = config;

例2 SASS|LESS|CSS-NEXTを使った記法を普通のCSSに変換してbundle.jsに渡す

ここまで読んでいただけた我慢強いかたならもうおわかりですね。単純に拡張子に対応するローダーを最初にかませるだけです。ここではLESSを使った例を紹介します。

{
    test: /\.less$/,
    loader: "style-loader!css-loader!less-loader",
},

簡単ですね。

おすすめのLoader

ちなみにpostcssというプロジェクトが提供するCSS AutoPrefixerというプラグインが非常に便利で、自動的にVendor Prefixを挿入してくれるという超絶ありがたいことをしてくれます。フロントが片手間のDevには本当にありがたい機能で、これがあるおかげで、CSSのバッドノウハウ的なことはまるっと忘れて標準CSSだけ書いて、あとは自動生成させてしまえます。

👉 POSTCSS-Loader