Help us understand the problem. What is going on with this article?

フロントエンド知らない私のwebpack入門 その1

jQuery 1.7 あたりで知識が止まっている、最近のフロントエンド開発を知らない私が入門しています、という記事です。

なんか長くなったし、気力も持たないので、その1ということで書いています。

webpackってなあに?

npm経由で入るアセットバンドラー (asset bundler)です。Webフロントエンド開発用のビルドツールです。

SPAなどを作る時、

  • HTML・JS・CSS・画像やウェブフォントなど一連のリソースをまとめてくっつけたり、
  • くっつけられるように加工したり、
  • JSをbabelで古いブラウザ向けに変換したり、
  • TypeScriptやらをJSに変換したり、

全部面倒見てくれます。

ただwebpackに上記の機能を全て内蔵しているわけもなく、プラグインやローダーとかいった追加の機能をプロジェクトにインストールして設定することにより利用できるようになります。

最近のフロントエンド開発だと、もうほぼwebpack前提みたいな感じはあります。

一言でいえば

簡単にHTMLで説明すると、

index.html
<html>

<head>
  <!-- あああああああっ! -->
  <link rel="stylesheet" href="css/reset.css" >
  <link rel="stylesheet" href="css/common.css" >
  <link rel="stylesheet" href="css/another.css" >
  <link rel="stylesheet" href="css/other.css" >
  <link rel="stylesheet" href="css/yet_another.css" >
</head>

<body>
  <!-- ああああああああああああああああああああああっ!!!! -->
  <script src="js/jquery.js"></script>
  <script src="js/jquery-ui.js"></script>
  <script src="js/utils.js"></script>
  <script src="js/other-utils.js"></script>
  <script src="js/main.js"></script>
</body>

</html>

index.html
<html>

<head>
</head>

<body>
  <!-- スッキリ! -->
  <script src="assets/bundle.js"></script>
</body>

</html>

になる感じです。 assets/bundle.js にギュギュッと内容が詰まっていく感じですね。

Javaやってた人は依存ライブラリ抱えたwarアーカイブとか考えるとわかりやすいと思います。

webpack は npm とかと何が違うの?

node

*.js ファイルを実行するマンです。Javaでいえば、 java.exe みたいな存在。

まあ要するにJS処理系のランタイムです。

npm / yarn

yarnについては記事を書いてます。

webpackがバンドラー(資材の変換・集積屋)なら、こちらはパッケージマネージャ(依存性管理屋)です。

こいつらは、プロジェクトの依存しているnpmモジュールを package.json を読み解き、インターネットからひっぱって プロジェクトフォルダ/node_modules とかに置いてくれるツールです。

webpackなど周辺のツールは プロジェクトフォルダ/node_modules に入っている依存npmモジュールを使って動きます。

yarnは一言でいうと、npmのイケてないところを直したベターなnpmです。並列DLを可能にしたり、バージョンロックファイル yarn.lock を生成して推移的な依存パッケージのバージョンも固定管理できます。

yarn.lock
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


abbrev@1:
  version "1.0.9"
  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"

ansi-regex@^2.0.0:
  version "2.1.1"
  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"

ansi-styles@^2.2.1:
  version "2.2.1"
  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"

※ yarnをWindowsで使う場合に重すぎて実用に耐えない問題が出ることがあります。その場合はnpmを使いましょう。

npm / yarn でプロジェクトの依存資材をひっぱってきて、webpackでプロジェクトソースとともにバンドル(がっちゃんこ)という使い方が基本になります。

コマンド実行ツールとしても使える

npmもyarnも package.jsonに書いてあるnpm-scriptsというコマンドを実行できる能力があります。bashのエイリアスみたいなものです。

たとえば、下記のような記述をしていれば、

package.json
...
"scripts": {
  "start" : "./test.sh"
}
...

以下のいずれかのコマンドで ./test.sh が実行されます。

$ npm run start
$ yarn run start

また、依存パッケージがインストールしたコマンドライン用ファイルを実行することもできます。たとえば、webpackをインストールすると、

$ npm install --save webpack

$ $(npm bin)/webpack --help
$ yarn run webpack -- --help

といった感じでwebpackのCLIを叩くことができます。(CLIの実体は通常、 node_modules/.bin/webpack に存在しています。)

bower

package.json を使わない独自の bower.json を使うnpmみたいなものという感じですが、私の見聞きしている範囲だと実質的に利用者がいない状態です。オワコン。

gulp / grunt

タスクランナーです。よく知らん。

知らなくても問題ない気がする。

フロントエンドはツール、ライブラリたくさんあるなあ!(怒)

まだまだこんなもんじゃないぜ…

フロントエンド開発は開発基盤を整えるコストが高いということは知っておいたほうがいいです。(何も知らないところからやるとキツイ)

大規模プロジェクトならごく一部の人が時間をかけてやればいいだけの話なんですが、小規模だとツーリングで工数を食いまくりかねないので注意しましょう。

JavaScript = 簡単 の時代はとっくに終わっています。

webpack 1 or webpack 2

Webpackには2バージョンあります。設定に互換性はありません。

他人のwebpackの設定ファイル(webpack.config.js)を読みとく上で、どちらが前提なのかは意識しましょう。

この記事ではwebpack2を使います。

簡単なwebpackプロジェクトを例にして見てみる

webpackの原理を勉強するために、簡単なプロジェクトを用意しました。

https://github.com/knjname/intro-webpack/tree/master/foo

上記プロジェクトではindex.htmlでCSS(+画像)とJSを下記タグで読み込むための設定がされています。 (foo/webpack.config.jsにwebpackの設定が記述されています。)

foo/index.html
<html>
    <head>
    </head>

    <body>
        <script src="assets/bundle.js"></script>
        <p class="class01" style="width: 300px; height: 300px;"></p>
        <p class="class02" style="width: 300px; height: 300px;"></p>
    </body>
</html>

↓webpackでビルドしたのちに、assets/bundle.js をロードすれば全ての依存JS・CSS・画像がロードされるようになっています。

$ npm install
$ $(npm bin)/webpack
Hash: afff16f2e178407fc9e4
Version: webpack 2.2.1
Time: 420ms
                               Asset     Size  Chunks             Chunk Names
a9ffcb80f75da73048bf2dae069ef383.png  5.67 kB          [emitted]  
                           bundle.js  20.6 kB       0  [emitted]  main
   [0] ./src/module01.js 73 bytes {0} [built]
   [1] ./src/module02.js 46 bytes {0} [built]
   [2] ./~/css-loader!./src/module01.css 312 bytes {0} [built]
   [3] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
   [4] ./src/barbaz.png 82 bytes {0} [built]
   [5] ./~/style-loader/addStyles.js 7.15 kB {0} [built]
   [6] ./src/module01.css 916 bytes {0} [built]
   [7] ./src/foobar.png 7.49 kB {0} [built]
   [8] ./src/index.js 95 bytes {0} [built]

webpackはどうやってモジュールをバンドルしているか

公式トップのこの図が全てを物語っています。

image

ルートモジュール(entryと呼ばれます)から、モジュールの依存性ツリーを辿っていって、全部の依存性を明らかにして、それらを処理しているというわけです。

↓下記のプロジェクトファイルについて、

foo/src/index.js
require('./module01.js');
require('./module02.js');

console.log('index.js has been loaded.');
foo/src/module01.js
require('./module01.css');

console.log('module01.js has been loaded.');
foo/src/module01.css
.class1 {
  background-image: url("./foobar.png");
}
.class2 {
  background-image: url("./barbaz.png");
}
foo/src/module02.js
console.log('module02.js has been loaded.');

ルートモジュール(entry)が foo/src/index.js だとしたら、↓こんな感じでツリー(グラフ)を辿っていきます。

                               : <依存元記述>
foo/src/index.js               : <= { entry: 'src/index.js' } ※ foo/webpack.config.js
  -> foo/src/module01.js       : <= require("./module01.js")
    -> foo/src/module01.css    : <= require("./module01.css")
      -> foo/src/foobar.png    : <= url("./foobar.png")
      -> foo/src/barbaz.png    : <= url("./barbaz.png")
  -> foo/src/module02.js       : <= require("./module02.js")

JSについては、require やら import 、CSSは url(...) やら見てwebpackが依存ツリーをたどっていきます。

それぞれのファイル種別をどうたどるかは、下記のようなルール記述で各モジュールに委ねることができます。

