Webpackをゼロから学ぶ(その1)
WebpackなどのフロントエンドDevOps的な分野は、なかなか面倒なイメージで今まで避けてきた部分だったので、改めてゼロからしっかりと学んでみる。
Webpack学習の問題点
- 初学者にとっていきなり理解するのは難しい。実践が必要
- 無関係なこともブラックボックス的に行われている
- まず先ずは設定(Configuration)が必要で、それなしだと動かない
- ドキュメントを読んで理解するのに時間を費やさないといけない
Webpackはどのような問題を解決するのか
JSにモジュール化を可能にさせる
Webpackを使わないとどうなるのか
htmlのスクリプトタグに読ませる/gulpなどでconcatする際の問題点
Webpackの実例
以下のようなプロジェクト構造があるとする。
. ├── css │ └── main.css ├── index.html ├── js │ ├── first.js │ ├── index.js (entry point) │ └── second.js └── package.json
Webpackの基本的なアイデアは、このindex.jsというエントリーポイントファイルに、必要となるcssやjsファイルを読み込ませ、バンドルさせるというもの。 このエントリーファイルが、Webpackの全ての基本となる。
Webpackを使用するために必要となるのが webpack
と webpack-cli
の2種類のパッケージなので、早速インストールを行う。
yarn init -y yarn add -D webpack webpack-cli
この状態でwebpackのバイナリファイルを実行してみる。
# yarnの場合 yarn -s run webpack # npmの場合 npx webpack # または直接下記を実行 ./node_modules/.bin/webpack
すると以下のようなErrorメッセージが返ってくる。
ERROR in Entry module not found: Error: Can't resolve './src' in ...
これはなぜかというと、WebpackのConfiguration設定前の基本的なディレクトリ構造が違うから。 Webpackのデフォルト設定では、
Entry: `src/index.js` => Output: `dist/main.js`
という方式になっている。
改めてWebpackのデフォルト方式に従って、以下のようなディレクトリ構造にする。
. ├── dist │ └── main.js ├── package.json ├── src │ ├── css │ │ └── main.css │ ├── index.html │ ├── index.js │ └── js │ ├── first.js │ └── second.js └── yarn.lock
各ファイルの中身はこんな感じ。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Webpack</title> </head> <body> <h1>Learning Webpack!</h1> <script src="js/index.js"></script> </body> </html>
// first.js console.log('First JS!')
// second.js console.log('Second JS!')
// index.js require('./js/first') require('./js/second')
以上のようなファイルの中身で実行すると、実行が成功して dist/main.js
が生成される。
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="",t(t.s=0)}([function(e,n,t){t(1),t(2)},function(e,n){console.log("First JS!")},function(e,n){console.log("Second JS!")}]);
中身はminifyされてしまっているが、console.logの内容が含まれていることが確認できる。
ここで、以下のようなcssファイルをエントリーポイントの index.js
で読もうとすると、これまたエラーを起こしてしまう。
/* main.css */ body { color: tomato; }
// index.js require('./css/main.css') require('./js/first') require('./js/second')
これは各ファイル形式に適したローダー(loader)での処理が必要なためである。
Webpackのモードについて
デフォルトではWebpackはproductionモードに設定されている。
yarn -s run webpack --mode=development
で実行をdevelopmentモードにして行うとビルド後のJSもminifyされない。
Webpackの設定(Config)
ここまではコマンド上だけで実行をしてきたが、Webpackを導入するときに欠かせないのがconfigファイル( webpack.config.js
)
必要な4ステップ
- Configファイル
- LoaderとPlugin
- devServerとHMR
基本のconfig
webpack.config.js
const path = require('path') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') } }
outputのパスは絶対パスでなければいけない。
css-loader, style-loaderの導入
cssなどを処理するときには、css-loader, style-loaderを入れる。
yarn add -D style-loader css-loader
以下のように使う。
webpack.config.js
const path = require('path') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] } }
useで指定しているloaderは、後ろから処理されることを知っておかなければいけない。
こうすることで、エントリーポイントのjsファイルでcssを読み込みすることができる。
file-loaderの導入
さらに画像とcss内での背景画像で読み込み記述を追加してみる。
. ├── index.html ├── package.json ├── src │ ├── css │ │ └── main.css │ ├── img │ │ └── lgtm.jpg │ ├── index.js │ └── js │ ├── first.js │ └── second.js ├── webpack.config.js └── yarn.lock
画像の読み込み処理時にはfile-loaderが必要になる。
yarn add -D file-loader
webpack.config.js
const path = require('path') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/dist/' }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|jpeg|svg|gif)$/, use: ['file-loader'] } ] } }
このままの設定でも問題なくコンパイルされるが、問題が生じる。 imgフォルダがdist内にコピーされず、画像がそのままコンパイルされるので、publicPathを追加する。 outputPathとnameを設定することで、コンパイル後もディレクトリ構造とファイル名が保たれる。
webpack.config.js
const path = require('path') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/dist/' }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|jpeg|svg|gif)$/, use: [ { loader: 'file-loader', options: { publicPath: '/dist/img/', outputPath: 'img/', name: '[name].[ext]' } } ] } ] } }
clean-webpack-pluginの導入
毎回distディレクトリ内を削除して再度バンドルするのは面倒なので、clean-webpack-pluginを導入することで簡易化する。
webpack.config.js
const path = require('path') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: '/dist/' }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|jpeg|svg|gif)$/, use: [ { loader: 'file-loader', options: { publicPath: '/dist/img/', outputPath: 'img/', name: '[name].[ext]' } } ] } ] }, plugins: [ new CleanWebpackPlugin() ] }
html-webpack-pluginの導入
htmlもdistフォルダにコンパイルするときは、html-webpack-pluginを導入する。
webpack.config.js
const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', // filename: '[name].bundle.[hash].js' などとハッシュ付きファイル名でも指定可能。html-webpack-pluginが上手く取り扱ってくれる。 path: path.resolve(__dirname, 'dist'), publicPath: '/dist/' }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|jpeg|svg|gif)$/, use: [ { loader: 'file-loader', options: { publicPath: '/dist/img/', outputPath: 'img/', name: '[name].[ext]' } } ] } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'html webpack plugin', filename: 'index.html', // distの先のhtmlファイル名の指定 template: 'index.html' // srcのテンプレートとなるhtmlファイル }), new CleanWebpackPlugin() ] }
distのhtmlの中身を見てみると、htmlの内容もjsファイルにバンドルされたということがわかる。
開発時に便利な設定
ソースマップの有効化
module.exports.devtool = 'inline-source-map' // 'inline-source-map' または 'source-map' が便利
を設定ファイルに追加すると、ソースマップが有効化されてデバッグしやすい。
webpack-dev-serverの導入とHot Module Replacement (HMR) の有効化
効率的な開発のためにはホットリロードを有効化したいところ。そこで、webpack-dev-server (WDS)を導入する。
yarn add -D webpack-dev-server
WDSを設定に追加する際は、以下のように設定を追加する。
module.exports.devServer = { contentBase: './dist', port: 1234, hot: true }
yarn run -s webpack-dev-server
HMRはwebpackモジュールからインスタンス化してpluginsに追加して有効化する。
ここまで設定ファイルをまとめると、以下のようになる。
package.json
{ "name": "js-infra", "version": "1.0.0", "main": "index.js", "author": "Sungjoon Park", "license": "MIT", "devDependencies": { "clean-webpack-plugin": "^3.0.0", "css-loader": "^2.1.1", "file-loader": "^3.0.1", "html-webpack-plugin": "^3.2.0", "standard": "^12.0.1", "style-loader": "^0.23.1", "webpack": "^4.32.2", "webpack-cli": "^3.3.2", "webpack-dev-server": "^3.5.1" } }
webpack.config.js
const path = require('path') const Webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: 'development', devtool: 'inline-source-map', devServer: { contentBase: './dist', port: 1234, hot: true }, entry: './src/index.js', output: { filename: '[name].bundle.[hash].js', path: path.resolve(__dirname, 'dist'), publicPath: '/dist/' }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.(png|jpg|jpeg|svg|gif)$/, use: [ { loader: 'file-loader', options: { publicPath: '/dist/img/', outputPath: 'img/', name: '[name].[ext]' } } ] } ] }, plugins: [ new Webpack.HotModuleReplacementPlugin(), new HtmlWebpackPlugin({ title: 'html webpack plugin', filename: 'index.html', template: 'index.html' }), new CleanWebpackPlugin() ] }