📜 ⬆️ ⬇️

Backbone.js for dummies

Backbone.js for dummies
Sometime in the late evening I had the idea to study Backbone.js and bind it to the service already written on jQuery. The service has already seriously expanded, and this piling up of click handlers, queries and logic handlers has sufficed me. Therefore, I, as a diligent student, climbed into the official documentation. But either I'm stupid, or my English let me down, or both of them together, but I didn't understand a damn thing. I read it a second time, carefully, and used google translate for especially talented places. I also read the ToDo List example. Everything seemed clear, exactly until I started writing. After that, I took everything I found on this library, both in English and translations . After reading a pile of documentation, I decided that now I thought I understood everything. I tensed, but ... A stone flower did not come out from master Danila, i.e. it turned out, but it was clearly not a flower, and the stone somehow smelled wrong. Then, as a diligent student, I decided to write “Hello, Kitty World” from scratch. Along the way, commenting and saving the steps in hg, I got an introduction to the backbone.js framework for people like me, especially gifted.

Task.


Choose a simple task. Write Hello, World? Too easy, as well as writing Hello, <name>. Can we write a GTD client with authorization and offline storage? This is already there and it does not help to understand our “spinal bone”. Let's make it easier. Create a page with 3 states. In the first state, the person enters the user name, in the second state he is congratulated if the entered name is found, in the third state they are upset if the name is not found. In my opinion, this task is easier and easier to learn, and in general it will allow you to look and check almost everything in the backbone.

We will save all steps through mercurial . Therefore, reading any step, you can unpack the zip archive (+ dropbox , if you delete it on people), go to the directory and go to the desired revision using the command
hg update --rev < > 

Then look at the code and understand what you do not understand :)

Step 0. Structure and pattern (rev 0)


structure
We will use an academic structure, such as in the picture. I confess, I do not know who the author of this sacred bullet, from whom I licked this cow, but I use in all the blanks. Single index.html file. There are styles in the css folder, pictures in the i folder, scripts in js. At the same time, let's throw in jquery, underscore and backbone scripts.
The html template is a blank page. Those. A page with an empty body and connected scripts and style.
Those. As you can see, unlike some modern javascript mvc framework, the project does not require special preparation, so an existing project can be “rewritten” to the backbone.
')

Step 1. Initial layout (rev 1)



Our page, in accordance with the task, should have 3 states: the input of the user name, the state when successful comparison, the state when the comparison failed. To begin with we make 3 divas, each state in its place.

 <div id="start" class="block"> <div class="userplace"> <label for="username"> : </label> <input type="text" id="username" /> </div> <div class="buttonplace"> <input type="button" value="" /> </div> </div> <div id="error" class="block">     . </div> <div id="success" class="block">  . </div> 


We will hide the #error and #success blocks from our eyes away with CSS.

 #error, #success { ... display: none; } 


In this step, we have fully prepared everything for the introduction of the backbone. These steps are identical for many single-page site implementations.

Step 2. Implement Router (rev 2)



Before 0.5.0, this class was called Controller. Its purpose is to handle hash navigation in the application. Those. It has never been fully understood by the controller, just the hash navigation is the application controller. It can be seen the logic of the developers took over and now we have the class Router.
What is location.hash for what it is used, and how to use it correctly, you can read at Habré ( here , here or here ).

First, let's create an impromptu menu at index.html
 <div id="menu"> <!--   --> <ul> <li><a href="#!/">Start</a></li> <li><a href="#!/success">Success</a></li> <li><a href="#!/error">Error</a></li> </ul> </div> 


