Node.js
js
scss
フロントエンド
webpack4

Webpack4でJavaScriptとCSSを結合しつつminfyする設定を書いてみた。

はじめに

フロントサイドを作る上でもはや当たり前になっているWebpackだが、Webpack4の情報が少ないのでメモ。

あくまでも、メモなので余計な設定とかあると思う。改善案とかあったらコメント欄へ

ディレクトリ構造

root/ - プロジェクトディレクトリ
 + dist/ - サーバーのドキュメントルート
 |  + js/ - JavaScriptの出力先
 |  + css/ - CSSの出力先
 |  ` * - その他のファイル
 + src
 |  + js/ - JavaScriptのソース
 |  | + app.js - JavaScriptのエントリポイント
 |  | ` *.js - その他のJavaScript
 |  ` scss/ - Scssのソース
 |    + app.scss - スタイルシートのエントリポイント
 |    ` _*.scss - その他のスタイルシート
 + package.json - node設定(下記参照)
 + webpack.config.js - webpack設定(下記参照)
 ` node_modules/ 

ファイルの内容

package.jsonの内容

package.json
{
    "scripts": {
        "watch": "webpack --mode development --watch --color --progress",
        "dev": "webpack --mode development",
        "prod": "webpack --mode production --env.production",
        "start": "webpack-dev-server --color --mode development"
    },
    "main": "webpack.config.js",
    "devDependencies": {
        "@babel/core": "^7.1.2",
        "@babel/preset-env": "^7.1.0",
        "@types/source-map": "^0.5.7",
        "autoprefixer": "^9.1.3",
        "babel-loader": "^8.0.4",
        "css-loader": "^1.0.0",
        "cssnano": "^4.1.3",
        "expose-loader": "^0.7.5",
        "extract-text-webpack-plugin": "^4.0.0-beta.0",
        "file-loader": "^2.0.0",
        "install": "^0.12.1",
        "mini-css-extract-plugin": "^0.4.1",
        "node-sass": "^4.9.3",
        "optimize-css-assets-webpack-plugin": "^5.0.1",
        "postcss-loader": "^2.1.6",
        "precss": "^3.1.2",
        "resolve-url-loader": "^2.3.0",
        "sass-loader": "^7.1.0",
        "style-loader": "^0.21.0",
        "uglifyjs-webpack-plugin": "^1.2.7",
        "webpack": "^4.17.1",
        "webpack-cli": "^3.1.0",
        "webpack-dev-server": "^3.1.6"
    },
    "private": true
}

Webpack.config.jsの内容

webpack.config.js
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");

module.exports = env => {
    const NODE_ENV = (env && env.production) ? 'production' : 'development';
    return [{
        mode: NODE_ENV,
        // サーバー
        devServer: {
            clientLogLevel: 'warning',
            historyApiFallback: true,
            hot: true,
            publicPath: '/',
            inline: true,
            overlay: true,
            contentBase: path.join(__dirname, 'dist'),
            host: '0.0.0.0',
            port: 3000,
            disableHostCheck: true
        },
        stats: {
            colors: true,
        },
        /* ----------------
          JS用モジュール
        ----------------- */
        // ソースのパス
        context: path.resolve(__dirname, './src/js'),
        // 出力するファイル
        entry: {
            app: path.resolve(__dirname, './src/js/app.js') // 全体のスクリプト
            // 任意で追加
        },
        // developmentモードのときにソースマップを出力する
        devtool: NODE_ENV === 'development' ? 'source-map' : 'none',
        // 出力先のファイル命名規則
        output: {
            path: path.resolve(__dirname, './dist'),
            filename: 'js/[name].js'
        },
        module: {
            rules: [{
                // 拡張子 .js の場合
                test: /\.js$/,
                rules: [{
                    // Babel を利用する
                    loader: 'babel-loader',
                    // Babel のオプションを指定する
                    options: {
                        presets: [
                            // プリセットを指定することで、ES2018 を ES5 に変換(IE五感にする)
                            '@babel/preset-env',
                        ]
                    }
                }]
            }]
        },
        // パッケージに含めないライブラリ
        externals: {
            jquery: 'jQuery'
        },
        plugins: [
            // bootstrap のコードから jQuery が直接見えるように
            // http://getbootstrap.com/docs/4.0/getting-started/webpack/#importing-javascript
            //new webpack.ProvidePlugin({
            //    $: "jquery",
            //    jQuery: "jquery",
            //    "window.jQuery": "jquery",
            //    Popper: ["popper.js", "default"],
            //}),
            // デバッグ
            new webpack.EnvironmentPlugin({
                NODE_ENV: NODE_ENV,
                DEBUG: NODE_ENV === 'development'
            }),
            new webpack.HotModuleReplacementPlugin()
        ],
        optimization: {
            // developmentモードでビルドした場合
            // minimizer: [] となるため、consoleは残されたファイルが出力される
            // puroductionモードでビルドした場合
            // minimizer: [ new UglifyJSPlugin({... となるため、consoleは削除したファイルが出力される
            minimizer: NODE_ENV === 'development' ? [
                // 頻繁に使用されるコードを整理
                new webpack.optimize.OccurrenceOrderPlugin(false)
            ] : [
                // 重複処理を削除
                //new webpack.DedupePlugin(),
                // 頻繁に使用されるコードを整理
                new webpack.optimize.OccurrenceOrderPlugin(true),
                // スクリプトを圧縮
                new UglifyJSPlugin({
                    cache: true,
                    parallel: true,      
                    uglifyOptions: {
                        warning: "verbose",
                        ecma: 6,       // 出力するスクリプトのバージョン
                        beautify: true,      // コードを整形
                        comments: false,     // コメントを残さない
                        mangle: true,
                        toplevel: true,
                        keep_classnames: false,
                        keep_fnames: false,
                        compress: {
                            unsafe_comps: true,
                            properties: true,
                            keep_fargs: false,
                            pure_getters: true,
                            collapse_vars: true,
                            unsafe: true,
                            warnings: false, // good for prod apps so users can't peek behind curtain
                            sequences: true,
                            dead_code: true, // big one--strip code that will never execute
                            drop_debugger: true,
                            comparisons: true,
                            conditionals: true,
                            evaluate: true,
                            booleans: true,
                            loops: true,
                            unused: true,
                            hoist_funs: true,
                            if_return: true,
                            join_vars: true,
                            drop_console: true // strips console statements
                        },
                        mangleProperties: {
                            ignore_quoted: true
                        }
                    }
                })
            ],
            splitChunks: {
                chunks: 'async',
                minSize: 30000,
                maxSize: 0,
                minChunks: 1,
                maxAsyncRequests: 5,
                maxInitialRequests: 3,
                automaticNameDelimiter: '~',
                name: true,
                cacheGroups: {
                    vendors: {
                        test: /[\\/]node_modules[\\/]/,
                        priority: -10
                    },
                    default: {
                        minChunks: 2,
                        priority: -20,
                        reuseExistingChunk: true
                    }
                }
            }
        },
        resolve: {
            extensions: ['.js']
        },

        watch: NODE_ENV === 'development'
    }, {
        /* ----------------
          CSS用モジュール
        ----------------- */
        context: path.resolve(__dirname, './src/scss'),

        mode: NODE_ENV,
        devtool: NODE_ENV === 'development' ? 'source-map' : 'none',
        stats: {
            colors: true,
        },
        entry: {
            app: path.resolve(__dirname, './src/scss/app.scss')
        },
        output: {
            path: path.resolve(__dirname, './dist'),
            filename: 'css/[name].css'
        },
        module: {
            rules: [{
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'style-loader', 'css-loader', 'postcss-loader']
            }, {
                test: /\.scss$/,
                use: ExtractTextPlugin.extract({
                    use: [
                        // CSSをバンドルするための機能
                        {
                            loader: "css-loader",
                            options: {
                                // オプションでCSS内のurl()メソッドの取り込みを禁止する
                                url: false,
                                // CSSの空白文字を削除する
                                minimize: NODE_ENV === 'development',
                                // ソースマップを有効にする
                                sourceMap: NODE_ENV === 'development' ? 2 : 0,
                                // 0 => no loaders (default);
                                // 1 => postcss-loader;
                                // 2 => postcss-loader, sass-loader
                                importLoaders: 2
                            }
                        },
                        // PostCSSのための設定
                        {
                            loader: "postcss-loader",
                            options: {
                                sourceMap: NODE_ENV === 'development',
                                plugins: () => {
                                    return [
                                        require('precss'),
                                        // Autoprefixerを有効化
                                        // ベンダープレフィックスを自動付与する
                                        require('autoprefixer')({
                                            grid: true
                                        })
                                    ];
                                }
                            }
                        },
                        //{
                        //    loader: 'resolve-url-loader'
                        //},
                        // Sassをバンドルするための機能
                        {
                            loader: 'sass-loader',
                            options: {
                                url: false,
                                // ソースマップの利用有無
                                sourceMap: NODE_ENV === 'development',
                                includePaths: [path.resolve(__dirname, 'node_modules')]
                            }
                        }

                    ]
                })
            }, {
                test: /\.(eot|otf|ttf|woff2?|svg)(\?.+)?$/,
                include: [
                    path.resolve(__dirname, 'node_modules')
                ],
                use: {
                    loader: 'file-loader',
                    options: {
                        path: path.resolve(__dirname, './dist'),
                        publicPath: './dist',
                        name: 'fonts/[name].[ext]'
                    }
                }
            }]
        },
        plugins: [
            new ExtractTextPlugin({
                filename: (getPath) => {
                    return getPath('css/[name].css')
                }
            }),
            new OptimizeCssAssetsPlugin({
                assetNameRegExp: /\.optimize\.css$/g,
                cssProcessor: require('cssnano'),
                cssProcessorPluginOptions: {
                    preset: ['default', { discardComments: { removeAll: true } }],
                },
                canPrint: true
            }),
            new MiniCssExtractPlugin({
                filename: "[name].css",
                chunkFilename: "[id].css"
            })
        ],
        watch: NODE_ENV === 'development',
        optimization: {
            // 圧縮設定
            minimizer: [
                new UglifyJSPlugin({
                    cache: true,
                    parallel: true,
                    sourceMap: NODE_ENV === 'development' // set to true if you want JS source maps
                }),
                new OptimizeCssAssetsPlugin({})
            ]
        }
    }]
};

使い方

プロジェクトディレクトリで、コマンドラインから以下のコマンドを実行し、パッケージをインストール

npm install

http://localhost:3000/にdistディレクトリがドキュメントルートのサーバーを起動する。この設定では、同一ネットワークにあるPCからもアクセスできる。例えばWifiでつながったiPhoneで同時チェックと言った使い方ができる。

src内のJavaScriptやScssを更新したときに自動的に再コンパイルされリロードするスグレモノ。実行結果はメモリに保持されるので高速な反面、distディレクトリ内のjsやcssが自動更新されるわけではないので注意。

npm run start

コンパイルして圧縮したjsやcssを出力したい場合は以下のコマンドを入力

npm run prod

単純に出力したい場合は、

npm run dev

ちなみに、UglifyJSでminifyする時こそEcmaScript6にする設定になっているが、babel-loaderでEcmaScript5にトランスパイルしているのでIEでもちゃんと動くスクリプトが出力される。

実際コーディングする(bootstrap4を使った例)

例えば、みんなだいすきbootstrapを使う場合、

npm install bootstrap

で普通にbootstrapをインストールする。

次にapp.jsとapp.scssでbootstrapを呼び出す。

/src/js/app.js
import $ from 'jquery';
// 全部使う場合はこれでいいがおすすめできない。
//import 'bootstrap';
// 使わないモジュールはコメントアウトしよう。
import 'bootstrap/js/dist/util';
import 'bootstrap/js/dist/alert'
import 'bootstrap/js/dist/button'
import 'bootstrap/js/dist/carousel'
import 'bootstrap/js/dist/collapse'
import 'bootstrap/js/dist/dropdown'
import 'bootstrap/js/dist/modal'
import 'bootstrap/js/dist/popover'
import 'bootstrap/js/dist/scrollspy'
import 'bootstrap/js/dist/tab'
import 'bootstrap/js/dist/tooltip'
//ここから下に自分のコードを入れる

別途_variables.scssを作成して、bootstrapの設定をオーバーライドする値を入れる。例えば、全体フォントを游ゴシックにしたい場合

./src/scss/_variables.scss
$font-family-base: -apple-system, BlinkMacSystemFont, "Yu Gothic Medium", "游ゴシック Medium", YuGothic, "游ゴシック体", "ヒラギノ角ゴ Pro W3", "メイリオ", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

と入れておこう。変数になっているところがポイント。この他変更できる値は、/node_modules/bootstrap/scss/_variables.scssを見ること。これをapp.scssから読み込む。

ss:./src/scss/app.scss
@import "variables";
// ここも例によってモジュール単位で呼び出すことを推奨
//@import "~bootstrap/scss/bootstrap";
// 使わないモジュールはコメントアウトしよう。
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";
@import "~bootstrap/scss/root";
@import "~bootstrap/scss/reboot";
// ここから上は必須
@import "~bootstrap/scss/type";
@import "~bootstrap/scss/images";
@import "~bootstrap/scss/code";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/tables";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/transitions";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/button-group";
@import "~bootstrap/scss/input-group";
@import "~bootstrap/scss/custom-forms";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/breadcrumb";
@import "~bootstrap/scss/pagination";
@import "~bootstrap/scss/badge";
@import "~bootstrap/scss/jumbotron";
@import "~bootstrap/scss/alert";
@import "~bootstrap/scss/progress";
@import "~bootstrap/scss/media";
@import "~bootstrap/scss/list-group";
@import "~bootstrap/scss/close";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/tooltip";
@import "~bootstrap/scss/popover";
@import "~bootstrap/scss/carousel";
@import "~bootstrap/scss/utilities";
@import "~bootstrap/scss/print";

// ここから下に自分のコードを入れる。
// こんな感じでBootstrapに用意されている関数を使ってらくらくコーディング。

.container
{
   @include media-breakpoint-down(sm) {
       max-width: 700px;
   }
   @include media-breakpoint-up(xl) {
       max-width: 1200px;
   }
}

で、HTML。

/dist/index.html
<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <link rel="stylesheet" href="css/app.css" />
    <title>Hello, world!</title>
  </head>
  <body>
    <div class="container">
      <h1>Hello, world!</h1>
    </div>
    <script src="js/app.js"></script>
  </body>
</html>

すると、srcディレクトリ内構造はこうなっているはずである。

   src
    + js/
    | + app.js
    ` scss/
      + app.scss
      ` _valiables.scss

この状態で

npm run start

を実行すると、http://localhost:3000/にアクセスしたときこのHTMLが表示される。また、jsやscssを編集すると、ブラウザが自動的にリロードされその結果がすぐにわかる。