Доброго дня, новички, сегодня мы попытаемся переделать нашу игрушку, разучивая основы новых для нас «технологий»:

  • AngularJS
  • DataBoom

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

Ну а во второй части с помощью DataBoom создадим замечательную очередь событий, как в оригинальной игре (напоминаю, что делаем по образу и подобию HeartStone). Забегая вперед скажу, что в следующий раз мы вообще избавимся от php сервера, и полностью перейдем на Databoom, но это уже совсем другая статья…

image


AngularJS


Начать стоит с того, что для инициализации ангуляр-приложения вам недостаточно будет просто подключить библиотеку и создать модуль в js файле. Для работы с angular вам потребуется работа с вашими html-файлами как с представлением (view), что, на мой взгляд, лучше для разделения представления и контроллера.

В ангуляре есть такая сущность как директивы – особые html-атрибуты, и одним таким мы воспользуемся для инициализации нашего приложения. Приложение может не охватывать всю страницу, а только отдельный блок на ней, все зависит от того, где вы его инициализируете. Инициализация происходит с помощью директивы ng-app:

<html ng-app>

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

<div ng-controller="myController">

Контроллеры в свою очередь не висят в воздухе, а привязаны к модулям. Само приложение тоже является модулем (главным, точкой входа в приложение), и, чтобы задать главенство одного из модулей, его название надо указать в директиве ng-app:

<html ng-app="App">

Модули создаются методом module объекта angular:

angular.module('Имя модуля', ['зависимость_1'])

Но сам по себе модуль мало полезен без контроллеров. Чтобы создать контроллер на нашем главном модуле, который мы определили точкой входа приложение (например), необходимо на объекте модуля вызвать метод controller:

myModule.controller('Имя контроллера', ['$scope',function($scope){

		$scope.var = 'some';
		$scope.foo = function(){};

	}]);

Объект $scope задает все переменные и функции области видимости контроллера, то есть то, что мы можем использовать в представлении. В данном случае в наших html-файлах мы можем работать с var и foo.

Вывод значений переменных осуществляется с помощью двойных фигурных скобок {{var}}, то есть:

html
<div>
{{var}}
</div>


выведет «some».

Ближе к сути


Разбираться с остальными тонкостями будем сразу на примерах, т.к. по ангулару есть некоторые статьи, хотя документация на angular.ru не вполне понятная (мне лично).

Первый подводный камень мы встретим, когда попытаемся сделать приложение модульным (на примере requirejs). Если сразу же прописать в html директиву ng-app, а после подключить библиотеку ангуляр методом require, то мы обнаружим, что ничего не работает. Так происходит потому, что DOM дерево на момент подключения библиотеки уже составлено.

На такой случай у объекта angular есть метод bootstrap:

    require(['domReady!'], function (document) {
        ng.bootstrap(document, ['App']);
    });

Таким образом мы привязываем модуль App как точку входа в приложение к document.

Первое, что мы переделаем в нашей игрушке – это меню, а именно единственное, что там есть – список игроков.

Код
define(['angularControllersModule', 'User', 'Directive'],function(controllers, User, Directive){

	/////////// Контроллер userList
	controllers.controller('userlistCtrl', ['$scope',function($scope){

		$scope.userlist = []; // Список игроков, пока пустой
		$scope.isMe = function(name){ // Функция
			if (name == User.login) {
				return 'me';
			};
		}
		$scope.letsFight = function(name){ // Функция, которую будем вызывать по клику
			return Directive.run('figthRequest',name);
		}

	}]);

	return controllers;

})

Здесь метод letsFight() (приглашение на бой) будет вызываться по клику на соответствующую кнопку около каждого игрока. В ангуларе это задается директивой ng-click:

	<li class="{{isMe(user.name)}}" ng-repeat="user in userlist">
		<span class="name">{{user.name}}</span>
		<span class="fightButton" ng-click="letsFight(user.name)"></span>
	</li>

Обратите внимание на разные вызовы функций: с фигурными скобками и без. Разница в том, что выражение в фигурных скобках вычисляется сразу, не дожидаясь каких-либо действий от пользователя, поэтому во втором случае мы фигурные скобки не применяем, тк надо, чтобы функция вызывалась только по клику.

Директива ng-repeat работает аналогично foreach в php или for of в ES6 – перебираются все объекты списка. Директива указана в теге <li>, что означает, что повторять мы будем именно его столько раз, сколько у нас элементов в массиве userlist.

Но изначально наш список игроков пуст, а получаем мы его по websockets с сервера. В приложении я попытался разделить библиотеки, модули и некие простые наборы инструкций (я назвал их Directives) для того, чтобы безболезненно иметь возможность их переписать.

