ちゃんと理解するWebpack5。2:Babel、画像の処理と複数バンドル
技術スタック Javascriptwebpack

ちゃんと理解するWebpack5。2:Babel、画像の処理と複数バンドル

2021.06.06 2021.06.06

こんにちはjunです。前回の記事ちゃんと理解するWebpack5。1:webpack基礎とSass・jsのバンドルの続きの記事です。前回はjsファイルのバンドル、scssのコンパイルを行いました。今回はそこから

  • 画像パスの解決
  • babelを用いたトランスコンパイル
  • 複数のバンドルファイルを出力

以上を解説したいと思います。これらができればひとまず意図通りのwebコンテンツが作れるようになります。コードは前回のものから発展させて使用します。それではまず画像パスの解決から行っていきます。

webpackで画像を取り扱う

画像が必要なシチュエーションを準備

sassでは背景画像などで画像のパスが必要となることがあります。/srcimgsディレクトリを作成します。

.
├── dist
│   ├── bundle.js
│   ├── index.html
│   └── style.css
├── package-lock.json
├── package.json
├── src
│   ├── imgs    // new!
│   ├── js
│   │   ├── functions.js
│   │   └── main.js
│   └── sass
│       ├── compnent.scss
│       ├── style.scss
│       └── variable.scss
├── node_modules
└── webpack.config.js

imgディレクトリにに画像を配置していきます。適当な画像sample.jpgをおいておき、sassにも適当なbackground-imageを設定します。

/src/sass/component.scss
.image-box{
    width: 100px;
    height: 100px;
    background-image: url('~/imgs/sample.jpg');
}

url('../imgs/sample.jpg');のような相対パスでなくurl('~/imgs/sample.jpg');としたのは運用上のテクニックです。後でこの説明をしますので、ひとまずこんなパスにしておきます。まだ設定していませんが、試しにビルドしてみます。

dist/style.css
.image-box {
  width: 100px;
  height: 100px;
  background-image: url("~/imgs/sample.jpg"); }

component.scssで定義した通りのURLとなりましたが、もちろん不正なので404となります。

/dist/index.html
    ...
    <body>
        <main>
            <div id='app'>

            </div>
            <input type="text" value="" id="inputs">
            <button id="submmit" >追加する</button>
            <div class='box'></div>
            <div class='image-box'></div>
        </main>
    </body>
    ...
    <!-- Failed to load resource: net::ERR_FILE_NOT_FOUND /~/imgs/sample.jpg-->

相対リンクでやってもdist配下ににimgsディレクトリと画像そのものがないので、404となります。そのためwebpackを用いて/src/imgsと配下の画像をdistに移動し、url('~/imgs/sample.jpg');のようなパスを変換させてあげる必要があります。

webpack4と5の違い

webpack4では画像の処理にurl-loaderraw-loaderfile-loaderを使用していましたが、webpack5ではすでにwebpackに搭載されているAsset Modulesで行います。ただバージョン4の書き方もまだ主流なので、今回は4と5の方法を紹介し、最終的には5の方法で実装しようと思います。

なお、目指す形としては/dist配下に/imgsというディレクトリが作られ、そこに配下の画像が移動され、sassの画像パスが正しくなっているように設定します。

webpack4の設定でやってみる

loaderをインストールとconfigの記述を変更

それでは従来の方法での説明をまずは行います。最初に必要なloaderをインストールします。

npm i -D url-loader file-loader

この2つを用いてパスの解決とファイルの移動を行うことができます。そしてwebpack.config.jsを以下のように変更します。

webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path'); // 追加
module.exports = {
    //バンドル対象のファイル
    entry: ['./src/js/main.js', './src/sass/style.scss'],
  
    mode:"development",
    
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/,
                exclude: /node_modules/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: { url: true } // trueに変更
                    },
                    'sass-loader',
                ]
            },
            {   //追加
                test: /\.(png|jpg|gif)$/i,
                use: [
                  {
                    loader: 'url-loader',
                    options: {
                      limit: 8192,
                      name:'./imgs/[name].[ext]'
                    }
                  },
                ],
               type: 'javascript/auto' // 大切
            },
        ],
    },
    resolve:{ //追加
        alias: {
          '~': path.resolve(__dirname, 'src')
        }
    },
    // ファイルの出力設定
    output: {
        //  出力ファイルのディレクトリ名
        path: `${__dirname}/dist`,
        // 出力ファイル名
        filename: "bundle.js"
    },
    plugins: [
        new MiniCssExtractPlugin({
        filename: 'style.css'
        })
    ]
};

