Webpackでバンドル

 
Web Tips.
Booskanium's
Booskanium's Web Tips.

ちゅんラヂのバンドル環境整備メモ

ちゅんラヂでビルド&バンドルが必要になった理由。
・メンテナンス性向上の為にソースコードをモジュール単位で分割した。
・npmで取り込んだライブラリをビルドさせたかった。
・細切れになったソースコードのリクエスト数を減らしたい。
その為にWebpack環境を整えた時の作業メモです。

すでにnode.jsがインストールされていて、npmも導入されていることを前提にしたメモです。
落書き人が手順を忘れない為のメモで、Webpackについて深く掘り下げていませんので、詳しくは下記の文献を参照してください。
また、このメモではJavaScriptやCSSのモジュール化の方法と、それをHTMLでどの様に取り込むのかについては言及していません。

Special thanks: Webpackについて参照した文献です。
webpack の基本的な使い方  webpack 5 CSS だけコンパイルしたい  webpack.config の黒魔術をとく  css-loader と style-loaderを間違えない 
Tips. Webpackの各種パラメータ記述を理解するには、JavaScript構文が読めると理解に役立つ。

用語について
・ビルド&バンドルはトランスパイル&バンドル、単純にバンドル、それとも単純にビルド?
 どの表現が良いのやら、善きに解釈して下さい。

1. 開発環境ディレクトリの検討

バンドル環境を整える前に、開発環境のディレクトリ構成をどうするかを検討しました。
これを疎かにすると後で運用が面倒な事になるので、開発環境作成に不慣れなら先駆者に相談したほうが良いです。
多少、個性的な構成かもしれませんがディレクトリ構成は以下を考慮しています。
・試験実行及びリリースのディレクトリはdist
・HTMLはバンドル対象外
 └→単純なのでMinifyしても効果が限定的
・複数のHTMLのJavaScriptとCSSバンドルは別々に
 └→webpack.config.jsを別々にする
  ※configが増えますが
   ・個々のconfig内容は単純
   ・個別にバンドルできる

▼ちゅんラヂ開発環境のルートディレクトリ
├◆git ※gitディレクトリ
├─ package.json
│ ※当開発環境の情報
├◆node_modules
│ ※Webpack導入で作成される
├─ package-lock.json
│ ※npmのバンドル依存情報
├◆dist (distributeの略語)
││ ※ちゅんラヂ実行ルートディレクトリ
││ ※リリースはこのディレクトリから
│├─ tunein2radio.html
│├─ tunein2player.html
│├─  ……
│├◆js
│││※バンドル後のJavaScript
││├─ tunein2radio.js
││├─ tunein2player.js
││└─  ……
│├◆lib
│││※バンドル対象外ライブラリ等
││├─ hls.js
││└─ dash.js
│├◆css
│││※バンドル後のCSS
││├─  tunein2radio.css
││├─  tunein2player.css
││└─  ……
│├◆img
│││※将来的にsrc配下に移動してバンドルする?
││├─ xxxxxx.png
││├─ ……
│::
├◆src
││※バンドル前のソースコード
│├◆js
││├─ main.tunein2radio.js
││├─ main.tunein2player.js
││├─  ……
││└◆ modules
││ ├─ 各種JSモジュール
││ ├─ ……
│: :
│├◆css
│││※バンドル前のCSSソースコード
││├─  main.tunein2radio.css
││├─  main.tunein2player.css
││└◆ modules
││ ├─ 各種cssモジュール
││ ├─ ……
│: :
│ ※configはHTMLの機能単位毎(バンドル単位)で作成
├─ webpack.config.tunein2rado.js
├─ webpack.config.tunein2player.js
├─ webpack.config.tunein2.css.js ※CSSは共通部分があるのでHTML毎のCSSはを一括ビルド
:

2. JavaScriptバンドル環境

Webpackでバンドルするプロジェクトの環境を整える手順

落書き人はhtmlファイル毎のJavaScriptを個々にバンドルします。
そして開発(development)モードでバンドルしてテスト、テストが完了したら本番(production)モードでバンドルして再確認してリリースという手順です。

