tohokuaikiのチラシの裏

技術的ネタとか。

Building Large jQuery Applications(jQueryを使った大規模アプリの開発)を適当和訳

http://www.slideshare.net/rmurphey/building-large-jquery-applications
にあるスライドの例によって適当な和訳。

というのは、来月からそれをやるから。
ということで、3月頃には
「あー、実際にこうしたらよかった」
「うぅぅ、失敗してもうた・・・」
という話もあるかも :-)

では、以下↓↓↓↓

jQueryによる大規模開発の時に遭遇する問題

jQuery基本的にDOM操作とAjaxを行うためのライブラリで、それ自体で特別に大規模開発を行うためのツールをそろえてくれるわけではない。

といっても、jQueryを使った大規模開発ができないというわけではない。

だけど、↓こんなのはさすがにどう考えても勘弁ですよね。

$(document).ready(function() {
    $('#myFeature li') .append('<div/>') .click(function(){
        var $this = $(this);
        var $div = $this.find('div');
        $div.load('foo.php?item=' + $this.attr('id'), function() {
            $div.show();
            $this.siblings() .find('div').hide();
        });
    })}
);
// NAVIGATION
function togglePage(section) 
{
    // if the clicked section is already the current section AND we're in full page mode
    // minimize the current tab
    if (jQuery('#md_tab_'+ section).hasClass('current') && jQuery('#md_tab_'+ section + ' a').hasClass('md_fullpage')) {
        // alert('clicked section is current section AND fullpage mode is active; teaser should load');
        // Minimize
        jQuery('#md_tabs_navigation a').removeClass('md_fullpage');
        jQuery('.md_body').hide();
        jQuery('#md_feature').slideDown('normal',function(){
            var bodyContent = jQuery('#md_body_'+ section);
            bodyContent.fadeOut('normal',function(){
                jQuery('#md_tabs_navigation a').each(function(){
                    var thisSection = jQuery(this).html().replace('<span<','').replace('</ span<','');
                    var thisSection_comp = thisSection.toLowerCase().replace(' ','_');
                    jQuery('#md_body_'+ thisSection_comp).load( '/app/modules/info/loadTeaser.php?sect='+ thisSection_comp, function(){
                        tb_init('.md_body a.thickbox, .md_body area.thickbox, .md_body input.thickbox');
                        bodyContent.animate({
                          height: 'toggle', opacity: 'toggle' }
                                            ,"slow");
                    });
                });
            });
        });
        jQuery('#expandtabs span').empty().append('Expand Tabs');
    }
    else {
        // if the clicked section is NOT the current section OR we're NOT in full page mode
        // then let's go to full page mode and show the whole tab
    }
}

じゃあ、こういったjQueryを使ったアプリケーションはどうやってこんがらがらずに作ることができるか?

ポイントとなるのは、「できるだけ細かい機能ごとのパーツに分け、適宜コードを書いていくこと」という考え方になる。

じゃあ実際にどうする?

とにかく、jQueryはDOM操作とAjaxにのみ使うようにして、フレームワークとしては使わないこと。


コードを体系化するには、Classを使って、実際に何をするかはMethodに書く。

こんな感じ

myApp.common.Messaging = Class.extend({
  animDuration : 500, hideDelay : 1500, init : function() 
  {
      this.el = $('<div class="message"/>').prependTo('body').hide();
      $.subscribe('/message/notification', $.proxy(this._showMessage, this));
  },
  
  _showMessage : function(msg) 
  {
        var hide = $.proxy(this._hideMessage, this);
        this.el.append('<p>' + msg + '</p>');
        if (!this.el.is(':visible')) {
            this.el.slideDown(this.animDuration, $.proxy(function() {
                this.showTimeout = setTimeout(hide, this.hideDelay);
            }, this));
                                                         
        }
        else {
            clearTimeout(this.showTimeout);
            this.showTimeout = setTimeout(hide, this.hideDelay);
        }
    },
    
    _hideMessage : function() {
        this.el.slideUp(this.animDuration);
        this._cleanup();
    },
    
    _cleanup : function() {
        this.el.empty();
    }
});

サーバはデータを送るだけで、HTMLを記述するのには使わなかったり、あるいはクライアントでHTMLを作りたい場合にはmustache.jsのようなテンプレートを使ったりする。

myApp.widgets.Tools = Class.extend({
 itemTemplate : '<li>' +
     '<input type="checkbox" name="{{description}}" checked>' +
     '<label for="{{description}}">{{description}}</label>' +
     '</li>',
    
  services : [], init : function(el) {
      this.el = el;
      this.el.delegate('input', 'click', $.proxy(this._handleChoice, this));
      $.subscribe('/service/add', $.proxy(this._handleAdd, this));
  },
  
  _handleChoice : function(e) {
      var input = $(e.target), service = input.attr('name');
      $.publish('/service/' + service + '/toggle', [ input.is(':checked') ]);
  },
  
  _handleAdd : function(service) {
      if ($.inArray(service, this.services) >= 0) {
          return;
      }
      var html = Mustache.to_html(this.itemTemplate, {
        description : service });
                                  
      this.el.append(html);
  }
 }
);
DOM表現を持っているコンポーネントを定義ときは一貫したパターンを使用する
var DomThinger = Class.extend({
  config : {},
  
  init : function(el /* jQuery object */, opts /* mixin */) {
      this.el = el;
      $.extend(this.config, opts);
  }
});

