JavaScript
nodejs
webpack

webpack 入門ハンズオン - 最小アプリを実際に作成してモジュールバンドラの利点を理解する

モジュールバンドラとは

webpack はモジュールバンドラです。
多くのWeb サイトでこのような説明を見かけますが、webpack が何をしてどのような利点があるのか自分はなかなか理解できずにいました🤔。
使わなくても良いのでは? 少なくとも自分は最初、そのように感じていました。
本記事ではそんなwebpack の良さが説明を軽く読んだだけでは理解できない人のために、実際に手を動かして実感できる形式でwebpack の魅力をご紹介していきたいと思います。
webpack をこれから始める、まだまだ利点を理解できていないみなさんもぜひ手元で作成してみてください。

用意するもの

  • nodejs, npm がインストールされたマシン
  • お好きなテキストエディタ

本記事で使用するコードは以下にコミットしてあります。
* https://github.com/TsutomuNakamura/dojo/tree/master/javascript/webpack-tutorial/ForQuiita

webpack の良さを完結に言うと

npm を使えばpackage.json を使ってアプリが依存しているパッケージを管理することができます。
bower も同じような感じで、フロントエンドのJavaScript モジュールを管理してくれます。
これで充分便利なのでwebpack は不要じゃないか? と思われるかもしれませんがnpm やbower を使っただけでは以下の問題が解決されません。

  • 複数のモジュールの呼び出しタイミングを正しく行わないとエラーが発生するという呼び出しタイミングの依存がJavaScript にはある
  • html から呼び出すJS を1 ファイルにまとめられる。もちろん自分が作成したモジュールも含めて1 フィアルにまとめてくれる
  • 各モジュールの変数スコープを閉じたものにすることができる。その結果、チーム内の他のメンバーが作成したモジュールと同じ変数名とならないように気にしないといけなかったりという心配がない

これらの利点を実際に最小のプログラムを作成して実感していきましょう。

bundlingイメージ
+--------+
| file   +--+
+--------+  |  +-------+
            |  |       |
+--------+  |  |       |
| file   +--+--+ file  |
+--------+  |  |       |
            |  |       |
+--------+  |  +-------+
| file   +--+
+--------+

webpack が無い場合

例えば以下のようなプロジェクトをサンプルに、webpack がなかった場合のプログラム作成について見てみましょう。
プログラムはコンソールにメッセージを出力するだけの簡単なアプリケーションです。

ファイル構成
practice-webpack/
 +- index.html
 +- js/
  +- scripts.js

それぞれのファイルは以下のようになっています。

practice-webpack/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Sample webpack</title>
  </head>
  <body>
    <h1>My webpack Page</h1>
    <script src="js/scripts.js"></script>
  </body>
</html>
practice-webpack/js/script.js
var msg_module01 = "module01";
console.log(msg_module01);
var msg_module02 = "module02";
console.log(msg_module02);

作成したらindex.html をChrome で開いてF12 キーを押下してみましょう。
するとconsole に以下のように表示されるはずです。

webpackjs_practice0000.png

出力結果
module01
module02

モジュール化する

では次に先ほどのscripts.js の内容をmodule01.js, module02.js ファイルに分割してモジュール化してscripts.js からインポートするように改修してみましょう。
scripts.js からインポートするように書くためhtml ファイルに変更は必要ありません。
また、ここではES6 の書式で書いて後ほどコンパイルします。

ファイル構成
practice-webpack/
 +- index.html
 +- js/
  +- scripts.js
  +- module01.js
  +- module02.js
practice-webpack/js/module01.js
var msg_module01 = "module01 stuff";
console.log(msg_module01);
practice-webpack/js/module02.js
var msg_module02 = "module02 stuff";
console.log(msg_module02);
practice-webpack/js/script.js
require("module01");
require("module02");

この状態でもう一度index.html を開いてみてください。

するとscript.js でエラーがでます。