foo/webpack.config.js
        rules: [{
            test: /\.css$/,
            loaders: ['style-loader', 'css-loader'],
        }, {
            test: /foobar\.png$/,
            loader: 'url-loader',
        }, {
            test: /barbaz\.png$/,
            loader: 'file-loader',
        },

依存性ツリーをたどるビルドツールはたくさんあるでしょうが、 いろんな種類のファイルを混ぜて辿れる拡張性の高い仕組みを用意しているのがwebpackのえらいところです。

そもそもJSでCSSをインポートしてるのが信じられない。なんなの?

普通、JSはJSしかrequireできないはずでは…? というのは、正しいですが、webpackの世界では(設定さえすれば)合法です。

それどころか画像をインポートすることも、別のXXXをインポートすることも設定次第で可能です。

今回の例では、 css-loader というやつを設定しており、CSSファイルをJSファイルに変換することにより、webpackが依存性を辿れるようにしています。

webpack.config.js
    module: {
        rules: [
            {
                // https://github.com/webpack-contrib/css-loader
                // 依存CSSファイルをJS化してくれるやつ
                // https://github.com/webpack-contrib/style-loader
                // JS化されたCSSを <style> タグの中に入れるやつ
                test: /\.css$/,
                loaders: ['style-loader', 'css-loader'], // 適用順序は css-loader => style-loader の順番.
            },

実際に css-loader がCSSファイルに作用すると、下記のような変換が起こります。

foo/src/module01.css_変換前
.class1 {
  background-image: url("./foobar.png");
}
.class2 {
  background-image: url("./barbaz.png");
}
foo/assets/bundle.js_変換後のmodule01.css
/* 2 */
/***/ (function(module, exports, __webpack_require__) {

exports = module.exports = __webpack_require__(3)();
// imports


// module
exports.push([module.i, "\nbody {\n    background-image: url(" + __webpack_require__(5) + ");\n}\n\np {\n    background-image: url(" + __webpack_require__(4) + ");\n}\n", ""]);

// exports


/***/ }),

CSSがJSの文字列になっているのがわかります。そして、URLの部分が __webpack_require__(N:number) になっているのがわかります。

実際は、postcssというCSSパーサを使って、CSSの外部ファイル参照部を解きほぐした上で、JSに変換しているようです。

https://github.com/webpack-contrib/css-loader/blob/master/lib/

CSSをJSでインポートした結果何が起きているの?

今のところの利用例だと、webpackにJS->CSSというモジュールの依存性があるということを伝えているに過ぎません。

次回以降で利用例をあげられればいいかなあ〜と。

画像の参照はどう処理しているの?

CSSから画像が参照されていますが、こいつらもwebpackは処理しようとします。(無視する設定を入れないかぎり)

今回は処理方式を2パターン用意しました。

  1. foobar.png: 画像をBase64URLという、CSSファイル内にそのまま文字列として埋め込める方式
  2. barbaz.png: ファイルを出力フォルダの下に ファイルのハッシュ値.png という名前にしてコピーしてくれる方式
foo/webpack.config.js
            {
                // https://github.com/webpack-contrib/url-loader
                // 依存ファイルを Base64 URL 化してくれるやつ
                test: /foobar\.png$/,
                loader: 'url-loader',
            },
            {
                // https://github.com/webpack-contrib/file-loader
                // 依存ファイルをassetsの下に ``ハッシュ.png` とかで`コピーしてくれるやつ
                test: /barbaz\.png$/,
                loader: 'file-loader',
            },

1は下記のように変換されました。

foo/assets/bundle.js
/* 7 */
/***/ (function(module, exports) {

module.exports = "data:image/png;base64,iVBORw0KGgoAA...."

2は下記のように変換されました。実質的に assets/a9ffcb80f75da73048bf2dae069ef383.png を外部参照する形になっています。

foo/assets/bundle.js
/* 4 */
/***/ (function(module, exports, __webpack_require__) {

module.exports = __webpack_require__.p + "a9ffcb80f75da73048bf2dae069ef383.png"; // __webpack_require__.p = "assets/";

/***/ }),

ということで

(やる気が続けば)次回に続きます。

(おまけ) 昔、私がSPAでやったバンドラもどきの手法について

ちなみに私が昔SPAの案件をやった時、こういうバンドル系のタスクは、下記のようにまわしていました。

  • 案件1: *.js などに書いた特別なコメントを見て再帰的に依存をたどってバンドルして単一の app.js を作成するツールを作成。
  • 案件2: アプリ起動時にルートモジュールから特別なコメントを見て再帰的に依存をたどってモジュールロードするようなJavaScriptを作成。(非同期でガンガンJSをAjaxロードしつつ、依存順にevalする。)

手作り感満載のソリューションですが、依存をたどっていくという発想はwebpackと同じですね。

knjname
Zenn: https://zenn.dev/knjname
http://knjname.hateblo.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away