タグ別アーカイブ: 開発

SQL::OOPなるPerlモジュールです

SQL::OOPです。Perlです。

ORマッパーとか難しくてよくわかりません。SQL::Abstractの書式が難しすぎて覚えれません。OOPスタイルのSQLジェネレータも色々あるようですが、あまり気に入ったものがなかった。

SQL::OOPはオブジェクト指向なインターフェースでSQL::Abstractライクな結果を得るものです。簡単なことは難しく、難しいことは簡単にできます。ここ1年くらいでいくつかのプロジェクトで使ってるけど、今のところ問題なさげ。

詳しくはpodを参照ください。

例1

my $select = SQL::OOP::Select->new();
$select->set(
    $select->ARG_FIELDS => '*',
    $select->ARG_FROM   => 'table',
    $select->ARG_WHERE  => sub {
        my $where = SQL::OOP::Where->new;
        return $where->and(
            $where->cmp('=', 'a', 1),
            $where->cmp('=', 'b', 1),
        )
    },
);

こうなります。改行とインデントはフェイクです。

SELECT
    *
FROM
    table
WHERE
    "a" = ? AND "b" = ?

例2

こんな使い方しないってもの含めて、結構何でもできます。

my $select = SQL::OOP::Select->new();
$select->set(
    $select->ARG_FIELDS => SQL::OOP->new(q{"ky1", "ky2", *}),
    $select->ARG_FROM   => q("tbl1", "tbl2", "tbl3"),
    $select->ARG_WHERE  => sub {
        my $where = SQL::OOP::Where->new();
        return $where->and(
            $where->cmp('>=', 'hoge1', 'hoge1'),
            $where->cmp('=', 'hoge2', 'hoge2'),
            $where->or(
                $where->cmp('=', 'hoge3', 'hoge3'),
                $where->cmp('=', 'hoge4', 'hoge4'),
                $where->between('price', 10, 20),
                $where->is_null('vprice'),
                SQL::OOP->new('a = b'),
                'a = b',
                SQL::OOP->new('c = ? ?', ['code1', 'code2']),
                $where->between('price', 10, 20),
            ),
            $where->or(
                $where->cmp('=', 'hoge3', undef),
                $where->cmp('=', 'hoge4', undef),
            ),
        )
    },
    $select->ARG_ORDERBY => SQL::OOP::Order->abstract([['hoge1', 1], ['hoge2']]),
    $select->ARG_LIMIT  => 11315,
    $select->ARG_OFFSET => 1,
);

こうなります。

SELECT
    "ky1", "ky2", *
FROM
    "tbl1", "tbl2", "tbl3"
WHERE
    "hoge1" >= ?
    AND
    "hoge2" = ?
    AND
    (
        "hoge3" = ?
        OR
        "hoge4" = ?
        OR
        "price" BETWEEN ? AND ?
        OR
        "vprice" IS NULL
        OR
        a = b
        OR
        a = b
        OR
        c = ? ?
        OR
        "price"
        BETWEEN ? AND ?
    )
ORDER BY
    "hoge1" DESC, "hoge2"
LIMIT
    11315
OFFSET
    1

ところで、setメソッドなどの引数が変態的な書式になってますが、これはタイポをコンパイルエラーにさせるのと、入力補完するためです。

jquery.jsontpl.jsというテンプレートエンジンだよ

jsonを使用したテンプレートエンジン「jquery.jsontpl.js」です。jQueryバージョン1.5以上で動作します。

特徴は、テンプレートのパースに正規表現置換やevalなどせず、jQueryの高レベルAPIのみを使用している点です。テンプレート内ではほぼ普通にjavascriptが使えますので、つまりテンプレート内で条件分岐、反復制御ができます。テンプレートは入れ子にもできます。よくある特殊制御構文とか特にありませんので、学習コストが低いかも知れません。

アルファバージョンです。数日でテキトーに作ったので、コンセプトすら正しいのか不明です。

[2011.01.29更新]

デモサイトを作りました[デモ]。あと、IE6でも動作するよう修正しました。正規表現つかっちまった。


テンプレートは下記のように使用します。

$.jsontpl.parse('./tpl.html','./data.json').appendTo(selector);

テンプレート内に下記のようなタグを記述すると変数名nameと解釈され、値に置換されます。