webpackjs_practice0000_00.png

これはrequire という構文はNodeJS では使えますがブラウザのJavaScript では使えないためこのようなエラーが出ます。
モジュールを作成したのでそれをscript.js からロードできるようにしたいのですがこのままではできません。
ここでフロントエンドのJavaScript でもrequire 構文等を使えるようにしてくれて、モジュールのインポートをできるようにしてうkれるのがwebpack です。

webpack

webpack を使うために、まずはnpm initでプロジェクトを作成しpackage.jsonファイルを作成しましょう(入力内容は、適当で構いません)。

practice-webpack/
$ npm init
......
package name: (practice-webpack)
version: (1.0.0)
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /path/to/practice-webpack/package.json:
......
Is this ok? (yes) 

webpack, webpack-cli をインストールします。

webpack,webpack-cliのインストール
$ npm install --save-dev webpack webpack-cli
$ npm install -g webpack webpack-cli

webpack.config.js ファイルを作成し、JS ファイルをコンパイルするルールを決めます。
次のwebpack.config.js ファイルはjs/scripts.js ファイルを読み込みコンパイルしたものをjs/scripts.min.js として出力する設定ファイルです。

practice-webpack/webpack.config.js
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');

module.exports = {
  context: __dirname,
  devtool: debug ? "inline-sourcemap" : false,
  entry: "./js/scripts.js",
  output: {
    path: __dirname + "/js",
    filename: "scripts.min.js"
  },
  plugins: debug ? [] : [
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
  ]
};

以上でwebpack を実行する環境が完成しました。
webpack --mode development コマンドを実行してjs ファイルをコンパイルしてみましょう。

practice-webpack/webpack.config.js
$ webpack --mode development
Hash: e96b7619ba85693a5917
Version: webpack 4.4.1
Time: 94ms
Built at: 2018-4-1 13:35:48
         Asset      Size  Chunks             Chunk Names
scripts.min.js  7.14 KiB    main  [emitted]  main
Entrypoint main = scripts.min.js
[./js/module01.js] 31 bytes {main} [built]
[./js/module02.js] 31 bytes {main} [built]
[./js/scripts.js] 53 bytes {main} [built]

コンパイルに成功するとjs/scripts.min.js ファイルが作成されますので、その中を確認してみましょう。
すると先頭に多くのボイラープレートと最後の方にmodule01.js, module02.js がロードされている記述を確認することができます。

