Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to understand the skills and patterns of Backbone.js

2025-02-22 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

This article will explain in detail the skills and patterns of how to understand Backbone.js. The content of the article is of high quality, so the editor will share it for you as a reference. I hope you will have a certain understanding of the relevant knowledge after reading this article.

Backbone.js is an open source JavaScript "MV*" framework that received a significant boost when it was first released three years ago. Although Backbone.js provides its own structure for Javascript applications, it leaves a large number of design patterns and decisions that developers use according to the needs of developers, and developers encounter many common problems when they develop with Backbone.js for the first time.

So, in this article, in addition to exploring a variety of design patterns that you can apply to your Backbone.js application, we will also focus on some common questions that confuse developers.

Perform deep replication of objects

The passing of all primitive type variables in JavaScript is value passing. Therefore, the value of the variable is passed when it is referenced.

Var helloWorld = "Hello World"; var helloWorldCopy = helloWorld

For example, the above code sets the value of helloWorldCopy to the value of helloWorld. So any changes to helloWorldCopy will not change helloWorld because it is a copy. On the other hand, the passing of all non-primitive type variables in JavaScript is reference passing, meaning that when the variable is referenced, JavaScript passes a reference to its memory address.

Var helloWorld = {'hello':' world'} var helloWorldCopy = helloWorld

For example, the above code sets helloWorldCopy as an alias for the helloWorld object, and you might guess that all changes to helloWorldCopy will be made directly on the helloWorld object. If you want to get a copy of the helloWorld object, you must make a copy of the object.

You may wonder, "Why does he (the author) explain these things passed by citation in this article?" Well, this is because there is no object replication in Backbone.js object delivery, which means that if you call the get () method in a model to get an object, then any changes to it will be operated directly on that object in the model! Let's use an example to see when it becomes a problem for you. Suppose you now have a Person model like this:

