頑張るときはいつも今

自称エンジニアのゴリラです。日々精進。

【読書記録】WebPackに入門してみた

先週記事をサボってしまいました。 今週は書きます。(とはいえ読書記録なので内容は薄いですが)

今回読んだ本

「Webpack実践入門」という本になります。 KDP(Kindle ダイレクト・パブリッシング)に出版されているようで、 ワンコインで購入できる技術書になります。

なぜ読んだか

今扱っているシステムでは、RailsMVCで構築されており一部jQueryで非同期処理をするような構成です。

若干時代遅れな点もあり、Vue.jsも導入していきたいなと思っています。

Rails6ではwebpackerが標準ですが、巷では - カスタマイズ性が低い - webpacker側でサポートしていないパッケージと相性が悪い ということもあり、導入推進のためwebpackを勉強し始めました。

読んで何が得れたか

  • webpackに関する基本的な知識、利用用途、知識を身に着ける
    • 用語と仕組みの説明→ハンズオンという形式なので、理論と実装を紐づけることができました
  • 有名どころなローダー(babel, sass, url)やプラグインを使ってみることができる
    • webpack単体では恩恵はあまりなくて、babal-loaderやsass-loaderといったものと組み合わせることで、フロントエンド開発を効率化することができます
  • 本番環境と開発環境でのwebpack.config.jsの使い分けの方法を理解できる
    • 本番環境と開発環境では、利用するローダ・プラグインや設定を分けたいといったことがあります。
    • ただ共通している部分もあるといった場合に、ハンズオンを通して本番環境と開発環境でwebpack.config.jsの使い分けを身に着けることができます。

そもそもwebpackって何?

  • フロントエンド開発用のモジュールバンドラ
    • モジュールバンドラを介して、複数のモジュール(JSファイルやcssファイル)をまとめてくれる

雑に表すと下図のようなイメージ

フレームワークなども含めて、複数(or 単体)にまとめて出力してくれます。

f:id:wa_football_1120:20200215121701p:plain

webpackを使うと何が嬉しいの?

メリット1 機能後にファイルをモジュール化できる

フロントエンド開発では、機能ごとにファイルを分割して開発することが主流です。 なんで機能ごとにファイルを分割するかと言うと、

  • 可読性の向上
    • どこで使われているかわからないコードをみるとすごい疲れますよね。しかも安易に消せないと言う。。
    • 機能ごとに別れていれば、システムのどの機能を実装していると言う全体把握にも役立ちます。
  • 開発作業の分担とテストがしやすくなる(=開発効率の向上)
    • 機能ごとにファイルが別れていれば、コンフリやデグレを引き起こす可能性を大幅に削減できます
  • 名前空間を生成できる
    • グローバル変数だとか使われているとまず、競合やクリティカルなバグが起きやすくなります
    • 名前空間を作ることで、その空間のみに影響範囲を止めることができます。
  • モジュールの保守性を高められる
    • リリースして、ある程度の期間が立って、「ここもう少しこうしたいな」と要望を受けたとしましょう
    • ここで、1つのファイルから該当のコードを探すの比べたら、機能ごとに別れていた方が機能拡張も修正もしやすいですよね

メリット2 外部モジュールも利用できる

フロントエンド周りでは、vuereactといったモダンなライブラリやフレームワークを利用する機会が大幅に多くなると思います。

webpackは外部モジュールを含めてバンドルすることができます。

メリット3 リクエスト数を減らせる

HTTP2.0が普及してきているので、メリットか分かりませんが、

複数のファイルがまとめられるため、リクエスト数が減ります。

HTTP2.0と1.1の違いはこちらが分かりやすかった

メリット4 依存関係を解決したファイルを出力できる

webpackが依存関係を自動的に解決してファイルを主力するため、依存関係による不具合を減らすことができます。 小規模なシステムでは、依存関係を追っていくことができそうですが、 中+大規模なシステムになってくるとまず依存関係を目グレップで追っていくのは無理ですし効率がかなり悪いです。

f:id:wa_football_1120:20200215124345j:plain

webpackを試してみる

実行環境はこんな感じです。

srcディレクトリ配下のapp.jsがモジュールadd.jstax.jsを利用します(依存関係) これらをバンドルした結果をpublic/js配下のbundle.jsに出力します。

