?

Log in

No account? Create an account
November 2016   01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

Backbone.js on Steroids

Posted on 2014.06.24 at 23:33
Tags: , , , , ,
Расскажу, пожалуй, про старую тему - разработку одностраничных JS-приложений. С тех пор, как я послежний раз об этом писал, прошло много времени - наверное, года 3. И с тех пор много чего изменилось. Появилось множество разных JS фреймворков, в моду вошел two-way databinding.

Однако, мэйнстримом (примерно 40% применений) на данный момент является Backbone.js, работающий в связке с jQuery и Underscore.js. Причиной этого, возможно, является его простота. Backbone прост в том смысле, что ему достаточно легко обучить команду, собирающуюся писать одностраничное приложение, и не имеющую в этом опыта. Это безусловный плюс backbone (как и его популярность), однако, его минусом является то, что он слишком прост. То есть, на голом backbone не так просто сделать что-то, кроме совсем простого.

Я расскажу про проблемы backbone, вытекающие из его простоты, и покажу, как их можно решить. Проблемы уйдут. Простота останется.


Начнем с элементарного. Простой модели - пусть это будет User.

	var User = Backbone.Model.extend({
		baseUrl : '/api/users',
	
		defaults : {
			name : "",
			email : ""
		}
	});


Пока проблем никаких нет, но они начинаются почти сразу, как только мы хотим сделать что-то полезное. Например - добавить к этой модели атрибут created, который будет содержать дату/время создания нашего пользователя. Человек, только начавший изучать бэкбон, скорее всего сделает примерно так.

	var User = Backbone.Model.extend({
		baseUrl : '/api/users',
	
		defaults : {
			name : "",
			email : "",
			created : new Date()
		}
	});


И мы не можем его за это осуждать - желание вполне естественное, и логика в нем есть. Только этот код работает совсем не так, как предполагал его автор.

Во-первых, этот new Date создается один раз при загрузке кода, и все экземпляры Users будут держать в created референс на один и тот же экземпляр Date. А "во-вторых" гораздо хуже. Штука в том, что в JSON такого типа данных, как Date - нет. При попытке сохранить это на сервер, а также получить эту модель с сервера, в поле created окажется вовсе не то, что автор ожидал увидеть.

Ужасно обидно получать по лбу граблями, когда даже толком не успел сделать шаг, но в итоге программист станет ученым, и станцует вокруг граблей примерно такой танец:

	var User = Backbone.Model.extend({
		baseUrl : '/api/users',
	
		defaults : function(){
			return {
				name : "",
				email : "",
				created : new Date()
			};
		},
		
		toJSON : function(){
			var toServer = Model.prototype.toJSON.apply( this, arguments );
			toServer.created = toServer.created.toJSON();
			return toServer;
		},
		
		parse : function( fromServer ){
			fromServer.created = new Date( fromServer.created );
			return fromServer;
		},
		
		validate : function( attrs ){
			if( !( attrs.created instanceof Date ) ){
				return new TypeError( "User.created is not Date" );
			}
		}
	});

	var user = new User(),
	    user2 = new User();
		
	user.set( 'created', new Date( '2012-12-12 12:12' ) );
	user2.set( 'created', user.get( 'created' ) );


Далее, когда программист захочет сделать наследование от какой-нибудь модели, его в кустах ждут маленькие такие, но очень прикольные грабельки. Сделаны с любовью. Ручная работа. Сначала программист будет верить в прекрасное, и попробует очевидный и прямолинейный способ:
var SuperUser = User.extend({
		defaults : function(){
			return {
				expiresAt : new Date()
			}
		}
	});


Это, как вы уже догадываетесь, совсем не тот способ, которым делаются простые вещи в backbone. Бэкбон попросту перетрет аттрибуты базового класса новыми, то есть - никакого created date by default не создастся. Надо (вернее сказать - не надо, но придется) как-то вот так:

	var SuperUser = User.extend({
		defaults : function(){
			return _.defaults({
					expiresAt : new Date()
				}, User.prototype.defaults() );
		},
	});


Но это ничего. Программист научится и этому, многому другому - например, не забывать вставлять куда надо get и set. И глаз его приобретет невероятную сноровку в разборе конструкций следующего вида:
model.set( 'nesting', some.get( 'thing' ).get( 'from' ).get( 'another' ).get( 'model );

вместо обычного
model.deep.nesting = some.thing.from.another.model;


Но о вложенных моделях поговорим в другой раз. Давайте для начала разберемся с элементарным - датами. Какой, однако, геморрой на нашу голову на ровном месте, да?

"Не должно быть так", скажет программист, изучающий backbone. И знаете что? Я с ним целиком и полностью согласен. Так быть не должно. Должно быть - вот так: программист берет свой первый вариант, и удалает из него лишнее. Пишет там вместо "new Date()" просто "Date". И волшебство! У него все-превсе сразу получается.

	var User = NestedTypes.Model.extend({
		baseUrl : '/api/users',
	
		defaults : {
			name : "",
			email : "",
			created : Date // use any constructor functions here to create new object by default,..
		}
			
		// ...and you'll get everything for free. No dances.
		// Object may implement 'toJSON' to be serialized properly
		// (as Date does, which is being automatically serialized to ISO)
		// Constructor will be invoked during 'parse'. Works fine with Date.
	});
	
	var SuperUser = User.extend({
		defaults : { // enjoy straightforward attribute inheritance 
			expiresAt : Date
		}
	});
	
	var user = new User(),
	    user2 = new User();
		
		
	// Also.
	user2.created = user.created; // You can now safely forget about get, and set.
	user.created = '2012-12-12 12:12'; // Enjoy automatic type coercion to Date on every 'set'.
	
	user.created = 'hjkfhfjdsk'; // ...and Invalid Date in 'created' (resulting in exception during toJSON) as result of type error.
	
	// The last but not least.
	// This thing, whenever received from server or manually set like this:
	user.set( 'attributeWithoutDefaultValue', '2012-12-12 12:12' )
	// will result in type error in console


Уже неплохо, правда? А ведь мы еще не добрались до nested models и коллекций. Хотите, чтобы было так? Сильно хотите?

Ну хорошо, хорошо. :) Я описал самую малую часть граблей, от которых вас освобождает вот этот мой маленький плагин к backbone:

https://github.com/Volicon/backbone.nestedTypes

Про остальное расскажу потом.

Comments:


rustler2000 at 2014-06-25 07:28 (UTC) (Link)
Про event bubbling расскажи. Из тестов, глядя на скорую руку, я так понял, что если надо хватать событие о изменение атрибута nested модели, то надо ручками писать?

По типу ```model.on('change:first.text', ...);```
Gaperton
gaperton at 2014-06-25 08:58 (UTC) (Link)
Надо почти как в обычном backbone (с поправкой на native properties). вот так: this.listenTo( model.first, 'change:text', ... ). Или model.first.on( 'change:text', ... )

Никакого хитрого синтаксиса событий плагин не добавляет. С native properties это не нужно.


Edited at 2014-06-25 09:12 am (UTC)
Gaperton
gaperton at 2014-06-25 09:06 (UTC) (Link)
А bubbling работает так. При любом изменении вложенной в атрибут коллекции или модели, генерируется change и change:attribute в главной модели. При этом, во время группового изменения наверх придет только один change - события склеиваются.

Это произойдет только в случае, если тип аттрибута указан как Model или Collection (или их подкласс).

Edited at 2014-06-25 09:08 am (UTC)
Previous Entry  Next Entry