バックボーン用のMVC実装の作成

画像



ある曇りの朝、古いプロジェクトの1つを適切にリファクタリングするといいと思いました。 これは、1つの3DシューティングゲームでHUDをカスタマイズするための非営利の軽量アプリケーションです。 私は2年前にそれを書いたが、暑くて経験が浅かった。 その結果、優れたスパゲッティコードの束は、すべての欠点にもかかわらず、その仕事をしていた。 より賢く、より経験豊富になったので、アプリケーションを完全に書き直し、新しいアーキテクチャを与え、サポートと更新を簡素化することにしました。 どうやってやるの? 答えは簡単に思えました-MVCを使用し、レベルに分割し、すべてを1つの全体に接続します。 そのため、強固な基盤となるシンプルで効果的なフレームワークを選択するという問題に直面しました。 簡単な調査の後、 backbone.jsを選択しました。 そのシンプルさと柔軟性が大好き。 ソースを開いて、すべてがどのように機能し、どのように機能するかを理解できます。 喜ばなかった唯一のニュアンスは、MVパターンです。 多数のビューでロジックを塗りつぶしたくなかったので、パズルの欠けている部分を提供する独自の自転車を書くというアイデアが生まれました。 さらに、新しいものを作成することは常に楽しくて興味深いです。 考え直すことなく、バックボーン用のコントローラーの実装に取り​​かかりました。



問題のステートメントと基本的な方法の実装



そのため、アプリケーションのすべての部分を1つの全体に統合するコントローラーを作成する機能が必要です。 各コントローラーは、すべてのモデルとコレクション(基本的なデザイナーと既に作成されたインスタンスの両方)にアクセスできる必要があります。 また、コンポーネント(ビュー)を作成し、適切に応答するためにイベントをリッスンできる機能も必要です。



コントローラのスケルトンは次のようになります。

