この記事にて、Browserifyを使う際に内部でどのような処理が行われてるかの理解を深め、transformモジュールやpluginの適切な利用、またそれらを自作する時の助けになればと思います。
対象は、Browserify、もしくは普段webpack使いだがBrowserifyも使ってみたい、もしくは既存のモジュールの改造、修正、いっそのこと自分で機能拡張をしたい、という方たちです。
また、理解の助けとなるよう、Browserifyのパイプライン処理に処理を絞った学習用のサンプルも用意しています(解説は後述)。
browserify-core-learn-sample
Streamについて
内部の挙動を知るうえで中心となるStreamについてをまず解説します。
Streamはデータの流れを作成します、一連の処理を逐次実行する為の骨組みと考えても良いでしょう、BrowserifyでのStreamの一連の流れは下記のようになっています。
ファイルの読み込み → transformモジュール適用 → パース → パッケージの収集 → バンドル → ファイル出力
BrowserifyではこういったStreamの連なりをパイプラインと呼んでいます、またこの記事でも今後パイプラインと呼ぶようにします。
Streamのパッケージ群
パイプラインの各処理には、それぞれパッケージが用意されています、BrowserifyでのStreamの根幹となるパッケージはreadable-streamです 1。
各Streamに識別用のラベルを付けてパイプラインを作成するlabeled-stream-splicer、ソースの読み込み・パッケージ収集をするmodule-deps、Streamヘルパパッケージのthrough2等がこのパッケージを継承しています。
labeled-stream-splicerについて
プラグインでパイプラインに手を加える事があるので、もう少し掘り下げます、パイプラインは、以下のようにして作成します。
var splicer = require('labeled-stream-splicer');
var through = require('through2');
var pipeline = splicer.obj([
'label1', [
through.obj(
function transform(row, enc, next) {
... 1 ...
this.push(row); next();
},
function flush(done) {
... 4 ...
done();
}),
through.obj(
function transform(row, enc, next) {
... 2 ...
this.push(row); next();
},
function flush(done) {
... 5 ...
done();
}),
],
'label2', [
through.obj(
function transform(row, enc, next) {
... 3 ...
this.push(row); next();
},
function flush(done) {
... 6 ...
done();
}),
],
]);
through2を使っていますが、readable-streamを継承したクラスインスタンスの登録も出来ます、例えばmodule-deps等がそうです。
readable-streamは、各データを一つ一つ処理するtransform関数、transform後の全データに対して処理をするflush関数に分かれています、番号は実行される順番です。
10/9追記
ここで、flush処理中に新たなデータを追加してみます、すると、以降のreadable-streamに新たなtransform処理が発生します。
var stream;
var pipeline = splicer.obj([
'label', [
stream = through.obj(
function transform(row, enc, next) {
... 1 ...
this.push(row); next();
},
function flush(done) {
... 3 ...
stream.push(新データ); // 新データを追加
done();
}),
through.obj(
function transform(row, enc, next) {
... 2 ...
... 4 ... // ※新たなtransform処理が発生
this.push(row); next();
},
function flush(done) {
... 5 ...
done();
}),
],
]);
既存のパイプラインが実行中でも、transform処理の漏れを気にする事なくデータの追加を行えます、また、途中で追加する事により以前のtransform処理をスキップ出来るという副次的な効果があったりと、結構柔軟な仕組みになっています。
追記ここまで
このパイプラインを追加・削除・置き換えして機能の追加、既存処理の改造等を行います、その際はプラグインとして提供した方が良いでしょう、ロジック上で手を加えるとコードの複雑さの原因になります、なお、transformモジュールからは基本的に手を加える事は出来ません。
transformモジュール、プラグインについて
この二つは、どちらもBrowserifyの機能を拡張するパッケージですが、適用範囲が変わります。
transformモジュール
transformモジュールは、主に読み込んだソースファイルに対しての何がしかの処理を担当します、AltJSのトランスパイル等が代表的な処理になると思います。
また、AltJSもTypeScript、Babel等複数ありますが、これらを干渉しないようにする為には、渡されたソースファイルの拡張子が対象かどうかを判定する手がよく取られます。
例えば、下記のようなAltJS対応があったとします。
browserify({ entries: ['src/index1.js', src/index2.ts] })
// 内部でtransformモジュールを登録している
.plugin('tsify', {
target: 'es5',
})
.transform('babelify');
tsifyでは、内部で拡張子がTypeScript用かを判定し、それ以外はスルーします、babelifyではTypeScript用をスルーします。
上記はお互いに干渉しないようにしていますが、下記のように連携する事も出来ます。
var babelify = require('babelify');
browserify({ entries: ['src/index1.js', src/index2.ts] })
// 内部でtransformモジュールを登録している
.plugin('tsify', {
target: 'es6',
})
.transform(babelify.configure({
extensions: ['.js', '.ts'],
}));
こうすると、tsifyでTypeScriptの.tsをES6へとトランスパイル後、次のbabelifyで.jsとES6の.tsがトランスパイルされます。
プラグイン
transformモジュールに対し、プラグインの有効範囲は広いです、Browserify実行前に呼び出される関数ってだけですから、外からBrowserifyに対して色んな事が出来ます、極端に言えば、パイプラインにtransformモジュール相当の処理を追加する事も出来ます。
ただし、何でも出来るからといって色々パイプラインの付け替えをするのはやらない方が良いでしょう、既存のラベル変更なんてしたら他のプラグインが動かなくなる可能性が出てきます、相性問題を避ける為にも既存のパイプラインに追加するか、event処理の追加程度に留めた方が良いかと思います。
Browserifyの挙動
ようやっとBrowserifyの解説です、Browserifyの内部ではパイプラインが動いています、大ざっぱな流れは以下のようになっています。
- (recordラベル)引数のentries、add関数で渡された、エントリーポイントとなるファイル名がmodule-depsに渡される
- (depsラベル)module-deps内の処理
- ファイルを読み込み、transformモジュール処理にかけられる、ここで例えばtsifyやbabelify等によりAltJSがJavaScriptへとトランスパイルされる。
- JavaScriptソースをパースし、他に利用しているパッケージ一覧を取得する、一覧の取得にはbrowser-resolveを利用している。
- (packラベルまで)構文チェック、ソースの体裁を整える
- (packラベル)browser-packでバンドル化
各パッケージのフロントエンドの役割が強いので特に書く事はないのですが、一つだけ僕が少し迷った処理について解説します。
エントリーポイントとなるソースファイルですが、これらはentriesかadd関数にて登録されます、しかし、内部のソースを追っても、パイプラインにwriteするだけで終わっています。
Browserify.prototype.require = function (file, opts) {
...
self.pipeline.write(row);
return self;
}
最初、これでどこでファイルを読み込んでるのか不明だったのですが、答えはmodule-depsの_transform関数にありました。
Deps.prototype._transform = function (row, enc, next) {
if (transformモジュールの場合) {
...
return next();
}
// ここにはファイルの時に来る
self.lookupPackage(row.file, function (err, pkg) {
...
});
}
パイプラインの処理について理解すればどうって事ないのですが、それまでは難しいものでありました。
module-depsについて
Browserifyで一番の核となるパッケージです、ソースファイル読み込み、transform適用、パッケージ収集をこなします、watchify等で高速化の為に利用しているcacheオプションも、このパッケージ用だったりします。
パッケージ紹介
以下では、自分がよく利用しているパッケージについて紹介します。
transformモジュール紹介
babelify
Babelのトランスパイラです
espowerify
Browserifyでpower-assertを使えるようにします。
プラグイン紹介
tsify
TypeScriptのトランスパイラです、プラグインとして提供されていますが、内部でtransformモジュールを登録する、という仕組みになっています。
factor-bundle
Browserifyでのバンドルファイルの分割機能を提供します、webpackのようにページ別のバンドルファイルに分けたりするのに便利です。
flexi-require
自作のプラグインですが、自分でも便利だと思うので紹介します。
bowerパッケージや、node_modules以外の自作パッケージもrequire出来るようになります、また、未使用のパッケージを含まないようにしたりも出来ます。
例えば、バンドルファイルをnode_modules配下だけのものと、アプリロジックとに分割したい時にこのパッケージを用いて
// アプリロジックのみ
browserify({ entries: 'src/index.js' })
.plugin('flexi-require', {
external: '*',
})
...;
// node_modules配下のみ
browserify()
.plugin('flexi-require', {
files: 'src/index.js',
require: '*',
})
...;
として分割する事が出来ます、その時に繰り返しですが未使用のパッケージは含みません、応用が利くので良い出来だと思っています(自画自賛)。
サンプルについて
サンプルでは、Browserifyのパイプラインとmodule-depsの簡易的な処理、transformモジュールとプラグインのサンプルを利用しています、transformモジュールとして、brainf*ckをAltJSとして使えるようにしています(brainf_ckify)、プラグインは単にソースファイルの数をカウントします(file-count)。
どちらも実用性はないです(brainf_ckifyは特に)、学習用となっています。
まとめ
いかがでしょうか、Browserifyを利用する際に少しでも手助けになれば幸いです。
-
内部で各ライブラリの分かれていますが、気にしなくても問題ないので割愛します ↩