Skip to content

RewriteToWebModulePattern

uupaa edited this page Jun 8, 2015 · 7 revisions

このエントリでは、レガシーなスタイルで書かれた JavaScript を、WebModule に使われている 実装パターン(WebModulePattern) で書き直す手順を説明します。


ES3スタイルで書かれたレガシーなコード

以下は ES3当時の古いスタイルで書かれた MyExample.js です。
MyExample をグローバル空間にエクスポートしています。

// Before (古いスタイルで書かれた、変更前のソースコード)
(function(win, undefined) {

function MyExample(value) {
    this._value = value || "";
}

MyExample.prototype.concat = function(a) {
    return this._value + a;
};

win.MyExample = MyExample;

})(window);
<script src="MyExample.js"></script>
<script>
new MyExample("Hello").concat("JavaScript"); // "HelloJavaScript"
</script>

スタイルをリライトし新しいコードに

先ほどのES3基準の古いスタイルのコードは、残念ながらブラウザ以外の環境で動作しません。頑張って全ての行を書き直す必要があります。スタイルを新しくしつつ、Node.js や WebWorkers でも動作するようにモジュール化していきます。

// for WebWorkers
importScripts("WebModule.js");
importScripts("MyExample.js");

new WebModule.MyExample("Hello").concat("JavaScript"); // "HelloJavaScript"
// for Node.js
require("./WebModule.js");
require("./MyExample.js");

new WebModule.MyExample("Hello").concat("JavaScript"); // "HelloJavaScript"

