Edited at

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を編集すると、ブラウザが自動的にリロードされその結果がすぐにわかる。