<var name="name"></var>

jsonデータは連想配列の配列として渡します。連想配列のキーがテンプレート変数名です。

[{"name":value}, {"name":value2}, {"name":value3}]

jQuery.jsontpl.getContextメソッドにてコンテキストオブジェクトcontextを取得することができます。コンテキストというのは要するに現在のテンプレートの範囲ということです。contextオブジェクトから、現在のコンテキストでの変数を取得したり、DOM操作をしたりできます。

// コンテキストオブジェクトを取得
var context = $.jsontpl.getContext();

// 変数取得
var name = context.getvar("name");

テンプレート内のdomにはid属性を指定してスクリプトでパース時に制御することができます。テンプレート内のid属性は、衝突を避けるためパース後に削除されます。テンプレート、つまり複製されるものなので、適用後にidが必要になるはずはないという訳です。別にclassでも構いませんが。

context.find("#flag")

変数スコープには注意が必要です。グローバル空間で衝突しないように下記のように記述するとよいです。

<script type="text/javascript">
//<![CDATA[
(function() {
    var context = $.jsontpl.getContext();
})();
//]]>
</script>

jsontplプラグインはjQuery.jsontpl名前空間を占有します。jQuery.jsontplはjQuery.subclassそのものなので、jQuery(selector)とjQuery.jsontpl(selector)はよく似ています。ただし、下記のメソッドを追加、またはオーバーライドしています。詳しくはソースを参照してください。

jQuery.jsontpl.ajax
jQuery.jsontpl.getTpl
jQuery.jsontpl.getJSON
jQuery.jsontpl.get
jQuery.jsontpl.getContext
jQuery.jsontpl.parse
jQuery.jsontpl.fn.getvar
jQuery.jsontpl.fn.parse
jQuery.jsontpl.fn.appendTo
jQuery.jsontpl.fn.prependTo
jQuery.jsontpl.fn.insertBefore
jQuery.jsontpl.fn.insertAfter

使用例

data.json

[
    {
        "name": "Yamada",
        "mail": "yamada@example.com",
        "blood": "A"
    },
    {
        "name": "Yamamoto",
        "mail": "yamamoto@example.com",
        "blood": "A"
    },
    {
        "name": "Suzuki",
        "mail": "suzuki@example.com",
        "blood": "B",
        "detail":{
            "data1":"hoge",
            "data2":"hoge",
            "data3":"hoge"
        }
    }
]

