Pull to refresh

Comments 27

Спасибо за обзор!
Тоже сделали соцсеть на базе Parse а-ля Instagram, но пока до лимитов на запросы она не добралась. Надеюсь, Parse работает над этой проблемой, иначе дорого.
А как вы делаете UI? Используете ли сториборды, ксибы?
В этом проекте все было реализовано на ксибах. А вообще я не против использования сторибордов.
чем плохи сториборды? (спрашиваю как совсем новичек в этом деле, желаюсь поучиться) у меня вот в гриде три вида кастомных ячеек и я все сделал через сториборд, а не отдельными ксибами (я начинающий и приложение маленькое), догадываюсь что подход не правильный, но выглядит все не так уж плохо. Где правда? )
Если кратко — сториборд
1) тормозит. При достаточном колиестве контроллеров, айпадовский тормозит на топовых аймаках этого года
2) если верстать юай прямо в нем, то нужно будет все то же самое повторять для айпад-версии
НО — его можно использовать как карту контроллеров, а вьюхи контроллеров держать в ксибах — ммы например так и делаем.
Спасибо за ответ.
Вот именно как «карта контроллеров» меня и привлек сториборд, да и в учебниках «для начинающих» не упоминается о подходе с ксибами, хотя с подходом «чисто код» много примеров, а вот с ксибами нет (может проглядел, но не попадалось). А вот как начал копать глубже то уже и пошло кодом и ксибами. Попробую ваш подход в следущем приложении, если оно конечно будет :))
Сейчас меня просто вогнал в ступор подход к событям. После c# это просто какой-то ад) Еще и такие противоречивые мнение про эти штуки типа KVO и т.д. Думаю что бы такого нагородить, наверно в итоге будет какойто мрак типа делегатов с методами, которые будут менять состояние UI элементов.
Не так все и сложно. А KVO используется не очень часто, в нашем довольно большом проекте — около 2х раз.
Код с KVO можно упростить с ReactiveCocoa. Применял его в нескольких поектах, хотя и не в полный рост.
Ой, не надо всех этих надстроек. Obj-c и Cocoa сами по себе достаточно выразительны и лаконичны, а там где нет — легко написать свой макрос.
А расскажите пожалуйста попобробнее про «Callback hell» и чем помог async.js? Сам веду проект использующий Parse в качестве бэкенда. Очень интересен Ваш опыт в этом плане :)
На cloud code основная сложность в том, чтобы вызвать финальный callback только после выполнения всех необходимых операций. Все операции идут асинхронно. Самый простое решение, расположить вызовы функций таким образом:
image
И в конечном callback, самой последней функции, мы просто вызываем наш финальный callback, который возвращает ответ клиенту.
Поначалу я забивал на этот ужас, а потом заинтересовался как же все-таки можно хотя бы(!) уменьшить ширину кода, есть ли какой-нибудь syntactic sugar. Сначала, я начал везде пользоваться promises — они делают код чуть чуть читабельнее, все-таки это не было панацеей и тогда я нашел async.js, которая позволяет получать финальный callback, после завершения сколь угодно большого количества функций выполняющихся параллельно, последовательно, в цикле с разными параметрами и т.д.

У меня стояла такая задача: после того как пользователь удаляет свою картинку, нам нужно удалить ее или произвести другие действия со всеми таблицами в которых она упоминается. Сначала это выглядело так (заранее прошу прощения за грязный код, но пример очень иллюстративный):

