Help us understand the problem. What is going on with this article?

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

はじめに

Webpackを用いてフロントエンドの環境を構築します。

ディレクトリ構造

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",
    "fix": "eslint --fix ./src"
  },
  "main": "dist/app.min.js",
  "devDependencies": {
    "@babel/cli": "^7.7.7",
    "@babel/core": "^7.7.7",
    "@babel/polyfill": "^7.7.0",
    "@babel/preset-env": "^7.7.7",
    "babel-loader": "^8.0.6",
    "copy-webpack-plugin": "^5.1.1",
    "core-js": "^3.6.1",
    "css-loader": "^3.4.0",
    "eslint": "^6.8.0",
    "eslint-config-google": "^0.13.0",
    "fs": "0.0.1-security",
    "mini-css-extract-plugin": "^0.9.0",
    "node-sass": "^4.13.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "sass-loader": "^8.0.0",
    "style-loader": "^1.1.2",
    "terser-webpack-plugin": "^2.3.1",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.10.1"
  },
  "private": true
}

Webpack.config.jsの内容

webpack.config.js
const webpack = require('webpack');
const fs = require('fs');
const path = require('path');
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const pjson = require('./package.json');
const build = new Date().toISOString();

module.exports = (env) => {
  /** @type {bool} 開発モードかの判定 */
  const isProduction = env && env.production;
  /** @type {string} ヘッダーに出力するバナー情報。主にpackage.jsonの値を使用 */
  const banner = `${pjson.name} v${pjson.version} | ${pjson.author.name} | license: ${pjson.license} | build: ${build}`;

  return {
    mode: env,
    target: 'node',
    devtool: !isProduction ? 'source-map' : false,
    devServer: {
      // ルート
      contentBase: path.join(__dirname, 'docs'),
      // 自動的にブラウザを開くか
      open: true,
      // ホスト
      host: '0.0.0.0',
      // ポート
      port: 9000,
      // 公開するサーバーのルート(/の場合localhost直下)
      publicPath: '/',
      // エラーをオーバーレイ表示する
      disableHostCheck: true,
      clientLogLevel: 'warning',
      historyApiFallback: true,
      hot: true,
      inline: true,
    },
    stats: {
      colors: true,
    },
    entry: {
      // 出力先のファイル名とそのソースを指定
      'app': 'src/js/app'.js
      // ...
    },
    // 出力先設定
    output: {
      // 出力先のパス
      path: path.resolve(__dirname, 'dist'),
      // 出力先のファイル命名規則(productionモードでは[ファイル名].min.jsとなる)
      filename: !isProduction ? '[name].js' : '[name].min.js',
      // ライブラリ名
      library: 'GracefulLibray',
      libraryTarget: 'umd',
      umdNamedDefine: true,
      globalObject: `(typeof self !== 'undefined' ? self : this)`,
    },
    // 圧縮設定
    optimization: {
      minimize: isProduction,
      minimizer: [
        // JSはTerserを使用する
        new TerserPlugin({
          terserOptions: {
            // 出力するECMAScipt(JavaScript)のバージョン。10(2019)まで指定可能だが対応しているブラウザがないので6(2015)にするのが無難。
            ecma: 6,
            compress: {
             // errorとwarn以外のコンソールを削除する
             drop_console: true
            },
            output: {
              // コメント類を削除
              comments: false,
              // コードを見やすくする
              beautify: false,
            },
            // ライセンス情報は、[name].LICENSEに出力される。
            // ※/*! ... **/内のテキストのみ。
            extractComments: {
              condition: true,
              banner: (f) => {
                // バナー
                return banner;
              },
            },
          },
        }),
        // CSS部分はoptimize-css-assetsを使用する
        new OptimizeCSSAssetsPlugin({
          cssProcessorPluginOptions: {
            preset: ['advanced', 
              { 
                autoprefixer: {
                  // autoprefixerによる vendor prefix の追加を行う   
                  add: true,   
                  // サポートするブラウザVersionの指定    
                  browsers: ["last 2 versions", "ie >= 11", "Android >= 4"]
                },
                // ライセンスも含めて、コメントを全て削除する
                discardComments: { removeAll: true }, 
                // CSSの定義のソートを行う    
                cssDeclarationSorter : { order: 'smacss' }
              }
            ],
          },
          canPrint: 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
            }
          }
        }
      },
      concatenateModules: false,
    },
    module: {
      // ルール設定
      rules: [
        // JavaScriptの設定
        {
          test: /\.js/,
          exclude: /node_modules/,
          use: [
            {
              loader: 'babel-loader',
            },
          ],
        },
        // SCSSの設定
        {
          test: /\.scss/,
          use:
          [
            {
              loader: MiniCssExtractPlugin.loader,
            },
            {
              loader: 'css-loader',
              options: {
                url: false,
                sourceMap: !isProduction,

                // 0 => no loaders (default);
                // 1 => postcss-loader;
                // 2 => postcss-loader, sass-loader
                importLoaders: 2,
              },
            },
            {
              loader: 'sass-loader',
              options: {
                sourceMap: !isProduction,
              },
            },
          ],
        },
        // TypeScriptの場合(別途ts-loaderとtypescriptをインストールする必要あり)
        // {
        //   test: /\.ts$/,
        //   use: "ts-loader"
        // }
      ],
    },
    // 名前解決
    resolve: {
      modules: [`${__dirname}/src`, 'node_modules'],
      extensions: ['.js', '.scss'],
    },
    plugins: [
      new webpack.BannerPlugin({
        // バナーを出力
        banner: banner,
      }),
      new MiniCssExtractPlugin({
        // スタイルシートの出力ファイル名はここで指定する
        filename: !isProduction ? 'style.css' : 'style.min.css',
      }),
    ],
    // パッケージに含めないライブラリ
    // externals: {
    //  jquery: 'jQuery'
    // },
};

使い方

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

npm install

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

npm run start

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

ESLintでコード整形を行う場合は、以下のコマンドを実行する。複数人で作業する場合、gitのpush時にhuskyなどで、このコマンドを自動実行するようにするといいかもしれない。

npm run fix

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

npm run prod

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

npm run dev

実際コーディングする(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
{
   // この例では、ブラウザの横のサイズに応じてcontainerの最大サイズを変更している。
   // 参考:https://getbootstrap.com/docs/4.4/layout/overview/#containers
   @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を編集すると、ブラウザが自動的にリロードされその結果がすぐにわかる。

更新履歴

2020/01/02

  • 情報が古すぎたので色々書き直し。JS圧縮プログラムのuglifyをTetherに変えたなど。
  • 今回から更新履歴を書くことに
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした