メインコンテンツへ移動

Backbone はなぜまだ生きているのか

無料2016-10-07#JS#backbone#backbone与angular#backbone与react#backbone优点

Backbone は時代遅れだと思っていたが、実際の業務ではそうではないことがわかった

はじめに

Backbone はおそらく最初に広く受け入れられたフロントエンド MVC フレームワークのようだ。確かに年配ではある(最初のリリースは 2010 年)。現���、目まぐるしい MV* フレームワークに直面すると、Backbone を思い出すことはほとんどなく、それを選ぶ理由などさらにない

本当にそうだろうか?Backbone を使い続けているのは単に乗り換えるのが面倒だからか?今でも Backbone を選ぶのは単に懐古主義か?

違う。それは十分に柔軟だからだ。Backbone は MV* フレームワークの中で最も強力ではない(実際には非常に弱いが)、しかし最も柔軟だ。他に並ぶものはない

一.Backbone v0.1.0

2010 年に戻って、Backbone が当初何をしようとしていたのかを見てみよう

構造

View        DOM 要素をラップ。jq イベントデリゲートと組み合わせて、ビュー関連ロジックを管理
-------
Collection  Model のコレクション
    Model   データ構造
データの追加・削除・更新・検索 -> CRUD(create、read、update、delete)トリガー -> Backbone.sync() -> Server
-------
Events                      sync(method, model, success, error)
任意のオブジェクトにカスタムイベントサポートを提供   CRUD から RESTful API への変換を完了し、jq.ajax() でリクエストを送信

図で示すと以下のようになる:

[caption id="attachment_1178" align="alignnone" width="814"]backbone-0.1.0 backbone-0.1.0[/caption]

最下層で最も重要な部分は Events だ。M から V への通信、M の変更を Server に通知することはすべて Events が提供するカスタムイベントによって完了される。 심지어 Backbone オブジェクト自体をイベントバスとして使用するテクニックさえある。以下の通り:

// トピック
var EVENT_DATA_READY = 'dataReady';
// 購読
Backbone.on(EVENT_DATA_READY, function(data) {
    console.log(data);  // Object {res: 1}
});
// 发布
Backbone.trigger(EVENT_DATA_READY, {res: 1});

Backbone が提供するすべてのクラス(Model、Collection、View、および最新バージョンの Router、History)は Events に基づいている。したがって、任意の Backbone インスタンスでカスタムイベントを使用できる。例えば Model インスタンス:

var EVENT_BEFORE_CHANGE = 'beforeChange';
var Model = Backbone.Model.extend({
    // Overriding set
    set: function(attributes, options) {
        this.trigger(EVENT_BEFORE_CHANGE, attributes, options);

        // Will be triggered whenever set is called
        if (attributes.hasOwnProperty('prop')) {
           this.trigger('change:prop');
        }

        return Backbone.Model.prototype.set.call(this, attributes, options);
    }
});
var model = new Model();
model.on(EVENT_BEFORE_CHANGE, function() {
    console.log(arguments); // ["key", "value"]
});
// test
model.set('key', 'value');

model プロパティが変更される前に、beforeChange 通知を発行した。つまり、Backbone が提供するネイティブイベントでは足りない場合、任意の beforeXXXafterXXX、および完全に独立したカスタムイベントを追加できる

データ同期

重要なソースコードは以下の通り:

// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
  'create': 'POST',
  'update': 'PUT',
  'delete': 'DELETE',
  'read'  : 'GET'
};

// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, uses jQuery to make a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
Backbone.sync = function(method, model, success, error) {
  $.ajax({
    url       : getUrl(model),
    type      : methodMap[method],
    data      : {model : JSON.stringify(model)},
    dataType  : 'json',
    success   : success,
    error     : error
  });
};

Model 内でリソースに対応する url を指定し、Model インスタンスが変更されたときに CRUD をトリガーし、jQuery.ajax() によって Server に通知する

RESTful API サポートを提供しているが、実際のシナリオに応じて書き換えることを推奨している。またはこのデータ同期メカニズムを使いたくない場合は、Model 内で url を指定しなければよい。強制的なルールは何もない

ルート

