browserifyでロードされたモジュールをユーザースクリプトから読み取る

More than 1 year has passed since last update.

ユーザースクリプト(もしくはブラウザ拡張)を書いていると、サイト側で定義されたクラスや関数を使いたくなることがある。しかし、サイト側がbrowserifyを利用していると、クラスや関数がbrowserifyのローダーの内部に隠蔽され、通常の方法ではアクセスできないことがある。

現在のbrowserifyの実装では、Function.prototype.callを上書きすることでモジュールを読み取ることができる。この記事ではその方法を説明する。

速習browserify

browserifyの雰囲気をつかむためにとりあえず動かしてみる。

# browserifyをインストール
npm init
npm install browserify

# foo.jsとbar.jsと、それらを読み込むmain.jsを用意する
echo "module.exports = 'foo'" > foo.js
echo "module.exports = 'bar'" > bar.js
cat <<EOF > main.js
var foo = require('./foo');
var bar = require('./bar');
console.log(foo + bar);
EOF

# main.jsを起点にしてビルドする
node_modules/.bin/browserify main.js > bundle.js

実行すると次のようなコードが出力される。

bundle.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = 'bar'

},{}],2:[function(require,module,exports){
module.exports = 'foo'

},{}],3:[function(require,module,exports){
var foo = require('./foo');
var bar = require('./bar');
console.log(foo + bar);

},{"./bar":1,"./foo":2}]},{},[3]);

関数部分の圧縮前のコードはbrowser-packのprelude.jsで読むことができる。コードの詳細はbrowserify (browser-pack) はどんなコードを出力するのか? - Qiitaなどで解説されている。

それぞれのモジュールは圧縮された関数(prelude.jsではouterという関数名)の引数として渡され、グローバル変数には代入されない1

モジュールを読み取る

outer関数の最初の引数(module)の構造は次のようになっている。

{
  モジュールのID: [
    モジュールをラップした関数,
    {
      // その関数の中でrequireされるモジュールの…
      パス: ID,
      ...
    }
  ],
  ...
}

二番目の引数(cache)として渡された空オブジェクトには、関数の実行後に

{
  モジュールのID: モジュールをラップした関数を実行した後のmodule.exports,
  ...
}

という値が入る。

モジュールのIDは他のモジュールが追加されたり削除されたりするだけで変わる可能性があるので、モジュールのIDから直接モジュールを取得しているとユーザースクリプトが壊れやすくなる。moduleからモジュールのパスとIDの対応関係を取り出して、パスを手掛かりにしてcacheから必要なモジュールの内容を読み取りたい。

outer関数内のモジュールの内容をラップした関数を実行する箇所で、moduleとcacheがモジュールの内容をラップした関数.call(実体はFunction.prototype.call)の引数として渡されているので、Function.prototype.callを上書きすれば値を読み取れる23

callのレシーバーがモジュールの内容をラップした関数なのかを正確に判別する方法はないが、関数の定義がfunction(require, module, exports) {で始まることを利用すれば多くのサイトでは問題のない精度で判別できる。

まとめ

今回紹介した方法には

  • モジュールの内容をラップした関数なのかを判定する方法が雑。
  • callの引数にmoduleとcacheを渡す意味はなさそうなので、将来的には使えなくなるかもしれない。
  • Function.prototype.callという基本的な関数を上書きするため、パフォーマンス上の問題がありそう。

といった問題がある。

とはいえ、サイト側のスクリプトから必要なコードをユーザースクリプトにコピペしたり、実行時にサイト側のスクリプトをダウンロードして、文字列操作やJavaScriptのパーサーを使って必要なコードを切り出すといった方法よりはメンテナンス性が高いのではないか。

他によさそうな方法があったら編集リクエストやコメントで教えてください。

リンク


This document is licensed under CC0.


  1. browserifyに-r ./fooのようにオプションを渡すと、モジュール./fooを読み出せるrequireがグローバルに定義される。 

  2. ここで引数として渡されるrequireは、そのモジュール内でrequireされるモジュールのIDとパスの対応しか持っていないので使えない。 

  3. requireがすでに定義されている環境(Node.jsなど)では、Function.prototype.callを呼び出す箇所が実行されない。