これが何でその一貫したパターンなのかよくわかりません・・・・

コンポーネント間のやりとりには、集中的なメッセージングハブ的なものを使う
myApp.common.Messaging = Class.extend(
{
  init : function() {
      this.el = $('<div class="message"/>').prependTo('body').hide();
      $.subscribe('/message/notification', $.proxy(this._showMessage, this));
  },

  _showMessage : function(msg) { /* ... */ },

  _hideMessage : function() { /* ... */ },
      
  _cleanup : function() { /* ... */ }
});

これは、メッセージを<div class="message"/>というタグに表示させることにして、その表示自体の方法はmyApp.common.Messaging._showMessage,myApp.common.Messaging._hideMessageに選択してしまっている。その登録を$.subscribeによって行っている。
'/message/notification'というのは、その識別子。

myApp.services._base = Class.extend(
{
  description : '',
  
  init : function(opts) {
      $.subscribe('/search/term', $.proxy(this._doSearch, this));
      $.publish('/message/notification', [ 'Service ' + this.description + ' added' ] );
  },
  
  _doSearch : function(term) {/* ... */ },
});

先ほど登録した'/message/notification'というメソッド識別子にたいして、引数[ 'Service ' + this.description + ' added' ]を$.publishで送ってやることで、'/message/notification'という識別子に対応したメソッドをFireさせる。


あとは、DRY(Don't repeat yourself)と疎結合に常に注意すること。

$.proxy

使用する関数内で(JavaScriptでは往々にして混乱しやすい)thisの意味を明確にする。

myApp.services._base = Class.extend({
  description : '',
  
  init : function(opts) {
      $.subscribe('/search/term', $.proxy(this._doSearch, this));
      $.publish('/message/notification', [ 'Service ' + this.description + ' added' ] );
  },
  
  _doSearch : function(term) { /* ... */ }
}); 

おそらく、これによって_doSearchの中のthisはmyApp.services._base(かあるいはそれをExtendしたインスタンス)になる(はず)。

$.fn.delegate

ちょっと気の利いたjQueryScripterならこれを使います。
($.fn.liveはもうやめよう)

myApp.widgets.Saved = Class.extend({
  init : function(el) { this.el = el;
      // event delegation for searching and removing searches
      this.el.delegate('li', 'click', $.proxy(this._doSearch, this));
      this.el.delegate('span.remove', 'click', $.proxy(this._removeSearch, this));
  },
  
  _doSearch : function(e) { /* ... */ },
  
  _removeSearch : function(e) { /* ... */ }
});

.liveだと、
this.el.live('click', function(){/*..*/})みたいな感じ。
.liveにせよ、.delegateにせよ、DOMの発生時に依存せずその要素にイベントを関連付できる機能*1

http://twitter.com/javascripter/status/13543258423

$.fn.liveより$.fn.delegateを使うほうが良いと思う。要素に対する操作という一貫性ではdelegateのほうが自然で、liveだと this.selectorが必要なだけなのにコンストラクタでfindが走るし、しかもコンテキストを絞れない。liveのAPIは汚い。

構造と依存関係をマネジメントする

ソリューションとしてはそんなになくって、それも「これ」というものは見当たらない。
*2

  1. RequireJS (building and loading)
  2. LABjs (loading only)
  3. modulr (building only)
  4. Roll your own
  5. Dojo?

要は、どのJavaScriptファイルを使って、どのタイミングでロードしたりとかそういったライブラリ・プラグイン同士のローディングや依存関係を解消するためのライブラリでしょうか。

注意

いろいろコードを書いたけど、それがその通り動作するわけではないです。
うまくいくときもあれば、うまくいかなときもある。

複雑なものを作るのでなければ、複雑に書く必要はなくって、ここで取り上げたような内容はたぶんそのプロジェクトに対してはやりすぎだと思う。

こんなのでいいじゃない。

$(document).ready(function() {
    $('#myFeature li') .append('<div/>') .click(function() {
        var $this = $(this);
        var $div = $this.find('div');
        $div.load('foo.php?item=' + $this.attr('id'), function() {
            $div.show();
            $this.siblings() .find('div').hide();
        });
    })
  });

繰り返しになるけど、この手法は明確に機能が分割されていて、しかも相関があるようなアプリケーションの場合や、常に改良を続けるアプリケーションの場合にうまくいくと思う。

他にもコードの組織化は、プラグインやオブジェクトリテラル(?)やプロトタイプ継承という選択肢もあります。
*3

*1:のよう。まだ使ったことないからわからない

*2: Only a handful of solutions, none that I love. これちょっとどういうニュアンスなんだろう・・・

*3: Other options for code org include plugins, object literals, and prototypal inheritance. これもどういうことだろう?