実際、Backbone の最初のバージョンにはルート制御は提供されておらず、MVC を実装するための比較的一般적인ツールセットを提供していただけだ。データ同期メカニズムでさえおまけのようなもの(使っても使わなくてもよい)

MVC

M と V には明確な位置付けがある(それぞれ Model と View に対応)。しかし C には明確な位置がなく、M と V に分散させるしかなかった。データ検証、データ同期などのビジネスロジックは Model に、インタラクションロジックは View に配置する

二.Backbone v1.3.3

設計理念

Philosophically, Backbone is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript.

生データとインターフェースの上に、一般的な最小セット を提案したい

したがって、这么多年过去了、ルートサポートのみが追加され、最初の Events、Model、Collection、View はほとんど変化していない

ルート構造

Router      ルートテーブルの確立を担当
-------
History     実際のルート制御を実装。url の変更、変更の監視、URL パラメータの抽出とルートコールバックの実行、ルートイベントのトリガー

図で示すと以下のようになる:

[caption id="attachment_1179" align="alignnone" width="761"]backbone-1.3.3 backbone-1.3.3[/caption]

ルートをルートテーブルの確立(Router)とルート制御(History)の 2 つの部分に分ける。実際の使用は以下の通り:

var App = Backbone.Router.extend({
    routes: {
        // key は path パターン、val はイベント名
        '': 'index',
        ':list1ItemId': 'list2',     // 'rsshelper/2'
        ':list1ItemId/:list2ItemId': 'detail'   // 'rsshelper/2/5'
    }
});
//--- run
var app = new App();
// listen route events
app.on('route:index', function() {
    console.log('trigger route:index');
}).on('route:list2', function(list1ItemId) {
    console.log('trigger route:list2');
}).on('route:detail', function(list1ItemId) {
    console.log('trigger route:list2');
});
// pushState を有効化、デフォルトは hashchange
Backbone.history.start({pushState: true,
    root: location.pathname.replace('index.html', '')
});

Router でルートテーブルを確立し、Router インスタンスを通じてルートイベントを監視し、最後に History を通じてルートを有効化する

Router が全権を握っているように見えるが、実際には Router は History の依存項であり、実際の制御は History によって完了される

ルート実装

//--- History
// ジャンプ関数、渡された fragment は手動で encode する必要がある
// options {trigger: true} を渡さないとルートイベントはトリガーされない、デフォルトはトリガーされない
// options {replace: true} を渡さないと現在の URL を上書きできない、戻れない、デフォルトは 1 条挿入、戻れる
// pushState/replaceState | location.hash 代入/location.replace | location.assign(url) リフレッシュ
navigate: function(fragment, options) {
  if (!History.started) return false;
  if (!options || options === true) options = {trigger: !!options};

  // Normalize the fragment.
  fragment = this.getFragment(fragment || '');

  // Don't include a trailing slash on the root.
  var rootPath = this.root;
  if (fragment === '' || fragment.charAt(0) === '?') {
    rootPath = rootPath.slice(0, -1) || '/';
  }
  var url = rootPath + fragment;

  // Strip the hash and decode for matching.
  fragment = this.decodeFragment(fragment.replace(pathStripper, ''));

  if (this.fragment === fragment) return;
  this.fragment = fragment;

  // If pushState is available, we use it to set the fragment as a real URL.
  if (this._usePushState) {
//!!! backbone は state パラメータを使用しない、{} を渡す
    this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);

  // If hash changes haven't been explicitly disabled, update the hash
  // fragment to store history.
  } else if (this._wantsHashChange) {
    this._updateHash(this.location, fragment, options.replace);
    // iframe url を更新
    if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
      var iWindow = this.iframe.contentWindow;

      // Opening and closing the iframe tricks IE7 and earlier to push a
      // history entry on hash-tag change.  When replace is true, we don't
      // want this.
      if (!options.replace) {
        iWindow.document.open();
        iWindow.document.close();
      }

//! iframe の window.location オブジェクトを渡す
      this._updateHash(iWindow.location, fragment, options.replace);
    }

  // If you've told us that you explicitly don't want fallback hashchange-
  // based history, then `navigate` becomes a page refresh.
  } else {
    return this.location.assign(url);
  }
  // ルートテーブルを検索
  if (options.trigger) return this.loadUrl(fragment);
}