And then with a flick of the wrist we add the work of routing to the example:
 var Controller = Backbone.Router.extend({ routes: { "": "start", //  hash- "!/": "start", //   "!/success": "success", //   "!/error": "error" //   }, start: function () { $(".block").hide(); //    $("#start").show(); //   }, success: function () { $(".block").hide(); $("#success").show(); }, error: function () { $(".block").hide(); $("#error").show(); } }); var controller = new Controller(); //   Backbone.history.start(); //  HTML5 History push 


With such simple code we created a simplest tab-oriented site with the ability to bookmark pages.

Step 3. The simplest View (rev 3)


View in backbone is a mix of controller and View from a standard MVC model. It’s easier to say, View here is widget / component on a page that can display itself, react to events and create events. We'll fill up with what the creators will think about and rename View to Widget (or component), as they previously did with Router.
We have a generated username verification widget, this is the start block. Let's make it so that if the name “test” is entered, then we move on to the hash tag #! / Success, which will show the success block. And if something else is entered, then go to the hash tag #! / Error, which will show, respectively, the error block. By the way, we'll remove the menu in one go, we won't need it anymore.

 var Start = Backbone.View.extend({ el: $("#start"), // DOM  widget' events: { "click input:button": "check" //     "" }, check: function () { if (this.el.find("input:text").val() == "test") //   controller.navigate("success", true); //    success else controller.navigate("error", true); //    error } }); var start = new Start(); 


By the way, did you notice that after you went to the result page, you can safely go back by pressing the Backspace button? This is the magic of hash navigation.

Remark. JQuery way (rev 4)


Have you noticed how much code we have already written? I think so, many of those who have read it before will get cognac exclaimed: “On jQuery this is done faster and easier.” I do not argue. The code that should be written on the initial layout is very simple:
  $("#start input:button").click(function () { //    var username = $("#username").val(); //     $("#start").hide(); //    if (username == "test") $("#success").show(); //   else $("#error").show(); //   }); 

But ... This code does not support hash navigation, poorly expands and is very poorly supported.
It's no secret that the programmer in his work is creating new applications only 20% of the time. 80% of his time, he will dock the modules, fix errors and expand the functionality of already created projects. And jQuery noodle support can be very expensive. The obvious way to avoid hemorrhoids on the fingers is to do a decomposition of projects, for which most bicycles are invented. Backbone is a finished bike. Why invent something new when a good uncle did it for you?

Step 4. Working with View via Template (rev 5)


View would not be a View, but a controller if it did not know how to display itself. The backbone has no mechanism for this. Funny Not at all ... Its purpose is not to give a tool for creating an application, but to give a template, using which one could create the most supported system. Therefore, in the backbone, you can use different template engines. For example, the John Resig engine embedded in underscore.js. Or connect Microsoft Template. And if it’s smartly to dodge, you can implement everything through Knockout.js (although its dump of logic and templates strains me)
We will not strain and just use the _.template from underscore.js to implement our ideas. To do this, create one empty block on the page, and put all the “fillers” into templates. The page styles will change accordingly.
 <div id="block" class="block"> </div> <!--     --> <script type="text/template" id="start"> <div class="start"> <div class="userplace"> <label for="username"> : </label> <input type="text" id="username" /> </div> <div class="buttonplace"> <input type="button" value="" /> </div> </div> </script> <!--   --> <script type="text/template" id="error"> <div class="error"> .  <%= username %>  . <a href="#!/">Go back</a> </div> </script> <!--   --> <script type="text/template" id="success"> <div class="success">  <%= username %> . <a href="#!/">Go back</a> </div> </script> 


In order to show the dynamics, we added the username to the result templates.
Store the user name and pass it to the template, we will be in the variable AppState
 var AppState = { username: "" } 


Write a View for each template.
 var Views = { }; var Start = Backbone.View.extend({ el: $("#block"), // DOM  widget' template: _.template($('#start').html()), events: { "click input:button": "check" //     "" }, check: function () { AppState.username = this.el.find("input:text").val(); //    if (AppState.username == "test") //    controller.navigate("success", true); //    success else controller.navigate("error", true); //    error }, render: function () { $(this.el).html(this.template()); } }); var Success = Backbone.View.extend({ el: $("#block"), // DOM  widget' template: _.template($('#success').html()), render: function () { $(this.el).html(this.template(AppState)); } }); var Error = Backbone.View.extend({ el: $("#block"), // DOM  widget' template: _.template($('#error').html()), render: function () { $(this.el).html(this.template(AppState)); } }); Views = { start: new Start(), success: new Success(), error: new Error() }; 

Comment. We have 3 views referenced to the same DOM element. In reality, this should not be. Logically, this should be a single widget. I deliberately incorrectly designed this step in order to show the possibility of working with several View. Later I will show how to avoid this puncture.

The controller will also undergo minor changes.
 var Controller = Backbone.Router.extend({ routes: { "": "start", //  hash- "!/": "start", //   "!/success": "success", //   "!/error": "error" //   }, start: function () { if (Views.start != null) { Views.start.render(); } }, success: function () { if (Views.success != null) { Views.success.render(); } }, error: function () { if (Views.error != null) { Views.error.render(); } } }); 

This is how we added dynamics to our application.

Step 5. Check for multiple users (rev 6)


The easiest way to check not only for test, but also for other users, is to check for finding the name in the array of users.
Create an array of Family, in which we fill in all the names.
 var Family = ["", "", ""]; //   

And we will do the check in the Start view code. Because underscore is already included in the application, let's do it through _.detect
 ... if (_.detect(Family, function (elem) { return elem == AppState.username })) //    ... 

What problems does this solution have? The main problem is that if tomorrow we need to change the physical location of the array of users (server, localstore, etc.), then we will have to change the logic of View. Those. View is so tied to the data access method that you have to change its code at the slightest sneeze.

Remark 2. jQuery way. Continued (rev 7)


But how much code did we have to add to jQuery code to support multiple users? Very little:
 $(function () { var Family = ["", "", ""]; $("#start input:button").click(function () { //    var username = $("#username").val(); //     $("span.username").text(username); $("#start").hide(); //    $("#" + ($.inArray(username, Family) != -1 ? "success" : "error")).show(); }); $("#error a, #success a").click(function () { $(".block").hide(); $("#start").show(); }); }); 

Well and accordingly to correct layout layout:
  ... <div id="error" class="block"> <!--   --> .  <span class="username"></span>  . <a href="javascript:void(0);">Go back</a> </div> <div id="success" class="block"> <!--   -->  <span class="username"></span> . <a href="javascript:void(0);">Go back</a> </div> ... 

Remember I said something about support, 80/20% and other dregs? So here. Forget it. There is nothing shameful for this application to write jQuery way code. You will spend time 10-20 times less than writing it all through Backbone. And the size of the code allows you to maintain this application at least after half a liter. There is nothing shameful to write in this way and earn your $ 5. Who does not agree, let his opinion stick in the comment.
I like to repeat the phrase that all the frameworks serve 2 goals, to make a billion project, a project for a million, and a project for $ 100 - a project for a couple of millions. Use the fact that saves you time and money more effectively.

Step 6. Application controller via Model (rev 8)


Backbone's great feature is the ability to link a model and a view. If you create a view with the model parameter, then in the initialize method of the view you can subscribe to the occurrence of a model change event. After that, View will receive messages with each change of the model or its part. And already on this message to attach a certain behavior of the provision, for example, its full or partial redrawing.
Remember I said that it is ugly when the same block is processed by several View. Let's try to get rid of this dominance handlers.
To begin with, from the AppState object, create a model that will contain the user name and application state:
 var AppState = Backbone.Model.extend({ defaults: { username: "", state: "start" } }); var appState = new AppState(); 

The second step is to remove the Success and Error views, and rename the view Start to Block, since it will handle several states, not just the start state. In the remaining view, rename the template field to templates in which all templates for different states will be stored.
  var Block = Backbone.View.extend({ templates: { //     "start": _.template($('#start').html()), "success": _.template($('#success').html()), "error": _.template($('#error').html()) }, 

In the view initializer, we subscribe to the model change event. On this event, hang the block redraw.
  initialize: function () { //     this.model.bind('change', this.render, this); }, 

The redraw function (render) will “draw” our main model with an appropriate template, depending on the state field of the model:
  render: function () { var state = this.model.get("state"); $(this.el).html(this.templates[state](this.model.toJSON())); return this; } 

The check function will also change. It will set the appropriate model fields:
  check: function () { var username = this.el.find("input:text").val(); var find = (_.detect(Family, function (elem) { return elem == username })); //    appState.set({ //      "state": find ? "success" : "error", "username": username }); }, 

By the way, after all these cases, we will not see anything, because The model was created before we described the View. Therefore, we will fire the change event after we create the View:
  var block = new Block({ model: appState }); appState.trigger("change"); 


If we didn’t have a navigation hash, I’d finish this step. But we flew navigation. Restore it. To do this, we will rewrite the router code.
  var Controller = Backbone.Router.extend({ routes: { "": "start", //  hash- "!/": "start", //   "!/success": "success", //   "!/error": "error" //   }, start: function () { appState.set({ state: "start" }); }, success: function () { appState.set({ state: "success" }); }, error: function () { appState.set({ state: "error" }); } }); 

Manual navigation is working, but when you click on the Check button, the page hash of the address does not change. Fix this annoying flaw by subscribing to the event of changing the state field of the model:
  appState.bind("change:state", function () { //       var state = this.get("state"); if (state == "start") controller.navigate("!/", false); // false ,     //    Router else controller.navigate("!/" + state, false); }); 

Events is a separate song in the backbone. The simplest events, DOM events, and model or collection change events can be intertwined with the events described by the user, forming a wonderful vintage of object-oriented and event-oriented programming. I advise you to study them before you start using Backbone.js in your project.

That's all with the biggest refactoring in this small project. And for the future, start designing the system with models, and not with View as I did, and your hair will, just will.

Step 7. Check for multiple users through the collection (rev 9)


What we implemented in step 5 has its drawback. We have mixed the display logic with the data management logic. We can not just now, without rearranging the logic of the View, to replace our array with the appeal to the service. For these purposes, collections are used in Backbone.
The collection in this framework is a sorted set of models that can handle, filter, or sort these models. Also collections are able out of the box to work with services on the REST interface. In fact, this is a layer between the widget and the database access methods.
Let's return from reasoning to our problem. Create a UserNameModel model. The only required field of this model will be the Name field, which by default has an empty value.
  var UserNameModel = Backbone.Model.extend({ //   defaults: { "Name": "" } }); 

Create a Family collection of UserNameModel models
  var Family = Backbone.Collection.extend({ //   model: UserNameModel, }); 

Add to the collection a method for checking if a user has the specified name in this collection.
  checkUser: function (username) { //   var findResult = this.find(function (user) { return user.get("Name") == username }) return findResult != null; } 

Create an instance of the Family collection.
  var MyFamily = new Family([ //   { Name: "" }, { Name: "" }, { Name: "" } ]); 

After that, the user check in View is reduced to calling the check method in the MyFamily instance
 var find = MyFamily.checkUser(username); //    


Conclusion


In the course of the article, we created an educational project that does not do a fig, but which does not fully cover my framework of this framework. The resulting file contains approximately 200 lines of code. This is more than the jQuery version, but these are good, easily extensible strings. Objects know about each other the necessary minimum and not more than that.
Backbone surprisingly turned out to be a good product that allows you to build a ridge for your service. It provides a platform for creating one-page services and various large dynamic applications. As already shown in the notes, it is sometimes unprofitable to use it sometimes, on small projects. But as soon as we grow, and the complexity of supporting our application grows exponentially, using the backbone can significantly reduce the labor costs for support, leaving time for the development of new functionality.

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


All Articles