функция очень большая, и читать не удобно.
exports.deletePictureFromUserProfile = function(deletingPictureId, customCallBack) {
	var arrayForDelete = [];
	picturesQuery = new Parse.Query("Picture");
	picturesQuery.equalTo("objectId", deletingPictureId);
	var query = new Parse.Query("LockSlot");
	query.matchesQuery("picture", picturesQuery);	
	query.find().then(function(results){
		if(results){
 		   for(var i=0,l=results.length; i < l ; i++){
			   	results[i].unset("picture");
 				results[i].save(null,{
 					success: function(myObject) {
 						return [];
 					},
 					error: function(myObject, error) {
 						console.error(error);
 						return [];
 					}
 				});
 			}
		}else{
			return [];
		}
	}).then(function(results){
		var query = new Parse.Query("xxxxx");
		query.matchesQuery("onePicture", picturesQuery);
		query.find().then(function(results){
			arrayForDelete.push.apply(arrayForDelete,results);
			return arrayForDelete;
		}).then(function(results){
			var query = new Parse.Query("xxxx");
			query.matchesQuery("onePicture", picturesQuery);
			query.find().then(function(results){
				arrayForDelete.push.apply(arrayForDelete,results);
				return arrayForDelete;
			}).then(function(results){
				var query = new Parse.Query("xxxx");
				query.matchesQuery("pictureFrom", picturesQuery);
				query.find().then(function(results){
					arrayForDelete.push.apply(arrayForDelete,results);
					return arrayForDelete;
				}).then(function(results){
					var query = new Parse.Query("xxxxx");
					query.matchesQuery("pictureTo", picturesQuery);	
					query.find().then(function(results){
						arrayForDelete.push.apply(arrayForDelete,results);
						return arrayForDelete;					
					}).then(function(results){

						var query = new Parse.Query("xxxxx");
						query.matchesQuery("picture", picturesQuery);
						query.find().then(function(results){
							arrayForDelete.push.apply(arrayForDelete,results);
							return arrayForDelete;												
						}).then(function(results){
							var query = new Parse.Query("xxxxx");
							query.matchesQuery("picture", picturesQuery);
							query.find().then(function(results){
								customCallBack();
							}).then(function(results){
								var query = new Parse.Query("Picture");
								query.equalTo("objectId", deletingPictureId);
								query.first().then(function(image){
									arrayForDelete.push(image);
									if(arrayForDelete){
										console.log("we got arrray for delete");
										var num_of_deleted_objects = 0;
										for(var i=0,l=arrayForDelete.length; i < l ; i++){
											var object = arrayForDelete[i];
											if(object){
												object.destroy().then(function(results){
													num_of_deleted_objects++;
													if(num_of_deleted_objects==l){
														customCallBack();
													}
												});
											}
										}
									}
								}, function(error) {
									console.error(error);
									customCallBack();
								});
							}, function(error) {
								console.error(error);
								customCallBack();
							});
						}, function(error) {
							console.error(error);
							customCallBack();
						});
					}, function(error) {
						console.error(error);
						customCallBack();
					});
				}, function(error) {
					console.error(error);
					customCallBack();
				});
			}, function(error) {
				console.error(error);
				customCallBack();
			});	
		}, function(error) {
			console.error(error);
			customCallBack();
		});
	}, function(error) {
		console.error(error);
		customCallBack();
	});
}