以下はWindows環境での説明です。

  • 手順
    コマンド
    要約
  • 開発ルートへ
    CD \xampp\htdocs\7design.jp\hp\trials_tr5
    開発管理のルートディレクトをカレントにする。
  • package.jsonを生成
    npm init -y
    生成されるpackage.jsonの例
    {
      "name": "tunein2",
      "version": "5.0",
      "description": "ちゅんラヂ",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "booskanium <xxxxxx@xxxxxxx.jp>",
      "license": "ISC"
    }
    ・"name": "tunein2",
     既定値はカレントディレクトリ名ですが適切なnameに打ち替えています。
    ・"description": "",
     プロジェクト名である"ちゅんラヂ" と記述
    ・authorやlicenseは前に
     npm config set init.author.name booskanium
     npm config set init.author.email xxxxxx@xxxxxxx.jp
     で設定してあります。「xxxxxx@xxxxxxx.jp」はロボットにメールアドレスを収集されたくないので伏せてあります。
    ・"license": "GNU LGPLv3"
     ちゅんラヂのlicenseに打ち替えています。
    さらに、Privete開発でnpmに公開しないので
    {
      "name": "tunein2",
      "version": "5.0",
      "description": "ちゅんラヂ",
      "private": true, 
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "booskanium <xxxxxx@xxxxxxx.jp>",
      "license": "GNU LGPLv3"
    }
    ・"main": "index.js" を削除
    ・"private": true, を追加
  • Webpackとwebpack-cliを入れる
    npm install -D webpack webpack-cli
    当該開発環境のルートディレクトリにWebpackとwebpack-cliが入る。
    -Dオプションは開発用インストール(ローカルインストール)で、package.jsonのdevDependenciesに記載が追加されら事を確認する。
    "devDependencies": {
    	"webpack": "^5.72.0",
    	"webpack-cli": "^4.9.2"
    }
    ※webpackのバージョンは、このメモを書くにあたり新たに導入して時点のもの
    ※グローバルインストールについて
    各プロジェクトが特定のバージョンが特定のwebpackバージョンで問題ない場合はグローバルインストールした方が良い。
    今回は当該PCの中に特定バージヨンのWebpackに依存するに開発環境が依存する物がある事から、ローカルインストールしている。
  • インストールが完了すると、インストールされたパッケージは node_modules というフォルダに保存され、package-lock.json という npm の設定ファイルが生成さる。
  • Webpackのバージョン確認で入ったか確認
    npx webpack -v
    以下の様にバージョン情報が表示される。
    webpack: 5.72.0
    webpack-cli: 4.9.2
    webpack-dev-server not installed
  • Webpackのヘルプを表示
    npx webpack -h
    一度、通しておくと便利な使い方が見えます。
  • Webpackの依存関係を表示
    npm webpack ls
    npm webpack ls -depth=n
  • HTMLのJavaScript記述最後へ
    tunein2radio.html
    tunein2player.html
    <!DOCTYPE html>
    <html lang="ja">
    <head>
    	<meta charset="UTF-8">
    </head>
    <body>
      <p>コンテンツhtml記述</p>
    
    	<script src=".js/~.js"></script>
    </html>
    ※バンドル後は「type"=module"」では無いので、読み込みタイミングを考慮して念の為にhtmlの最後に記述。
  • webpack.config.js
    webpack.config.tunein2radio.js
    webpack.config.tunein2player.js
    ビルドする設定ファイルはhtml毎に作成している。この理由は
    ・同じプロジェクト内のhtml毎に管理しやすい。
    ・一つのconfigに詰め込むと複雑になってしまう。
    ・ビルドがhtml毎に個別にできる。 等

    webpack.config.tunein2radio.js
    const path = require('path');
    const webpack = require('webpack');
    const pjson = require('./package.json');
    const config = {
        entry: path.resolve(__dirname, "./src/js/main.tunein2radio.js"),
        output: {
            path: path.resolve(__dirname, 'dist/js'),
            filename: 'tunein2radio.js'
        },
        plugins: [
            new webpack.BannerPlugin({
                banner: `${pjson.name} v${pjson.version} | ${pjson.author} | license: ${pjson.license}`
            })
        ]
    };
    module.exports = (env, argv) => {
        if (argv.mode === 'development') {
            config.mode = 'development';
            config.devtool = 'source-map';
        }
        if (argv.mode === 'production') {
            config.mode = 'production';
        }
        return config;
    };

    webpack.config.tunein2player.js
    cconst path = require('path');
    const webpack = require('webpack');
    const pjson = require('./package.json');
    const config = {
        entry: path.resolve(__dirname, "./src/js/main.tunein2player.js"),
        output: {
            path: path.resolve(__dirname, 'dist/js'),
            filename: 'tunein2player.js'
        },
        plugins: [
            new webpack.BannerPlugin({
                banner: `${pjson.name} v${pjson.version} | ${pjson.author} | license: ${pjson.license}`
            })
        ]
    };
    module.exports = (env, argv) => {
        if (argv.mode === 'development') {
            config.mode = 'development';
            config.devtool = 'source-map';
        }
        if (argv.mode === 'production') {
            config.mode = 'production';
        }
        return config;
    };
    単発entryポイントにてoutputを単純指定にしたかったので設定ファイルを別にした。
  • 機能毎のビルドスクリプト
    npm run t2radio_dev
    npm run t2radio
    npm run t2player_dev
    npm run t2player
    project.jsonにビルド用のscriptを追加
    "scripts": {
      "t2radio_dev": "webpack --mode development -c ./webpack.config.tunein2rado.js",
      "t2radio": "webpack -c ./webpack.config.tunein2rado.js",
      "t2player_dev": "webpack --mode development -c ./webpack.config.tunein2player.js",
      "t2player": "webpack -c ./webpack.config.tunein2player.js"
    },
    							
  • エントリーポイント
    ~.src.js
    モジュールの記述方法はJavaScriptのモジュールを参照。
  • モジュール
    modue1.js
    const module1 = () => {
        console.log("hello");
    }
    module.exports = module1
    モジュールの詳細はここを参照。
  • private開発の設定
    package.jsonをエディッタで開いて編集
    Webpack導入時に作成されたpackage.json
    {
      "name": "myproject",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "keywords": [],
      "author": "",
      "license": "ISC"
    }
    上記に下記の変更を加える。
    npmパッケージを公開しないので
     "description": "",   //削除可能 (※注意)
      //"main": "index.js", //削除 (※注意)
      "private": true,  //追加(※注意)
    ・descriptionは不要
    ・mainは削除
    ・pripeteは必須
    複数html+JSのビルドを個々に行いたいので
      "scripts": {
      	//radio部分のビルド
        "t2radio_dev": "webpack --mode development -c ./webpack.config.tunein2rado.js",
        "t2radio": "webpack -c ./webpack.config.tunein2rado.js",
      	//埋め込みPlayer部分のビルド
        "t2player_dev": "webpack --mode development -c ./webpack.config.tunein2player.js",
        "t2player": "webpack -c ./webpack.config.tunein2player.js"
      },
  • バンドル実行

    開発モードなら

    npm run t2radio_dev
    または
    npx webpack --mode development -c ./webpack.config.tunein2rado.js
    または
    node ./node_modules/.bin/webpack --mode development -c ./webpack.config.tunein2rado.js
    
    npm run t2player_dev
    または
    npx webpack --mode development -c ./webpack.config.tunein2playter.js
    または>
    node ./node_modules/.bin/webpack --mode development -c ./webpack.config.tunein2player.js
    								

    本番モードなら

    npm run t2radio_dev
    または
    npx webpack -c ./webpack.config.tunein2rado.js
    または
    node ./node_modules/.bin/webpack -c ./webpack.config.tunein2rado.js
    
    npm run t2player_dev
    または
    npx webpack -c ./webpack.config.tunein2playter.js
    または>
    node ./node_modules/.bin/webpack -c ./webpack.config.tunein2player.js
    								

