Hatena::Groupmoz-addon

Ci.nsIZIGOROu

2008-05-22

MozLab の ModuleManager のソースを読む

| 13:31 |  MozLab の ModuleManager のソースを読む - Ci.nsIZIGOROu を含むブックマーク はてなブックマーク -  MozLab の ModuleManager のソースを読む - Ci.nsIZIGOROu

js ってちょっと大きめの物を作ろうと思うととっちらかる傾向があるので、MozLab で使われてる ModuleManager を使ってみたいなーと思い、ソースコード読んで見る事にしました。

ちなみに読む前提知識は mozIJSSubScriptLoader の挙動を知っている事が最低限必要です。

ModuleManager(searchPath, suffixList) コンストラクタ

/**
 * @param Array searchPath
 * @param Array suffixList
 */
function ModuleManager(searchPath, suffixList) {
    this._searchPath = [];

    var pathItem;
    if(searchPath)
        for each(pathItem in searchPath) 
            if(pathItem.match(/^\./))
                this._searchPath.push(
                    Components.stack.caller.filename.replace(/\/[^/]+$/, '/' + pathItem));
            else
                this._searchPath.push(pathItem);
    
    this._suffixList = suffixList || ['.js'];
    this._loader = Components
        .classes['@mozilla.org/moz/jssubscript-loader;1']
        .getService(Components.interfaces.mozIJSSubScriptLoader);
    this._requireCache = {};
}

ModuleManager のコンストラクタですけど、

searchPath
モジュール検索対象パス配列で渡す。省略可能。
suffixList
検索対象拡張子配列で渡す、省略時は [".js"]
searchPath

こちらは "." で始まる場合は呼び出し元のスクリプトファイルからの位置と見なされるみたいですね。

Components.stack.caller.filename が呼び出し元のスクリプトファイルパス文字列です。

あとは mozIJSSubScriptLoader を作って、_requireCache を初期化して終了。

require(type, logicalUrl) メソッド

元は prototype プロパティメソッドがまとめて定義してあるので、個別定義に書き換えてありますが内容は同じです。

/**
 * @param String type
 * @param String logicalUrl
 * @return Object
 */
ModuleManager.prototype.require = function(type, logicalUrl) {
    var directoryOfCaller = Components.stack.caller.filename.replace(/\/[^/]+$/, '');
    var realUrl = this._locate(
        logicalUrl,
        [directoryOfCaller].concat(this._searchPath),
        this._suffixList);

    if(realUrl)
        switch(type) {
        case 'class_p': // DEPRECATED
            return this._loadClassPrivateEnv(realUrl);
            break;
        case 'class':
            return this._loadClassSharedEnv(realUrl);
            break;
        case 'package':
            return this._loadPackage(realUrl);
            break;
        default:
            throw new Error('Unknown module type. (' + type + ')');
        }
    else
        throw new Error('No script with given logical URL available. (' + logicalUrl + ')');

    return null;
};

コンストラクタの searchPath を省略した際は呼び出し元スクリプト存在する場所が検索パスとして採用されるのがthis._locate()の第二引数から分かります。

_locate()メソッドは後の処理から類推すると文字列であろう事が推測出来ます。

typeに応じて呼び出すメソッドを切り替えて、その返り値をそのまま返すって事ですね。

DEPRECATED を除けば、

type method
class _loadClassSharedEnv(realUrl)
package _loadPackage(realUrl)

と言う対応になります。

_loadClassSharedEnv(realUrl) メソッド

/**
 * @param string realUrl
 * @return Function
 */
ModuleManager.prototype._loadClassSharedEnv = function(realUrl) {
    var cacheKey = ['class', realUrl];

    var classConstructor = this._requireCache[cacheKey];
    if(!classConstructor) {
        var proto = {
            module: this
        };
        classConstructor = function() {
            if(proto.constructor)
                proto.constructor.apply(this, arguments);
        };

        this._requireCache[cacheKey] = classConstructor;

        this._loader.loadSubScript(realUrl, proto);

        for(var name in proto) 
            if(name != 'inheritor' &&
               name != 'constructor')
                classConstructor.prototype[name] = proto[name];

        if(proto.inheritor)
            classConstructor.prototype = proto.inheritor();
        
    }
    return classConstructor;
};

呼び出し元は require("class", realUrl) ですね。呼び出しはキャッシュされます。

キャッシュ存在しない場合、prototype オブジェクトに代入するひな形として、proto を作り、そこの module プロパティに ModuleManager のインスタンスを代入していると。

そしてコンストラクタですが、constructor() メソッドの wrapper として実装しています。

mozIJSSubScriptLoader::_loadSubScript()で、この prototype のひな形に realUrl で定義されている内容を書き込みます。

この書き込まれた状態の proto から inheritor, constructor を除いた全てのプロパティが実際の prototype プロパティに代入されます。

最後に inheritor ですが、単なる関数呼び出しとしてオブジェクトを返すようにしてやれば、それが prototype プロパティに上書きされると言う物ですね。従って inheritor が定義してあって、constructor 以外のプロパティが定義してあったとしても、残念ながら上書きされて消されてしまいます。

実際には、

var foo;

function constructor() {
  // なんか処理
}

function getFoo() { return foo; }
function setFoo(aFoo) { foo = aFoo; }

みたいに "class" を記述するんだと思います。(多分、後で確かめる)

_loadPackage(realUrl) メソッド

ModuleManager.prototype._loadPackage = function(realUrl) {
    var cacheKey = ['package', realUrl] // BUG
    var pkg = this._requireCache[cacheKey];
    if(!pkg) {
        pkg = {
            module: this
        };
        this._requireCache[cacheKey] = pkg;

        this._loader.loadSubScript(realUrl, pkg);
    }
    return pkg;
};

require("package", realUrl) の時に呼び出されます。

まぁ、定義そのものを返すって感じですね。一つだけ例外を言えば、その定義されたオブジェクトプロパティにやはり ModuleManager のオブジェクトが入ってるって事です。また class と同様にキャッシュされます。

inject() メソッド

ModuleManger.prototype.inject = function(logicalUrl, target) {
    var directoryOfCaller = Components.stack.caller.filename.replace(/\/[^/]+$/, '');
    var realUrl = this._locate(
        logicalUrl,
        [directoryOfCaller].concat(this._searchPath),
        this._suffixList);

    if(realUrl)
        this._loader.loadSubScript(realUrl, target);
    else
        throw new Error('No script with given logical URL available. (' + logicalUrl + ')');        
}

これは mixin 用ですね。特定のオブジェクトに対して loadSubScript する wrapper ですが、相対パスが使えるのが違う所ですね。

まとめ

ModuleManager ベースの開発だと二通りの定義方法がある。

class
普段の JavaScriptclass 定義とはちょっと異なっていて、constructor と inheritor と言う新しい定義方法になってる。また定義時は loadSubScript 経由なんで prorotype とか不要
package
旧来の定義方法だが、module プロパティが突っ込まれる
inject
特定のオブジェクトダイナミックに拡張したい場合に使う

って感じかなぁ。

凄い使えそう。と言うか使おう。

piro_orpiro_or2008/05/22 13:49UxUではModule Manager自体の使い方は結局よく分からないままとりあえず流用してるという状態で、メリットとか全然生かせてないですね……
あとFirefox 3なら標準の機能であるJavaScript Code Moduleを使う方が良いような気がします。

ZIGOROuZIGOROu2008/05/22 15:29piro兄さんきた−!

> Firefox 3なら標準の機能であるJavaScript Code Moduleを使う方が良いような気がします。

今知った件><
勉強してきm(ry