古い ES3 スタイルな MyExample.js に施す変更は以下の通りです。

  1. 先頭部分に "use strict" を追加します

    (function(win, undefined) {
    "use strict";
  2. undefined を除去します

    • ES5 strict mode では undefined は上書きできません
    • 引数を1つ少なく渡す事で undefined を作成するアドホックなコードは lint ツールが普及する前のカビが生えた古いやり方です。削りましょう
    (function(win) {
    "use strict";
  3. ヘッダ(宣言)とボディ(実装)が癒着しないようにコード内にコメントを追加し、ブロックを明確にします

    (function(win) {
    "use strict";
    
    // --- dependency modules ----------------------------------
    // --- define / local variables ----------------------------
    // --- class / interfaces ----------------------------------
    function MyExample(value) {
        this._value = value || "";
    }
    
    // --- implements ------------------------------------------
    MyExample.prototype.concat = function(a) {
        return this._value + a;
    };
    
    })(window);
  4. Node.js でも global オブジェクトが取得できるように (function(win) { ... })(window); の部分を書き換えます

    • コードの先頭に moduleExportermoduleClosure 関数を追加します
    • ファイルの最後に return MyExample; } を追加します
    // before
    
    (function(win) {
    
        ...
    
    })(window);
    // after
    
    (function moduleExporter(name, closure) {
    "use strict";
    
    var entity = GLOBAL["WebModule"]["exports"](name, closure);
    
    if (typeof module !== "undefined") {
        module["exports"] = entity;
    }
    return entity;
    
    })(MyExample, function moduleClosure(global) {
    "use strict";
    
            :
            :
    
    return MyExample; // return entity
    });
  5. MyExample.prototype.concat = function(a) { ... }; と直書きしている function 式を
    function 文 (function MyExample_concat(a) { ... }) の形に直します

    • function 式を function 文に直すことで、メソッドの実体(Body)とAPIの宣言部(Header)を分離できます
      • DevTools のプロファイラを使ったパフォーマンス・チューニングするためにも function 文への変換が必要になります
    • 実装(Body)を implements ブロックに記述し、宣言部分(Header)を class / interfaces ブロックに記述します
    • Object.create を伴った宣言を行い、将来 getter や setter も定義できるようにしておきます
    // before
    
    MyExample.prototype.concat = function(a) {
        return this._value + a;
    };
    // after
    
    // --- class / interfaces ----------------------------------
    MyExample.prototype = Object.create(MyExample, {
        constructor:    { value: MyExample              },
        concat:         { value: MyExample_concat,      },
    });
    
    // --- implements ------------------------------------------
    function MyExample_concat(a) {
        return this._value + a;
    }
  6. MyExample.repository を追加します

    • class / interfaces ブロックに、モジュールのリポジトリURL を追加します。
    • MyExample.repository の値は MyExample.help, Help("MyExample") 実行時に使用します
    // --- class / interfaces ----------------------------------
    function MyExample(...) {
        ...
    }
    
    MyExample.repository = "https://github.com/{GitHubユーザ名}/MyExample.js";
    MyExample.prototype = Object.create(MyExample, {
        ...
    });
  7. MyExample のコンストラクタや concatメソッドに Function Attribute を追加します

    • JavaDoc(JsDoc) スタイルは使わず Validate 形式で記述します
    • MyExample#concatMyExample.prototype.concat を省略したものです
    // --- class / interfaces ----------------------------------
    function MyExample(value) { // @arg String = ""
        this._value = value || "";
    }
    
    MyExample.repository = "https://github.com/{GitHubユーザ名}/MyExample.js";
    MyExample.prototype = Object.create(MyExample, {
        constructor:    { value: MyExample              }, // new MyExample(value:String = ""):MyExample
        concat:         { value: MyExample_concat,      }, // MyExample#concat(a:String):String
    });
    
    // --- implements -------------------------------------------
    function MyExample_concat(a) { // @arg String
                                   // @ret String
        return this._value + a;
    }
  8. $valid$type などのバリデーションコードを @dev コードブロックの内側に配置します。

    function MyExample(value) { // @arg String = ""
    //{@dev
        $valid($type(value, "String|omit"), MyExample, "value");
    //}@dev
    
        this._value = value || "";
    }
    
    // --- implements -------------------------------------------
    function MyExample_concat(a) { // @arg String
                                   // @ret String
    //{@dev
        $valud($type(a, "String"), MyExample_concat, "a");
    //}@dev
    
        return this._value + a;
    }
  9. Minifier にリネームされたくない識別子やプロパティを object.concat のドットシンタックス形式から object["concat"] のブラケット形式に書き換えます

    • Closure Compiler の ADVANCED_OPTIMIZATION MODE に対応するためには、この作業が必要です
    • モジュールの外部から参照する名前はリネームされないようにガードが必要です
    // before
    
    MyExample.prototype.concat = MyExample_concat;
    // after
    
    MyExample["repository"] = "https://github.com/{GitHubユーザ名}/MyExample.js";
    MyExample["prototype"] = Object.create(MyExample, {
        "constructor":  { "value": MyExample            },
        "concat":       { "value": MyExample_concat,    },
    });

まとめ

ここまでのコードをまとめると、こうなります。

(function moduleExporter(name, closure) {
"use strict";

var entity = GLOBAL["WebModule"]["exports"](name, closure);

if (typeof module !== "undefined") {
    module["exports"] = entity;
}
return entity;

})(MyExample, function moduleClosure(global) {
"use strict";

// --- dependency modules ----------------------------------
// --- define / local variables ----------------------------
// --- class / interfaces ----------------------------------
function MyExample(value) { // @arg String = "" - comment

//{@dev
    $valid($type(value, "String|omit"), MyExample, "value");
//}@dev

    this._value = value || "";
}

MyExample["repository"] = "https://github.com/uupaa/MyExample.js";
MyExample["prototype"] = Object.create(MyExample, {
    "constructor":  { "value": MyExample          },  // new MyExample(value:String = ""):MyExample
    "concat":       { "value": MyExample_concat   },  // MyExample#concat(a:String):String
});

// --- implements ------------------------------------------
function MyExample_concat(a) { // @arg String
                               // @ret String
//{@dev
    $valud($type(a, "String"), MyExample_concat, "a");
//}@dev

    return this._value + a;
}

return MyExample; // return entity
});