この記事はNextremer Advent Calendar 2018の11日目の記事です。
Reactの開発環境を構築する際 create-react-app
に頼りきりで、会社のprojectのwebpack.config.js
に何が書いてあるのかわからなかったため、webpackの基本的な使い方を勉強しました。
今後、フロントエンド開発を行っていく上で、避けては通れないwebpackの超超基本的な設定方法について見ていきたいと思います。
Webpackとは?
webpackは、jsファイル、画像ファイル、スタイルシートなど、様々なファイルを一つにまとめる機能を持ち、その個々のファイル(モジュール)を束ねる(バンドルする)という役割から、モジュールバンドラーと呼ばれています。
webpackを使うメリットは以下の通りです。
1. ライブラリの依存関係を解消する
下記二つのファイルは、a.js
からb.js
を読み込んでいるため、.html内にscriptタグで読み込む際、b.js
->a.js
の順番で読み込まなければ、存在しないファイルを参照しようとしてエラーとなります。
import name from './b.js';
console.log(name);
const name = 'dog';
export default name;
<script src="a.js"></script>
<script src="b.js"></script>
<!-- a.js内でb.jsが読み込めないためエラーとなる -->
このような依存関係を自動で解決してくれます。
2. リクエストの回数が減る
複数ファイルを一つにまとめられるので、リクエスト回数が減るのはイメージしやすいと思います。
3. 大規模な開発に向いている
最終的に一つのファイルにまとめられるので、クラスや関数など、細かい単位でのファイル分け(モジュール化)を前提とした開発が出来るため、コードの見通しがよくなるのと、名前の競合防止の役割を果たします。
ローダー
ローダーは様々な種類のものがあり、webpackを利用する上で、非常に重要な役割を果たします。
モジュールはJavascriptの概念ですが、ローダーを使用することで .css
.png
などのファイルもバンドルしてひとつにまとめることが出来ます。
今回使用するローダー
css-loader
cssファイルを読み込むためのローダー
style-loader
読み込んだcssのスタイルをstyle要素としてページに反映させるローダー
url-loader
画像ファイルをDataURL化して.jsファイルとして読み込むためのローダー
file-loader
画像をファイルとして出力し、パスを管理するためのローダー
url-loaderでJavascript化された画像データはオリジナルよりサイズが大きくなるため、一定のファイルサイズ以上は、Javascript化せずにファイルとして出力する、というような使い方をします。
url-loaderは、ファイルサイズが小さく個数の多い画像ファイルをバンドルする時に効果を発揮します。
babel-loader
モジュールをまとめる前に、babelによるトランスパイルを挟むためのローダー
*トランスパイルとは...新しいJavaScriptの記法に対応していないブラウザでは動作しないため、動作が確実な古いJavascriptの記法に変換すること。また、reactで一般的に使われるJSXという記法も、babelによって通常のJavaScript記法に変換することで、ブラウザで動作するようにします。
プラグイン
Webpackの機能を拡張するためのライブラリが多数提供されています。プラグインを利用することで、バンドルファイルから共通のモジュールを切り出したり、バンドルファイルを読み込むためのhtmlファイルを自動生成したり、バンドルされたcssを別ファイルとして出力したりといったことが出来ます。
今回使うもの下記の一つだけです。
extract-text-webpack-plugin
cssファイルをバンドルしたファイルとは別のスタイルシートとして出力するプラグイン
簡単にまとめると
- webpackは複数のファイルをまとめるモジュールバンドラーとしての機能が主な役割
- ファイルをまとめる時に、
.js
意外のものを読み込めるようにするローダー - その他、プラグインによって機能を拡張出来る
実践
それでは、webpackを使ってReactのコードを動かし、Hello worldを出力するまでを行っていきます。
まず、今回のプロジェクトディレクトリを作成します。
mkdir hello-webpack
cd hello-webpack/
続いて、package.jsonを作成します。package.jsonやinstallコマンドについての説明は省略します。
npm init
// 問いかけが終わるまでエンターを押します。
// すると、下記のようなpackage.jsonファイルが作成されます。
{
"name": "hello-webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
必要なnpmモジュールをinstallします。
npm install -S react react-dom
// reactに必要なモジュール
npm install -D webpack webpack-cli
// webpack本体と、webpackコマンドを使えるようにするためのもの
-
-S
オプションは--save
の省略形 -
-D
オプションは--save-dev
の省略形
次に、必要なローダー類をインストールします。
npm install -D css-loader style-loader url-loader file-loader
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react
babelとつくものが沢山出てきて混乱すると思うので説明していきます。
babel-loader
はbabelをローダーとして使用するためのもの
@babel/core
はbabel本体
@babel/preset-***
とは...?
babelでトランスパイル(JS -> JSの変換)をする時に使用するもので、@babel/preset-***
のような名前で***にpresetの種類が入ります。Reactプロジェクトの場合は、@babel/preset-react
により、JSXで書かれているものをJavaScriptに変換し、次に@babel/preset-env
で古いJavaScriptに変換の順で行いブラウザで動作するようにします。
必要なディレクトリ、ファイルを作成します。
mkdir -p public/ src/images
touch public/index.html src/index.js src/style.css src/hello.js
touch webpack.config.js .babelrc
imagesの中にpng
jpg
どちらかの拡張子の画像ファイルを入れてください
hello-webpack/
├── node_modules/
├── package-lock.json
├── package.json
├── public // バンドルしたファイルをここに吐き出す
│ └── index.html // webpackでバンドルしたjsファイルをindex.htmlで読み込む
├── src // バンドルする前のソースコードをここへ
│ ├── index.js // 起点となるファイル
│ ├── Hello.js // Helloと画像を表示するコンポーネント
│ ├── style.css
│ └── images
│ └── dog.png // 好きな画像を入れてくだい
├── .babelrc // babelの設定ファイル
└── webpack.config.js // webpackの設定ファイル
<!DOCTYPE html>
<html lang="ja">
<head>
<title>Hello Webpack</title>
</head>
<body>
<!-- id rootの要素にReactでレンダリングする -->
<div id="root"></div>
<!-- webpackによりbundle.jsというファイル名でまとめたものを読み込む -->
<script src="bundle.js"></script>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';
import Hello from './Hello';
ReactDOM.render(
<Hello />,
document.getElementById('root')
);
import React from 'react';
import './style.css';
import dog from './images/dog.png'; // 自分で入れた画像のファイル名、下のimgタグのsrc要素で読み込む
export default () => (
<div>
<div className="hello">Hello Webpack!</div>
<img src={dog} />
</div>
);
const path = require('path'); // パスを指定するためにnode標準で搭載されるpathを読み込む
module.exports = {
mode: 'development',
devtool: 'eval-source-map',
entry: path.join(__dirname, '/src'),
output: {
path: path.join(__dirname, '/public'), // バンドルしたファイルの吐き出し先を設定
filename: 'bundle.js' // ファイルの名前を指定
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 51200, // url-loaderを使うサイズの上限を指定
name: './images/[name].[ext]'
// limit以上のサイズの時、どこになんて名前で画像を吐き出すのかを設定。
// [name].[ext]は、もとの画像nameと拡張子で吐き出す。
}
}
]
},
]
}
}
webpackの設定ファイル、webpack.config.jsの内容
path
Node.jsの標準モジュール
path.join(__dirname, '/src')
// joinは引数のパスを連結させる。__dirnameはカレントのディレクトリのパスが出力される。
mode
- production -> バンドルしたファイルが、空白/改行が除去されファイルが最小化される。
- development ->コード判読がしやすいように、空白/改行を除去せずオリジナルのコードを維持する。
devtool
ソースマップの種類を指定。開発時には、限りなくオリジナルのソースコードを得られるeval-source-mapがおすすめ。詳しい種類については下記を参照。
https://webpack.js.org/configuration/devtool/
entry
起点となるjsファイルを指定。/srcのようにディレクトリを指定すると、そのディレクトリ配下のindex.jsがデフォルトで読まれる。
output
- path -> 吐き出す先のディレクトリを指定。
- filename -> 吐き出すファイルの名前を指定。
module/rules
- test => 読み込む拡張子
- use => 利用するローダー、配列で後ろのものから適用される。
- exclude => 除外するディレクトリやファイル
loaderにbabelを使用しているため、babelの設定を記述する必要があります。webpack.config.js
にまとめて書くことも出来ますが、今回は.bebelrc
ファイルに切り出しています。こうすることで、webpack.config.js
でbabel-loaderが使用されている時に、babelの設定は.babelrc
のものが適用されます。
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
"presets"には配列を渡します。envはmodulesオプションを設定するために配列になっています。
*presetは実行される際に後ろに記述したものから適用されます。上記のように記述することで、presetのreact -> envの順で解釈されます。
続いて、webpackを実行するコマンドをpackage.jsonに追加します。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.config.js"
},
--configオプションはデフォルトでwebpack.config.jsを参照。説明のために指定。
それでは、一通りの設定が終わったので、webpackを実行してみましょう
npm run build
> webpack --config webpack.config.js
Hash: f67d3e2ea3f0dc087604
Version: webpack 4.27.1
Time: 1050ms
Built at: 2018-12-11 13:23:45
Asset Size Chunks Chunk Names
bundle.js 2.11 MiB main [emitted] main
Entrypoint main = bundle.js
[./node_modules/css-loader/dist/cjs.js!./src/style.css] 185 bytes {main} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {main} [built]
[./src/Hello.js] 318 bytes {main} [built]
[./src/images/dog.png] 7.9 KiB {main} [built]
[./src/index.js] 174 bytes {main} [built]
[./src/style.css] 1.06 KiB {main} [built]
+ 14 hidden modules
output先に指定したpublic/ディレクトリをみてみると
public
├── bundle.js
└── index.html
無事にbundle.jsとしてファイルが吐き出されています。
index.htmlを開いてみると
無事に表示出来ていることが確認できました!
CSSファイルを別で吐き出すプラグインを設定してみる
今のままだと、cssファイルに使用しているのは [style-loader, css-loader]
なので、cssファイルが読み込まれた後、style要素として挿入されています
これを埋め込みではなく.cssファイルとして出力してみましょう。
必要なプラグインをインストール
npm install -D extract-text-webpack-plugin@next
下記を変更
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 追加
module.exports = {
mode: 'development',
devtool: 'eval-source-map',
entry: path.join(__dirname, '/src'),
output: {
path: path.join(__dirname, '/public'),
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/, // 変更
use: ExtractTextPlugin.extract({ // 読み込んだプラグインを適用させる。
fallback: 'style-loader',
use: ['css-loader'],
}),
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 51200,
name: './images/[name].[ext]'
}
}
]
},
]
},
plugins: [ // プラグインを追加
new ExtractTextPlugin('style.css') // 引数に出力する名前を渡す
]
}
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- 出力されたstyle.cssを読み込む -->
<link rel="stylesheet" href="style.css">
<title>Hello Webpack</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
もう一度webpackを実行してみます。
npm run build
// 無事style.cssとしてスタイルシートが出力されている
public/
├── bundle.js
├── index.html
└── style.css
以上がwebpackでReactを動かす基本的な設定となります。
開発用サーバーを立てるwebpack-dev-server
このままでは、ファイルを変更するたびにbuildコマンドを実行しなければならずとても面倒です。webpack-dev-serverを使うことで、webpackと連動しファイル変更時に自動で再ビルドされ、ブラウザー更新までしてくれます。
npm install -D webpack-dev-server
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.config.js",
"start": "webpack-dev-server --open" // この行を追加
}
- --openはコマンド実行時に自動でブラウザで開いてくれるオプション
module.exports = {
// ... 省略
entry: path.join(__dirname, '/src'),
output: {
// ... 省略
},
devServer: { // webpack-dev-serverの設定を追加
contentBase: path.join(__dirname, '/public'), // 監視するディレクトリを指定
port: 8084 // 立ち上げるポートを指定
},
module: {
// ...省略
}
}
設定が終わったので実際に立ち上げてみましょう。
npm start
すると、自動的にブラウザが立ち上がって、ビルドした状態のページが表示されます。なお、ビルド結果のファイルはメモリに保存されているだけなので、実際には出力されません。
webpack-dev-serverでサーバが立ち上がった状態で、src/内のファイルの変更をかけてみてください。自動で変更が検知されてブラウザに反映されたと思います。