📜 ⬆️ ⬇️

Writing MVC implementation for Backbone

image

One gloomy morning, I thought that it would be a good idea to thoroughly refactor one of my old projects. This is a non-commercial lightweight application for customizing HUD in one 3D shooter. I wrote it 2 years ago, was hot and inexperienced. As a result, a bunch of excellent spaghetti code, which, despite all its flaws, did its job. Having become wiser and more experienced, I decided to completely rewrite the application, give it a new architecture, simplify support and updating. How to do it? The answer seemed simple - to use MVC, divide it into levels, and tie everything together. So I was faced with the problem of choosing a simple and efficient framework that would become a solid foundation. After a quick study, I chose backbone.js . Loved its simplicity and flexibility. You can simply open the source and understand how everything works and works. The only nuance that didn’t please was the MV pattern. I didn’t want to blur logic on numerous views, so the idea was born to write my own bike, which would provide the missing pieces of the puzzle. Plus, creating something new is always exciting and interesting. Without thinking twice, I started implementing controllers for the backbone.

Task setting and implementation of basic methods


So, I need the ability to create controllers that link all parts of an application into one. Each controller should have access to all models and collections (both to basic designers, and to already created copies). It also requires the ability to create components (views) and be able to listen to their events in order to respond appropriately.

The controller skeleton will look like this:
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() {} } 

')
So far, everything is very simple. But for a complex application, we need to have several controllers, it is desirable that the set of collections and models be common to the entire application. This is how Application - a basic constructor that will merge controllers into a single application - comes to the rescue.

The skeleton of the application will look like this:
 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() {}, } 

It would also be useful to immediately create instances of all collections at the time the application starts. And it would be nice to call the callback function of each controller after the application starts. This callback must be called at the moment when all the preliminary data is ready. Thus, each controller will “know” that the application is ready for operation. Without hesitation, add methods:
 // Create collection instances upon application start Application.buildCollections() // Initialize all application controllers Application.initializeControllers() 


It remains only to teach the controllers to communicate with each other. For this purpose, we create another entity that will enable communication between all the components of the application.

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


Now that the base objects are defined, we can proceed to the concrete implementation of all parts of our application.

Application Implementation


Let's start with the main designer - Application . The base class is implemented in the same way as the backbone does.
 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)); }; 


Further, using _.extend , we extend the prototype:
 _.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); } }); 


To initialize our controllers, we need two methods. Application.initializeControllers will create instances and subtract sets of collections and models to store the links directly in the application itself. And Application.launchControllers will go through already created controllers and execute Controller.onLaunch callback.
 _.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); } ... }); 


To provide communication between controllers and give the opportunity to subscribe to events from specific components, let's add the Application.addListeners method, which delegates work to our EventBus:
 _.extend(Application.prototype, { ... /** * Abstract fuction that will be called during application lauch */ addListeners: function(listeners, controller) { this.eventbus.addListeners(listeners, controller) } ... }); 


To work with models and collections, we will need functions for obtaining a reference to an instance, references to a constructor, and a method for creating a new entity. Consider a specific implementation using models as an example; functions for collections will work in a similar way.
 _.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) { ... }, ... }); 


Now, our basic application constructor is ready. The Application.parseClasses method should be mentioned. The fact is that I decided to transfer lists of controllers, models, collections and view as an array of strings. Getting at the entrance
 [ 'myApplication.controller.UserManager', 'myApplication.controller.FormBuilder' ] 

The Application.parseClasses function will turn this array into a mapping.
 { 'UserManager': myApplication.controller.UserManager, 'FormBuilder': myApplication.controller.FormBuilder } 


So I solve 2 problems. First, all links will automatically be associated with a unique identifier that is equal to the name of the constructor. This will save the developer from having to bother with the names for each individual entity. Secondly, we can determine the basic parts, without waiting until they are available. This will allow you to upload files in random order. Parsing names into links will occur only after all the scripts have been loaded.

Controller implementation


The controller will get a little simpler code, so we delegate all work with models and collections to Application . To start the announcement:

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

And then you can expand the prototype
 _.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; } }); 


Add methods to work with Views:
 _.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); } ... }); 


We delegate work with models and collections to our 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); } ... }); 


And finally, let our controllers communicate using Application.EventBus
 _.extend(Controller.prototype, { ... /** * Delegate method to fire event */ fireEvent: function(selector, event, args) { this.application.eventbus.fireEvent(selector, event, args); } ... }); 

The base constructor for the controller is ready! Very little is left :)

EventBus implementation


To begin with we will describe the designer. To enable the controller to listen to events from the view, we need to slightly extend the basic Backbone.View prototype. The fact is that we need a certain selector by which events will be tracked. To do this, we introduce the alias property, which will automatically be assigned when the component is created. And add the fireEvent method, which will call the “native” View.trigger() and EventBus about the new event.

 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); } }); }; 


Now you can safely extend the prototype. We use EventBus.addListeners to subscribe to new events, and EventBus.fireEvent wears the necessary handler and executes it.
 _.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']; } }); 


Hooray! Now all the main parts are implemented! Final touch
 Application.extend = Backbone.Model.extend; Controller.extend = Backbone.Model.extend; 

Now we can create inherit from our basic constructors using the extend function.

Documentation and examples



Source files and documentation on the official Backbone.Application page

I also created a simple example - this is classic ToDo using MVC. Source code and comments for implementation can be viewed here - github.com/namad/Backbone.Application/blob/master/examples/ToDo/js/todos.js

And as a bonus, a more complex example, for which I wrote this whole bike - visualHUD , the HUD editor for my favorite game Quake Live. At the moment, the new version is still in development, you need to finish a bunch of small things, but in general, all the functionality works and you can feel it with your own hands. Who cares, the source of the old version of the Google code

PS This is my first article of a similar nature and I have no idea what happened :) So any adequate feedback is worth its weight in gold. Thank!

Source: https://habr.com/ru/post/149626/


All Articles