А с async.parallel так:
функция очень большая, но читать удобнее
function deletePictureObjFromUserProfile(picture, customCallBack){
	var resultArrayForDelete = [];
	resultArrayForDelete.push(picture);
	async.parallel([
		function(callback){
			var query = new Parse.Query("xxxx");
			query.equalTo("pict", picture);	
			query.find().then(function(results){
				
				if(results.length>0){
					var currentOperation =0;
					var numOfOperation = results.length;
					function enumCallBack(){
						currentOperation++;
						if(currentOperation==numOfOperation)
							callback();
					};
		 		   for(var i=0,l=results.length; i < l ; i++){
					   	results[i].unset("pict");
		 				results[i].save(null,{
		 					success: function(myObject) {
		 						enumCallBack();
		 					},
		 					error: function(myObject, error) {
		 						console.error(error);
		 						enumCallBack();
		 					}
		 				});
		 			}
				}else{
					callback();
				}
			},
			function(error){
				console.error(error);
				callback();
			});
		},
		function(callback){
			var query = new Parse.Query("xxxxx");
			query.equalTo("onePict", picture);	
			query.find().then(function(results){
				resultArrayForDelete.push.apply(resultArrayForDelete, results);
				callback();
			},
			function(error){
				console.error(error);
				callback();
			});
		},function(callback){
			var query = new Parse.Query("xxxxx");
			query.equalTo("anotherPict", picture);	
			query.find().then(function(results){
				resultArrayForDelete.push.apply(resultArrayForDelete, results);
				callback();
			},
			function(error){
				console.error(error);
				callback();
			});
		},function(callback){
			var query = new Parse.Query("xxxxxx");
			query.equalTo("pictFrom", picture);	
			query.find().then(function(results){
				resultArrayForDelete.push.apply(resultArrayForDelete, results);
				callback();
			},
			function(error){
				console.error(error);
				callback();
			});
		},function(callback){
			var query = new Parse.Query("xxxxxxx");
			query.equalTo("pictTo", picture);	
			query.find().then(function(results){
				resultArrayForDelete.push.apply(resultArrayForDelete,results);
				callback();
			},
			function(error){
				console.error(error);
				callback();
			});
		},function(callback){
			var query = new Parse.Query("xxxxxxxxxxx");
			query.equalTo("pict", picture);	
			query.find().then(function(results){
				resultArrayForDelete.push.apply(resultArrayForDelete, results);
				callback();
			},
			function(error){
				console.error(error);
				callback();
			});
		},function(callback){
			var query = new Parse.Query("xxxxxxx");
			query.equalTo("pict", picture);
			query.find().then(function(results){
				resultArrayForDelete.push.apply(resultArrayForDelete,results);
			},
			function(error){
				console.error(error);
				callback();
			});
		}
	],
	function(error, results){
		if(error)
			console.error(error);
		if(resultArrayForDelete.length>0){
			Parse.Object.destroyAll(resultArrayForDelete, function(success, error) {
				if(error)
					console.error(error);
				customCallBack();
			},
			function(error){
				customCallBack();
			});
		}else{
			customCallBack();
		}
		
	});
}
У стандартных Parse.Promises есть замечательная особенность: их можно выстраивать в цепочки. Т.е. если один из коллбеков then (success или error) вернет Promise, то Promise возвращенный сам then не будет завершен пока не завершится Promise из коллбека. В коде выглядит немного симпатичнее чем на словах :)

Чище всего выглядят тесты, поэтому приведу кусок оттуда. Я использую для тестирования JQUnit. ok() и equal() — это функции проверок этого фреймворка, а start(); — функция завершающая асинхронный тест.

       asyncTest("Register and activate account", function() {
            var password = "SECRET",
                user, newUser,
                deed, registrationId;

            Parse.Cloud.run("register", {
                params: {
                    firstName: "REMOVEME",
                    lastName: "REMOVEME"
                },
                password: password
            }).then(function(id) {
                registrationId = id;
                return Parse.User.signUp("testUser", "testPassword").then(function() {
                    return Parse.Promise.as();
                }, function(error) {
                    return Parse.User.logIn("testUser", "testPassword");
                });
            }).then(function() {
                return Parse.Cloud.run("activate", {
                    id: registrationId,
                    password: password
                }).then(function() {
                    ok(false, "User can activate if Deed is not created!");
                    return Parse.Promise.error();
                }, function(error) {
                    // Если вернуть удачно завершенный Promise даже из error callback, дальше 
                    // по цепочке будет вызван success callback
                    // Тот же фокус наоборот сработает для success callback выше
                    ok(true, "Can't activate if deed is not created yet");
                    return Parse.Promise.as();
                });
            }).then(function() {
                // Тут интересный момент: и success и error callback возвращают пустой успешно
                // завершенный Promise. А это значит мы всегда попадем в Teardown
                ok(true, "All ok!");
                return Parse.Promise.as();
            }, function(error) {
                ok(false, error && error.message ? error.message : JSON.stringify(error));
                return Parse.Promise.as();
            }).then(function() {
                // Teardown
                return Parse.Object.destroyAll(_.compact([user, newUser, deed]));
            }).then(function() {
                start();
            }, function() {
                start();
            });
        });


Прокомментировал в коде пару трюков.

