Personal Maps: создаём сервис AngularJS. Часть 4.

Владимир | | Ajax, AngularJS, JavaScript, Web разработка.

personal_maps_logo_4

Это четвёртая статья цикла о разработке web приложения под названием Personal Maps.

В предыдущей части мы начали рассматривать клиентскую часть приложения и разобрались, какие компоненты входят в её состав. Также мы определили, как компоненты будут взаимодействовать между собой. В результате у нас получилась достаточно гибкая архитектура, которая позволяет изменять, добавлять или удалять компоненты не затрагивая других части приложения.

Сейчас мы переходим к практической реализации. И начнём с разработки сервиса AngularJS, который называется Places и работает с REST API серверной части приложения, т.е. выполняет операции чтения, создания, удаления и редактирования объектов, которые пользователь размещает на карте.

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

Примечание. Вы можете скачать исходный код приложения или поработать с демо версией.

Source Demo

Рассмотрим, как создаётся сервис AngularJS.

Для начала нам нужно создать модуль приложения (файл js/app.js).

var app = angular.module('personalmaps');

И затем добавить к этому модулю сервис с помощью метода factory (файл js/services/places.js)

app.factory('Places', ['$http', '$rootScope', function($http, $rootScope) {
    var service = {};
    return service;
}]);

Здесь я специально привёл упрощённый код, чтобы выделить операцию создания сервиса.

В первом параметре метода factory нужно указать имя нового сервиса, а во втором – передать массив, содержащий имена зависимостей и функцию, которая создаёт сервис. Вообще можно использовать сокращённый вариант записи (опустить имена зависимостей), но это не рекомендуется, т.к. если вы используете упаковщики JavaScript кода, которые сокращают имена переменных, то будут проблемы.

Сам сервис является обычным JavaScript объектом, который имеет свои методы (мы их добавим чуть позже). Angular при подключении одних компонентов к другим использует так называемое внедрение зависимостей (DI – dependency injection). Вы можете подробно почитать о ней в документации на официальном сайте. А сейчас главное понимать, что любой компонент приложения, к которому будет подключён сервис Places, фактически получит доступ к объекту service (строка 2).

Теперь обратите внимание на объекты $http, $rootScope. Оба они являются стандартными компонентами фреймворка и подключены к нашему сервису через систему DI. $http сам является сервисом для работы с AJAX запросами. А $rootScope это объект, который предоставляет механизмы для отслеживания состояния моделей и работы с событиями. В каждом приложении AngularJS автоматически создаётся дерево, содержащее объекты scope, в корне которого находится $rootScope. Их использование мы рассмотрим подробнее, когда будем разрабатывать контроллеры.

Загрузка списка существующих объектов

Первым шагом в работе нашего сервиса является загрузка существующих объектов. Выполнить эту операцию можно следующим образом:

var places = [];

function getPlaces() {
	$http({method: 'GET', url: 'api/places'})
		.success(function(data, status, headers, config) {
			places = data;
			$rootScope.$broadcast('places:updated');
		})
		.error(function(data, status, headers, config) {
			console.log(data);
		});
}
getPlaces();

Создаём функцию, которая отправляет запрос к REST API приложения и, в случае успешного получения данных, сохраняет их в массиве places. Для отправки запроса мы используем сервис $http.

Примечание. В AngularJS входит сервис $resource, который предназначен для работы с различными REST API, но в данном случае API очень простое и использование этого сервиса по сравнению с $http особых преимуществ не даёт (во всяком случае, количество кода остаётся примерно таким же).

Мы ещё не создали REST API в нашем приложении, но сейчас главное понимать, что при отправке GET запроса к URL http://site.url/api/places мы получим ответ в JSON формате, содержащий данные объектов:

[
	{
		"id":"27",
		"p_title":"Kiev",
		"p_description":"",
		"p_lng":"50.4",
		"p_lat":"30.76",
		"p_user_id":"2"
	},
	{
		"id":"38",
		"p_title":"Montreal",
		"p_description":"Montreal",
		"p_lng":"-73.59",
		"p_lat":"45.54",
		"p_user_id":"2"
	},
]

Эти данные мы сохраняем в массиве places и инициируем событие

$rootScope.$broadcast('places:updated');

В результате, все компоненты приложения, которые слушают это событие, получат уведомление о том, что список объектов обновился.

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

Для этого мы добавим нашему сервису следующие методы:

  • getAll – возвращает полный массив объектов;
  • get – ищет объект по его id;
  • add – создаёт новый объект и сохраняет его на сервере;
  • update – изменяет существующий объект и сохраняет изменения на сервере;
  • delete – удаляет существующий объект;
  • save – вызывает add или update в зависимости от значения id (id может быть сформирован только на сервере, поэтому если id не задан, то объект считается новым и вызывается метод add, в противном случае, вызывается update).

В результате наш сервис примет вид:

app.factory('Places', ['$http', '$rootScope', function($http, $rootScope) {

    var places = [];

    function getPlaces() {
		...
    }
    getPlaces();

    var service = {};

    service.getAll = function() {
        return places;
    }

    service.get = function(id) {
		...
    }

    service.add = function(place) {
		...
    }

    service.update = function(place) {
		...
    }

    service.delete = function(place) {
		...
    }

    service.save = function(place) {
		...
    }

    return service;
}]);

Обратите внимание, все методы, которые должны быть доступны другим объектам, мы присваиваем переменной service.

Теперь рассмотрим эти методы подробнее.

Получение объектов

service.getAll = function() {
	return places;
}

service.get = function(id) {
	var place = null;
	angular.forEach(places, function(value) {
		if (parseInt(value.id) === parseInt(id)) {
			place = value;
			return false;
		}
	});
	return place;
}

getAll просто возвращает массив с объектами.
get в цикле проходит по массиву places и сравнивает id объектов с заданным. Если совпадение найдено, возвращается соответствующий объект, в противном случае, возвращается null. Обратите внимание, для создания цикла используется метод angular.forEach. Чтобы прервать цикл, функция, которая выполняет сравнение id, возвращает false.

Создание и обновление объектов

service.add = function(place) {
	$http({method: 'POST', url: 'api/places', data: place})
		.success(function(data, status, headers, config) {
			places.push(data);
			$rootScope.$broadcast('place:added', data);
		})
		.error(function(data, status, headers, config) {
			$rootScope.$broadcast('place:error', data);
		});
}

service.update = function(place) {
	$http({method: 'PUT', url: 'api/places/' + place.id, data: place})
		.success(function(data, status, headers, config) {
			$rootScope.$broadcast('place:updated', data);
		})
		.error(function(data, status, headers, config) {
			$rootScope.$broadcast('place:error', data);
		});
}

Методы add и update очень похожи, но есть несколько отличий.
Во-первых, для обновления объекта используется запрос типа PUT, а для создания нового – POST.
Во-вторых, к URL PUT запроса нужно добавить id объекта, который мы хотим изменить.
В-третьих, после создания нового объекта мы должны добавить его в массив places (строка 4), в случае обновления объекта эту операцию делать не нужно, т.к. объект уже находится в массиве.

После получения ответа сервера мы отправляем соответствующее событие: place:added, place:error, place:updated и передаём в нём ответ сервера (аргумент data). В результате, компоненты, которые слушают эти события, смогут показать пользователю описание ошибки или сообщение об успешном выполнении операции.

Удаление объектов

service.delete = function(place) {
	$http({method: 'DELETE', url: 'api/places/' + place.id})
		.success(function(data, status, headers, config) {
			angular.forEach(places, function(value, i) {
				if (parseInt(value.id) === parseInt(place.id)) {
					places.splice(i, 1);
					return false;
				}
			});
			$rootScope.$broadcast('place:deleted', data);
		})
		.error(function(data, status, headers, config) {
			$rootScope.$broadcast('place:error', data);
		});
}

Для удаления объекта мы отправляем запрос типа DELETE к api/places/id_объекта. В случае успешного выполнения данной операции, находим объект в массиве places и удаляем его (строка 6). Затем отправляем событие place:deleted. В случае возникновения ошибки, массив places не трогаем, а просто инициируем событие place:error.

Посмотреть код сервиса полностью можно на GitHub.

В результате мы получили сервис, который предоставляет все необходимые методы для работы с объектами и выполняет синхронизацию данных с серверной частью приложения. Таким образом, если мы захотим изменить API серверной части приложения, то в клиентской части изменять придётся только данный сервис.

В следующей статье мы займёмся тестированием нашего приложения.

Содержание