Var Person = Backbone.Model.extend ({defaults: {'name':' John Doe', 'address': {' street': '1st Street'' city': 'Austin',' state': 'TX'' zipCode': 78701})

And suppose you create a new person object:

Var person = new Person ({'name':' Phillip W'})

Now let's make a few changes to the properties of the new object person.

Person.set ('name',' Phillip W.')

The above code modifies the name property in the person object. Next let's try to modify the address property of the person object. Before we do that, let's add a check to the address property.

Var Person = Backbone.Model.extend ({validate: function (attributes) {if (isNaN (attributes.address.zipCode)) return "Address ZIP code must be a number!" }, defaults: {'name':' John Doe', 'address': {' street': '1st Street'' city': 'Austin',' state': 'TX'' zipCode': 78701}})

Now we will try to use an incorrect ZIP code to modify the address property of the object.

Var address = person.get ('address'); address.zipCode =' Hello World';// should generate an error because the ZIP code is invalid person.set ('address', address); console.log (person.get (' address')); / * print an object containing the following attributes. {'street':' 1st Street' 'city':' Austin', 'state':' TX' 'zipCode':' Hello World'} * /

Why is this? Hasn't our verification method already returned an error?! Why is the attributes property still changed? The reason, as mentioned earlier, is that Backbone.js does not copy the model's attributes object; it just returns what you request. So, you may guess that if you request an object (such as the address above), you will get a reference to that object, and all your modifications to that object will be directly manipulated in the actual object in the model (so this modification will not lead to validation failure, because the object reference has not changed). This problem is likely to cause you to spend hours debugging and diagnosing.

This problem will catch some novice Backbone,js users and even experienced but not vigilant JavaScript developers. This issue has caused a lot of discussion in the Backbone.js section of GitHub issues. As Jeremy Ashkenas pointed out, performing deep replication is a very thorny problem, and it can be a very expensive operation for objects with greater depth.

Fortunately, jQuery provides some deep copy implementations, $. Extend. By the way, a dependent plug-in for Underscore.js,Backbone.js provides a similar method _. Extend, but I will avoid using it because it does not perform deep replication.

Var address = $.extend (true, {}, person.address)

We now have an exact copy of the address object, so we can modify its contents at will without worrying about changing to the address object in person. You should be aware that this pattern applies to the example above only because all members of the address object are primitive values (numbers, strings, etc.), so you must use it carefully when the deeply copied object also contains sub-objects. You should know that performing a deep copy of an object has a small performance impact, but I've never seen it cause any obvious and visible problems. However, if you perform a deep copy of a complex object or thousands of objects at a time, you may want to do some performance analysis. This is the reason for the next pattern.

Create a Facades for an object

In the real world, requirements change frequently, so do the JSON data that is returned from the terminal through queries of models and collections. If your view is tightly coupled to the underlying data model, this will make you feel very troublesome. Therefore, I created fetchers and setters for all objects.

Many people are in favor of this model. That is, if any underlying data structure is changed, the view layer should not be updated too much; when you have only one data entry, you are less likely to forget to perform a deep copy, and your code will become more maintainable and debuggable. But the downside is that this model will swell your models and collections a little bit.

Let's use an example to clarify this pattern. Suppose we have a Hotel model that contains rooms and currently available rooms, and we want to be able to obtain the corresponding rooms through the bed size value.

Var Hotel = Backbone.Model.extend ({defaults: {"availableRooms": ["a"], "rooms": {"a": {"size": 1200, "bed": "queen"}, "b": {"size": "bed": "twin"}, "c": {"size": 1100, "bed": "twin"}}, getRooms: function () {$.extend (true, {}, this.get ("rooms")) }, getRoomsByBed: function (bed) {return _ .where (this.getRooms (), {"bed": bed});})

Let's assume that you will release your code tomorrow, and the terminal developer forgot to tell you that the data structure of rooms has changed from Object to array. Your code now looks like this:

Var Hotel = Backbone.Model.extend ({defaults: {"availableRooms": ["a"], "rooms": [{"name": "a", "size": 1200, "bed": "queen"}) {"name": "b", "size": 900, "bed": "twin"}, {"name": "c", "size": 1100 "bed": "twin"}], getRooms: function () {var rooms = $.extend (true, {}, this.get ("rooms")), newRooms = {} / / transform rooms from an array back into an object _ .each (rooms, function (room) {newRooms [room.name] = {"size": room.size, "bed": room.bed}}) }, getRoomsByBed: function (bed) {return _ .where (this.getRooms (), {"bed": bed});})

In order to transform the Hotel into the data structure expected by the application, we have only updated one method, which allows our entire App to still work properly. If we didn't create a fetcher for rooms data, we might have to update the data entry for each rooms. Ideally, you would want to update all interface methods in order to use a new data structure. But if you have to release the code as soon as possible because of time constraints, this pattern can save you.

By the way, this pattern can be thought of as an facade design pattern because it hides the details of object replication, and it can also be called a bridge design pattern because it can be used to transform desired data structures. So it's a good habit to use fetchers and setters on all objects.

Store data but not synchronize to the server

Although Backbone.js dictates that models and collections are mapped to REST-ful terminals, you sometimes find that you just want to store data in models or collections and not synchronize them to the server. Some other articles about Backbone.js, such as "Backbone.js Tips: Lessons From the Trenches", have explained this pattern. Let's quickly use an example to see when this pattern will come in handy. Suppose you have a ul list.

One Two. . . N

When the n value is 200 and the user clicks on one of the list items, that list item is selected and a class is added to display it visually. One way to implement it is as follows:

Var Model = Backbone.Model.extend ({defaults: {items: [{"name": "One", "id": 1}, {"name": "Two", "id": 2}) {"name": "Three", "id": 3}]}}) Var View = Backbone.View.extend ({template: _ .template ($('# list-template'). Html ()), events: {"# items li a": "setSelectedItem"}, render: function () {$(this.el) .html (this.template (this.model.toJSON ();}, setSelectedItem: function (event) {var selectedItem = $(event.currentTarget) / / Set all of the items to not have the selected class $('# items li a'). RemoveClass ('selected'); selectedItem.addClass (' selected'); return false;}}); = 0; iMel -) {% >

Now we want to know which item is selected. One way is to traverse the entire list. But if the list is too long, it can be an expensive operation. Therefore, when the user clicks on the list item, we should store it.

Var Model = Backbone.Model.extend ({defaults: {selectedId: undefined, items: [{"name": "One", "id": 1}, {"name": "Two", "id": 2}) {"name": "Three", "id": 3}]}}) Var View = Backbone.View.extend ({initialize: function (options) {/ / Re-render when the model changes this.model.on ('change:items', this.render, this)) }, template: _ .template ($('# list-template'). Html ()), events: {"# items li a": "setSelectedItem"}, render: function () {$(this.el) .html (this.template (this.model.toJSON ();}, setSelectedItem: function (event) {var selectedItem = $(event.currentTarget) / / Set all of the items to not have the selected class $('# items li a'). RemoveClass ('selected'); selectedItem.addClass (' selected'); / / Store a reference to what item was selected this.model.set ('selectedId', selectedItem.data (' id')); return false;}})

Now we can easily search our model to determine which item is selected, and we avoid traversing the document object model (DOM). This pattern is useful for storing some of the external data you want to track; keep in mind that you can create models and collections that do not need to be associated with the terminal.

The negative effect of this pattern is that your model or collection does not really adopt the RESTful architecture because they are not perfectly mapped to network resources. In addition, this pattern will inflate your model a little bit; and if your terminal strictly receives only the JSON data it expects, it will cause you a little bit of trouble.

Render part of the view instead of the entire view

When you develop a Backbone.js application for the first time, your view will generally look like this:

Var View = Backbone.View.extend ({initialize: function (options) {this.model.on ('change', this.render, this);}, template: _ .template ($(' # template'). Html ()), render: function () {this.$el.html (template (this.model.toJSON ()); $('# atemplate, this.$el) .html (this.model.get ('a')) $('# baked, this.$el) .html (this.model.get ('b'));}})

Here, any changes to your model will trigger a complete re-rendering of the view. When I first used Backbone.js for development, I also used this pattern. But as my code swelled, I quickly realized that this approach was unmaintainable and unideal, because any change in the properties of the model would cause the view to be completely re-rendered.

When I encountered this problem, I immediately searched Google for how other people did it and found an article written by Ian Storm Taylor's blog, "Break Apart Your Backbone.js Render Methods," in which he mentioned ways to listen for individual property changes in the model and respond to only part of the view re-rendering. Taylor also mentioned that the re-render method should return its own this object so that those individual re-render methods can be easily concatenated. The following example has been modified to make it easier to maintain and manage, because we only update the view of the corresponding part when the model properties change.

Var View = Backbone.View.extend ({initialize: function (options) {this.model.on ('change:a', this.renderA, this); this.model.on (' change:b', this.renderB, this);}, renderA: function () {$('# asides, this.$el) .html (this.model.get ('a')); return this }, renderB: function () {$('# baked, this.$el) .html (this.model.get ('b')); return this;}, render: function () {this .renderA () .renderB ();}})

It is also mentioned that many plug-ins, such as Backbone.StickIt and Backbone.ModelBinder, provide key-value binding between view elements and model properties, which can save you a lot of similar code. So, if you have a lot of complex form fields, you can try using them.

Keep the model and view separate

As Jeremy Ashkenas pointed out in Backbone.js 's GitHub issues, Backbone.js does not implement any real separation of concerns between the data layer and the view layer, except that models cannot be created by their views. Do you think the separation of concerns should be implemented between the data layer and the view layer? Other Backbone.js developers, such as Oz Katz and Dayal, and I agree that there is no doubt that the answer should be yes: models and collections, which represent the data layer, should disable any entry to their views, thus maintaining a complete separation of concerns. If you don't follow this separation of concerns, your code will soon become as tangled as spaghetti, and no one will like it.

Keeping your data layer and view layer completely separate can make you have more modular, reusable and maintainable code. You can easily reuse and extend models and collections in your application without worrying about the views that are bound to them. Following this pattern allows new developers to join the project quickly into the code. Because they know exactly where view rendering occurs and where the application's business logic is stored.

This pattern also enforces the principle of single responsibility, which states that each class should have only one separate responsibility, and its responsibilities should be encapsulated in this class, because your model and collection should only be responsible for processing data. Views should only be responsible for rendering.

Parameter Mapping in Router

Using examples is the best way to show how this pattern is generated. For example, there are search pages that allow users to add two different filter types, foo and bar, each with a large number of options. So, your URL structure will look like this:

'search/:foo' 'search/:bar'' search/:foo/:bar'

Now, all routes use an exact view and model, so ideally, you'd be happy to use the same method, search (). However, if you check Backbone.js, you will find that there is no parameter mapping of any kind; these parameters are simply thrown into the method from left to right. So, in order for them to all use the same method, you eventually have to create different methods to correctly map parameters to the search () method.

Routes: {'search/:foo':' searchFoo', 'search/:bar':' searchBar', 'search/:foo/:bar':' search'}, search: function (foo, bar) {}, / / I know this function will actually still map correctly, but for explanatory purposes, it's left in. SearchFoo: function (foo) {this.search (foo, undefined);}, searchBar: function (bar) {this.search (undefined, bar);}

As you might think, this mode will quickly inflate your routing. When I first used the contact pattern, I tried to use regular expressions to do some parsing in the actual method definition to "magically" map these parameters, but this only worked if the parameters were easy to distinguish. So I abandoned this method (I still use it in Backbone plug-ins sometimes). I raised this issue on issue on GitHub, and Ashkenas advised me to map all parameters in the search method.

The following code has become more maintainable:

Routes: {'base/:foo':' search', 'base/:bar':' search', 'base/:foo/:bar':' search'}, search: function () {var foo, bar, I; for (I = arguments.length-1; I > = 0; iMel -) {if (arguments [I] = = 'something to determine foo') {foo = arguments [I] Continue;} else if (arguments [I] = = 'something to determine bar') {bar = arguments [I]; continue;}

This mode can completely reduce the expansion of the router. However, be aware that it is not valid for unrecognized parameters. For example, if you have two parameters that pass ID and both of them are represented in XXXX-XXXX mode, you will not be able to determine which ID corresponds to which parameter.

Model.fetch () will not erase your model.

This problem usually stumbles novice users of Backbone.js: model.fetch () does not clean up your model, but merges the retrieved data into your model. So, if your current model has the XGraI journal y and z attributes and you get a new y and z value through fetch, then x will keep the original value of the model, and only the values of y and z will be updated. The following example visually illustrates this concept.

Var Model = Backbone.Model.extend ({defaults: {x: 1, y: 1, z: 1}); var model = new Model (); / * model.attributes yields {x: 1, y: 1, z: 1} * / model.fetch () / * let's assume that the endpoint returns this {y: 2, z: 2,} * / / * model.attributes now yields {x: 1, y: 2, z: 2} * /

The PUT request requires an ID attribute

This problem usually occurs only among newcomers to Backbone.js. When you call the .save () method, you send a HTTP PUT request that your model has an ID property set. HTTP PUT is designed as an update action, so it makes sense to send a PUT request that your model already has an ID attribute. In an ideal world, all your models would have a property called id, but the reality is that the ID attribute of the JSON data you receive from the terminal does not always happen to be named id.

So, if you need to update your model, make sure your model has an ID before saving it. Version 0.5 or later of Backbone.js allows you to use idAttribute to change the name of the ID property when the name of the ID property variable returned by the terminal is not idAttribute.

If you are using a version of Backbone.js that is still less than 0.5, I recommend that you modify the parse method in the collection or model to map the desired ID property to the real ID property. Here is an example that allows you to quickly master this technique. Let's assume that you have a cars collection whose ID property is called carID.

Parse: function (response) {_ .each (response.cars, function (car, I) {/ / map the returned ID of carID to the correct attribute ID response.cars.cars.id = response.cars.carID;}); return response;}

Model data in page loading

Sometimes you will find that you need to initialize your collection and model with data when the page is loaded. Some articles about the Backbone.js pattern, such as Rico Sta Cruz's "Backbone Patterns" and Katz's "Avoiding Common Backbone.js Pitfalls," talk about this pattern. Using the server language of your choice, you can easily implement this pattern by embedding code into the page and placing the data in the properties or JSON of a single model. For example, in Rails, I would use this:

/ / a single attribute var model = new Model ({hello:}); / / or to have json var model = new Model ()

Use this mode to improve your search engine rankings by "rendering your page right away", and it can completely reduce the time it takes to start and run your application by limiting its HTTP requests.

Model properties that deal with validation failures

You often wonder which model properties failed to validate. For example, if you have an extremely complex form field, you may want to know which model property validation failed so that you can highlight the corresponding form field. Unfortunately, reminding your view of which model property validation failed is not directly implemented in Backbone.js, but you can use different schemas to deal with this problem.

Return an error object

A pattern that informs your view of which model property validation fails is to return an object with a certain flag detailing which model property validation failed, as follows:

/ / Inside your model validate: function (attrs) {var errors = []; if (attrs.a < 0) {errors.push ({'message':' Form field an is messed upbringing, 'class': 'a'}) } if (attrs.b < 0) {errors.push ({'message':' Form field b is messed upsetting, 'class':' b'});} if (errors.length) {return errors }} / / Inside your view this.model.on ('invalid', function (model, errors) {_ .each (errors, function (error, I) {$('.'+ error.class) .addClass ('error'); alert (error.message);});})

The advantage of this model is that you deal with all the invalid information in one place. The disadvantage is that if you deal with different invalid attributes, your property validation will become a larger switch or if statement.

Broadcast traditional error events

An alternative pattern suggested by my friend Derick Bailey is to trigger custom error events for individual model properties. This allows your view to bind specified error events for individual properties.

/ / Inside your model validate: function (attrs) {if (attrs.a < 0) {this.trigger ('invalid:a',' Form field an is messed upsetting, this);} if (attrs.b < 0) {this.trigger ('invalid:b',' Form field b is messed upsetting, this) }} / / Inside your view this.model.on ('invalid:a', function (error) {$(' a'). AddClass ('error'); alert (error);}); this.model.on (' invalid:b', function (error) {$('b'). AddClass ('error'); alert (error);})

The advantage of this pattern is that your view is bound to a specific type of error event, and if you have clear instructions for each type of property error, it can clean up your view code and make it more maintainable. One drawback of this pattern is that if there are too many different attribute errors that you want to handle, your view code will become more bloated.

Both patterns have their advantages and disadvantages, so you should consider which pattern is more suitable for your use case. If you want to use one method for all validation failures, the first method is a good choice; if each of your model properties has explicit UI changes, the second method is better.

HTTP status code 200trigger error

If the terminal accessed by your model or collection returns invalid JSON data, they will trigger a "error" event, even if your terminal returns a HTTP status code of 200. This usually occurs when doing local development based on simulated JSON data. So a good idea is to throw all the simulation JSON files you are developing into the JSON validator for verification. Or install a plugin for your IDE that can catch any malformed JSON.

Create a generic error display

Creating a common error display means that you have a unified pattern for handling and displaying error messages, which can save you time and improve the user's overall experience. In any Backbone.js application I've developed, I've created a generic view to handle warnings.

Var AlertView = Backbone.View.extend ({set: function (typeOfError, message) {var alert = $('.in-page-alert'). Length? $(' .in-page-alert'): $('.body-alert'); alert .removeClass (' error success warning') .html (message) .fadeIn () .delay (5000) .fadeOut () })

The above view first checks to see if in-page-alert div has been declared within the view. If it is not declared, it returns to the generic body-alert div that is declared somewhere in the layout. This allows you to send a consistent error message to your users and provide a valid backup in-page-alert div if you forget to specify a specific div. The above pattern simplifies your handling of error messages in the view, as shown below:

This.model.on ('error', function (model, error) {alert.set (' TYPE-OF-ERROR', error);})

Update the document title applied on a single page

This is more useful than focusing on anything. If you are developing a single-page application, please remember to update the document title of each page! I wrote a simple Backbone.js plug-in, Backbone.js Router Title Helper, which implements this function simply and elegantly by extending the Backbone.js router. It allows you to specify an object constant for a title, its key is mapped to the method name of the route, and the value is the page title.

Backbone.Router = Backbone.Router.extend ({initialize: function (options) {var that = this; this.on ('route', function (router, route, params) {if (that.titles) {if (that. Titles [router]) document.title = that.titles [router]; else if (that.titles.default) document.title = that.titles.default Else throw 'Backbone.js Router Title Helper: No title found for route:' + router +' and no default route specified.';}});})

Caching objects in a single-page application

When we talk about single-page applications, another pattern you need to follow is to cache objects that will be reused. This technique is quite simple and straightforward:

/ / Inside a router initialize: function () {this.cached = {view: undefined, model: undefined}}, index: function (parameter) {this.cached.model = this.cached.model | | new Model ({parameter: parameter}); this.cached.view = this.cached.view | | new View ({model: this.cached.model});}

This mode will make your application as fast as pebbles, because you don't need to reinitialize your Backbone.js object. However, it may cause your application to take up quite a lot of memory; therefore, I usually only cache objects that run through the entire application. If you have developed Backbone.js applications before, you may ask yourself, "what if I want to retrieve the data?" Then you can retrieve the data every time the route is triggered.

/ / Inside a router initialize: function () {this.cached = {view: undefined, model: undefined}}, index: function (parameter) {this.cached.model = this.cached.model | | new Model ({parameter: parameter}); this.cached.view = this.cached.view | | new View ({model: this.cached.model}); this.cached.model.fetch ();}

This mode works well when your application has to retrieve the latest data from the terminal (for example, an inbox). However, if the data you are retrieving depends on the state of the application (assuming the state is maintained by your URL and parameters), you will update the data even if the state of the application has not changed since the user last visited the page. A better solution is to update the data only when the parameter of the application is changed.

/ / Inside a router initialize: function () {this.cached = {view: undefined, model: undefined}}, index: function (parameter) {this.cached.model = this.cached.model | | new Model ({parameter:parameter}); this.cached.model.set ('parameter', parameter); this.cached.view = this.cached.view | | new View ({model: this.cached.model}) } / / Inside of the model initialize: function () {this.on ("change:parameter", this.fetchData, this);}

JSDoc functions and classes for Backbone.js

I love documenting and am a big fan of JSDoc, and I use JSDoc to document all Backbone classes and methods that follow the format shown below:

Var Thing = Backbone.View.extend (/ * * @ lends Thing.prototype * / {/ * * @ class Thing * @ author Phillip Whisenhunt * @ augments Backbone.View * @ contructs Thing object * / initialize () {}, / * * Gets data by ID from the thing. If the thing doesn't have data based on the ID, an empty string is returned. * @ param {String} id The id of get data for. * @ return {String} The data. * / getDataById: function (id) {}})

If you document the Backbone class in this format above, you can document a beautiful document that contains all your classes and methods with parameters, return values, and descriptions. Make sure that initialize is always the first declared method, as it helps to generate JSDoc. If you want to see an example of a project that uses JSDoc, please refer to HomeAway Calendar Widget. There is a Grunt.js plug-in, the grunt-jsdoc-plugin plug-in, which uses them to generate documentation as part of the build process.

Practice test-driven development

In my opinion, if you are using Backbone.js, you should make your model and collection follow test-driven development (TDD). I began to follow TDD by writing failed Jasmine.js unit tests for my models and collections for the first time. Once the unit tests I wrote were written and failed, I expelled the models and collections. With this, all my Jasmine tests will be passed, and I am confident that all of my modeling methods will work as expected. Because I always follow TDD, my view layer is already quite easy to write and extremely thin. When you first start to implement TDD, you will definitely slow down, but once you go deep into it, your productivity and code quality will be greatly improved.

On how to understand Backbone.js skills and patterns to share here, I hope that the above content can be of some help to you, can learn more knowledge. If you think the article is good, you can share it for more people to see.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report