practice-webpack/js/scripts.min.js
$ cat js/scripts.min.js
......
/******/ (function(modules) { // webpackBootstrap
/******/        // The module cache
/******/        var installedModules = {};
/******/
/******/        // The require function
/******/        function __webpack_require__(moduleId) {
/******/
......
__webpack_require__(/*! ./module01.js */ "./js/module01.js");
__webpack_require__(/*! ./module02.js */ "./js/module02.js");
......

上記ファイルをhtml から読み出すだけでmodule01, module02 のスクリプトを利用することができるようになるわけです。
index.html ファイルのscript タグでscripts.min.js ファイルを読み込むように修正をしましょう。

practice-webpack/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>Sample webpack</title>
  </head>
  <body>
    <h1>My webpack Page</h1>
    <script src="js/scripts.min.js"></script>
  </body>
</html>

index.html をWeb ブラウザで開いてF12 キーを押下してコンソールを表示してみましょう。するとmodule01, module02 が実行されることが確認できます。
webpack を使うことでこのように複数のmodule を1 つのファイルにまとめることができるようになるのです。

変数のスコープについて

module02 にmodule01 の変数を参照するように、以下の変更を加えてみましょう。

practice-webpack/index.html
var msg_module02 = "module02 stuff";
console.log(msg_module02);
console.log(msg_module01);    // <- module01.js で定義されている変数msg_module01 を参照

もう一度webpack を実行してコンパイルし、index.html をブラウザで開くとmsg_module02 変数が参照できないためエラーが発生します。

webpackjs_practice0001.png

このようにwebpack を使用することでmodule01.js に定義した変数がmodule02.js では参照できないようになります。
変数のスコープをモジュール内で完結できるようになることによって、大規模な開発や複数人での開発において、モジュール間の変数名や関数名の重複によるバグを未然に防ぐことができるようになります。
またモジュール内で定義した変数や関数はモジュール内で完結することになるので、他モジュールの変数や関数に依存するプログラムは作れなくなるので、呼び出し順序依存によるエラーを取り除くことができます。

  • webpack 利用前
    webpackjs_practice0001_00.png

  • webpack 利用後
    webpackjs_practice0001_01.png

jquery, lodash を使った実践的プログラミング

次は実践的にjquery やlodash を使ってそれをwebpack でバンドリングする、実践的なプログラミングを組んでみましょう。
まずはjquery とlodash をインストールします。

jqueryとlodashのインストール
$ npm install --save jquery lodash

module01.js はjquery を使用してスクリプトを作成していきます。

practice-webpack/js/module01.js
var $ = require('jquery');
$('h1').html('new text');

module02.js はlodash を使用してスクリプトを作成していきます。
テスト用のダミーJSON ですが、これはhttps://mockaroo.com/で作成したものをお借りしています。

practice-webpack/js/module02.js
var _ = require('lodash');

var people = [{
  "id": 1,
  "first_name": "Jordon",
  "last_name": "Bilton",
  "email": "jbilton0@foxnews.com",
  "gender": "Male",
  "ip_address": "193.22.86.69"
}, {
  "id": 2,
  "first_name": "Avrom",
  "last_name": "Wyborn",
  "email": "awyborn1@eventbrite.com",
  "gender": "Male",
  "ip_address": "137.94.204.63"
}, {
  "id": 3,
  "first_name": "Tracee",
  "last_name": "Hapgood",
  "email": "thapgood2@wordpress.org",
  "gender": "Female",
  "ip_address": "139.119.151.253"
}, {
  "id": 4,
  "first_name": "Shelly",
  "last_name": "Cordel",
  "email": "scordel3@hostgator.com",
  "gender": "Female",
  "ip_address": "81.107.252.80"
}, {
  "id": 5,
  "first_name": "Andros",
  "last_name": "Coonihan",
  "email": "acoonihan4@so-net.ne.jp",
  "gender": "Male",
  "ip_address": "22.145.31.17"
}];

// Count how many wemans are there.
var femaleCount = _.filter(people, {gender: 'Female'}).length;
document.getElementsByTagName("h1")[0].innerHTML += "" + femaleCount + " females!";

webpack コマンドを実行してブラウザでindex.html ファイルを開いてみましょう。

webpack
$ webpack --mode development

webpackjs_practice0002.png

成功です!!
前のサンプルで確認しましたが、もちろんこれらのjqueryやlodash は各モジュールの閉じたスコープとなっているのでmodule01.js からlodash を呼び出したり、module02.js からjquery を呼び出したりといったことはできなくなっています。

しあげ

本番環境用にコンパイルする場合はwebpack コマンドのオプションに"--mode production" を指定するようにしてください。
production オプションをつけることにより、js/scripts.min.js が圧縮された形式で作成されるようになります。

productionオプション
$ webpack --mode production
......
$ head -c 256 js/scripts.min.js
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call......

そしてこのwebpack コマンドは本番用資材作成のコマンドとして、package.json スクリプトとして登録しておくと良いでしょう。

practice-webpack/package.json
{
  "name": "practice-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": { ...... },
  "devDependencies": { ...... },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production"
  },
  "author": "",
  "license": "ISC"
}

上記のように登録しておくことで"npm run build" と実行すればproduction モードでコンパイルが走るようになります。

build
$ npm run build
-> webpack --mode production

その他便利な機能

以上でwebpack の基本的な使い方のレクチャーは完了ですが追加で便利な機能や、やっておくと良いことなどについて説明します。

watch

個人的に開発中にオススメな機能としてwatch オプションがあります。
webpack.config.js に"watch: true" の記載を追加してwebpack をwatch モードで起動すると、webpack はファイルの変更を自動で検知して自動でコンパイルしてくれるようになり、ソースコードを書き換えるたびにwebpack コマンドを手動で走らせる必要がなくなります。

practice-webpack/webpack.config.js
 var debug = process.env.NODE_ENV !== "production";
 var webpack = require('webpack');

 module.exports = {
   context: __dirname,
   devtool: debug ? "inline-sourcemap" : false,
   entry: "./js/scripts.js",
   output: {
     path: __dirname + "/js",
     filename: "scripts.min.js"
   },
   plugins: debug ? [] : [
     new webpack.optimize.OccurrenceOrderPlugin(),
     new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
-  ]
+  ],
+  watch: true
 };

ターミナルで--watch オプションを追加してwebpack を起動してみましょう。

watchモードでwebpackの起動
$ webpack --watch --mode development

待受状態になるので、そのままの状態で試しにmodule01.js をテキストエディタで編集してみましょう。

practice-webpack/js/module01.js
 var $ = require('jquery');
 $('h1').html('new text');
+$('h1').append(' append text');

すると自動的にwebpack のコンパイルが走ります。

自動コンパイル
Hash: 14af6b84820c6d7bdf3a
Version: webpack 4.4.1
Time: 403ms
Built at: 2018-4-7 14:33:15
         Asset      Size  Chunks             Chunk Names
scripts.min.js  2.09 MiB    main  [emitted]  main
Entrypoint main = scripts.min.js
[./js/module01.js] 99 bytes {main} [built]
    + 6 hidden modules

開発中は編集のたびにwebpack コマンドを実行するよりこちらのほうが効率が良いのでおすすめです。

ES6 に対応させる

webpack のモジュールとしてbabel を使うようにすればコードの文法をES6 に対応させることができます。

installbabel
$ npm install babel-loader babel-core babel-preset-env --save

webpack.config.js を以下のように書き換えます。

practice-webpack/webpack.config.js
 var debug = process.env.NODE_ENV !== "production";
 var webpack = require('webpack');

 module.exports = {
   context: __dirname,
   devtool: debug ? "inline-sourcemap" : false,
   entry: "./js/scripts.js",
   output: {
     path: __dirname + "/js",
     filename: "scripts.min.js"
   },
   plugins: debug ? [] : [
     new webpack.optimize.OccurrenceOrderPlugin(),
     new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
   ],
-  watch: true
+  watch: true,
+  module: {
+    rules: [{
+      test: /\.js$/,
+      exclude: /node_modules/,
+      use: [{
+        loader: 'babel-loader',
+        options: {
+          presets: ['env']
+        }
+      }]
+    }]
+  }
 };

これでコードをES6 準拠で書く環境が整いましたので、ES6 準拠な文法で書き換えてみましょう。

practice-webpack/js/
-var $ = require('jquery');
+import $ from 'jquery';
 $('h1').html('new text');
 $('h1').append(' append text');
コンパイル
Hash: ad5766d143f3bcd6fd46
Version: webpack 4.4.1
Time: 403ms
Built at: 2018-4-7 14:37:36
         Asset      Size  Chunks             Chunk Names
scripts.min.js  2.09 MiB    main  [emitted]  main
Entrypoint main = scripts.min.js
[./js/module01.js] 292 bytes {main} [built]
    + 6 hidden modules

コンパイルが成功しました!
index.html を開けばエラー無く画面に表示されるはずです。

webpackjs_practice0003.png

以上、webpack ハンズオンでした。
webpack を使うことで多くのモジュールを使ったアプリや大人数なチームでのアプリ開発も、もう怖くありません😎。

参考