root@e9bc5cdc81ea:/my_webpack# yarn -v
1.21.1

tree                                                                                                                                ?[master]
.
├── Dockerfile
├── docker-compose.yaml
└── getting-started-webpack
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    │   ├── index.html
    │   └── js
    │       └── bundle.js
    └── src
        ├── js
        │   └── app.js
        └── modules
            ├── add.js
            └── tax.js

package.json作成 & 必要なモジュール群をインストール

まずはpackage.jsonを作成して、webpackをインストールします。

root@e729ef118310:/my_webpack# yarn init -y
yarn init v1.21.1
warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications.
warning package.json: No license field
success Saved package.json
Done in 0.11s.

root@e729ef118310:/my_webpack# yarn add --dev webpack webpack-cli

root@e729ef118310:/my_webpack# yarn add jquery

package.jsonにモジュールが追加されていることを確認します。

  • dependencies
    • 出力されるファイルにバンドルされるモジュール
    • 本番環境で必要なモジュール群
    • yarn add {モジュール名}で入れます
  • devDependencies
    • 開発時に必要なモジュール
    • yarn add -dev {モジュール名}で入れます
{
  "name": "my_webpack",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11"
  },
  "dependencies": {
    "jquery": "^3.4.1"
  }
}

package.jsonスクリプトを書く

package.jsonscriptsフィールドにエイリアスと対応するコマンドを記述することで

yarn run {エイリアス}で実行することができます。

  "scripts": {
      // yarn run devで実行ができるようになる
      "dev": "webpack --mode development --watch --hide-modules --config webpack.config.js"
  }

webpack.config.jsを書く

webpack.config.jsとはwebpackがエントリーポイント(どこを起点に依存関係を解決していくか)や出力先(バンドルしたファイルの保存先)を設定することができます

const path = require('path');


module.exports = {
    mode: 'development',
    entry: './src/js/app.js',
    output: {
        filname: 'bundle.js',
        path: path.resolve(__dirname, 'public/js')
    }
}
  • mode

    • webpackの動作モードで、development,production,noneから選択します
    • developmentの場合は、エラー表示やデバッグしやすいファイルを出力するため開発時にはこちらを選択します
    • productionの場合は、ファイルの圧縮やモジュールの最適化などがされるので、本番環境に適用する場合にはこちらを選択します
  • entry

    • 上でwebpackは依存関係を解析して、自動的に解決すると書きましたが、その依存関係の解析を開始する始点になります
  • output

    • バンドルの設定です。
    • filename
      • バンドルした結果のファイル名
    • path
      • バンドルしたファイルの保存先

モジュール作成

モジュールadd.jstax.jsを作成します。

add.jsはそのままの通り、2つの引数を受け取って合計を返すモジュールです。

tax.jsは値段と税率を引数で受け取り、税込み価格を返すモジュールです。

exportを利用して、外部のファイルが利用できるようにします。

//add.js
export default function add(n1, n2) {
    return n1 + n2;
}
//tax.js
export default function(price, salesTax) {
    return Math.round(price * salesTax);
}

エントリーポイントの作成

webpackが解析を始めるエントリーポイントを作成していきます。 先ほど作成したモジュールとjqueryをimportして、bodyにベタ書きするプログラムです。

import $ from 'jquery';
import add from './modules/add';
import tax from './modules/tax'

const price1          = 100;
const price2          = 500;
const totalPrice      = add(price1, price2);

const salesTax        = 1.08;
const priceIncludeTax = tax(totalPrice, salesTax);

$('boxy').text(priceIncludeTax);

ビルドしてみる

先ほど記述した scriptsでビルドするとpublic/js/bundle.jsが作成されていると思います。

root@e729ef118310:/my_webpack# yarn run dev
yarn run v1.21.1
warning package.json: No license field
$ webpack --mode development --watch --hide-modules --config webpack.config.js

webpack is watching the files…

Hash: 9e92343bbee09bc21d50
Version: webpack 4.41.6
Time: 559ms
Built at: 02/15/2020 6:11:34 AM
    Asset     Size  Chunks             Chunk Names
bundle.js  316 KiB    main  [emitted]  main
Entrypoint main = bundle.js

ビルドしたファイルを読み込む

public/index.htmlからはbundle.jsを読み込めば完了です。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Getting Started </title>
    </head>
    <body>
        <script src="js/bundle.js"></script>
    </body>