それでは細かい説明をします。

file-loaderの設定

file-loaderではjpg,png,gifの拡張子を見つけた時の処理を記述します。

webpack.config.js
{
    test: /\.(png|jpg|gif)$/i,
    use: [
        {
        loader: 'url-loader',
        options: {
            limit: 8192,
            name:'./imgs/[name].[ext]'
        }
        },
    ],
    type: 'javascript/auto' //大切
},

loaderにはurl-loaderを指定してファイルをURIに変換できるようにします。

distでは画像はimgsディレクトリ配下に格納させますので、name:'./imgs/[name].[ext]'としておきます。sass,jsの画像ファイルの参照もこのディレクトリ構成にあったパスに変更してくれます。

webpack5で従来の方法を使う場合、type: 'javascript/auto'を忘れないようにしてください。公式ドキュメントの記述はこちら。 webpack5にはAsset Moduleというものがすでにあり、それがfile-loader達と似たような働きを行います。もし先ほどの記述がないと二重で処理が入るなどがしておかしくなります。

エイリアスの設定

webpack.config.js
{
    resolve:{ //追加
        alias: {
          '~': path.resolve(__dirname, 'src')
        }
    },
},

resolveではwebpackがデフォルトで持っている、名前解決に新しい設定を追加したり、変更することができます。ここでは先ほど~/img/test.pngみたいな書き方をしたパスを正しいパスに変換する処理を書いています。

~はエイリアスとして設定しました。'~': path.resolve(__dirname, 'src')とすることで、~を見つけたらpath.resolve(__dirname, 'src')というに変換するんだなとwebpackが読み取ります。ちなみにpath.resolve(__dirname, 'src')というのはこのファイル(今回のwebpack.config.js)が置かれているOS上のパスのことです。macのあるディレクトリで構築していると以下の様に変換されます。

/Users/jun/Desktop/my_apps/webpack_practice/src

もう少しざっくり説明するとこのsrcディレクトリを指しています。

.
├── dist
│   ├── bundle.js
│   ├── index.html
│   └── style.css
├── package-lock.json
├── package.json
├── src            //←ここ!! = path.resolve(__dirname, 'src')
│   ├── imgs 
│   ├── js
│   │   ├── functions.js
│   │   └── main.js
│   └── sass
│       ├── compnent.scss
│       ├── style.scss
│       └── variable.scss
├── node_modules
└── webpack.config.js

つまり、sassで~/imgs/test.pngとすれば~を自動に変換してsrc配下のimgsの画像にバインドされます。このように特定ディレクトリやそのパスを別名で呼ぶことをエイリアス(alias)といいます。

ではなぜエイリアスで呼ぶことがいいのでしょうか?それは階層が深くなるほど、相対パスの指定では苦労するからです。他にも相対パスで指定すると、imgsディレクトリの位置を変えたいとなった時に全ての相対パスを変更する必要があります。srcディレクトリをエイリアスにすることで、相対パスでしていなくても良くなります。画像を使いたい場合は

background-image('~/imgs/test.png')
import img from '~/imgs/test.png';
console.log(img);

と指定すればよくなります。ディレクトリ構成の変更にも柔軟に対応できます。

エイリアスは干渉しなければいろいろ設定できます。今回はsrc直下ですが、src/imgsを指す画像用エイリアスなんかも作成できます。プロジェクトに合わせて設定しましょう。

これで画像ファイルが利用できる様になります。次はwebpack5の方法でやってみましょう。

webpack5でのやり方

webpack5の方法では結構簡単になりました。webpack4でインストールしたraw-loader,url-loader,file-loaderは必要ありません。

webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
module.exports = {
    //バンドル対象のファイル
    entry: ['./src/js/main.js', './src/sass/style.scss'],
  
    mode:"development",
    
    module: {
        rules: [
            {
                test: /\.(sa|sc|c)ss$/,
                exclude: /node_modules/,
                use: [
                    MiniCssExtractPlugin.loader,
                    {
                        loader: 'css-loader',
                        options: { url: true }
                    },
                    'sass-loader',
                ]
            },
            {
                test: /\.(png|jpg|gif)$/i,
                // ここから変更。useがなくなり。typeが変更されている。
                generator: {
                    filename: 'imgs/[name][ext][query]'
                },
                type: 'asset/resource'
            },
        ],
    },
    resolve:{
        alias: {
          '~': path.resolve(__dirname, 'src')
        }
    },
    // ファイルの出力設定
    output: {
        //  出力ファイルのディレクトリ名
        path: `${__dirname}/dist`,
        // 出力ファイル名
        filename: "bundle.js"
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: 'style.css'
        })
    ]
};

