かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

今更の webpack 4 導入とbabel使うところまでのメモ。

3年ほど山ごもりをしていて、浦島太郎状態なので、その間に進んだフロント周りのキャッチアップを ちょっとづつ進めています。
今回はwebpackについて、フリーな個人制作が多くgulpで何とかなるしと秘伝のタレ使いまわしていたのですが、JavaScriptのモジュール化とかすると流石にwebpack使い方くらいは知ってないと…と感じる部分があり今更のwebpackをやってみました。(ゴーギャンだって35歳で仕事やめて画家をめざしたのですから、何かを始めるのに遅すぎることはないですよね!)

webpackとは何か

babelとセットだったり、CSSも1つのJSファイルにできるという解説があったり、参考にしようと使われてるGitHubリポジトリ見に行ったら巨大な設定ファイルがあったりで分かりづらくなるのですが、webpackは要するにJSの依存関係を解決して一つにまとめる機能を提供しているもの。なのかなという印象です。

webpackを使うメリット

  • 自作のJSファイルだけでなく、npmでインストールしたモジュールなど含め、読み込み順などの依存関係を解決して1つのファイルにまとめてくれる
  • 各モジュールを閉じたスコープにしてまとめてくれるので、モジュール名や変数名が被るのを気にする必要がなくなる importする側では気にする必要はあるけど

単に1つのファイルにまとめるだけならgulpとかでconcatしてしまえば1つのファイルにすることができるのですが、順番を担保するためにファイル名を01-xxx.js, 02-yyy.jsとかにしたり、グローバルオブジェクトを作っておき、各ファイルを即時関数でスコープ化してグローバルオブジェクトを介してメインのスクリプトから使うようにした構成にして、1つのファイルにconcatした後にファイルの全体を!function(){..<1つにしたJS>..}();で囲むみたいなハックじみたことをしてましたw
そんなハックは必要ない。そうwebpackを使えばね。
そんな感じ。

webpackのinstall

yarn

$ yarn add --dev webpack webpack-cli

npm

$ npm install --save-dev webpack webpack-cli

設定ファイルwebpack.config.jsの作成

$ touch webpack.config.js

webpackではプロジェクトのルートにwebpack.config.jsというファイルがあればこれを設定ファイルとして自動的に読み込むようです。

webpack.config.js

const path = require('path');

module.exports = {
  // webpack 4 からは必須
  // 'production' | 'development' | 'none'
  mode: 'development',
  // メインとなるファイル
  entry: './src/index.js',
  // 出力に関する設定
  output: {
    // 出力先のディレクトリ
    path: path.resolve(__dirname, 'dist/js'),
    // 出力ファイル名
    filename: 'bundle.js'
  }
};

webpackの実行

$ yarn run webpack

./dist/js配下にbundle.jsが出力されました。

entry / output

entryは、メインとなるファイル。エントリーポイントとか書かれているのを目にすることが多いです。
outputはwebpackでまとめたファイルの出力先を・ファイル名を指定します。出力先のパスは絶対パスでないとダメらしい(ちゃんと調べてないです...)のでpath.resolve(__dirname)を使って絶対パスに変換しています。
entryoutputは指定が無ければ、それぞれ./src/index.js, ./dist/main.jsになります。

複数ファイルをentryにして別々に出力する方法

webpack.config.js

module.exports = {
  entry: {
    app:   './src/index.js',
    print: './src/print.js',
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist/js'),
  }
};
// 出力
// -> dist/js/app.bundle.js
// -> dist/js/print.bundle.js

entryにオブジェクトで起点となるファイルを指定、entryに渡したオブジェクトのキーが出力ファイル名の'[name]'部分になる。
cf. Output Management | webpack

mode

webpack4からはproduction/development/noneいずれかのmodeの指定が必要になったようです。
modeによって使用されるオプションが変わるようです。

production

すべての種類の最適化を行ったバンドルファイルが生成されます。
また、ウォッチモードは持ちません。

development

evtoolはevalで実行されます。 Devtool | webpack また、ウォッチモード時のために差分ビルドの高速化がされています。

none

すべての設定を無効にします。

cf. 次のリリースであるwebpack 4の主な変更点まとめ - 技術探し

modewebpack.config.jsに書いておいても、コマンド(CLI)から指定して上書きすることができます。

modeをコマンド(CLI)から指定する方法
$ yarn run webpack --mode=production

--mode=オプションでモードを指定してwebpackを実行することができます。
cf. https://webpack.js.org/concepts/mode/#usage

npm scriptにモードを指定したコマンドを作成しておくと便利そうです。

configファイルを環境ごとに分ける

CLIでモードを指定しなくても、--configオプションを使うとconfigファイルそのものを切り替えることもできるようです。

$ yarn run webpack --config <config file name>

環境によって処理が大きく異なるのであれば、webpack.config.jsの中で分岐をさせるより、設定ファイルそのものを別にしてしまったほうが見通しが良くなりそう。

--mode=でモード指定するにしろ、--configで設定ファイルを切替えるにしろ

"script": {
  "build": "./node_modules/.bin/webpack --mode=production"
};

のような感じでnpm scriptにしてしまえば、どんな指定をしているか気にすることなく使うことができそうです。

使用していないモジュールの除去 (Tree Shaking)

cf. Tree Shaking - webpack

Tree Shaking とはデッドコードを除去する機能のコト。
webpack4は--mode=productionのときに自動的にTree Shakingされるようです。

/src/index.js

import { foo, bar } from './test.js';
console.log( foo() );