//--- Router
// Manually bind a single named route to a callback. For example:
//
//     this.route('search/:query/p:num', 'search', function(query, num) {
//       ...
//     });
//
route: function(route, name, callback) {
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
  if (_.isFunction(name)) {
    callback = name;
    name = '';
  }
  if (!callback) callback = this[name];
  var router = this;
  Backbone.history.route(route, function(fragment) {
    var args = router._extractParameters(route, fragment);
    if (router.execute(callback, args, name) !== false) {
      router.trigger.apply(router, ['route:' + name].concat(args));
      router.trigger('route', name, args);
      Backbone.history.trigger('route', router, name, args);
    }
  });
  return this;
}

実際のルートテーブル確立時(Router.route() 内の Backbone.history.route())、Router インスタンス全体を History に渡す。したがって History は Router より上にある

History 内のルート実装方式も pushState -> onhashchange -> iframe poll のダウングレード方案だが、Backbone はデフォルトで pushState を使用しない。手動で pushState を有効化する必要がある。これは其它フレームワークのルート実装方案とは異なる:

// pushState を有効化、デフォルトは hashchange
Backbone.history.start({pushState: true,
    root: location.pathname.replace('index.html', '')
});

サーバーサイドサポート

Backbone ルートはサーバーサイドのサポートが必要だ。以下の通り:

Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly.

少なくとも定義されたルートがアクセス可能であることを保証する必要がある。パスのアクセスを強制要求するため、同期ミラーサイトを作りたくない/作る必要のないシングルページアプリケーションにとって、この点はとても厄介だ。ルートの柔軟性を大きく制限する

また root の問題にも注意する必要がある

If your application is not being served from the root url / of your domain, be sure to tell History where the root really is, as an option: Backbone.history.start({pushState: true, root: "/public/search/"})

デフォルトでは history.start() 後直接 / にジャンプし、ページが所在する現在のパスにはならない。したがって上記のサンプルはとても奇妙なことをしている:

// pushState を有効化、デフォルトは hashchange
Backbone.history.start({pushState: true,
    //!!! root を現在のパスに変更
    root: location.pathname.replace('index.html', '')
});

ルートはシングルページアプリケーションにおいて重要な役割を果たすが、結局ビジネスロジックではない。ルートのためにこれほど苦労するのは面倒だ

三.Backbone とシングルページアプリケーション

シングルページアプリケーションには少なくとも以下が必要:

  • ビュー(偽ページ)

  • ルート(ページと URL の関係を確立)

  • テンプレート(キャッシュからページを回復するために使用)

  • キャッシュメカニズム(シングルページアプリケーションの大きな利点の 1 つ)

Backbone は最初の 3 つを提供しているが、应用场景を満たすのは難しい。なぜなら:

  • 子ビューをサポートしていない。しかし少し複雑なシナリオではネストされたビューが必要

  • ルートは非常に使いにくく、面倒で、柔軟性もない

  • テンプレートは underscore から来ており、同様に子テンプレートをサポートしていない

もちろん、無理に使うことも可能だ。ただ、より多くのことを行い、より多くのコードを書く必要がある。そして利点はすべてが掌握之中にあることだ

ほぼすべての詳細が掌握之中にある。これはとても実利的な利点であり、Backbone と其它フレームワークとの最大の差別だ。制約が少なく、非常に柔軟

しかしそれでもシングルページアプリケーションでの Backbone 採用は推奨しない。なぜならあまりにも骨が折れるからだ。必要ない。Angular の方が明らかに適切で便利

四.柔軟���

フレームワークとして、Backbone は最も柔軟だ。Angular などの重量級選手と比較して、Backbone の制限は最も少なく、自由度は最も高い:

  • ネイティブ View イベントが多すぎない。自分で簡単に追加できる

  • Model を変更したくない。強制的に手動で DOM を操作したい。随意。そして手動で Model を一致させればよい

  • 性能が悪すぎる。多くの不要な render() がある。内置の addremovechangesort イベントを使わない。レンダリングが必要な時にカスタムイベントで View にレンダリングを通知

  • underscore テンプレートが弱すぎる。jade に変えたい。随意に交換可能。反正 view.render() は完全に制御可能

  • jQuery が大きすぎる。Zepto に変えたい。可能。これを変更するコストはほとんどない(Zepto がサポートしていない jQuery API を使用していない場合)

  • underscore が遅すぎる。lodash に変えたい。もちろん可能。完全にコストなし

