PhoenixアプリでBootstrap 4 & PurgeCSS

今日は、[Phoenix](https://phoenixframework.org/) (v1.5)アプリで[Bootstrap 4](https://getbootstrap.com/docs/4.6/getting-started/introduction/)と[PurgeCSS](https://purgecss.com/)の設定方法についてまとめます。

最近（2021年前半）[Tailwind](https://tailwindcss.com/docs)がCSSフレームワークとして流行っているようです。しかしながら、ちょこっと触ってみた結果の印象ですが、個人的にその仕様で特に便利と思われる部分は見当たりませんでした。むしろ複雑性が増し、生産性が低下するような気がします。¯\_(ツ)_/¯

長年の実績のあるSASSとBootstrapフレームワークに何も問題がないので、どうしてもTailwindでないとできない素晴らしい機能がでてくるまで、[SASS](https://sass-lang.com/)とBootstrapを使わない理由が個人的にはありません。Bootstrapには各種コンポーネントが準備されていて、それを変数で自由にカスタマイズできます。イチからコンポーネントを組み立てる必要はありません。ややこしいルールも設定もありません。基本的にCSSですから。[PurgeCSS](https://purgecss.com/)も使えるので不要なCSSはコンパイル時に取り除くことができます。

それでも[Tailwind](https://tailwindcss.com/docs)を使用したい方は、Pragmatic Studioの[この記事](https://pragmaticstudio.com/tutorials/adding-tailwind-css-to-phoenix)にわかりやすく設定方法を説明されています。

[2021/04/10(土) 00:00〜開催のautoracex #22](https://autoracex.connpass.com/event/209999/)での成果です。
[English edition](https://dev.to/mnishiguchi/adding-bootstrap-4-and-purge-css-to-phoenix-1-5-331m)

https://autoracex.connpass.com/event/209999/

https://getbootstrap.com/docs/4.6/getting-started/introduction/

https://purgecss.com/

## 1. Bootstrapをインストール

```sh
cd path/to/my/phoenix/app
npm install --prefix assets --save-dev bootstrap@4 purgecss-webpack-plugin glob-all
```

## 2. Bootstrapのscssファイルをimport

次にBootstrapのscssファイルをPhoenixアプリの`assets/css/app.scss`にimportします。

```diff_sass:assets/css/app.scss
  /* This file is for your main application css. */
  @import "../node_modules/nprogress/nprogress.css";
+ @import "../node_modules/bootstrap/scss/bootstrap.scss";
```

## 3. PurgeCSSの設定

厳密に言えば、これはあってもなくても良いのですが、おそらく大部分のBootstrapのCSSは未使用になると思われますので、CSSファイルを小さくするためにPurgeCSSを使うことは得策であると考えています。設定は比較的簡単で、Phoenixアプリの`assets/webpack.config.js`を開き３箇所変更するのみです。

- PurgeCSSが正しくCSS関連ファイルを発見するために複数のパターンを適用したいので、`glob` の代わりに[`glob-all`](https://www.npmjs.com/package/glob-all) を使用します。
- [purgecss-webpack-plugin](https://www.npmjs.com/package/purgecss-webpack-plugin)を使用します。
- PhoenixアプリでPurgeCSSに読み込ませたいファイルは下記のとおりです。
  - viewモジュール
  - テンプレート
  - JavaScript

```diff_jsx:assets/webpack.config.js
  const path = require('path');
- const glob = require('glob');
+ const glob = require('glob-all');
  const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
  const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  const TerserPlugin = require('terser-webpack-plugin');
  const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
  const CopyWebpackPlugin = require('copy-webpack-plugin');
+ const PurgecssPlugin = require('purgecss-webpack-plugin');

  module.exports = (env, options) => {
    const devMode = options.mode !== 'production';

    return {

      ...

      plugins: [
        new MiniCssExtractPlugin({ filename: '../css/app.css' }),
        new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
+       new PurgecssPlugin({
+         paths: glob.sync([
+           '../lib/**/*.ex',
+           '../lib/**/*.leex',
+           '../lib/**/*.eex',
+           './js/**/*.js',
+           // ライブラリをインストールしたあとに忘れがち
+           './node_modules/some_library/**/*.js',
+         ]),
+       }),
      ]
      .concat(devMode ? [new HardSourceWebpackPlugin()] : [])
    }
  };
```

プロダクションのみにPurgeCSSをしようするのであれば、下記の通り分岐できます。

```assets/webpack.config.js
module.exports = (env, options) => {
  const devMode = options.mode !== 'production';

  return {

    ...

    plugins: [
      new MiniCssExtractPlugin({ filename: '../css/app.css' }),
      new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
    ].concat(
      devMode
        ? [
            // development only
            new HardSourceWebpackPlugin(),
          ]
        : [
            // production only
            new PurgecssPlugin({
              paths: glob.sync([
                '../lib/**/*.ex',
                '../lib/**/*.leex',
                '../lib/**/*.eex',
                './js/**/*.js',
                // ライブラリをインストールしたあとに忘れがち
                './node_modules/some_library/**/*.js',
              ]),
            }),
          ]
    ),
  }
};
```

個人的にはdevelopmentとproductionとの両方に適用するのが好みです。NPMにライブラリをインストールしたときに、それをPurgeCSSに登録するのを忘れると、必要なCSSが取り除かれてしまいますが、本番でそれが起きるとパニックに陥るかもしれません。

## 4. 不要なPhoenixデフォルトのCSSを削除

BootstrapのCSSにはいくつかPhoenixデフォルトのCSSと全く同じCSSクラスが含まれているので、それらは削除することができます。ただし`.alert:empty`は、フラッシュメッセージが空のときに隠す便利なCSSなので残してきます。

```diff_scss:scss
- .alert {
-   padding: 15px;
-   margin-bottom: 20px;
-   border: 1px solid transparent;
-   border-radius: 4px;
- }
- .alert-info {
-   color: #31708f;
-   background-color: #d9edf7;
-   border-color: #bce8f1;
- }
- .alert-warning {
-   color: #8a6d3b;
-   background-color: #fcf8e3;
-   border-color: #faebcc;
- }
- .alert-danger {
-   color: #a94442;
-   background-color: #f2dede;
-   border-color: #ebccd1;
- }
- .alert p {
-   margin-bottom: 0;
- }
  .alert:empty {
    display: none;
  }
```

## 5. Bootstrapの変数をカスタマイズ

Bootstrapのスタイリングで満足であれば、このステップは不要です。僕は色や幅などをよく変更します。Bootstrapの変数は[Bootstrapのリポジトリ](https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss)で確認できます。

Phoenixアプリに`assets/css/_variables.scss`ファイルを作り、そこにカスタマイズしたい変数の定義をコピペします。コピーした定義から[`!default`](https://sass-lang.com/documentation/variables#default-values)を削除します。あとは値を好きなように変更します。下記は一例です。

```scss:assets/css/_variables.scss
$white:    #ffffff;
$gray-100: #eceff1;
$gray-200: #cfd8dc;
$gray-300: #b0bec5;
$gray-400: #90a4ae;
$gray-500: #78909c;
$gray-600: #607d8b;
$gray-700: #546e7a;
$gray-800: #455a64;
$gray-900: #37474f;
$black:    #263238;
$blue:     #2962FF;
$indigo:   #304FFE;
$purple:   #AA00FF;
$pink:     #C51162;
$red:      #D50000;
$orange:   #FF6D00;
$yellow:   #FFD600;
$green:    #00C853;
$teal:     #00BFA5;
$cyan:     #00B8D4;
```

そしてそのファイルを`assets/css/app.scss`にimportします。

```diff_scss:assets/css/app.scss
  /* This file is for your main application css. */
+ @import 'variables';
  @import '../node_modules/nprogress/nprogress.css';
  @import '../node_modules/bootstrap/scss/bootstrap.scss';
```

## 6. カスタムscssファイルをimport

あとは自由に好きなようにスタイリングをします。カスタマイズしたBootstrapの変数にもアクセスできるはずです。

```diff_scss:assets/css/app.scss
  /* This file is for your main application css. */
  @import 'variables';
  @import '../node_modules/nprogress/nprogress.css';
  @import '../node_modules/bootstrap/scss/bootstrap.scss';
+ @import 'my_custom_styles';
```

以上！