Я так понимаю что для любой CommonJS совместимой реализации Promises этот код будет выглядеть примерно так же. После того как я освоил эти трюки мне сильно захотелось что-то похожее получить на стороне клиента (IOS). Смотрел в сторону ReactiveCocoa. Но почему-то там все так красиво не выходит.

А еще мне сильно упростило код использование расширения классов. Выглядит у меня это примерно так:

 beens.Points = Parse.Object.extend("Points", {}, {
        get: function (user, date) {
            var query = new Parse.Query("Points");
            query.equalTo("user", user);
            query.equalTo("date", date);
            return query.first().then(function (record) {
                if (typeof record === "undefined") {
                    record = new beens.Points();
                    record.set("user", user);
                    record.set("userId", user.id);
                    record.set("date", date);
                }
                return Parse.Promise.as(record);
            });
        }
    });


С этой библиотечкой все становится еще красивее: github.com/icangowithout/parse-ph

Для декларации Cloud функций я сделал свою обертку, которая ждет из функции реализующей логику Promise и уже его результат интерпретирует как:

.then(function (result) {
    response.success(result);
}, function (error) {
    console.log("ERROR: " + JSON.stringify(error));
    response.error(error);
});


Прошу прощения за огрызок. Код слегка не универсален и ждет совего рефакторинга.

Как я написал выше, для тестов мне приглянулся JQUnit. Я просто размещаю тесты в каталоге public и открываю как обычные веб страницы. Причем я использую отдельное приложение для тестирования, без или с минимумом ограничений прав (по тесту это видно :)). И планирую использовать еще одно для тестирования с установленными ограничениями. И одно для публикации рабочей версии приложения. Пока даже не могу до конца оценить насколько эта мысль была удачной. Явных проблем еще не выявил.

Поддержание симметричной структуры хранилищ в Parse это боль. Эту мысль я полностью поддерживаю :) Не знаю, что с этим делать. Радует только то, что прогон тестов по хранилищу с отключенными ограничениями генерирует большую часть модели данных :)

Есть еще маленькое ноухау по поводу тестирования. Но пока все на уровне экспериментов. Возможно когда-нибудь напишу статью :)))

PS: Спасибо за статью! Было очень приятно краем глаза взглянуть на устройство чужих проектов использующих этот backend. В сети пока не так уж много интересных решений на этот счет. Слохно найти хороший пример.
Еще один момент, возможно кому-то будет полезен:
В моем случае IDE Brackets + модуль JSHint вычищают значительную часть самых глупых ошибок еще до убликации кода на Parse. А команда:

parse develop <имя приложения>


позволяет сделать процесс публикации незаметным.

Кроме того, для Brackets уже достаточно много интересных модулей, делающих разработку приятнее.
А async.js удобно использовать, когда тебе надо параллельно несколько функций запустить, например, когда у тебя запуск cloud code не укладывается в timeout.

Да, тут надо всю информацию по разработке на Parse в отдельную статью оформлять. Я еще могу поделиться тем, как писал код для подсчета статистики — там я столкнулся и c timeout и с burst limit. А еще с тем, что на parse не работают setTimeout() и sortBy()
Для параллельных процессов в Parse.Promise есть метод when(). Выглядит примерно так:

        return Parse.Promise.when([userQuery.find(), 
                                   commentQuery.find(), 
                                   Parse.Cloud.run("someFunction")])
        .then(function(users, comments, someResult) {
            return Parse.Promise.as("Bingo!");
        }, function(userError, commentsError, callError){
            return Parse.Promise.error("Error!");
        });


А по поводу статистики будет очень интересно :) Я строил рейтинги пользователей. И для меня, как человека выросшего на SQL, построить эту часть системы было особенно сложно.
Ну я предполагал, что можно красивое решение найти, жаль что мы тут поспешили. Спасибо за очень полезную информацию!

