webpack で Node サーバー用のコードを bundle する

  • 36
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

そもそも webpack とは

webpack を用いると様々な依存ライブラリや自作した JavaScript のコード、CSS、画像などまでもを一つのファイルにまとめあげることができる。
そうすることで何度も HTTP リクエストを発生させなくて済むため、サーバー負荷の軽減などが期待される。
例えば、あなたの書いている JavaScript が jQueryunderscore.js を用いていて、さらに規模が大きくて複数ファイルになってたりすると

<script src="/lib/jquery.min.js"></script>
<script src="/lib/underscore.min.js"></script>
<script src="/my/app1.min.js"></script>
<script src="/my/app2.min.js"></script>

みたいな感じになるのが普通である。

しかし webpack を使えばこれら4ファイルをまとめあげることができるので

<script src="/my/app.min.js"></script>

だけで済むようになる。
しかも意図しなければライブラリで用いられる変数(この場合だと $, _ 等が該当)はグローバルに置く必要がないため、グローバル変数を汚すこともなくなる。
自作コードが複数ファイルに分かれていると1つのグローバル変数を作ることが多いが、もはやそんな必要はない。ライブラリや自作ファイルの読み込み順を意識する必要もない。

まぁこれを webpack 自体の解説記事にするつもりはないので、これくらいにしておきましょう。もっと知りたい人はぐぐってください。

概要

色々な方面で webpack でクライアント向けのコードをビルドしているチュートリアルはよく見かけるが、サーバーサイドを何とかしているサンプルはほとんどない。
その理由は単純明快で、それらのサンプルのターゲットはクライアントでの挙動だ。なのでサーバーは適当なものでいいのである。
しかし isomorphic なアプリケーションとなるとサバクラ両方で同じコードが走るので、クライアント向けだけビルドしてサーバーサイドは放置しておくのはどうか。

と思ったので作ってみました。
https://github.com/srd7/webpack-bundle-server-code

動機としては、babelをモリモリ使ったコードをそのまま動かすことはできるけど、ES5に落としたコードを回した方が速そうってのが大きいです。ただし実際に速度測ったわけではないのでどの程度効果があるのかは知らない。

クライアントサイドとサーバーサイドの違い

クライアントサイドにおいては依存ライブラリの先はすべて JavaScript のハズである。だってブラウザで動かすっていう大前提があるもん。
一方サーバーサイド(Node.js)においてはそんな前提はないので、バイナリに依存してるライブラリがあったりする。なので一つの JavaScript ファイルに落とし込むことは不可能。
そのため、ライブラリはまとめあげようとしない必要があります。

解説

さっき貼ったリンク先の webpack.prod.js について解説します。
それ以外のファイルはお遊びなのでどうでもいい。さらに server のコード部について取り出しましょう。

server: {
  entry: ["./src/server.js"],
  target: "node",
  // dependencies which is not starts with "src/" or "./"
  externals: /^(?!^(src|\.)\/)/,
  resolve: commonResolve,
  output: {
    path: path.resolve(process.cwd(), "prod"),
    filename: "server.min.js",
    libraryTarget: "commonjs2"
  },
  module: {
    loaders: [
      { test: /\.js$/, loader: require.resolve("babel-loader"), exclude: /node_modules/ }
    ]
  },
  stats: {
    colors: true
  },
  plugins: commonPlugins,
  devtool: "source-map"
}

大事なのは externalslibraryTarget です。

読み込まないファイルの指定方法 external

externals は一言で説明すると 一つにまとめ上げないファイル です。
この場合は正規表現で「先頭が"src/" で始まってないか "./" で始まってないもの」と指定しています。
ここには乗ってませんが上の方で aliassrc を指定しています。これは Node.js やったことある人なら誰もが気にしたことのある相対パス地獄を回避するために入れています。こうすれば require("src/my/file") みたいな使い方ができる。
今回は src/server.js にて expressjadeimport しているので、それを回避するようにしています。

詳しい指定の仕方は 公式ドキュメント から引用いたしますと

{
    output: { libraryTarget: "commonjs" },
    externals: [
        {
            a: false, // a is not external
            b: true, // b is external (require("b"))
            "./c": "c", // "./c" is external (require("c"))
            "./d": "var d" // "./d" is external (d)
        },
        // Every non-relative module is external
        // abc -> require("abc")
        /^[a-z\-0-9]+$/,
        function(context, request, callback) {
            // Every module prefixed with "global-" becomes external
            // "global-abc" -> abc
            if(/^global-/.test(request))
                return callback(null, "var " + request.substr(7));
            callback();
        },
        "./e" // "./e" is external (require("./e"))
    ]
}

すなわち、
1. object を使うと個別に指定
2. 正規表現を使うとざっくりと指定
3. 関数を使ってより細かく指定
といった感じ。
もちろんライブラリによっては中身はただの JavaScript だから取り込んでしまった方がいいものもあるので、それらは極力取り込むようにこだわれってもいいと思います。

読み込まなかったものをどうやって取得するか libraryTarget

externals で 読み込み回避をしたファイルをどう読み込むかを指定します。
何も書いていないとグローバル変数から取得しようとするのでエラーになります。
こちらも 公式ドキュメント に記載があり、引用させていただくと

Which format to export the library:

"var" - Export by setting a variable: var Library = xxx (default)

"this" - Export by setting a property of this: this["Library"] = xxx

"commonjs" - Export by setting a property of exports: exports["Library"] = xxx

"commonjs2" - Export by setting module.exports: module.exports = xxx

"amd" - Export to AMD (optionally named - set the name via the library option)

"umd" - Export to AMD, CommonJS2 or as property in root

Default: "var"

If output.library is not set, but output.libraryTarget is set to a value other than var, every property of the exported object is copied (Except amd, commonjs2 and umd).

要するに

  • var(default): グローバル変数から使おうとする
  • this: thisから呼び出そうとする
  • commonjs/commonjs2: require して呼び出そうとする (違いはよくわからん)
  • amd: define で呼ばれる(?) よくわからん
  • umd: factoryrequire されて define される(?) よくわからん

です。サーバーサイド用なら commonjscommonjs2 の指定でいいでしょう。クライアントサイド用で似たようなことするなら、var 指定になりますね。

こうすることでサーバーサイドについての自作ファイルを最小限にしてデプロイできる。もちろん依存ライブラリは npm に解決してもらう必要があるが。

参考