webpackでCSSやSASSを使う

webpackでは、JSファイルだけでなくCSSやSASSもバンドルの対象にできる。
このエントリではその方法を書いていく。

webpackのバージョンは以下。

  • webpack@4.0.1
  • webpack-cli@2.0.9

下準備

webpackをインストール。

$ npm i -D webpack webpack-cli

npm scripts
出力されたファイルの中身を見たいので、モードはdevelopmentにしておく。

"scripts": {
  "build": "webpack --mode development"
},

webpack.config.js
このように書いてビルドすると、src/index.jsをエントリポイントとして、dest/bundle.jsとして出力される。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
}

src/index.jsに次のように書いてみると、dest/bundle.jsが生成される。

const body = document.querySelector('body');
body.innerHTML += 'Hello World!';
// 一部省略
/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("const body = document.querySelector('body');\nbody.innerHTML += 'Hello World!';\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

dest/index.htmlを作ってdest/bundle.jsを読み込むとHello World!と表示されるが、これに以下のスタイルをあてるのが、今回の目標。

/* ./src/style.css */
body {
  color: red;
}

css-loader

webpackでは、Loaderを使うことで、JavaScript以外のものでもJavaScriptで扱えるようになる。
CSS用にはcss-loaderがあるので、インストールする。
https://github.com/webpack-contrib/css-loader

$ npm i -D css-loader

webpack.config.jsを編集。
moduleの部分が、Loaderの設定。CSSファイルにはcss-loaderを使うように設定している。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
  module: {
    rules: [
      {test: /\.css$/, use: ['css-loader']}
    ],
  },
}

src/index.jsCSSを読み込むようにしてビルドしてみると、スタイルシートの中身を読み込めていることが分かる。

import css from './style.css';

console.log(css.toString());
// body {
//   color: red;
// }

つまり、src/style.cssの内容も含めて、dest/bundle.jsにバンドルされている。

// bundle.js
// 一部省略
/***/ "./src/style.css":
/*!***********************!*\
  !*** ./src/style.css ***!
  \***********************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval("exports = module.exports = __webpack_require__(/*! ../node_modules/css-loader/lib/css-base.js */ \"./node_modules/css-loader/lib/css-base.js\")(false);\n// imports\n\n\n// module\nexports.push([module.i, \"body {\\n  color: red;\\n}\\n\", \"\"]);\n\n// exports\n\n\n//# sourceURL=webpack:///./src/style.css?");

しかしこのような形で読み込めても、実際にスタイルとして使うことは出来ない。
この段階でdest/index.htmlを表示しても、スタイルは反映されていない。
dest/index.htmlを開いた際にスタイルシートとして読み込まれるようにする必要がある。

style-loader

そこで利用するのが、style-loaderである。
https://github.com/webpack-contrib/style-loader

公式によれば、スタイルタグを使うことでCSSが利用可能になるらしい。
Adds CSS to the DOM by injecting a style tag

$ npm i -D style-loader

webpack.config.jsにも追加。

module: {
  rules: [
    {test: /\.css$/, use: ['style-loader', 'css-loader']}
  ],
},

これでビルドすると、src/index.jsimportしたスタイルをそのままdest/index.htmlで読み込むことができ、文字が赤くなる。

ちなみに、先程src/index.jsの中でimport css from './style.css';と書いたが、これは動作確認のためであって、実際にはcssという変数を参照する必要はないため、以下のように書けばよい。

import './style.css';

extract-text-webpack-plugin

2018/10/24 追記
extract-text-webpack-plugin2018年4月にドキュメントが更新されwebpack@4での使用が非推奨になった。
webpack@4では mini-css-extract-plugin の使用が推奨されている。
numb86-tech.hatenablog.com 追記終わり

style-loaderを使わず、extract-text-webpack-pluginを使うという方法もある。
https://github.com/webpack-contrib/extract-text-webpack-plugin

これは、Loaderで変換した結果をテキストとしてファイルに抽出するプラグイン

ちなみに、2018.2.28現在の最新バージョンである3.0.2はwebpack@4に対応していないので、プレリリースである4.0.0をインストールする必要がある。

$ npm i -D extract-text-webpack-plugin@next

webpack.config.jsを編集。

const path = require('path');
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dest'),
  },
  module: {
    rules: [
      {test: /\.css$/, use: ExtractTextPlugin.extract({use: 'css-loader'})}
    ],
  },
  plugins: [
    new ExtractTextPlugin('style.css'),
  ],
}

この状態でビルドすると、dest/style.cssが生成される。

body {
  color: red;
}

その一方で、dest/bundle.jsには、CSSの内容が含まれなくなる。

そのため、dest/index.htmlのなかでlinkタグを使ってdest/style.cssを読み込むようにする。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" type="text/css" href="./style.css">
  <title>Learn CSS</title>
</head>
<body>
  <script src="./bundle.js"></script>
</body>
</html>

スタイルシートのなかで画像ファイルを使う

スタイルシートのなかで画像を読み込んでいる場合、その画像もwebpackで出力することになるため、そのための対応が必要になる。