</html>

ローダについて

フロントエンドはjavascriptだけではなく、css(scss)や画像ファイルも必要です。

js以外のファイルを同じようにバンドルできる形に変換するプログラムがローダになります。

TypeScriptVueコンポーネントもローダを介してバンドルすることが可能です。

冒頭のWebpackのイメージをもう少し細かく分離すると下のような形になります。 f:id:wa_football_1120:20200215152033j:plain

代表的なローダーの紹介とwebpack.config.jsへの記載方法をメモしていきます。

babel-loader

babel-loeaderはES6以降のコードをES5のコードに変換するローダーです。 IEなど古いブラウザでも動くようなjs構文に変換してくれます。

babel-loaderを追加

babale-loaderを使う場合には@babel/core@babel/preset-envが必須なので一緒に追加します。 - @babel/core : Babel本体 - @babel/preset-env: ECMAを変換するために使う

root@9e670eed7c44:/my_webpack# yarn add --dev babel @babel/core @babel/preset-env

webpack.config.jsを追記

ローダの設定はmoduleブロックに記述していくことになります。 - test - どの形式のファイルを対象とするか - include - 適用対象のフォルダ - use - 使うローダを設定

module: {
        rules: [
            {
                test: /\.js&/, //ローダの対象ファイルの形式?
                include: path.resolve(__dirname, 'src/js'), // ローダ対象のフォルダ(excludeで対象外のフォルダを設定できる)
                use: [
                    {
                        loader: 'babel-loader', 
                        options: {
                            presets: [
                                [
                                    '@babel/preset-env'
                                ]
                            ]
                        }
                    }
                ]
            }
        ]
    }

sass-loader, css-loader, style-loader

scssファイルをバンドルするために3つのローダを使います。

  • sass-loader
  • css-loader
    • cssをモジュールに変換する
  • style-loader
    • cssのスタイルが記述されたstyleタグをHTMLに追加する

モジュールを追加

sass-loaderを使うためにはnode-sassも同時にいれる必要があります。

root@9e670eed7c44:/my_webpack# yarn add --dev sass-loader node-sass css-loader style-loader

webpack.config.jsに追記

rulesブロックに追記します。

今回は複数のローダを利用するのですが、ローダは配列に入れた逆順で実行されます。