3. CSSバンドル環境

CSSをバンドルする環境を整える手順

CSSを機能別にファイル分割してメンテナンス性を高めました。その結果としてCSSファイルが細分化してしまい、ページを開く時の読み込みリクエスト数が増えてしまいました。そこでCSSファイルもWebpackでバンドルする事にしました。
なおJavaScriptファイル中にまとめずに、CSSファイルはJavaScriptとは別にバンドルしています。その理由は
・ローディング時のCSS反映を遅延をさせたくない。
・落書き人はCSSとJavaScriptは別物と捉えている。

下記の手順でローダーやプラグインをインストールして、configを記述しました。
各ローダーやプラグインのGitHUBを参照すると、バンドルの大凡な手順が理解できますのでリンクを張っときます。

  • 手順
    コマンド
    要約
  • css-loaderのインストール
    npm i -D css-loader
    https://github.com/webpack-contrib/css-loader
    JSファイル内にCSSを文字列として取り込むローダー。
  • sass sass-loaderのインストール
    npm i -D sass sass-loader
    sass
    This package is a distribution of Dart Sass, compiled to pure JavaScript with no native code or external dependencies. It provides a command-line sass executable and a Node.js API.
    sass-loader
    Loads a Sass/SCSS file and compiles it to CSS.
  • postcss postcss-loader autoprefixer cssnanoをインストール
    npm i -D postcss postcss-loader autoprefixer cssnano
    CSSの解析で、変数とミックスインをサポートし、将来のCSS構文、インライン画像などをトランスパイルする。
    PostCSS
    postCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.
    PostCSS is used by industry leaders including Wikipedia, Twitter, Alibaba, and JetBrains. The Autoprefixer PostCSS plugin is one of the most popular CSS processors.
    postcss-loader
    Loader to process CSS with PostCSS.
    autoprefixer
    PostCSS plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use. It is recommended by Google and used in Twitter and Alibaba.
    cssnano
    cssnano is a modern, modular compression tool written on top of the PostCSS ecosystem, which allows us to use a lot of powerful features in order to compact CSS appropriately.
  • mini-css-extract-plugin webpack-remove-empty-scriptsをインストール
    npm i -D mini-css-extract-plugin webpack-remove-empty-scripts
    npm i -D webpack-fix-style-only-entries
    mini-css-extract-plugin
    This plugin extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.
    ebpack-remove-empty-scripts
    The plugin remove empty scripts generated by usage only a style (css/scss/sass/less/stylus) without a js script in entry.
    webpack-fix-style-only-entries
    This is a small plugin developed to solve the problem of having a style only entry (css/sass/less/stylus) generating an extra js file.
  • webpack.config.jsを名前を変えて準備
    webpack.config.tunein2.css.jsのconfig
    //webpack.config.tunein2radio.css.js
    const path = require('path');
    const webpack = require('webpack');
    const pjson = require('./package.json');
    
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    const FixStyleOnlyEntriesPlugin = require("webpack-fix-style-only-entries");
    const sass = require("sass");
    const cssnano = require("cssnano");
    const autoprefixer = require("autoprefixer");
    
    const MODE = process.env.MODE || process.env.NODE_ENV || "development";
    const ENABLED_SOURCEMAP = MODE === "development";
    
    const TARGET_DIR = path.resolve(__dirname, './src/css');
    
    module.exports = {
        mode: MODE,
        target: ["web", "es5"],
        entry: {
          radio: `${TARGET_DIR}/main.tunein2radio.css`,
          player: `${TARGET_DIR}/main.tunein2player.css`
        },
        output: {
            path: path.resolve(__dirname, 'dist/css'),
            filename: 'dummuy[name].js'
        },  module: {
        rules: [
          {
            test: /\.css|\.c(a|c)ss/,
            use: [
              MiniCssExtractPlugin.loader,
              {
                loader: "css-loader",
                options: {
                  // url() を require() に変換しない
                  url: false,
                  // development の時だけ source map を出力する
                  sourceMap: MODE === "development",
                  // css-loader の前に loader を 2つ (postcss, sass) 実行する
                  importLoaders: 2,
                },
              },
              {
                loader: "postcss-loader",
                options: {
                  postcssOptions: {
                    plugins: [
                      [
                        cssnano,
                        {
                          // コメントを削除する
                          preset: [
                            "default",
                            { discardComments: { removeAll: true } },
                          ],
                        },
                      ],
                      [autoprefixer, { grid: true }],
                    ],
                  },
                },
              },
              {
                loader: "sass-loader",
                options: {
                  // dart-sass を使用する
                  implementation: sass,
                },
              },
            ],
          },
        ],
      },
      plugins: [
        new FixStyleOnlyEntriesPlugin(),    //不要なJSファイルを除外する
        new MiniCssExtractPlugin({          //CSSを圧縮るする
          filename: "tunein2[name].css",
        }),
      ],
    }
    上記ではエントリーとなる2つのCSSを一括してビルド&バンドルでして、それぞれ別のファイルに出力しています。
    一括してビルドする理由は、CSSのモジュールのソースコードで共有している部分があるからです。
    複数のエントリーとアウトプットに対応する記述の指定箇所は、
    ・entry:でビルドする複数の「プロパティ名:エントリーCSSファイル」を指定
    ・それが順番(rule:定義の逆順)にビルド&バンドルされる
    ・ビルド&バンドル中の「[name]」部分に処理中のentryのプロパティ名が代入される
    上記config実行用のScript
    package.jsonのscript欄に以下のコマンドを記述
    "t2css": "webpack -c ./webpack.config.tunein2.css.js",
    バンドルコマンドは「npm run t2css」
    SCSS,SASSのバンドル可能
    SCSS,SASSのトランスパイルを行っていますのでSCSSであってもバンドル可能です。
    PostCSSのバンドル可能
    PostCSSのパーサーも組み込んでありますので、新しいCSS3もバンドル可能です。