変わった箇所はpng,jpg,gifのrulesの設定です。4の設定ではloaderを使用するためにuseで設定しました。しかし5ではtype: 'asset/resource'を使用すること4で実装していた動きを実装できます。

エイリアスなどの設定は変わりません。webpackにおける画像ファイルの設定以上となります。

babelを用いたトランスコンパイル

これらの設定があればひとまずsassとjsを用いた作成ができそうです。ですがwebエンジニアを悩ませるこれの対策をしなくてはいけません。

Interner Explorer(以後IE)です。特にJSが影響を受けます。JSにはES5、ES6という2種類の記述方法があります。今回はその違いの説明は省きますが、ES6はES5より効率的な書き方ができます。しかしIEはES5の書き方しか受け付けず、ES6の書き方は構文エラーを起こして実行できないというクソ仕様です。

そのためES6のJSを使用するにはES5の記述に変換する必要があります。その変換をしてくれるのがBabelです。webpackにはbabel-loaderというものがあるので、それを利用してバンドルと同時に変換(トランスコンパイル)を行いましょう。

試しにIEでは使用できないアロー関数と定数宣言を書いておきます。

main.js
import $ from 'jquery';
import funcs from './functions';

$('#submmit').on('click',()=>{
    return funcs.addNewText('#app','#inputs');
})

const message = "use in IE";
()=>{
    console.log(message)
}

ちなみにbundle.jsは以下の通りに書かれていました。ES6の書き方がで出力されています。

bundle.js
//...
// const message = \"use in IE\";\n()=>{\n    console.log(message)
//...

モジュールをインストールし、JSのruleを設定

まずはloaderをインストールします。

npm install -D babel-loader @babel/core @babel/preset-env

そしてドキュメントのままですがJSファイルに対してのruleを追加し、babel-loaderを適用させる様にします。

webpack.comfig.js
// ...
module: {
    rules: [
        // 追加
        {
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                }
            }
        },
        {
            test: /\.(sa|sc|c)ss$/,
            exclude: /node_modules/,
            use: [
                MiniCssExtractPlugin.loader,
                {
                    loader: 'css-loader',
                    options: { url: true }
                },
                'sass-loader',
            ]
        },
        // ...
    ],
},
// ...

これでbabelが有効になり、トランスコンパイルされます。実際に動かしてみてbundle.jsをみてみると

bundle.js
var message = \"use in IE\";\n\n(function () {\n  console.log(message);\n})

このようにES5の書き方に直してくれました。

特定のビルドだけでトランスコンパイルしたい時

ちなみに今回は数行のコードなので十分ですが、本番では大量のファイルと記述を変換するので時間がかかったりメモリを食います。npm run watchでもそこそこメモリを食う様になります。対策としては環境変数を用いて本番ビルドの時だけトランスコンパイルさせる様にします。簡単な例としてまずpackge.jsonでnode.jsの変数をコマンド上で定義します。

