category

webpackで共有ライブラリを使う&作るビルド設定

webpackをシンプルに導入すると、SPAのようにモノリシックなファイルにバンドルされますが、設定しだいでモジュール化も可能です。(モノリスなライブラリ作成ノウハウについては、ES2015で静的なライブラリ塊をバンドルするで解説しています)

共有ライブラリのコンシューマー設定

たとえば、ReactJSを利用するプロジェクトでは、ライブラリのコンシューマー(アプリ本体)の冒頭で、以下のようにライブラリをインポートします。

import React from "react";
import ReactDOM from "react-dom";

class SomeComponent extends React.Component {
(以下コード本体)

webpackを特に意識せずにセットアップしてビルドすると生成されるJavascriptは、このアプリに加えてreact, react-domをバンドルしたものになります。

この構成はSPAのようにこのファイルだけで動作するアプリであれば効率的ですが、複数ページ構成のアプリの場合には、毎回ライブラリを含めて再読み込みすることになります。

この例でreact, react-domをバンドルから除外するには、webpack.config.jsの設定にexternalsの定義を追加します。

var webpack = require('webpack');
module.exports = {
  entry: {
    app: './src/app.jsx',
  },
  output: {
    path: __dirname + "/build-dev",
    filename: "[name].js"
  },
  resolve: {
    extensions: ['', '.js', 'jsx']
  },
  module: {
    loaders: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react']
        }
      }
    ]
  },
    externals: {
               'react': 'React',
               'react-dom': 'ReactDOM',
           }
};

この指定でビルドした場合、react, react-domはCDNやWebサーバからスタティック配信したライブラリを利用することになります。
以下のようにHTMLから別途インクルードします(アプリのjsファイルをインクルードする前に読み込む)。

<script src="https://unpkg.com/react@15/dist/react.min.js"></script>
<script src="https://unpkg.com/react-dom@15/dist/react-dom.min.js"></script>

<script src="build-dev/app.js"></script>

共通ライブラリを作成する

上記のようにCDNからビルド済みのライブラリをインクルードできる場合、本質的には自前でビルドする必要はないのですが、ビルドが必要なパーツを組み込んで1つの共通ライブラリを作ることも可能です。

アプリとは別プロジェクトで、ライブラリだけをバンドルするプロジェクトを作ります。

モジュールのエクスポート形式にはCommonJSやAMD, UMDなどいくつかの形式がありますが、<script>タグでブラウザからインクルードする原始的な方式向けには、以下のようにwindowオブジェクトにライブラリのインターフェースオブジェクトを羅列するだけのJavascriptコードを書けば動作します。

window.ReactDOM = require('react-dom');
window.React = require("react");

webpack.config.jsにはとくに工夫は必要ありません。

var webpack = require('webpack');
module.exports = {
  entry: {
    react_bundle: './src/bundle.js',
  },
  output: {
    path: __dirname + "/build-dev",
    filename: "[name].js",
  },
  resolve: {
    extensions: ['', '.js', 'jsx']
  },
  module: {
    loaders: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react']
        }
      }
    ]
  },
};

当然ですが、このケースではできあがったライブラリを1つインクルードすれば足ります。

<script src="build-dev/react_bundle.js"></script>

<script src="build-dev/app.js"></script>

注意点

共有化した場合の一般的な注意点として、ライブラリとコンシューマーのバージョンがずれていると不整合が起こるためバージョン制御 or 固定は必要です。

また、本番環境向けにuglifyをかけると名前解決できずにエラーになる可能性があります。
上記の例では、Reactオブジェクトが実行時にundefinedエラーになります。

uglifyにはwebpack.config.jsで指定するUglifyJsPluginのほか、ビルド時のwebpackコマンドの-pオプションでも一定の変換がかかります。

ユースケース:初期化の必要な拡張ライブラリ

このように、複数のライブラリを結合しなければならないユースケースとして、ライブラリ側で何らかの初期化処理を終えておく必要がある場合があります。

たとえば、Material UIというUIコンポーネントがreact-tap-event-pluginというスマートフォンのタップイベント処理ライブラリに依存しています。

react-tap-event-pluginはReact.jsのプライベートAPIと密結合しており、実行時に初期化が必要なため、Reactと切り離して配信できません。

本来的にはやや無理のある設計でありReactのポリシーしだいで行き詰まることも考えられるため、可能であれば他の選択肢を探った方が無難ではあります。

分かりづらい多重インクルード問題

かなり奥深いケースとして、ライブラリの名前解決のトラブルによって二重にインクルードするトラブルが起こり得ます。

実例としては、Requiring react-addons-css-transition-group includes React in webpack builds that exclude reactで報告されているものがあり、簡単にいうとrequire('react')require('./react')は別のライブラリと認識されます。

この場合、require(‘./react’)の記述に沿ってアプリ側にもライブラリがバンドルされ、実行時に読み込むものと二重にインポートされるため、動作が不定となります。

状況を追っていくと、元のライブラリはrequire(‘./react’)という書き方にはなっていないため、どうやらnpmがライブラリ間の依存性解決している際にパスを書き換えているような印象です。

対策としては、以下のようにwebpack.config.jsで表記ゆれも含めて除外指定することが有効です。

    externals: {
               'react': 'React',
               './react': 'React',
               'react-dom': 'ReactDOM',
               './react-dom': 'ReactDOM',
		   }

類似の実例として、Consuming a UMD created by Webpack results in duplicate libs in outputに報告があります。

まとめ:Webサイト向けのライブラリ構成に難あり

JavascriptはCoffeeScriptなどのaltJS(alternative javascript)を経て、いま次世代Javascript規格のES2015が普及しつつあります。

ES2015は正式なECMAScriptではありますが、動作するブラウザの普及率がいつ100%になるのか見通しが立たないため、結論としてJavascript開発は既存ブラウザ向けにコンパイル(変換)&パッケージングする手順が必要です。

ブラウザ向けのツールチェインとしては、現時点でwebpackとbrowserifyが代表的ですが、SPA的に1ファイルにバンドルする想定が強く、サイト単位で共通利用するライブラリのビルドは難易度が高いように思います。

各ページ向けにバンドルした場合、単に「5ページ読むと共通ライブラリを5回ロードするので不効率だ」という点も問題はあるのですが、ひとまず適切に動作はするので致命的ではない面もあります。

本来的に困るケースは1ページに複数のコンポーネントを読むような用途です。
Javascriptはグローバルにオブジェクトをロードするので、ライブラリを重複読み込みすると不定の実行時エラーが起きます。

実際のWeb開発では、HTMLテンプレートの部品として複数のJavascriptコンポーネントを呼び出したいケースがあり、複数のページ間でコンポーネントを共有したいシナリオでは共有ライブラリの運用を手堅く決めて作る必要に迫られます。