アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Browserify の require オプションでモジュールを外部公開する

Browserify で bundle している JavaScript 内のモジュールを外部公開したくなり、その方法を検討してみた。

前提条件

検討にあたり前提条件や設計方針をまとめておく。

  • Browserify は CLI として利用
    • 最近の bundle は CLI で管理しているため
    • 設定をあわせれば Node インターフェイスでも同じことは可能なはず
  • CLI は package.json の npm-scripts から呼び出す
    • 最近の開発タスクは npm-scripts で管理しているため
    • 設定をあわせれば gulp とかでも同じことは可能なはず
  • JavaScript は ES2015 以降で実装、transplier には babelify を利用
    • TypeScript 派なら tsify でもよい
  • モジュールの外部公開とその参照を同時におこなうサンプルを作成する
    • 参照が有効であることを実証したい
  • 公開、参照は ES2015 の export/import で記述
    • CommonJS は使用しない
    • ES2015 以降を採用するだから、その範疇で実装する

require オプション

Browserify でモジュール公開機能は substack/node-browserify によると --require オプションを利用するようだ。短縮版は -r、以降はこちらで説明。このオプションの詳細は README の multiple bundles に解説されている。公式ドキュメントのサンプルには

$ browserify -r ./robot.js > static/common.js

とある。これを実行して生成された JavaScript は

{
},{}],"./robot.js":[function(require,module,exports){
}

のようになり、

var robot = require('./robot.js');
console.log(robot('boop'));

というふうに参照できる。-r で公開したモジュールの参照は指定されたパスで解決するらしい。深いパス、例えば

$ browserify -r ./src/lib/Library.js > src/assets/lib.js

のようにすると requireimport へ指定するパスは ./src/lib/Library.js になる。HTML から読み込む場合は当然ながら公開モジュールを先、参照する側を後に定義しなければならない。

<script src="lib.js"></script>
<script src="app.js"></script>

公開するモジュール名を変更する

公式ドキュメントの external requires には

$ browserify -r through -r duplexer -r ./my-file.js:my-module > bundle.js

というサンプルがある。重要なのは -r ./my-file.js:my-module の部分。ファイルのパスに続けて :modulename を指定することで、その名前による参照が可能となる。

前述のパスよりもこちらのほうが便利。モジュール名を自由に変更できるため開発環境ではファイル名を Library.js のような PascalCase にしておき、公開するときは npm 的な library という小文字 (+ ハイフンなど) にするといった運用も可能。

実践編

掲載しているサンプルは必要最小にまとめているので、詳細を知りたい場合は記事の最後にリンクしたリポジトリを参照のこと。プロジェクトを以下のように構成。

.
├── package.json
└── src/
    ├── assets/
    │   └── index.html
    └── js/
        ├── app/
        │   └── App.js
        └── lib/
            ├── Library.js
            └── Util.js

公開モジュールを含む Library.js 実装は

import Util from './Util.js';

/**
 * Outputs the "Library" to the console.
 */
function Echo() {
  console.log( 'Library' );
}

export { Util, Echo };

とする。Util.js に定義した class と自身のローカル関数を ES2015 Modules 形式で公開。これを参照する App.js

import { Util, Echo } from 'library';

// Application entry point
window.onload = () => {
  const date = Util.formatDate();
  Echo();
  console.log( '[' + date + '] Application was launched.' );

  const elm = document.querySelector( '.date' );
  if( elm ) {
    elm.textContent = date;
  }
};

となる。こちらは公開されたものをひとつのモジュールから個別に import。これらをコンパイルする npm-scripts を以下のように定義。

{
  "babel": {
    "presets": [ "es2015" ]
  },
  "browserify" : {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "build:js-lib": "browserify -r ./src/js/lib/Library.js:library -d | exorcist ./src/assets/lib.js.map > ./src/assets/lib.js",
    "build:js-app": "browserify ./src/js/app/App.js --im -d | exorcist ./src/assets/app.js.map > ./src/assets/app.js",
    "build": "npm-run-all -p build:js-lib build:js-app",
    "watch:js-lib": "watchify -r ./src/js/lib/Library.js:library -v -o \"exorcist ./src/assets/lib.js.map > ./src/assets/lib.js\" -d",
    "watch:js-app": "watchify ./src/js/app/App.js -v --im -o \"exorcist ./src/assets/app.js.map > ./src/assets/app.js\" -d",
    "watch": "npm-run-all -p watch:js-lib watch:js-app",
    "release:js-lib": "cross-env NODE_ENV=production browserify -r ./src/js/lib/Library.js:library | uglifyjs -c warnings=false -m > ./dist/lib.js",
    "release:js-app": "cross-env NODE_ENV=production browserify ./src/js/app/App.js --im | uglifyjs -c warnings=false -m > ./dist/app.js",
    "release:clean": "rimraf ./dist",
    "release:copy": "cpx \"./src/assets/**/{*.html,*.eot,*.svg,*.ttf,*.woff,package.json}\" ./dist",
    "release": "npm-run-all -s release:clean release:copy -p release:js-lib release:js-app"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-power-assert": "^1.0.0",
    "babel-register": "^6.9.0",
    "babelify": "^7.3.0",
    "browserify": "^13.0.1",
    "cross-env": "^1.0.8",
    "exorcist": "^0.4.0",
    "mocha": "^2.5.3",
    "npm-run-all": "^2.3.0",
    "rimraf": "^2.5.3",
    "uglify-js": "^2.7.0",
    "watchify": "^3.7.0"
  }
}

タスク名に :js-lib とあるものがライブラリとなる公開モジュール、:js-app はそれを参照する側になる。重要なポイントを解説してゆく。

:js-lib では -r と前述の公開モジュール名変更により library という名前にした。しかし参照する側も Browserify でコンパイルしているため、そのまま import すると npm install していないから参照エラーになる。この問題を回避するため :js-app では --im オプションを利用して解決できない参照は無視しておく。

これは Electron を試す - 開発環境の構築で試した方法。Electron の Main プロセスを Browserify でビルドする際は fspath など Node 由来のモジュールがあるため --im で参照の解決を実行時へ遅延させている。

今回も原理は同じ。参照の解決は HTML 側で

<script src="lib.js"></script>
<script src="app.js"></script>

のように読み込まれた時となる。

まとめ

Browseify といえば単一ファイルを生成するもの、という印象があった。そのため複数ファイルを出力するなら webpack を利用したほうがよいという意見を聞いたこともある。しかし

  • モジュールを公開できて
  • 複数ファイルを出力する

ぐらいの用途ならば Browserify & npm-scrpts でも十分に対応可能なことがわかった。

Browserify だけでは複数ファイルを出力できないが、個別にタスクを定義して npm-run-all で連結すれば同等の機能を実現可能。この記事の npm-scripts だと buildwatchrelease でそうしている。

最後にサンプル プロジェクト全体を公開する。

Copyright © 2009 - 2023 akabeko.me All Rights Reserved.