body {
  color: red;
  background-image: url('./images/background-image.png');
}

画像を変換するためのLoaderが必要なのだが、それを用意せずにビルドしようとしても、エラーになる。

ERROR in ./src/images/background-image.png
Module parse failed: Unexpected character '�' (1:0)
You may need an appropriate loader to handle this file type.

この対応方法も、いくつか種類がある。

file-loader

まず、file-loaderを使うパターン。
https://github.com/webpack-contrib/file-loader

これを使うことで必要な画像が出力され、その画像へのURLが、出力されたスタイルシートに記述される。

webpack.config.jsを編集して、.pngに対してfile-loaderを使うようにする。

module: {
  rules: [
    {
      test: /\.css$/,
      use: ExtractTextPlugin.extract({
        use: 'css-loader',
      })
    },
    {
      test: /\.png$/,
      use: 'file-loader',
    },
  ],
},

これでビルドすると、以下のdest/style.cssが出力されると同時にdest/f20a9eae72f4385000603750517663e5.pngというファイルも出力されるため、画像を読み込めるようになる。

body {
  color: red;
  background-image: url(f20a9eae72f4385000603750517663e5.png);
}

url-loader

file-loaderだと、新しくファイルを出力することになる。
url-loaderを使うと、ファイルは出力せず、スタイルシートのなかにDataURLとして出力される。
https://github.com/webpack-contrib/url-loader

file-loaderではなくurl-loaderを使うようにすると、以下のようなスタイルシートが出力される。

body {
  color: red;
  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAP0lEQVQ4T2OcOXPmfwYiwL1794hQxcDAOGogznAaDUOcQTMEkk15eTlROUVJSYm4nDJqIM5wGg1D3Dll0CcbAMflUeHimVpmAAAAAElFTkSuQmCC);
}

スタイルシートで読み込む画像ファイルはwebpackで出力しない

file-loaderにしろurl-loaderにしろ、src/に画像を入れておき、ビルドの結果dest/に画像やDataURLが出力される。
つまり、同じデータが二重に存在することになる。

それを避ける方法として、スタイルシートで読み込む画像は最初からdest/に用意しておき、webpackの対象外にするという方法がある。

まず、src/に画像を置くのではなくdest/images/background-image.pngを用意する。
src/style.cssの内容は変えない。

body {
  color: red;
  background-image: url('./images/background-image.png');
}

この状態でビルドしようとすると、src/images/background-image.pngを参照しようとして、ファイルが見つからずエラーになる。

css-loaderのオプションでURLの解決を無効にすることで、この問題を解決できる。

module: {
  rules: [
    {
      test: /\.css$/,
      use: ExtractTextPlugin.extract({
        use: {loader: 'css-loader', options: {url: false}},
      })
    },
  ],
},

こうすると、urlの部分はsrc/style.cssに記述したものがそのままdest/style.cssに出力されるため、dest/にある画像を参照できる。

body {
  color: red;
  background-image: url('./images/background-image.png');
}

複数のスタイルシート

複数のスタイルシートimportした場合。

import './style.css';
import './style-font-size.css';

以下のようなCSSが出力される。

body {
  color: red;
}
body {
  color: orange;
  font-size: 30px;
}

重複部分を上手く調整したりはしない。ただ単に後からimportしたものが反映される。

css-loader の minimize

minimizetrueにすると、出力結果が圧縮される。

module: {
  rules: [
    {
      test: /\.css$/,
      use: ExtractTextPlugin.extract({
        use: {loader: 'css-loader', options: {minimize: true}},
      })
    }
  ],
},
plugins: [
  new ExtractTextPlugin('style.css'),
],
body{color:red}body{color:orange;font-size:30px}

SASS

SASS(SCSS)を使いたい場合は、sass-loaderを使う。
https://github.com/webpack-contrib/sass-loader

node-sassが必須なので、それも一緒にインストールする。

$ npm i -D node-sass sass-loader

複雑なことは何もなく、SASSをCSSに変えてしまい、後はこれまで書いてきた方法でCSSを処理すればいい。
だから、Loaderを使って最初にSASSをCSSに変換してしまえばよい。

まず、src/style.scssを定義する。

body {
  color: red;
  background-image: url('./images/background-image.png');
}

そしてこれを、JSファイルのなかで読み込む。

import './style.scss';

最後に、webpack.config.jsを編集する。

module: {
  rules: [
    {
      test: /\.scss$/,
      use: ExtractTextPlugin.extract({
        use: [{loader: 'css-loader', options: {url: false}}, 'sass-loader'],
      })
    },
  ],
},

Loaderの対象を.scssにする。
そしてまずsass-loaderCSSに変換し、それをcss-loaderで変換、最後にextract-text-webpack-pluginを使っている。
Loaderは後ろに書いたものから実行されることに注意。

これで、dest/スタイルシートが出力される。

sass-loaderで変換した後はCSSとして扱われるので、extract-text-webpack-pluginではなくstyle-loaderを使うことももちろん出来る。

参考資料