Bootstrap 4 × Webpack 4 な開発環境をつくる方法

# 概要
- **[Bootstrap4](https://getbootstrap.jp/docs/4.3/getting-started/introduction/) を適用したWebフロントエンドアプリをWebpack4な環境(+npm,babelなど)で開発する方法のメモです**
- 期待される効能
   - **\<script\>タグや\<link\>タグをつかってBootstrapのたくさんある関連ファイル(jsやcssの類)を読み込まなくて済むのでサーバーへのリクエスト数を減らせる**
   - **ビルド過程でs(a)css→cssコンパイルも同時に行えるのでスタイル変更・確認がはかどる**
   - **webpack-dev-serverを使えばオートリロード機能でjsアプリ部分の開発もはかどる**
- 本稿で作ったサンプルはこちら  
(1つのJSファイルと1つのCSSファイルのみ構成にできる)
    - https://riversun.github.io/bootstrap4webpack/
- 全ソースコードはこちら
    - https://github.com/riversun/npm_webpack4_bootstrap4  
    (cloneして **npm install** → **npm start** でサンプルが表示されます)

## 環境

- **ES6**
- **Bootstrap 4.3.1**
- **Webpack 4.40.2**

# 必要なnpmパッケージのインストール

- babel,webpackといった[フロントエンド開発用パッケージ](https://qiita.com/riversun/items/29d5264480dd06c7b9fb)を入れる

```shell
npm install --save-dev @babel/core @babel/preset-env babel-loader core-js css-loader style-loader url-loader webpack webpack-cli webpack-dev-server
```

- scssビルドなどcss生成のためのパッケージを入れる

```shell
npm install --save-dev mini-css-extract-plugin node-sass sass-loader postcss-loader autoprefixer
```

- bootstrap 関連のパッケージを入れる

```shell
npm install bootstrap jquery popper.js
```

これらのパッケージをいれると、package.jsonは以下のような感じになる(抜粋)

```js:package.json
  "devDependencies": {
    "@babel/core": "^7.6.0",
    "@babel/preset-env": "^7.6.0",
    "autoprefixer": "^9.6.1",
    "babel-loader": "^8.0.6",
    "core-js": "^3.2.1",
    "css-loader": "^3.2.0",
    "mini-css-extract-plugin": "^0.8.0",
    "node-sass": "^4.12.0",
    "postcss-loader": "^3.0.0",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.0.0",
    "url-loader": "^2.1.0",
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.9",
    "webpack-dev-server": "^3.8.1"
  },
  "dependencies": {
    "bootstrap": "^4.3.1",
    "jquery": "^3.4.1",
    "popper.js": "^1.15.0"
  }
```

([**package.json**のソースコード全文はこちら](https://raw.githubusercontent.com/riversun/npm_webpack4_bootstrap4/master/package.json))


## webpack.config.jsを作る

```js:webpack.config.js(全体像)
const packageJson = require('./package.json');
const version = packageJson.version;
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AutoPrefixer = require('autoprefixer');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');

module.exports = (env, argv) => {

   ・・・略・・・,
        module: {
  ・・・略・・・,

            rules: [
      
                {
                    test: /\.(sa|sc|c)ss$/,
                    use: [
                        {
                            loader: MiniCssExtractPlugin.loader,
                            options: {
                                hmr: process.env.NODE_ENV === 'development',
                            },
                        },
                        {
                            loader: 'css-loader',
                        },
                        {
                            loader: 'postcss-loader',
                            options: {
                                plugins: [
                                    AutoPrefixer(
                                        {
                                            grid: 'autoplace'
                                        },
                                    ),
                                ],
                            },
                        },
                        {
                            loader: 'sass-loader',
                        }
                    ],
                },
 
            ],
        },
        resolve: {
            alias: {}
        },
        plugins: [
            new webpack.BannerPlugin(`[name] v${version} Copyright (c) 2019 Your Name`),
            new webpack.ProvidePlugin({
                $: 'jquery',
                jQuery: 'jquery',
                'window.jQuery': 'jquery'
            }),
            new MiniCssExtractPlugin({
                filename: argv.mode === 'production' ? `css/[name].css` : `css/[name].css`,  //`[name].min.js`
            }),
        ],

    };
・・・略・・・
```

([**webpack.config.js**のソースコード全文はこちら](https://raw.githubusercontent.com/riversun/npm_webpack4_bootstrap4/master/webpack.config.js
))



以下、重要なポイントをみていく

## ポイント1: jQueryの変数名参照を有効にする

**webpack.config.js**の**plugins: []**以下に着目。
以下のようにするとjsコードの中から、指定した変数名でjqueryモジュールを参照できるようにする。
ここでは**$**、**jQuery**、**window.jQuery**としてjQueryを参照できるようになった。
これでjsコード内で**import $ from 'jquery'**などと、いちいち書く必要はなくなる。

```js
 new webpack.ProvidePlugin({
     $: 'jquery',
     jQuery: 'jquery',
     'window.jQuery': 'jquery'
 }),
```

## ポイント2: scssのコンパイルをして、cssファイルを生成する

**webpack.config.js**の**module: {rules: []** 以下に着目。

**MiniCssExtractPlugin**をつかって、cssを外部ファイルとして生成する。

**Webpack<font color=red>3</font>**まで使われていた**[extract-text-webpack-plugin](https://github.com/webpack-contrib/extract-text-webpack-plugin)**は<font color=red>deprecated</font>なので**Webpack4**では**[MiniCssExtractPlugin](https://github.com/webpack-contrib/mini-css-extract-plugin)**を使う。

また、**[Autoprefixer](https://github.com/postcss/autoprefixer)**は、CSSのベンダープレフィックス(-webkit-など)を自動的に追加してくれる**[PostCSS](https://github.com/postcss/postcss)**のプラグイン。**IE**向けのグリッド対応をするときに<font color=red>昔は**grid:true**と書いていたが、今ではこれはダメで</font>、最新版では**grid:autoplace**になっているので注意。

```js
{
     test: /\.(sa|sc|c)ss$/,
     use: [
         {
             loader: MiniCssExtractPlugin.loader,
             options: {
                 hmr: process.env.NODE_ENV === 'development',
             },
         },
         {
             loader: 'css-loader',
         },
         {
             loader: 'postcss-loader',
             options: {
                 plugins: [
                     AutoPrefixer(
                         {
                             grid: 'autoplace'
                         },
                     ),
                 ],
             },
         },
         {
             loader: 'sass-loader',
         }
     ],
 }

```

また、
**webpack.config.js**の**plugins: []**以下に

```js
            new MiniCssExtractPlugin({
                filename: argv.mode === 'production' ? `css/[name].css` : `css/[name].css`,
            }),
```

のようにする。

**注：<font color=red>AutoprefixerのBrowserlistの書き方</font>**

Autoprefixerでは、ブラウザをどこまで対応させるかを指定する機能がある。

以下のようにwebpack.config.js内に**browsers: ['last 2 versions']**のように書くと

```webpack.config.js
{
         loader: 'postcss-loader',
         options: {
             plugins: [
                 AutoPrefixer(
                     {
                         grid: 'autoplace',
                         browsers: ['last 2 versions']
                     },
                 ),
             ],
         },
     },
```

<font color=red>↓のような警告メッセージが出る</font>


```shell
  Replace Autoprefixer browsers option to Browserslist config.
  Use browserslist key in package.json or .browserslistrc file.

  Using browsers option cause some error. Browserslist config
  can be used for Babel, Autoprefixer, postcss-normalize and other tools.

  If you really need to use option, rename it to overrideBrowserslist.

  Learn more at:
  https://github.com/browserslist/browserslist#readme
  https://twitter.com/browserslist
```

そこで、**Browserlist**はwebpack.config.jsではなく、**package.json**に書く。

```js:package.json
  "browserslist": [
    "last 2 versions",
    "Android 4.0",
    "iOS 8.1"
  ],
```

([**package.json**のソースコード全文はこちら](https://raw.githubusercontent.com/riversun/npm_webpack4_bootstrap4/master/package.json))


## index.scss

**\*.scss**ファイルをsrcに以下のような**index.scss**として置いた場合、

```scss:index.scss
// padding for fixed nav-bar
body {
  padding-top: 70px;
}

// padding for tab content
.tab-content {
  background-color: transparent;
  padding-top: 10px;
}

//customize bootstrap theme
$theme-colors: (
        dark: #999999
);

//import boostrap's scss
@import "~bootstrap/scss/bootstrap.scss";
```

([**index.scss**のソースコード全文はこちら](https://raw.githubusercontent.com/riversun/npm_webpack4_bootstrap4/master/src/index.scss))


ビルド時に**css/app.css**として生成される。

その他のフロントエンド関連のwebpack設定は**[こちら](https://qiita.com/riversun/items/29d5264480dd06c7b9fb)**にて説明している。


## index.html

次に、Bootstrap4向けに以下のようなindex.htmlを用意した

```index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <title></title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="stylesheet" href="css/app.css">
</head>
<body>
<header>
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
        <a class="navbar-brand" href="#">Bootstrap with Webpack4</a>
    </nav>
</header>


<div class="container-fluid">

    <div class="row">

        <div class="p-1 col-sm-12">
            <button id="bt1" type="button" class="btn btn-primary">Primary</button>
            <button id="bt2" type="button" class="btn btn-secondary">Secondary</button>
            <button id="bt3" type="button" class="btn btn-success">Success</button>
            <button id="bt4" type="button" class="btn btn-danger">Danger</button>
            <button id="bt5" type="button" class="btn btn-warning">Warning</button>
            <button id="bt6" type="button" class="btn btn-info">Info</button>
            <button id="bt7" type="button" class="btn btn-light">Light</button>
            <button id="bt8" type="button" class="btn btn-dark">Dark</button>
            <button id="bt9" type="button" class="btn btn-link">Link</button>
        </div>
    </div>

    <div class="row">
        <div class="p-1 col-sm-12">
            <ul class="list-group">
                <a href="#" class="list-group-item list-group-item-action">Link1</a>
                <a href="#" class="list-group-item list-group-item-action">Link2</a>
                <a href="#" class="list-group-item list-group-item-action">Link3</a>
                <a href="#" class="list-group-item list-group-item-action">Link4</a>
            </ul>
        </div>

    </div>

    <div class="row">

        <div class="p-1 col-sm-12">
            <button id="flip-tab" type="button" class="btn btn-primary col-sm-12">Flip tab</button>
        </div>
    </div>
    <div class="row">
        <div class="p-1 col-sm-12">

            <!-- Tab buttons -->
            <ul class="nav nav-tabs">
                <li class="nav-item">
                    <a href="#tab-no1" id="tab1" class="nav-link active" data-toggle="tab">Tab1</a>
                </li>
                <li class="nav-item">
                    <a href="#tab-no2" id="tab2" class="nav-link" data-toggle="tab">Tab2</a>
                </li>
            </ul>

            <!-- Tab panes -->
            <div class="tab-content">
                <div id="tab-no1" class="tab-pane active">
                    <div class="row">
                        <div class="col-12 col-sm-6">
                            content-1-1
                        </div>
                        <div class="col-12 col-sm-6">
                            content-1-2
                        </div>
                    </div>
                </div>
                <div id="tab-no2" class="tab-pane">
                    <div class="row">
                        <div class="col-6 col-sm-6">
                            content-2-1
                        </div>
                        <div class="col-6 col-sm-6">
                            content-2-2
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="js/app.js"></script>
</body>
</html>
```

([**index.html**のソースコード全文はこちら](https://raw.githubusercontent.com/riversun/npm_webpack4_bootstrap4/master/public/index.html
))

webpack.config.jsで、1つのjsファイルと1つのcssファイルを生成するように設定したので

jsファイルの読み込みは以下のように1つでOKだし、

```html
<script src="js/app.js"></script>
```

cssファイルの読み込みも以下のように1つでOK

```html
<link rel="stylesheet" href="css/app.css">
```

（ちなみに、cssファイルもjsにバンドルできるが、本稿ではcssは外だしというスタイルとしているのはMiniCssExtractPluginの項で説明したとおり）

## index.js

以下のようなindex.jsを作成した

Bootstrapを**import 'bootstrap';**でインポートする
また、**import './index.scss';**で**scss**をインポートしている

```js:index.js
import 'bootstrap';
import './index.scss';

$('button[id^=bt]').on('click', e => {
    alert(`${$(e.target).attr('id')} "${$(e.target).text()}" clicked`);
});

$('.list-group-item').on('click', e => {
    alert(`"${$(e.target).text()}" clicked`);
});

$('#flip-tab').on('click', e => {
    $('.nav-tabs :not(.active)').tab('show')
});

$('a[data-toggle="tab"]').on('click', e => {
    alert(`${$(e.target).attr('id')} clicked`);
});

```


あとは、Bootstrapスタイル(jQueryで操作する)でコードを書く。

**$**で**jQuery**が参照できているのは、さきほど**webpack.config.js**で以下を設定したため。

>```js:webpack.config.js(抜粋)
> new webpack.ProvidePlugin({
>     $: 'jquery',
>     jQuery: 'jquery',
>     'window.jQuery': 'jquery'
> }),
>```

ここまでのファイルは以下のような[ディレクトリ構成](https://github.com/riversun/npm_webpack4_bootstrap4)で配置している

```ディレクトリ構成
[workdir]
├── node_modules
├── public
│   └── index.html
├── src
│   ├── index.js
│   └── index.scss
├── package.json
├── package-lock.json
└── webpack.config.js
```

# ビルドしてwebpack-dev-serverで確認

実際にwebpack-dev-serverで確認してみる

```
npm start
```

すると、ブラウザが起動し、以下のようにBootstrapで作った画面が表示された。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/c919fd38-502b-b2ed-146a-94c0c2ab04f0.png)

たとえば、ここで、**src/index.scss**をいじって、以下のようにBootstrapのテーマカラー**dark**を変更してみた。


```scss(変更前)
$theme-colors: (
        dark: #999999
);
```

↓

```scss(変更後)
$theme-colors: (
        dark: #ff0000
);
```

```scss:index.scss
// padding for fixed nav-bar
body {
  padding-top: 70px;
}

// padding for tab content
.tab-content {
  background-color: transparent;
  padding-top: 10px;
}

//customize bootstrap theme
$theme-colors: (
        dark: #ff0000
);

//import boostrap's scss
@import "~bootstrap/scss/bootstrap.scss";
```

すると、上部のnav-barの背景色(スタイルをnavbar-darkにしているのでテーマカラーdarkに連動する）が即座に反映された↓

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170905/e6921610-ff0c-5819-4fec-ba767a6c6280.png)

このようにBootstrapのスタイル適用や確認もwebpackな世界の恩恵をうけることができる。

# まとめ

- Bootstrap4を用いた開発をnpmやWebpackな環境で便利に実施する方法について説明しました
- ソースコードは https://github.com/riversun/npm_webpack4_bootstrap4 にあります