/src/test.js

export const foo = () => {
  console.log('module foo');
  return 'Foo';
};
export const bar = () => {
  console.log('modele bar');
  return 'Bar';
};

👇 webpack
--display-used-exportsオプションで使われているモジュールが何か表示できる

$ yarn run webpack --mode=production --display-used-exports
...
    Asset      Size  Chunks             Chunk Names
bundle.js  1.05 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 350 bytes {0} [built]
    | ./src/index.js 187 bytes [built]
    | ./src/test.js 163 bytes [built]
    |     [only some exports used: foo]
✨  Done in 0.71s.

👇 出力

/dist/bundle.js

!function(e){var t={}; /*...<中略>...*/}([function(e,t,r){"use strict";r.r(t);console.log((console.log("module foo"),"Foo"))}]);

1行になってちょっと見辛いけど、console.log('module bar');の文字が消えてる。
=> Tree Shakingされている!

minimize.true の時のみ Tree Shaking されるっぽい

min化しないようにするminimize.falseオプションを設定するとproductionモードでもTree Shakingされないっぽい。

webpack.config.js

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/js'),
    filename: 'bundle.js'
  },
  optimization: {
    // min化しない
    minimize: false
  }
};

👇 先ほどのindex.jstest.jsproductionモードでwebpack

$ yarn run build --mode=production --display-used-exports
    Asset      Size  Chunks             Chunk Names
bundle.js  4.01 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 350 bytes {0} [built]
    | ./src/index.js 187 bytes [built]
    | ./src/test.js 163 bytes [built]
    |     [only some exports used: foo]
✨  Done in 0.62s.

👇 出力

/dist/bundle.js

// 略
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./src/test.js
const foo = () => {
  console.log('module foo');
  return 'Foo';
}

const bar = () => {
  console.log('module bar');
  return 'Bar'
}

// CONCATENATED MODULE: ./src/index.js
console.log( foo() );

/***/ })

[only some exports used: foo]が表示され使ってないモジュールは感知してる様ですが、 const barの定義が消えていません。
min化された時はconsole.log( foo() );の中に直接foo関数の内容が入っていたので、そもそも出力のフォーマットが全く違うみたいです。

I can also confirm that /* unused harmony export */ comments are missing from the production output when optimizations.minimize=false. cf. https://github.com/webpack/webpack/issues/6992#issuecomment-383756787

他にもsideEffectsオプションとかいろいろとある様ですが、それだけで記事1つになりそうな感じだったので割愛

webpackでbabelを使う

webpack設定ファイルのmoduleオプション内に、webpackでバンドルする前に下処理したい設定ができる。
下処理をするプラグインがいわゆるXXXX-loader

babelを使う為のローダーのインストール

yarn

$ yarn add --dev babel-loader @babel/core @babel/cli @babel/preset-env

npm

$ npm install --save-dev webpack babel-loader @babel/core @babel/cli @babel/preset-env

@babel/core @babel/cli @babel/preset-env はbabel本体関連

rulesの設定

webpack設定ファイルのmoduleオプション内に何をどう処理をするのかをrulesとして作成する。

webpack.config.js

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist/js'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        // 対象ファイル
        test: /\.js$/,
        // 除外するディレクトリ
        exclude: /node_modules/,
        use: [
          {
            // 利用するローダー
            loader: "babel-loader",
          }
        ]
      }
    ]
  }
};

babelの設定はプロジェクトのルートにあるbabel.config.jsが使用される
webpack.config.js内にoptionsとしてbabelの設定を書くこともできる。 webpack.config.js内に設定があればこちらが優先されるっぽい。

rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    use: [
      {
        loader: "babel-loader",
        options: {
          babelrc: false,
          presets: [["@babel/preset-env", {
            modules: false,
            useBuiltIns: "usage"
          }]]
        }
      }
    ]
  }
]

/src/index.js

import { foo, bar } from './test.js';
console.log( foo() );

/src/test.js

export const foo = () => {
  console.log('module foo');
  return 'Foo';
};
export const bar = () => {
  console.log('modele bar');
  return 'Bar';
};

👇 webpack with バベる 出力ファイルが見やすいようにnoneモードで

$ yarn run build --mode=none

👇 出力

// 略
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "foo", function() { return foo; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bar", function() { return bar; });


var foo = function foo() {
  console.log('module foo');
  return 'Foo';
};
var bar = function bar() {
  console.log('module bar');
  return 'Bar';
};

babelで変換できてる。

Tree Shakingさせるにはbabelでimport文がrequireに変換されないように、modules: falseを指定しておくのが良いっぽい。

まとめ

webpack設定がややこしいイメージがあったけど、メインはJavaScriptモジュールの依存関係を解決してバンドルする機能を提供するもで、その部分はシンプルだと知れた。

実現したい事によってoptionを設定していったり、
CSS含めてwebpackでゴリっとするためにloaderを色々組み合わせてた設定を作っていくってのは大変そうだと思いました。個人的にはCSSとして出力するCSSコンパイルとかはwebpackじゃなくても良いのでは?とも思うけど、node_moduleフォルダの肥大化を思うとgulpかあJSまわりだけwebpackってのもナンダカナーとも感じる訳で、なかなか闇っぽい...

babelすることを「バベる」とか言ったりするらしいけど、webpackの場合は、「ウェブパックる」とか言うのだろうか?割とどーでもいいけど、ちょっと気になった。


[参考]

人騒がせな名画たち

人騒がせな名画たち

速習webpack 速習シリーズ

速習webpack 速習シリーズ