Controller = { views: {}, // views hash map models: {}, // models hash map collections: {}, // collections hash map // Set of methods to get existing view, get view constructor and create new view using constuctor getView: function() {}, getViewConstructor: function() {}, createView: function() {}, // Set of methods to get existing model, get model constructor and create new model using constuctor getModel: function() {}, getModelConstructor: function() {}, createModel: function() {}, // Set of methods to get existing collection, // get collection constructor and create new collectionusing constuctor getCollection: function() {}, getCollectionConstructor: function() {}, createCollection: function() {}, // This method will subscribe controller instance to view events addListeners: function() {} }
      
      







これまでのところ、すべてが非常に簡単です。 しかし、複雑なアプリケーションの場合、複数のコントローラーが必要です。コレクションとモデルのセットは、アプリケーション全体に共通であることが望ましいです。 Application



は私たちの助けになります-コントローラを単一のアプリケーションに結合する基本的なコンストラクターです。



アプリケーションのスケルトンは次のようになります。

 Application = { //Method that will initialize all controllers upon applicaiton launch initializeControllers: function() {}, // Set of methods to get existing model, get model constructor and create new model using constuctor getModel: function() {}, getModelConstructor: function() {}, createModel: function() {}, // Set of methods to get existing collection, get collectionconstructor and create new collectionusing constuctor getCollection: function() {}, getCollectionConstructor: function() {}, createCollection: function() {}, }
      
      





アプリケーションの起動時に、すべてのコレクションのインスタンスを即座に作成することも役立ちます。 また、アプリケーションの起動後に各コントローラーでコールバック関数を呼び出すと便利です。 このコールバックは、すべての予備データが準備できたときに呼び出す必要があります。 したがって、各コントローラーは、アプリケーションが動作する準備ができていることを「認識」します。 ためらうことなく、メソッドを追加します。

 // Create collection instances upon application start Application.buildCollections() // Initialize all application controllers Application.initializeControllers()
      
      







互いに通信するようにコントローラーに教えるだけです。 この目的のために、アプリケーションのすべてのコンポーネント間で通信を確立できるようにする別のエンティティを作成します。



 EventBus = { // Function to add event listeners addListeners: function() {}, // Function to fire event listneres fireEvent: function() {} }
      
      







基本オブジェクトが定義されたので、アプリケーションのすべての部分の具体的な実装を進めることができます。



アプリケーションの実装



メインコンストラクター-Applicationから始めましょう。 バックボーンと同じ方法で基本クラスを実装します。

 var Application = function(options) { _.extend(this, options || {}); // Create a new instance of EventBus and pass the reference to out application this.eventbus = new EventBus({application: this}); // Run application initialization if needed this.initialize.apply(this, arguments); // Create documentReady callback to lauch the application $($.proxy(this.onReady, this)); };
      
      







さらに、 _.extend



.extendを使用して、プロトタイプを拡張します。

 _.extend(Application.prototype, { // Hash maps to store models, collections and controllers models: {}, collections: {}, controllers: {}, /** * Abstract fuction that will be called during application instance creation */ initialize: function(options) { return this; }, /** * Called on documentReady, defined in constructor */ onReady: function() { // initialize controllers this.initializeControllers(this.controllers || {}); // call to controller.onLauch callback this.launchControllers(); // call application.lauch callback this.launch.call(this); }, /** * Function that will convert string identifier into the instance reference */ parseClasses: function(classes) { var hashMap = {}; _.each(classes, function(cls) { var classReference = resolveNamespace(cls), id = cls.split('.').pop(); hashMap[id] = classReference; }, this); return hashMap; }, /** * Abstract fuction that will be called during application lauch */ launch: function() {}, /** * Getter to retreive link to the particular controller instance */ getController: function(id) { return this.controllers[id]; }, /** * Function that will loop throught the list of collection constructors and create instances */ buildCollections: function() { _.each(this.collections, function(collection, alias) { this.getCollection(alias); }, this); } });
      
      







コントローラーを初期化するには、2つのメソッドが必要です。 Application.initializeControllers



は、コレクションおよびモデルのコレクションをインスタンス化および減算して、アプリケーション自体にリンクを直接格納します。 そして、 Application.launchControllers



は、すでに作成されたコントローラーを通過し、 Controller.onLaunch



コールバックを実行します。

 _.extend(Application.prototype, { ... /** * Fuction that will loop through all application conrollers and create their instances * Additionaly, read the list of models and collections from each controller * and save the reference within application */ initializeControllers: function(controllers) { this.controllers = {}; _.each(controllers, function(ctrl) { var classReference = resolveNamespace(ctrl), id = ctrl.split('.').pop(); // create new Controller instance and pass reference to the application var controller = new classReference({ id: id, application: this }); controller.views = this.parseClasses(controller.views || []); _.extend(this.models, this.parseClasses(controller.models || [])); _.extend(this.collections, this.parseClasses(controller.collections || {})); this.buildCollections(); this.controllers[id] = controller; }, this); }, /** * Launch all controllers using onLauch callback */ launchControllers: function() { _.each(this.controllers, function(ctrl, id) { ctrl.onLaunch(this); }, this); } ... });
      
      







コントローラー間の通信を確保し、特定のコンポーネントからのイベントをサブスクライブできるようにするために、 Application.addListeners



メソッドを追加しApplication.addListeners



メソッドは、作業をEventBusに委任します。

 _.extend(Application.prototype, { ... /** * Abstract fuction that will be called during application lauch */ addListeners: function(listeners, controller) { this.eventbus.addListeners(listeners, controller) } ... });
      
      







モデルとコレクションを操作するには、インスタンスへの参照、コンストラクターへの参照、および新しいエンティティを作成するメソッドを取得する関数が必要です。 例としてモデルを使用した特定の実装を検討してください;コレクションの場合、関数は同様に機能します。

 _.extend(Application.prototype, { ... /** * Getter to retreive link to the particular model instance * If model instance isn't created, create it */ getModel: function(name) { this._modelsCache = this._modelsCache || {}; var model = this._modelsCache[name], modelClass = this.getModelConstructor(name); if(!model && modelClass) { model = this.createModel(name); this._modelsCache[name] = model; } return model || null; }, /** * Getter to retreive link to the particular model consturctor */ getModelConstructor: function(name) { return this.models[name]; }, /** * Function to create new model instance */ createModel: function(name, options) { var modelClass = this.getModelConstructor(name), options = _.extend(options || {}); var model = new modelClass(options); return model; }, /** * Getter to retreive link to the particular collection instance * If collection instance isn't created, create it */ getCollection: function(name) { ... }, /** * Getter to retreive link to the particular collection consturctor */ getCollectionConstructor: function(name) { ... }, /** * Function to create new collection instance */ createCollection: function(name, options) { ... }, ... });
      
      







これで、アプリケーションの基本的なコンストラクタの準備が整いました。 Application.parseClasses



メソッドについて言及する必要がありApplication.parseClasses



。 実際、コントローラー、モデル、コレクション、およびビューのリストをストリングの配列として転送することにしました。 入り口に着く

 [ 'myApplication.controller.UserManager', 'myApplication.controller.FormBuilder' ]
      
      





Application.parseClasses関数は、この配列をマッピングに変換します

 { 'UserManager': myApplication.controller.UserManager, 'FormBuilder': myApplication.controller.FormBuilder }
      
      







そこで、2つの問題を解決します。 まず、すべてのリンクは、コンストラクターの名前と同じ一意の識別子に自動的に関連付けられます。 これにより、開発者は個々のエンティティの名前を気にする必要がなくなります。 次に、ベースパーツが使用可能になるのを待たずに、ベースパーツを決定できます。 これにより、ファイルをランダムな順序でアップロードできます。 リンクへの名前の解析は、すべてのスクリプトがロードされた後にのみ行われます。



コントローラーの実装



コントローラーは少し単純なコードを取得するため、モデルとコレクションのすべての作業をApplication



委任しApplication



。 開始するには、発表:



 var Controller = function(options) { _.extend(this, options || {}); this.initialize.apply(this, arguments); };
      
      





そして、プロトタイプを拡張できます

 _.extend(Controller.prototype, { views: {}, models: {}, collections: {}, initialize: function(options) { }, /** * Add new listener to the application event bus */ addListeners: function(listeners) { this.getApplication().addListeners(listeners, this); }, /** * Abstract fuction that will be called during application lauch */ onLaunch: function(application) { }, /** * Getter that will return the reference to the application instance */ getApplication: function() { return this.application; } });
      
      







ビューを操作するメソッドを追加します。

 _.extend(Controller.prototype, { ... /** * Getter that will return the reference to the view constructor */ getViewConstructor: function(name) { return this.views[name]; }, /** * Function to create a new view instance * All views are cached within _viewsCache hash map */ createView: function(name, options) { var view = this.getViewConstructor(name), options = _.extend(options || {}, { alias: name }); return new view(options); } ... });
      
      







モデルとコレクションの作業をApplication



委任しApplication





 _.extend(Controller.prototype, { ... /** * Delegate method to get model instance reference */ getModel: function(name) { return this.application.getModel(name); }, /** * Delegate method to get model constructor reference */ getModelConstructor: function(name) { return this.application.getModelConstructor(name); }, /** * Delegate method to create model instance */ createModel: function(name, options) { return this.application.createModel(name) }, /** * Delegate method to get collection instance reference */ getCollection: function(name) { return this.application.getCollection(name); }, /** * Delegate method to get collection constructor reference */ getCollectionConstructor: function(name) { return this.application.getCollectionConstructor(name); }, /** * Delegate method to create collection instance */ createCollection: function(name, options) { return this.application.createCollection(name); } ... });
      
      







そして最後に、コントローラーがApplication.EventBus



を使用して通信できるようにしApplication.EventBus





 _.extend(Controller.prototype, { ... /** * Delegate method to fire event */ fireEvent: function(selector, event, args) { this.application.eventbus.fireEvent(selector, event, args); } ... });
      
      





コントローラーの基本的なコンストラクターの準備ができました! 少しだけ残った:)



EventBusの実装



まず、コンストラクターについて説明します。 コントローラがビューからイベントをリッスンできるようにするには、 Backbone.View



基本プロトタイプをわずかに拡張する必要があります。 実際には、イベントを追跡する特定のセレクタが必要です。 これを行うために、コンポーネントの作成時に自動的に割り当てられるalias



プロパティを導入します。 そして、ネイティブのView.trigger()



を呼び出し、新しいイベントをEventBus



EventBus



するfireEvent



メソッドを追加します。



 var EventBus = function(options) { var me = this; _.extend(this, options || {}); // Extend Backbone.View.prototype _.extend(Backbone.View.prototype, { alias: null, /* * Getter that wll return alias */ getAlias: function() { return this.options.alias; }, /* * Instead of calling View.trigger lets use custom function * It will notify the EventBus about new event */ fireEvent: function(event, args) { this.trigger.apply(this, arguments); me.fireEvent(this.getAlias(), event, args); } }); };
      
      







これで、プロトタイプを安全に拡張できます。 EventBus.addListeners



を使用して新しいイベントをサブスクライブし、 EventBus.fireEvent



が目的のハンドラーを配置して実行します。

 _.extend(EventBus.prototype, { // Hash Map that will contains references to the all reginstered event listeners pool: {}, /** * Function to register new event listener */ addListeners: function(selectors, controller) { this.pool[controller.id] = this.pool[controller.id] || {}; var pool = this.pool[controller.id]; if(_.isArray(selectors)) { _.each(selectors, function(selector) { this.control(selector, controller); }, this) } else if(_.isObject(selectors)) { _.each(selectors, function(listeners, selector) { _.each(listeners, function(listener, event) { pool[selector] = pool[selector] || {}; pool[selector][event] = pool[selector][event] || []; pool[selector][event].push(listener); }, this); }, this) } }, /** * Function to execute event listener */ fireEvent: function(selector, event, args) { var application = this.getApplication(); _.each(this.pool, function(eventsPoolByAlias, controllerId) { var events = eventsPoolByAlias[selector]; if(events) { var listeners = events[event] controller = application.getController(controllerId); _.each(listeners, function(fn) { fn.apply(controller, args); }); } }, this); }, /** * Getter to receive the application reference */ getApplication: function() { return this.options['application']; } });
      
      







やった! これで、すべての主要部分が実装されました! 最後のタッチ

 Application.extend = Backbone.Model.extend; Controller.extend = Backbone.Model.extend;
      
      





これで、 extend



関数を使用して、ベースコンストラクターから継承を作成できます。



ドキュメントと例





公式のBackbone.Applicationページのソースファイルドキュメント



また、簡単な例を作成しました-これはMVCを使用した古典的なToDoです。 実装のソースとコメントはここにあります-github.com/namad/Backbone.Application/blob/master/examples/ToDo/js/todos.js



ボーナスとして、この自転車全体を書いたより複雑な例は、お気に入りのQuake LiveゲームのHUDエディターであるvisualHUDです。 現時点では、新しいバージョンはまだ開発中です。多くの小さなことを終える必要がありますが、一般に、すべての機能が動作し、自分の手でそれに触れることができます。 Googleコードの古いバージョンのソースコード



PSこれは、この性質の私の最初の記事であり、何が起こったのか分かりません:)それで、適切なレビューは金の重さの価値があります。 よろしくお願いします!



All Articles