package.json
  "scripts": {
    "build": "es5=true npx webpack-cli build",
    "watch": "npx webpack-cli watch",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

今回の場合、npm run buildした時にes5=trueという変数が定義されます。そしてwebpack.config.jsのrulesをこんな風に変更してみます。

webpack.comfig.js
// ...
// rulesを外で定義しておく。
let rules = [
    {
        test: /\.(sa|sc|c)ss$/,
        exclude: /node_modules/,
        use: [
            MiniCssExtractPlugin.loader,
            {
                loader: 'css-loader',
                options: { url: true }
            },
            'sass-loader',
        ]
    },
    {
        test: /\.(png|jpg|gif)$/i,
        generator: {
            filename: 'imgs/[name][ext][query]'
        },
        type: 'asset/resource'
    }
]

// es5がtrueならばバベルを適用
if(process.env.es5){
    rules.push(
        {
            test: /\.m?js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
        }
    )
}

module.exports = {
    //バンドル対象のファイル
    entry: ['./src/js/main.js', './src/sass/style.scss'],
  
    mode:"development",
    
    module: {
        rules: rules
    },

    // ...
}

rulesmodule.exportsの外に出しておいて、process.env.es5の値によってrulesを変更できる様にします。これでrulesの分岐ができました。buildの時だけBabelが使用され、watchの時はBabelが無しになります。この辺はプロジェクトごとに好きに設定してみてください。

複数のバンドルファイルを出力

最後に複数のバンドルファイルを出力する方法を解説します。今は参照されているアセットファイルを全てbundle.js、bundle.cssにしています。しかしプロジェクトによっては

  • 一般ユーザーが閲覧するページのjs/css
  • 管理画面など特定のユーザーのみが使用するページのjs/css

など複数パターンのファイルを出力したい時があります。例えば私はよく管理画面のUIはbootstrapとvueを使って構築してしまいます。そして一般画面はせいぜいjqueryを使用して200行にも満たないこともあります。管理画面はbootstrap合わせていろんなライブラリを使うため本番ビルドしてもかなりファイル容量を食います。一方、一般画面はそれほど大きくなりません。そんな時に全て一つのbundle.js/cssにまとめては一般画面に重いファイルを配ってしまいますし、場合によっては管理画面の構築コードが漏れてしまうのでよろしくありません。

作成目標とsrcを作成

このような状況もよくあるので複数のバンドルファイルを出力できる様にしましょう。上記の様な状況として管理画面のadmin.js,admin.cssmain.js,main.cssが必要になったとしましょう。

.
├── dist
│   ├── admin.css   // 作成目標
│   ├── admin.js    // 作成目標
│   ├── imgs
│   │   └── test.png
│   ├── index.html
│   ├── main.css    // 作成目標
│   └── main.js     // 作成目標
├── package-lock.json
├── package.json
├── node_modules
├── src
│   ├── imgs
│   │   ├── sample.png
│   │   └── test.png
│   ├── js
│   │   ├── admin.js    // 作成
│   │   ├── functions.js
│   │   └── main.js
│   └── sass
│       ├── admin.scss  // 作成
│       ├── compnent.scss
│       ├── style.scss
│       ├── utility
│       └── variable.scss
└── webpack.config.js

srcにエントリーとなるadmin.js,admin.scssを作成します。中身は適当にadminのみで使われている記述にしてみてください。ここでは省きます。

webpack.config.jsを設定

そしてwebpack.config.jsentry,output,pluginsを以下の様に変更します。

webpack.config.js
module.exports = {
    entry:{
        main:['./src/js/main.js','./src/sass/style.scss'],
        admin:['./src/js/admin.js','./src/sass/admin.scss']
    },

    // 省略

    output: {
        //  出力ファイルのディレクトリ名
        path: `${__dirname}/dist`,
        // 出力ファイル名
        filename: "[name].js"
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css'
        })
    ]
};

注目して欲しいのはentryです。

// 変更前
entry: ['./src/js/main.js', './src/sass/style.scss'],

// 変更後
entry:{
    main:['./src/js/main.js','./src/sass/style.scss'],
    admin:['./src/js/admin.js','./src/sass/admin.scss']
},

今まではそれぞれのファイルを直接配列で指定してましたが、変更後ではオブジェクトにしています。オブジェクトのキーは[name]として利用できます。例えば、outputでこの様に利用します。

output: {
    //  出力ファイルのディレクトリ名
    path: `${__dirname}/dist`,
    // 出力ファイル名
    filename: "[name].js"
},
plugins: [
    new MiniCssExtractPlugin({
        filename: '[name].css'
    })
]

変更前は全てbundle.js,style.cssと一定の名前でしたが、こうするとオブジェクトのキー名に応じて、main.jsadmin.jsなど作成されます。実際にビルドをしてみると、main.jsadmin.jsmain.cssadmin.cssが作成されました。

以上!

以上が今回の内容です。画像・Babelそして複数ファイルパターンができればもうプロジェクトで十分利用可能です。次回はhtmlをsrc配下で利用できるようにします。src配下のみで作業してコマンド打って完成したファイルがdistに出せる様にします。そしてまとめとしてこれらの構成とEJSを使用したプロトタイプページの作成をしてみます。

Copyright © 2021 jun. All rights reserved.