Backbone は DOM 操作を jQuery/Zepto に実装させ、Collection 操作を underscore/lodash に実装させる。自分自身はデータとインターフェース間の最も核心的な部分のみを保証する。また内置のレンダリングメカニズムもない。Backbone はどのものをどこに配置すべきかを示唆するだけで、示唆された位置に配置しなくても全く問題ない。特に精細な制御が必要なシナリオに適している

五.应用场景

以前は Backbone は時代遅れで役に立たないと思っていた。なぜなら其它の様々な MV* フレームワークと比較して、Backbone はあまりにも弱く見えるから

業務では易企秀、搜狐快站などのようなドラッグアンドドロップでページを快速生成するものを実装する必要があった。最初はデータバインディングが問題だと考え、Angular を採用することを決定した。すぐに Drag&Drop を Angular と組み合わせるのが非常に難しいことがわかった。なぜなら DnD は DOM 要素を保持する必要があるが、Angular は絶えず新しい DOM 要素を作成し、直接 DOM 要素にアクセスすることを望まないから。状態の不一致を引き起こす可能性があるため

柔軟な組み合わせ

Backbone に変更し、すぐにその巨大な利点を発見した。Backbone は其它のサードパーティ製ものと簡単に組み合わせられる。なぜなら制約と制限がないから。DOM にアクセスしたい?随意。直接修正したい?随意。状態の不一致はレンダリングに影響するか?もちろん影響する。しかしレンダリング部分もすべて自分で書いたものだ。何が影響するかは明確

DOM 要素に依存するサードパーティ製ライブラリ、例えば DnD は、重量級 MV* フレームワークと組み合わせるのが難しい。なぜなら大而全のフレームワークは一般的に道徳的制約があるからだ:直接 DOM を修正することを強く推奨せず、Model を修正し、レンダリングメカニズムに DOM を再レンダリングさせるべき。Backbone を使用すればこのような問題はない。それは緩く、随意で、自由で制御可能

無理に組み合わせられたとしても、サードパーティ製ライブラリがニーズを満たせず、拡張改写が必要だと发现すれば、MV* フレームワークの層々の制限がすべてを非常に困難にすることがわかる。なぜならフレームワークは多くの未知の内部メカニズムを提供しており、一部を触ると全体に影響する可能性がある。一方 Backbone はほとんどメカニズムを内置しておらず、Model、View は思い通りに捏ねられる

詳細制御

Backbone を使用すればすべてが掌握之中にあることを保証できる。すべての Model プロパティ、すべての change、すべての render() は完全に制御可能。Backbone はイベントメカニズムのみを提供しているようだ。其它はすべて自分で捏ねる。ロジックは明確で、それほど多くの未知の内部メカニズムがない。厚いベストプラクティスを小心翼翼に遵守する必要はない

余分な render() が存在することを发现すれば、簡単に render() 内でフィルタリングできる。内置の change イベントが使いにくいと发现すれば、完全に使用せず、簡単にカスタムイベントに変更できる……ほぼすべての詳細が制御可能。Model、View は少しも神秘的でも特殊でもなく、すべてカスタムイベントをサポートする普通のオブジェクトに過ぎない

シンプルでクリーン

シンプルなデータバインディング(Model、View)のみが必要な場合、Backbone は間違いなく最良の選択だ。なぜなら任何の制限を強要せず、太多の不要なものを導入せず、ちょうどかろうじてニーズを満たせ、追加コストもないから(学習コスト、移行コストなど)

P.S. 実際、最初のバージョンの Backbone が提供していたものですでに十分だった。後から追加された Router と History はむしろ鶏肋だ。なぜならルートは非常に柔軟である必要はなく、しかし十分に便利である必要があるからだ。私たちが欲しいルートは URL と対応する処理関数とのつながり。Angular ルートのように。一方、一貫して通用性を追求する Backbone はこのような柔軟ではなく(強制的制約、サーバーサイドサポートが必要)、便利でもない(デフォルト root/)ルートを提供した。使っていてももちろん辛い

参考資料

コメント

コメントはまだありません

コメントを書く