Более того, у нас есть отдельный обработчик этих директив (Directives.js), который просто вызывает нужные директивы по имени, которое мы получаем откуда угодно, например по websockets или ajax. В простейшем случае это просто apply(), но я создал таблицу в базе данных, которая описывает порядок вызова директив. Что я имею в виду: когда мы пытаемся вызвать директиву с именем myDir, если в таблице есть соответствие, то вызывается та директива, которая там указана, своего рода ссылка. Но смысл это базы в том, чтобы еще и удобно задавать pre и post директивы. То есть то, что будет вызываться до и после вызова определенной директивы.

image

И хранятся все эти директивы в отдельной папке, подключаясь тогда, когда надо:

modules/directive.js
define(['DB','is'],function(DB){

	var path = 'Actions/';


	var Directive = {
		run: function(directiveName, args){

			var args = args || [''];
			if (!is.array(args)) {
				args = [args]
			};
			DB.getDerictive(directiveName, exec);

			function exec(directiveName){
				// Directive.preAction ?
				if (typeof directiveName == undefined || typeof directiveName == 'string') {
					action = directiveName;
				}else{
					action = directiveName.action;
				}
				Directive._apply(action,args);
			}

		},
		_apply: function(actionName,args){

			require([path + actionName],function(action){
				action.run.apply(action,args);
			});	
			
		}
	}

	return Directive;
})

По websockets с сервера мы получаем имя директивы и аргументы, вызываем её с помощью этого модуля:

	socket.onmessage = function (e){
		if (typeof e.data === "string"){
			var request = JSON.parse(e.data);
			Directive.run(request.function,request.args);
		};
	}

Таким же образом мы получаем инструкцию на добавление игроков в наш пустой список.

Код
define(['angularСrutch'],function(angularСrutch){

	var action = {
		run: function(list){

			for(key in list){
				angularСrutch.scopePush('userListTpl', 'userlist',{name: key});
			}

		}
	}

	return action;
})

Уверен, вы уже обратили внимание на зависимость с говорящим названием angularСrutch. Этот модуль обеспечивает доступ к модулям ангуляр извне. Дело в том, что изменить данные контроллера ангуляр не так просто. Нельзя просто вызвать какой-то метод или переписать значение параметра. Даже если вы присвоите модуль ангуляр какой-то переменной, область видимости $scope все равно для вас останется не доступной напрямую.

Для этих целей можно воспользоваться такой постройкой:

var el = document.querySelector( mySelector );
var scope = angular.element(el).scope();
scope.userlist.push(user);

Все замечательно, есть доступ к $scope, но и здесь все не так просто. Изменять данные $scope можно сколько угодно, но в представлении ничего не изменится. Ангуляр попросту не заметил, что вы что-то поменяли, он не отслеживает любое изменение параметров $scope, а делает это только в своем цикле $digest(), который вызывается при определенных действиях пользователя. Чтобы вызвать его вручную, вызовем метод $apply на нашем scope:

Измененный код
scope.$apply(function () {
    scope.userlist.push(user);
});

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

Не буду останавливаться на контроллерах списка карт в руках игроков и на арене, тут все аналогично, а лучше перейдем к реализации очереди событий.

Очередь на DataBoom


Вы все (многие) знаете, что если дать несколько заданий подряд и быстро (атаковать, колдовать, закончить ход), всё не будет делаться одновременно, создавая мелькание объектов на экране.

В базе данных таблица очереди выглядит так:

{"for":"user_1","motion":"opGetCard","motionId":2},
{"for":"user_2","motion":"opGetCard","motionId":1},

Для какого игрока предназначено действие, имя инструкции и id действия для очерёдности.

Для организации очереди я создал две инструкции: модуль stack.js с единственным методом push (стек на самом деле не очередь, просто слово нравится) и метод push модуля DB, отвечающего за взаимодействие с базой DataBoom:

stack.js
define(['DB', 'User'],function(DB, User){

	var module = {
		push: function(forWho, motion, expandObj) {
			
			var expandObj = expandObj || null;
			var motionObj = {
				'for': forWho,
				'motion': motion
			};

			if (expandObj) {
				motionObj[expandObj.prop] = [{id:expandObj.id}];
			};

			DB.push('motionQueue',motionObj);

		}
	}

	return module;
})


Использовать такой интерфейс просто:

stack.push(User.login,'myTimerStart');

Вызывает инструкцию myTimerStart для пользователя User.login

Извлекать инструкции будем простой функцией setInterval() каждые 2 секунды:

setInterval(function(){
	Directive.run('getNextAction');
}, 2000);

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

DB.getNextAction(User.login, this.actionStart); // Для кого ищем инструкции и колбэк

Искать нужную инструкцию в таблице мы можем благодаря возможности query запросов, то есть запросов с фильтром, сортировкой и лимитом:

var config = {
	baseHost: 'https://t014.databoom.space',
	baseName: 'b014'
}

var db = databoom(config.baseHost, config.baseName); // Инициализируем базу данных

var filter = "(motionId gt " + window.motionId + ") and (for eq '" + forWho + "')"; // Условия выборки

/*
Поддерживаются все стандартные условия:
eq - равно
ne - не равно
lt - меньше
le - меньше или равно
gt - больше
ge - больше или равно
*/

db.load('motionQueue',{
	filter: filter,
	orderby: "motionId", // Сортировка
	top: 1 // Ограничение на кол-во выбираемых записей
})

И всё бы выглядело просто, если бы мы не имели сложных инструкций типа «игрок положил карту с такими-то параметрами на стол». Тут нам необходимо знать, какая карта, какие у нее параметры. Да, создадим еще одно поле в базе «аргумент» или «карта». Делать еще один запрос к базе для выборки информации о карте?

У DataBoom, слава богам, есть решение на этот счет — опция expand, которая говорит о том, что надо расширить возвращаемый объект данными из другой таблицы, в нашем случае таблицы с картами.

Таблица карт
image

Сама по себе привязка в базе выглядит так:

... ,"card":[{ "id": "probe"}], ...

ID записи в другой таблице. Причем заметьте, квадратные скобки явно намекают на то, что это массив, то есть привязка может быть множественной.

При расширении ответа базы инструкцией expand мы получим ту же самую запись из базы, где вместо объекта { «id»: «probe»} будет объект выборки по id из соответствующей таблицы:

{
	id:"...",
	collections:[{ id: "motionQueue"}],
	"for":"user_1",
	"motion":"opPutCard",
	"motionId":2
	card:[
  		{
  		id:"probe",
  		title:"probe",
  		mana:1,
  		attack:1,
  		health:1
		}
	],
}

Заключение


  • Все основные премудрости angular нахрапом понять не получается, излишне сложен. Сложно взаимодействовать извне, не понравилось.
  • DataBoom понравился в целом, хотя многое плохо документировано и на английском (хотя компания, насколько мне известно, русскоязычная), приходится изучать методом тыка какие-то моменты.

Ресурсы


Комментарии (6)


  1. seokirill
    08.09.2015 21:07
    +2

    Игра не играбельна, годна только в качестве примера.
    Прошлая версия на MatreshkaJS была гораздо более играбельной. Чуть позже выложу ее на тот же сервер в отдельную папочку.
    А лучше дождитесь новой статьи, там, скорее всего, будет пример самый близкий к альфа-альфа-версии игры.


  1. dshster
    09.09.2015 11:48
    +2

    Непонятно зачем вам нужен requirejs вместе с angular — в последнем есть свои зависимости (инжекции), а все модули сливаются в один файл. Во-вторых, если вам необходимо вручную обновлять scope через $apply, значит вы меняете данные scope не в контроллере, а в функции link директивы (или где-то еще), но это не angular way, с таким же успехом можно было бы использовать и jquery. В третьих вы всё же тянете jquery для ajax запросов в databoom.js, хотя в angular есть прекрасная работа с http.
    По-моему довольно странное использования angular и для начинающих я бы этот кейс не советовал.


    1. seokirill
      09.09.2015 12:19

      В ангуляре есть зависимости, да, но мне удобнее в разных файлах хранить отдельные части.
      Про «не angular way» я в курсе, но вот бывают такие необходимости, влиять на $scope из сторонних модулей.
      Предлагаете оборачивать их в ангулярские?
      Я не разобрался, как, получая инструкции по websockets обрабатывать их. Думал обернуть модуль вебсокетов в модуль ангуляр, но код контроллера не будет отрабатывать, если его нигде не инициализировать. Или я не смог это понять.

      Статей по ангуляр много, видео есть. Я пытался рассказать о подводных камнях, на которых можно время потерять.


      1. dshster
        09.09.2015 16:42
        +2

        В ангуляре есть свои сервисы, фабрики, провайдеры, в них прописывается логика работы с бизнес-моделью, они инжектятся в контроллеры и переносят в него результат своей работы, изменения scope в контроллере автоматически отображается при digest-цикле. Зачем изобретать что-то своё — непонятно.

        И хранится всё так же в разных файлах, просто перед запуском собирается всё в один js-файл (gulp, grunt, webpack, чем душе угодно). При публикации на продакш-сервер этот файл можно минимизировать. И никаких лишних библиотек и зависимостей, иначе получается, что вы решаете подводные камни, которые сами же и соорудили.


      1. dshster
        09.09.2015 16:49

        По сокетам есть ангуляровские библиотеки, не нужно изобретать своё:
        http://ngmodules.org/modules?query=websocket


        1. seokirill
          09.09.2015 23:17

          Благодарю за информацию!