(今回の場合は、sass → css → styleの順番で実行する必要があります)

            {
                test: /\.scss$/,
                include: path.resolve(__dirname, 'src/scss'),

                use:[
                    //下から順番に実行される(scss-loader -> css-loader -> style-loader)
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }

プラグイン

webpack自体を拡張させるためのプログラムです。

バンドルする際にプラグインで追加した処理を行うことができます。

ProvidePlugin

指定したモジュールを全てのファイルから変数として利用できるようにするプラグインです。

jqueryなどほぼ全てのファイルで利用されるモジュールを毎回importする必要がなくなります。

このプラグインについては、webpackをインストールした時点ですでに利用することができます。

webpack.config.js

プラグインを利用するために、webpack自体を読み込んおきます。

const webpack = require('webpack')

プラグインの設定はpluginsブロックに記述していきます。 ProvidePlugnを利用してjquery$で利用するようにしています。

plugins:[
        new webpack.ProvidePlugin({
            $: 'jquery' // juqueryを$として使うことができる
        })
    ]

Watchモード

watchモードは、ファイルの変更差分を監視して、

変更があった際に自動的にビルドを再実行してくれる機能です。

自分で都度、ビルドする必要がなくなります。

webpack.config.jsに追記することもできるのですが、

--watchをつけてコマンドを実行すればwatchモードになります。

なので、package.jsonのscriptで使い分けた方が利便性は高いです。

  "scripts": {
      "dev": "webpack --watch",
      "build": "webpack"
  }

webpack-dev-server

node.jsで作られた開発用サーバです。

jsファイルやリソースに変更があると即座に反映してくれます。

webpack-dev-serverの注意点としては、ファイルを出力しないことです。

ビルドした結果はオンメモリに保持するため、実際にビルドする際にはwebpackコマンドを実行する必要があります。

インストール

yarnで追加できます。

開発で使うものなのでローカルインストールしておきます。

root@9e670eed7c44:/my_webpack# yarn add --dev webpack-dev-server
yarn add v1.21.1

webpackに設定を追加

devServerプロパティにwebpack-dev-serverの設定を追加していきます。

    devServer: {
        open: false, //サーバ起動時に自動的にブラウザを起動するか?(コンテナ上で試していたのでfalse) 
        host: '0.0.0.0', //コンテナで動作させているため,
        disableHostCheck: true, // 
        port: 3000, // サーバが待ち構えるポート番号
        openPage: 'index.html', // 
        contentBase: path.join(__dirname, 'public'), // コンテンツのルートディレクトリ
        watchContentBase: true,
    }

開発環境と本番環境の切り分け

開発環境ではwatchモードを有効にしたり、 webpack-dev-serverを動かしたいとい要望が出てくると思います。

もちろんconfigをそれぞれ作ればいいのですが、重複部分なども同じように 書いていくのは無駄が多くなります。

そこでベース(共通部分)+本番のみの部分+開発のみの部分といった形で切り分けをしていきます。

ディレクトリ構造

tree -L 1
.
├── node_modules
├── package.json
├── public
├── src
├── webpack.base.config.js
├── webpack.dev.config.js
├── webpack.prod.config.js
├── yarn-error.log
└── yarn.lock

必要なモジュールをインストール

webpackのconfigをマージするwebpack-merge

本番環境で利用するterser-webpack-pluginをインストールします。

root@9e670eed7c44:/my_webpack# yarn add --dev webpack-dev-server
root@05d680e73065:/my_webpack# yarn add --dev terser-webpack-plugin

ベース部分の作成

webpack.base.config.jsにベース部分を記載していきます。

エントリーポイントの設定や出力先、ローダーは同じように設定します。

const path = require('path')

module.exports = {
    entry: '.src/js/app.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'public/js')
    },
    module: {
        rules: [
            {
                test: /\.js&/, //ローダの対象ファイルの形式?
                include: path.resolve(__dirname, 'src/js'), // ローダ対象のフォルダ(excludeで対象外のフォルダを設定できる)
                use: [
                    {
                        loader: 'babel-loader', 
                        options: {
                            presets: [
                                [
                                    '@babel/preset-env'
                                ]
                            ]
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                include: path.resolve(__dirname, 'src/scss'),

                use:[
                    //下から順番に実行される(scss-loader -> css-loader -> style-loader)
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    }
}

開発用の設定

開発環境では、developモードを有効にして、webpack-dev-serverも利用します。

webpack-mergeを利用して、共通設定に追加しているイメージです。

const merge = require('webpack-merge');

const baseConfig = require("./webpack.base.config.js");

module.exports   = merge(baseConfig, {
    mode: 'development',
    watch: true,
    devServer: {
        open: true,
        host: '0.0.0.0',
        port: 3000,
        openPage: 'index.html',
        contentBase: path.resolve(__dirname, 'public'),
        watchContentBase: true,
    },
    //ソースマップの表示
    devtool: 'cheap-module-eval-source-map'
});

本番環境の設定

本番環境でも同じようにwebpack-mergeを利用して、

共通設定に追加していきます。

const merge = require('webpack-merge');

const baseConfig = require('./webpack.base.config');

const TerserPlugin = require("terser-webpack-plugin")

module.exports = merge(baseConfig,{
    mode: 'production',
    // production時に有効になるoptimizationを上書き
    optimization: {
        minimizer: [
            new TerserPlugin({
                terserOptions: {
                    //console.logを削除する
                    compress: { drop_console: true}
                }
            })
        ]
    }
})

package.jsonにコマンドの使い分けを追記

最後に本番環境と開発環境で使い分けるコマンドを

package.jsonscriptsに記述していきます。

yarn run devyarn run prodで環境に合わせたビルドが可能になります。

  "scripts": {
    "start": "webpack-dev-server",
    "dev": "webpack --config webpack.dev.config.js",
    "prod": "webpack --config webpack.prod.config.js",
    "build": "webpack"
  }

まとめ

今回もつらづらと本を読んだメモを書いただけになりますが、 これまでコピペ使っていたwebpackが少しだけ読めるようになりました。

ただwebpackは設定できる項目がかなり多いようなので、 プロジェクトに合わせた設定が必要になってきます。

まだまだ習得には時間がかかりそうです。。