index.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
    <title>jQuery Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Script-Type" content="text/javascript" />
    <style type="text/css">
    table,th,td {
        border:1px solid #ccc;
    }
    th {
        background-color: #e0e0f0;
    }
    </style>
    <script src="./common/js/jquery.1.5.js" type="text/javascript"></script>
    <script src="./common/js/jquery.jsontpl.js" type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[
        $(document).ready(function(){
            // テンプレートとJSONをサーバーから取得
            $.jsontpl.parse('./tpl.html','./data.json').appendTo('.movie');
        });
    //]]>
    </script>
</head>

<body>
    <table class="movie">
        <tr>
            <th>名前</th>
            <th>メール</td>
            <th>血液型</td>
            <th>A型</th>
            <th>詳細情報</th>
        </tr>
    </table>
</body>
</html>

tpl.html

<tr>
    <td><var name="name"></var></td>
    <td><var name="mail"></var></td>
    <td><var name="blood"></var></td>
    <td id="flag"></td>
    <td id="subtable" class="subtable">
    </td>
</tr>
<script type="text/javascript">
//<![CDATA[
(function() {
    // コンテキストオブジェクトを取得
    var context = $.jsontpl.getContext();

    // テンプレート内で条件分岐例
    if (context.getvar("blood") == 'A') {
        context.find("#flag").html("true");
    }
    // サブテンプレートを挿入
    $.jsontpl.parse('./tpl2.html', context.getvar("detail")).appendTo('#subtable');
})();
//]]>
</script>

tpl2.html

<div class="hoge">
    data1:<var name="data1"></var>
    <br />data2:<var name="data2"></var>
    <br />data3:<var name="data3"></var>
</div>

jquery.jsontpl.js(最新版はgithubにて)

/*!
 * jquery.jsontpl - client side template engine powered by jQuery
 *
 * @requires jQuery version 1.5 or higher
 * @version 0.0.3
 */
(function ($) {

    $.jsontpl = $.subclass();

    /**
     * プラグイン開発用のデバッグコンソール
     */
    var console = {
        log : (window.console) ? window.console.log : function () {}
    };

    /**
     * デフォルトパラメータ
     */
    $.jsontpl.params = {
        classPrefix : 'jsontpl',
        __trailing__: null
    };

    /**
     * デフォルトパラメータをオーバーライド
     *
     * @param obj {object}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.setParams = function (params) {
        $.extend($.jsontpl.params, params);
        return this;
    };

    /**
     * Ajaxキャッシュ{url : data}
     */
    var cache = {};

    /**
     * キャッシュ機構を追加したajaxメソッド
     *
     * @param params {object}
     * @returns {XMLHttpRequest | empty object}
     */
    $.jsontpl.ajax = function (params) {
        if (cache[params.url]) {
            params.success(cache[params.url]);
            return {};
        }
        params.success = (function() {
            var url = params.url;
            var orgSuccess = params.success;
            return function(a) {
                cache[url] = a;
                orgSuccess(a);
            }
        })();
        return $.ajax(params);
    };

    /**
     * テンプレート取得用のAjaxラッパーメソッド
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getTpl = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (tpl) {
                ret = $.jsontpl(tpl);
            }
        });
        return ret;
    };

    /**
     * JSON取得用のAjaxラッパーメソッド
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getJSON = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            dataType: 'json',
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (json) {
                ret = json;
            }
        });
        return ret;
    };

    /**
     * テンプレートとJSONの取得用のAjaxラッパーメソッド
     *
     * @param tpl {string} テンプレートファイル名
     * @param json {string} JSONファイル名
     * @returns {jQuery.jsontpl} テンプレートとJSON
     */
    $.jsontpl.get = function (tpl, json) {
        return {
            tpl  : $.jsontpl.getTpl(tpl),
            json : $.jsontpl.getJSON(json)
        };
    };

    /**
     * テンプレート内のコンテキストを保持
     */
    var context;

    /**
     * テンプレートから現在のコンテキストを取得するAPI
     * @returns jQueryエレメント
     */
    var getContext = $.jsontpl.getContext = function () {
        return context || $('body');
    }

    /**
     * テンプレート内から現在のコンテキストでの変数を取得するAPI
     * @param name {string} 変数名
     * @returns 値
     */
    $.jsontpl.fn.getvar = function (name) {
        return this.data('jsontpl')['var'][name];
    }

    /**
     * テンプレートパース
     *
     * @param tpl {jQuery element | string} jQueryエレメントまたはファイル名
     * @param json {object | string} JSONオブジェクトまたはファイル名
     * @returns {jQuery element}
     */
    var _parse =  function (tpl, json) {
        if (typeof tpl === 'string') {
            tpl = $.jsontpl.getTpl(tpl);
        }
        if (typeof json === 'string') {
            json = $.jsontpl.getJSON(json);
        } else if ((json && json.constructor !== Array)) {
            json = [json];
        }

        var ret = $.jsontpl('<div />');

        for (var idx in json) {
            var data = json[idx];
            var tpl_tmp = $.jsontpl("<div />");
            tpl_tmp.data("jsontpl", {'var':data});
            var current_context = context;
            context = tpl_tmp;
            tpl_tmp.html(tpl.clone());
            for (var tplvar in data) {
                tpl_tmp.find("[name=" + tplvar + "]").replaceWith(data[tplvar]);
            }
            ret.append(tpl_tmp.children());
            context = current_context;
        }
        ret.find("[id]").removeAttr("id");
        return ret.children();
    };

    /**
     * テンプレートパースAPI
     *
     * @param tpl {jQuery element | string} jQuery element or filename
     * @param json {object | string} object or filename
     * @returns {jQuery element}
     */
    $.jsontpl.parse = _parse;

    /**
     * テンプレートパースAPI
     *
     * @param json {object | string} object or filename
     * @returns {jQuery.jsontpl element}
     */
    $.jsontpl.fn.parse = function (json) {
        return _parse(this, json);
    };

    $.jsontpl.fn.appendTo = function (selector) {
        getContext().find(selector).append(this);
        return this;
    };
    $.jsontpl.fn.prependTo = function (selector) {
        getContext().find(selector).prepend(this);
        return this;
    };
    $.jsontpl.fn.insertBefore = function (selector) {
        getContext().find(selector).before(this);
        return this;
    };
    $.jsontpl.fn.insertAfter = function (selector) {
        getContext().find(selector).after(this);
        return this;
    };

})(jQuery);

 

jQueryテンプレートエンジンを改良してみた

こないだ作ったjquery.jsontpl.jsを改良してみた。

プレースホルダーとして<var class="Foo"></var>などと記述すると変数Fooの値で置換される。または、class=".Foo.jsontpl-var"な任意の要素のinnerHTMLに変数Fooの値が設定される。みたいな仕様。

index.html

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
    <title>jQuery Template</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Script-Type" content="text/javascript" />
    <script src="./common/js/jquery.1.5.js" type="text/javascript"></script>
    <script src="./common/js/jquery.jsontpl.js" type="text/javascript"></script>
    <script type="text/javascript">
    //<![CDATA[
        $(document).ready(function(){
            // set global params
            jQuery.jsontpl.setParams({classPrefix:'jsontpl'});

            // jQueryエレメントをテンプレートとして使用
            $.jsontpl.parse($('#templates tr'),'./data.json').appendTo('.movie');

            // jQueryエレメントをテンプレートとして使用(別パターン)
            $.jsontpl('#templates tr').parse('./data.json').appendTo('.movie');

            // テンプレートとJSONをサーバーから取得
            $.jsontpl.parse('./tpl.html','./data.json').appendTo('.movie');

            // テンプレートのみサーバーから取得
            var json = {
                "Name": "test1",
                "ReleaseYear": "test2",
                "Director": "test3"
            };
            $.jsontpl.parse('./tpl.html', json).appendTo('.movie');

            // テンプレートとJSONをサーバーから取得
            $.jsontpl.getTpl('./tpl.html').parse('./data.json').appendTo('.movie');
        });
    //]]>
    </script>
</head>

<body>
    <div id="templates" style="display:none;">
        <table>
            <tr><!-- template -->
                <th class="Name jsontpl-var"></th>
                <td class="ReleaseYear jsontpl-var"></td>
                <td class="Director jsontpl-var"></td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    <var class="Name"></var>
                    <var class="ReleaseYear"></var>
                </td>
            </tr>
        </table>
    </div>
    <table class="movie" summary="summary">
    </table>
</body>
</html>

data.json

[
{ "Name": "The Red Violin", "ReleaseYear": "1998", "Director": "Francois Girard" },
{ "Name": "Eyes Wide Shut", "ReleaseYear": "1999", "Director": "Stanley Kubrick" },
{ "Name": "The Inheritance", "ReleaseYear": "1976", "Director": "Mauro Bolognini" }
]

結果

下記のようになります。

<table summary="summary" class="movie">
    <tbody><tr><!-- template -->
                <th class="Name">The Red Violin</th>
                <td class="ReleaseYear">1998</td>
                <td class="Director">Francois Girard</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Red Violin
                    1998
                </td>
            </tr><tr><!-- template -->
                <th class="Name">Eyes Wide Shut</th>
                <td class="ReleaseYear">1999</td>
                <td class="Director">Stanley Kubrick</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    Eyes Wide Shut
                    1999
                </td>
            </tr><tr><!-- template -->
                <th class="Name">The Inheritance</th>
                <td class="ReleaseYear">1976</td>
                <td class="Director">Mauro Bolognini</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Inheritance
                    1976
                </td>
            </tr><tr><!-- template -->
                <th class="Name">The Red Violin</th>
                <td class="ReleaseYear">1998</td>
                <td class="Director">Francois Girard</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Red Violin
                    1998
                </td>
            </tr><tr><!-- template -->
                <th class="Name">Eyes Wide Shut</th>
                <td class="ReleaseYear">1999</td>
                <td class="Director">Stanley Kubrick</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    Eyes Wide Shut
                    1999
                </td>
            </tr><tr><!-- template -->
                <th class="Name">The Inheritance</th>
                <td class="ReleaseYear">1976</td>
                <td class="Director">Mauro Bolognini</td>
                <td class="conbination">
                    <!-- placeholer not required -->
                    The Inheritance
                    1976
                </td>
            </tr><tr>
    <th class="Name">The Red Violin</th>
    <td class="ReleaseYear">1998</td>
    <td class="Director">Francois Girard</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Red Violin
        1998
    </td>
</tr><tr>
    <th class="Name">Eyes Wide Shut</th>
    <td class="ReleaseYear">1999</td>
    <td class="Director">Stanley Kubrick</td>
    <td class="conbination">
        <!-- placeholer not required -->
        Eyes Wide Shut
        1999
    </td>
</tr><tr>
    <th class="Name">The Inheritance</th>
    <td class="ReleaseYear">1976</td>
    <td class="Director">Mauro Bolognini</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Inheritance
        1976
    </td>
</tr><tr>
    <th class="Name">test1</th>
    <td class="ReleaseYear">test2</td>
    <td class="Director">test3</td>
    <td class="conbination">
        <!-- placeholer not required -->
        test1
        test2
    </td>
</tr><tr>
    <th class="Name">The Red Violin</th>
    <td class="ReleaseYear">1998</td>
    <td class="Director">Francois Girard</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Red Violin
        1998
    </td>
</tr><tr>
    <th class="Name">Eyes Wide Shut</th>
    <td class="ReleaseYear">1999</td>
    <td class="Director">Stanley Kubrick</td>
    <td class="conbination">
        <!-- placeholer not required -->
        Eyes Wide Shut
        1999
    </td>
</tr><tr>
    <th class="Name">The Inheritance</th>
    <td class="ReleaseYear">1976</td>
    <td class="Director">Mauro Bolognini</td>
    <td class="conbination">
        <!-- placeholer not required -->
        The Inheritance
        1976
    </td>
</tr></tbody></table>

jquery.jsontpl.js

/*!
 * jquery.jsontpl - client side template engine powered by jQuery
 *
 * @requires jQuery version 1.5 or higher
 * @version 0.0.2
 */
(function ($) {

    $.jsontpl = $.subclass();

    /**
     * debug console for plugin develop
     */
    var console = {
        log : (window.console) ? window.console.log : function () {}
    };

    /**
     * default params
     */
    $.jsontpl.params = {
        classPrefix : 'jsontpl',
        __trailing__: null
    };

    /**
     * Override params
     *
     * @param obj {object}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.setParams = function (params) {
        $.extend($.jsontpl.params, params);
        return this;
    };

    /**
     * ajax cache {url : data}
     */
    var cache = {};

    /**
     * modified $.ajax for cache mechanism
     *
     * @param params {object}
     * @returns {XMLHttpRequest | empty object}
     */
    $.jsontpl.ajax = function (params) {
        if (cache[params.url]) {
            params.success(cache[params.url]);
            return {};
        }
        params.success = (function() {
            var url = params.url;
            var orgSuccess = params.success;
            return function(a) {
                cache[url] = a;
                orgSuccess(a);
            }
        })();
        return $.ajax(params);
    };

    /**
     * $.ajax wrapper for template get
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getTpl = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (tpl) {
                ret = $.jsontpl(tpl);
            }
        });
        return ret;
    };

    /**
     * $.ajax wrapper for JSON get
     *
     * @param url {string}
     * @returns {jQuery.jsontpl}
     */
    $.jsontpl.getJSON = function (url) {
        var ret;
        $.jsontpl.ajax({
            type: "GET",
            url: url,
            cache: true,
            async: false,
            dataType: 'json',
            error: function (error) {
                console.log(error.statusText + " at " + url);
            },
            success: function (json) {
                ret = json;
            }
        });
        return ret;
    };

    /**
     * $.ajax wrapper for getting tpl & json
     *
     * @param tpl {string} template filename
     * @param json {string} json filename
     * @returns {jQuery.jsontpl} template & json
     */
    $.jsontpl.get = function (tpl, json) {
        return {
            tpl  : $.jsontpl.getTpl(tpl),
            json : $.jsontpl.getJSON(json)
        };
    };

    /**
     * Parse template
     *
     * @param tpl {jQuery element | string} jQuery element or filename
     * @param json {object | string} object or filename
     * @returns {jQuery element}
     */
    var _parse =  function (tpl, json) {
        if (typeof tpl === 'string') {
            tpl = $.jsontpl.getTpl(tpl);
        }
        if (typeof json === 'string') {
            json = $.jsontpl.getJSON(json);
        } else if ((json.constructor !== Array)) {
            json = [json];
        }
        var ret = $('<div></div>');
        for (var obj in json) {
            var a = tpl.clone();
            for (var tplvar in json[obj]) {

                // replace <var>
                a.find("var." + tplvar).after(json[obj][tplvar]).remove();

                // replace innerHTML of class *.placeholder
                var placeholder = $.jsontpl.params.classPrefix + '-var';
                a.find("." + placeholder + "." + tplvar)
                    .html(json[obj][tplvar])
                    .removeClass(placeholder);
            }
            ret.append(a);
        }
        return $.jsontpl(ret.html());
    };

    /**
     * Parse template
     *
     * @param tpl {jQuery element | string} jQuery element or filename
     * @param json {object | string} object or filename
     * @returns {jQuery element}
     */
    $.jsontpl.parse = _parse;

    /**
     * Parse template
     *
     * @param json {object | string} object or filename
     * @returns {jQuery.jsontpl element}
     */
    $.jsontpl.fn.parse = function (json) {
        return _parse(this, json);
    };
})(jQuery);

 

jQueryプラグインをうまいことカプセル化する設計方法その2

jQueryの次期バージョンの1.5ではjQuery自体のサブクラスを生成できるらしい。

なので、jQueryプラグインをうまいことカプセル化する方法として、プラグイン毎にjQueryサブクラスを用意するという選択肢が増えた。あんまり検証していないけど。

プラグインのソースもAPIもシンプルでいいと思うけど、パフォーマンス的にはどうなんだろう。

[2011.02.02追記] subclass()はjQuery1.5のリリースでsub()に変更になったようです

/**
 * jquery.myplug サブクラス方式
 *
 * SYNOPSIS
 *
 * $.myplug("target").method1();
 * $.myplug("target").method2();
 * $.myplug("target").method1().method2();
 */
;(function($) {

    /**
     * プラグインの名称
     */
    var plugname = 'myplug';
    $[plugname] = $.sub(); // [2011.02.02]subclass() -> sub()に変更
    $[plugname].fn.method1 = function(params) {return this}
    $[plugname].fn.method2 = function(params) {return this}
})(jQuery);

[追記2011.1.18][更新2011.1.18]

ベンチマークとってみた。jQueryプラグインをうまいことカプセル化する設計方法で構造的には一番合理的と思われたインナークラス方式と今回のサブクラス方式、公式ドキュメントに紹介されてる方式を比較。結果は初期化についてはサブクラス方式のほうが8〜10倍速かった。メソッドコールについてはほぼ差はなし。

  インナークラス サブクラス 公式ドキュメント
ロード 100.00(483.20ms) 422.72(2042.60ms) 97.14(469.40ms)
インスタンス化 100.00(670.60ms) 10.47(70.20ms) 8.71(58.40ms)
メソッドコール 100.00(2434.20ms) 99.17(2414.00ms) 215.37(5242.60ms)
ロード&インスタンス化 100.00(1483.80ms) 26.84(398.20ms) 11.83(175.60ms)

 

ベンチマークは下記のスクリプトにて。計測はFirefoxのみで行った。

// ベンチマーク
function bench(funcs, num1, num2) {
    function a(func) {
        var t1 = new Date();
        for (var i = 0; i < num1; i++) {
            func();
        }
        return new Date() - t1;
    }
    var res = {};
    for (var i = 0; i < num2; i++) {
        for (var idx in funcs) {
            res[idx] = (res[idx] || []);
            res[idx].push(a(funcs[idx]));
        }
    }
    var aves = [];
    for (var idx in res) {
        var sum = 0;
        for (i = 0; i < res[idx].length; i++) {
            sum = sum + res[idx][i];
        }
        aves.push(sum / res[idx].length);
    }
    for (var i in aves) {
        console.log((+i + 1) + ': '
                    + (aves[i] * 100 / aves[0]).toFixed(2)
                    + '(' + aves[i].toFixed(2) + 'ms)');
    }
}

// サブクラス方式ロード
var subClassLoad = function() {
    ;(function($) {
        var plugname = 'myplug2';
        $[plugname] = $.subclass();
        var default_params = {};
        $[plugname].fn.pluginit = function(params) {
            $.extend(default_params, params, {})
        }
        $[plugname].fn.method1 = function(params) {return 'myplug1 a'}
        $[plugname].fn.method2 = function(params) {return 'myplug2 b'}
    })(jQuery);
}
// インナークラス方式ロード
var innerClassLoad = function(){
    ;(function($) {
        var plugname = 'myplug';
        var Class = function(elem, params){
            this.elem = elem;
            this.elem.data(plugname, this);
            this.params = params;
        }
        Class.prototype.method1 = function(params){
            return 'myplug a';
        };
        Class.prototype.method2 = function(params) {
            return 'myplug b';
        };
        var default_params = {};
        $.fn[plugname] = function(params){
            return new Class(this, $.extend(default_params, params, {}));
        }
    })(jQuery);
}
// サブクラス方式ロード
var formalLoad = function(){
    ;(function($) {
        var plugname = 'myplug3';
        var methods = {
            init    : function(params){return this;},
            method1 : function(params){return 'myplug3 a';},
            method2 : function(params) {return 'myplug3 b';}
        };
        $.fn[plugname] = function(method) {
            if ( methods[method] ) {
                return methods[ method ]
                    .apply( this,
                           Array.prototype.slice.call( arguments, 1 ));
            } else if ( typeof method === 'object' || ! method ) {
                return methods.init.apply( this, arguments );
            } else {
                $.error( 'Method ' +  method
                        + ' does not exist on jQuery.' + plugname );
                return this;
            }
        };
    })(jQuery);
}

innerClassLoad();
subClassLoad();
formalLoad();

// インナークラス方式インスタンス化
var innerClassInit = function() {var a = $("#target1").myplug();}
// サブクラス方式インスタンス
var subClassInit = function(){var b = $.myplug2("#target1");}
// 公式ドキュメント方式インスタンス
var formalInit = function(){var c = $("#target1");};
// インナークラス方式メソッドコール
var a = $("#target1").myplug();
var innerClassMethodCall = function() {a.method1();};
// サブクラス方式メソッドコール
var b = $.myplug2("#target1");
var subClassMethodCall = function(){b.method1();};
// 公式ドキュメント方式メソッドコール
var c = $("#target1");
var formalMethodCall = function(){c.myplug3('method1');};
// サブクラス方式ロード&インスタンス
var subClassLoadAndInit = function() {
    subClassLoad();
    subClassInit();
};
// インナークラス方式ロード&インスタンス
var innerClassLoadAndInit = function() {
    innerClassLoad();
    innerClassInit();
}
// 公式ドキュメント方式ロード&初期化
var formalLoadAndInit = function() {
    formalLoad();
    formalInit();
};
bench([innerClassLoad, subClassLoad, formalLoad], 100000, 5);
bench([innerClassInit, subClassInit, formalInit], 5000, 5);
bench([innerClassMethodCall, subClassMethodCall, formalMethodCall], 1000000, 5);
bench([innerClassLoadAndInit, subClassLoadAndInit, formalLoadAndInit], 10000, 5);

 

jQueryプラグインをうまいことカプセル化する設計方法

 

“There are only two hard things in Computer Science: cache invalidation and naming things”

だそうです。そういう訳で、jQueryプラグインのメソッド名が他のプラグインと衝突するのを回避するための設計方法を列挙してみた。今のところ、一番下の方法が自分的にはしっくり来ている。

[2011.02.02追記]  2011年はsub()方式がおすすめです。

jquery.myplugの基本形。基本的に$.fn.extend()は使わない方針。

/**
 * jquery.myplug基本形
 *
 * SYNOPSIS
 *
 * $('#target').myplug();
 * $('#target').myplug({param1 : val1, param2 : val2});
 */
;(function($) {

    var default_params = {
        param1 : 'a',
        param2 : 'b'
    };

    $.fn.myplug = function(params) {

        params = $.extend(default_params, params, {});

        // do something

        return this;
    }
})(jQuery);

 

複数メソッドを提供したい。

/**
 * jquery.myplug マルチメソッド(悪い例)
 *
 * SYNOPSIS
 *
 * $('#target').myplugMethod1(params);
 * $('#target').myplugMethod2(params);
 */
;(function($) {
    $.fn.myplugMethod1 = function(params) {return this;}
    $.fn.myplugMethod2 = function(params) {return this;}
})(jQuery);

上記はどこでも推奨していない悪い例。myplugというプラグインが$.fn配下にmyplugMethod1とmyplugMethod2という2つの名前を定義しているので、プラグイン間で衝突する可能性がある。

 

/**
 * jquery.myplug マルチメソッド(別案1)
 *
 * SYNOPSIS
 *
 * $('#target').myplug.method1();
 * $('#target').myplug.method2();
 *
 */
;(function($) {
    $.fn.myplug = {};
    $.fn.myplug.method1 = function(params) {}
    $.fn.myplug.method2 = function(params) {}
})(jQuery);

上記の例では、複数のメソッドを含む構造体を$.fn.myplugという名前で定義している。プラグイン間の衝突は回避できたけど、メソッド内でどうjQueryオブジェクトを取得するのか分からない。

 

/**
 * jquery.myplug マルチメソッド(別案2)
 *
 * SYNOPSIS
 *
 * $('#target').myplug().method1();
 * $('#target').myplug().method2();
 * $("#target").myplug().method1().myplug().method2();
 */
;(function($) {

    $.fn.myplug = function () {
        var jq = this;
        return {
            method1 : function(){return jq;},
            method2 : function(){return jq;}
       }
    };
})(jQuery);

thisの代わりにjqにjQueryオブジェクトを格納。インターフェースがイマイチだけど、まあまあ有り得る形。

 

/**
 * jquery.myplug 公式ドキュメント方式
 *
 * SYNOPSIS
 *
 * $('#target').myplug('method1', params);
 * $('#target').myplug('method2', params);
 */
;(function($) {

    var plugname = 'myplug';

    // 全メソッドをここに定義
    var methods = {
        init    : function(params){return this;},
        method1 : function(params){return this;},
        method2 : function(params) {return this;}
    };

    // 以下は共通
    $.fn[plugname] = function(method) {
        if ( methods[method] ) {
            return methods[ method ]
                .apply( this, Array.prototype.slice.call( arguments, 1 ));
        } else if ( typeof method === 'object' || ! method ) {
            return methods.init.apply( this, arguments );
        } else {
            $.error( 'Method ' +  method + ' does not exist on jQuery.' + plugname );
            return this;
        }
    };
})(jQuery);

jQuery公式ドキュメントでも推奨しているらしい方式。メソッド内ではthisがjQueryオブジェクトだし、開発は今までどおりで分かりやすい。ただ、メソッド名が文字列っていうのは何となく抵抗がある。

 

/**
 * jquery.myplug インナークラス方式
 *
 * SYNOPSIS
 *
 * var instance = $('#target').myplug(params);
 * instance.method1();
 * instance.method2();
 * instance.method1().method2();
 *
 * var instance2 = $('#target').data('myplug'); // can get the instance later
 */
;(function($) {

    /**
     * プラグインの名称
     */
    var plugname = 'myplug';

    /**
     * 全プラグイン共通のインナークラスコンストラクタ
     */
    var Class = function(elem, params){
        this.elem = elem;
        this.elem.data(plugname, this);
        this.params = params;
    }

    /**
     * メソッドを定義
     */
    Class.prototype.method1 = function(params){
        return this;
    };
    Class.prototype.method2 = function(params) {
        return this;
    };

    /**
     * デフォルトパラメータ
     */
    var default_params = {

    };

    /**
     * 全プラグイン共通
     */
    $.fn[plugname] = function(params){
        return new Class(this, $.extend(default_params, params, {}));
    }
})(jQuery);

プラグインひとつにつき、インナークラスを一つ定義する。元ネタ。インターフェースは、DOMに紐づいたmyplugインスタンスを引き回すのが特徴で、メソッドチェーンもできる。myplugインスタンスは$('#target').data('myplug')などとすれば後から取得できる。広く普及している、DOMを引き回すような方式とは異なるので混乱の元かも知れないけど、今のところ一番無難な方式の気がする。

ここまで来ると、プラグイン間で継承できるようなフレームワーク作ってみようとか、夢が広がる。

 

あと、同じ名前空間つながりで、イベント処理も必ず$('#target').bind('click.'+plugname, func)ってやっておいたほうが身のためです。すでに痛い目みた。