4. イメージファイルをバンドル

4.今の所、Webpackに頼らない方法でバンドルしています

自発的にBase64でHTML,CSS,JavaScriptに抱え込んでいます。
選局欄の放送局アイコンは多いですが、これは選局リストデータ(JSON)の中にBase64で抱え込んでいます。

Tips.1 package.json

後で

バンドル対象となる開発環境ディレクトリのパッケージ情報をJSON書式で記述したものです。 詳しくはNPMとpackage.jsonを概念的に理解するを参照。 s

下記は先のちゅんラヂのバンドルの為に手動で置換、または追加したものです。

  • 名前
    要約

後で

Tips.2 webpack.config.js

後で

Tips.3 主なLoaderとPlugin

後で

Tips.4 複数のEntryとOutputを一括処理

バンドル条件と手順が同じな場合

この場合は「module.exports」は一つで、複数Entry記述が順番に処理されて、複数Entryのプロパティ名がOutputで[name]に代入されるので、Outputファイル名が別になる。

 :
const TARGET_DIR = path.resolve(__dirname, './src/js');
module.exports = {
 :
    entry: {
      js1: `${TARGET_DIR}/main.js1.js`,
      js2: `${TARGET_DIR}/main.js2.js`
    },
    output: {
        path: path.resolve(__dirname, 'dist/js'),
        filename: 'main.[name].js'
    },  module: {
 :

この事例は、ちゅんラヂではCSSのビルド&バンドルで利用しています。

バンドル条件と手順が別な場合

この場合は、複数の条件&手順を別に記述する。module.exportsで別々に記述した条件&手順を順番に実行させる。

 :
const js1 = {
 :
    entry: {
    	path.resolve(__dirname, './src/js')',
    	filename: 'main.js1.js'
    },
    output: {
      path: path.join(__dirname, './dist/js'),
    	filename: 'main.js1.js',
    },
 :
};
const js2 = {
 :
    entry: {
    	path.resolve(__dirname, './src/js')',
    	filename: 'main.js2.js'
    },
    output: {
      path: path.join(__dirname, './dist/js'),
    	filename: 'main.js2.js',
    },
 :
};
module.exports = [
  js1, js2
];