А можешь поподробнее описать как ты environment для тестов поднял?
«Я просто размещаю тесты в каталоге public и открываю как обычные веб страницы» то есть папка public лежит в cloud на parse? А как там код можно запускать не через API, а в браузере?
Да все верно тесты лежат в виде обычных html-страниц в cloud на Parse. Сейчас они запускаются только вручную.
Тесты разбиваются на несколько отдельных страниц, для удобства тестирования отдельных подсистем.

В Parse сейчас заведено 3 приложения. 1-е для публикации, 2-е для разработчиков клиентской части и тестирования со всеми ограничениями, 3-е для backend разработки и тестирования. На текущем этапе эту схему полноценно пока реализовать не удалось. По сути большая часть работ происходит в 3-м приложении в т.ч. разработка клиентской части.

Тестируемые компоненты делятся на 2-х типа:

1. Cloud Code
Для них через JavaScript API создаются тестовые данные и проверяется работоспособность функций и триггеров. В тесте жестко прописаны ключи приложения которое тестируется. Для этих целей у нас заведено отдельное тестовое приложение с ослабленными ограничениями прав доступа.

2. Модули не зависимые от Parse API
Такой код выносится в отдельные Javascript модули и тестируется, как обычная JavaScript библиотека независимо. Модуль дублируется в public для теста и cloud для Cloud кода. В браузере такие модули я подключаю через require.js. В Cloud Code через его родной require().

С такими тестами очень удобно отслеживать результаты запросов. Уточнить какие-то вещи отладной кода теста. Можно выполнять запросы вручную.

CI в проекте я пока не внедрял. JQUnit умеет генерировать отчеты в XML, а сервер непрерывной интеграции может запускать их через безголовый браузер вроде phantome.js. Планирую поднять его позже.
Не могли бы вы объяснить, почему для чата использовался платный сервис PubNub, а не XMPP?
Для использования XMPP нужен сервер. Клиент не хотел сервера ни в каком виде. Даже для шедулинга и запуска некоторых скриптов на Parse мы использовали беслатный сервис iron.io, только потому что клиент настаивал на server less решении.
В следующий раз приглашаю опробовать наш QuickBlox — вот готовый пример iOS кода чата на XMPP, оптимизированный нами и заскейленный амазоном. XMPP сервер автоматически предоставляется с админкой и прочим фаршем. По цене на платных/enterprise пакетах дешевле, чем Parse и Kinvey + есть уникальные фичи, такие как видеозвонки, которых у прочих BaaS просто нет.

Если интересно, напишу статью здесь о том, как легко вставить текстовый чат и видеочат в своё iOS или Android приложение.
Мы рассматривали Ваш сервис в качестве backend для нашего проекта на начальных этапах. И честно говоря выбор был сложный :)

С радостью почитал бы про его преимущества в сравнении с Parse. Интереснее всего узнать про Ваш ответ на Cloud Code, типовое устройство системы безопасности, тестирование и подключение сторонних компонент :)

Хотя и использование XMPP тема тоже весьма актуальная.
было бы интересно почитать статью про чат, и думаю не только мне!
кстати — маленький лайфхак с базой для тестирования в Parse. У нас необходимость в тестовой базе появилась только после выхода в продакшн, так что само приложение под iOS было готово. Я завел новое приложение в parse.com, подставил ключики в iOS приложение, разрешил в настройках парса создание классов на стороне пользователя, потыкался по всему функционалу приложения и вуаля — на парсе пустая база со всеми нужными классами и полями. По крайней мере справедливо для parseSDK.
Да, вроде это тоже работает, но полной уверенности в том что вся модель простроиться нету. Например не совсем понятно как будут создаваться Pointer на объекты. Пару раз мы давали тестерам протестировать работу приложения с не обновленной моделью базы для, специально чтобы проверить как работает автосоздание полей, вроде все ок, но при обновлении продакшен БД на такие эксперементы я бы не пошел.
Вернее Pointer понятно как создаются. Но есть такой кейс: если ты в одном месте(по ошибке) присваиваешь полю не тот тип, а поля еще нет, то тогда оно создастся с этим типом и потом, когда вызовется код, который проставляет в это поле значение с правильным типом, появится ошибка.
Sign up to leave a comment.

Articles