- PVSM.RU - https://www.pvsm.ru -
Желание разработать собственный Angular.js webApi модуль возникло при работе с большим количеством http-запросов в проекте.
Важно было не просто создать файл с константами, а разработать некий модуль для упрощения поддержки существующего функционала. В свою очередь, необходимо было и позаботиться о возможном последующем расширении без нарушения целостности текущего ядра модуля.
Задачи, которые должен решать будущий webApi модуль:
Дальше поговорим о каждом из этих пунктов подробнее.
Речь идет об использовании одного запроса в нескольких местах приложения (контроллеры, сервисы, фабрики). Когда методов 10-20, то внести изменения в каждый запрос не составит большой проблемы. Но когда речь идет о количестве 100, 200 и более url'ов, поддержка такого кода вызывает все больше трудностей.
Пример #1
Есть метод, который получает список групп пользователей. Допустим, он выводится в соответствующий dropdown. При выборе группы происходит подгрузка ее юзеров по другому запросу, куда передается "id" группы. Также на странице имеется дополнительный функционал для визуализации каких-то данных пользователей.
// получаем все группы
$http.get("api/group-manage/get-all")
// получаем пользователей выбранной группы
$http.get("api/group-manage/2/get-users")
В другом контроллере есть таблица, где нужно вывести всех пользователей системы по конкретной группе. Эта страница предоставляет значительно больше возможностей для анализа и менеджмента пользователей.
// получаем пользователей выбранной группы
$http.get("api/group-manage/2/get-users")
В действительности похожих запросов может быть значительно больше.
Решение проблемы
Создать специальный файл с константами, содержащий список всех http-запросов на сервер, т.е. всех используемых в приложении url'ов.
Данный подход позволит сэкономить время при поиске необходимого запроса в проекте и его модификации.
Опять же, работать с таким файлом получится только при наличии небольшого количества запросов к серверу. Трудно себе представить удобную поддержку файла с константами, которых более 200.
Этот подход позволит решить вытекающую выше проблему. Как именно будет происходить определение независимых категорий — определяет сам разработчик. Для простоты можно ориентироваться на имя контроллера-метода из api
.
// http://yourapi.com/api/group-manage/2/get-users
// http://yourapi.com/api/group-manage/get-all
Из примера выше видно, что в запросах есть общий корень /api/group-manage/
. Создаем категорию с соответствующим названием groupManage.js
.
В Angular.js среде данный файл объявляется как constant, который в дальнейшем подключается к основному функционалу webApi модуля.
Таких групп в проекте может быть несколько. Но теперь мы определенно знаем где искать запросы, относящиеся к менеджменту групп.
Если же вызывать добавленный url напрямую, то рано или поздно появится череда однотипных зависимостей в коде. Поэтому, необходимо создать общий блок, предоставляющий список всех существующих запросов для оперирования ими в "ядре" webApi.
Одной из самых сложных задач была разработка ядра, которое сможет оперировать всеми запросами к серверу, при этом не только не раскрывать свою внутреннюю реализацию, но и предоставлять возможность легкого конфигурирования webApi модуля под конкретное Angular.js приложение.
Пример запроса выглядит следующим образом:
{ Url: '/api/acc/login', CustomOptions: false, Method: 'post', InvokeName: 'login' }
Такой подход позволяет не только группировать несколько url по общему типу, но еще и иметь представление, что каждый метод будет делать с данными.
В директории webApi/config/
находится файл с настройками API. Именно там мы и указываем DOMAIN url.
Пример #2
Практически все современные Angular.js приложения работают с системой аутентификации. Обычно это post-метод, который отправляет на сервер данные юзера (login, password).
При успешном respons'e происходит оповещение главному роуту приложения, после чего пользователь будет перенаправляется на страницу с функционалом.
Вызываем метод:
webApi.login({
"Login": "user",
"Password": "qwerty"
}).success(function(response, status, headers, config){
// какие-то действия
})
Таким образом, мы работаем на специальном уровне абстракций, что позволяет сосредоточиться на процессе построения логики приложения. Желательно давать адекватные наименования методам, чтобы можно было понять, что делает конкретный вызов:
// объявляем запрос в настройках
{ Url: '/api/acc/logout', CustomOptions: false, Method: 'get', InvokeName: 'logout' }
// где-то вызываем метод
webApi.logout([]);
Наверное, сейчас не совсем понятно использование пустого массива в get-методе, но дальше об этом всем будет рассказано.
Довольно часто при разработке приложения, сервер-сайд предоставляет клиенту следующий формат запросов:
И чтобы в при отправке запроса на сервер в нужном месте приложения не производить лишних операций с построением такого url, был разработан специальный компонент, автоматически генерирующий правильный адрес на основе полученных параметров.
// объявляем запрос в настройках
{ Url: '/api/admin/update/profile/{id}/block', CustomOptions: false, Method: 'put', InvokeName: 'blockAdminProfileById' }
// где-то вызываем метод
webApi.blockAdminProfileById({
"url": { "id": 5 }
});
Сгенерированный запрос: /api/admin/update/profile/5/block
(плюс domain url, разумеется).
И если нам нужно отправить на сервер более сложный запрос (например, длительность блокировки и тип), то просто указываем остальные параметры в качестве полей объекта "url":
// объявляем запрос в настройках
{ Url: '/api/admin/update/profile/{id}/block/{type}/{time}', CustomOptions: false, Method: 'put', InvokeName: 'blockAdminProfileById' }
// где-то вызываем метод
webApi.blockAdminProfileById({
"url": {
"id": 5,
"type": "week",
"time": 2
}
});
Сгенерированный запрос: /api/admin/update/profile/5/block/week/2
. И теперь пользователь будет заблокирован системой на 2 недели.
Шаблонизация работает для всех типов запросов, включая get. Желательно все запросы формировать именно таким образом: экономим время, не засоряем внутренний код лишними операциями.
Передача данных в теле запроса
Следует отметить, что если Вы хотите отправить помимо шаблонизированного url на сервер еще и какие-то данные (например, post-запрос), то необходимо их передать следующим образом:
webApi.createPost({
"data": {
"title": "Test post",
"category": "sport",
"message": "Content..."
}
});
Естественно, можно использовать одновременно и url-шаблонизацию, и передачу объекта с данными. Последний будет отправлен на сервер в теле запроса.
Работа с GET-методами
Тут никакие данные в теле запроса не передаются, но всем известно, что get-запрос может быть сформирован так:
api/admin/news/10?category=sport&period=week
или так:
api/admin/manage/get-statistic/5/2016
или же так:
api/admin/manage/get-all
.
Рассмотрим каждый из вариантов генерации.
// Case #1 -> api/admin/manage/get-all
// в настройках -> "Url" : 'api/admin/manage/get-all', ...
// вызываем метод
webApi.getAllAdmins([]).success(//...)
// Case #2 -> api/admin/manage/get-statistic/5/2016
// в настройках -> "Url" : 'api/admin/manage/get-statistic/{id}/{year}', ...
// вызываем метод
webApi.getAdminStatsticById({
"url": {
"id": 5,
"year": 2016
}
}).success(//...)
// Case #3 -> admin/news/10?category=sport&period=week
// в настройках -> "Url" : 'admin/news', ...
// вызываем метод
webApi.getNews({
before: ['10'],
after: {
"category": "sport",
"period": "week"
}
}).success(//...)
Со вторым типом запросов мы уже разобрались выше.
В первом же случае мы всегда передаем пустую коллекцию, когда нужно просто отправить запрос на сервер.
В случае #3 поле before определяет ряд параметров, которые идут до знака "?", а поле after — набор "ключ-значение". Естественно, в некоторых случаях можно оставить before пустой коллекцией [].
Параметр CustomOptions в настройках
Get-запрос без шаблонизации url:
webApi.getNewsById([10, {"headers": {"Content-Type": "text/plain"} } ]);
Во всех остальных случаях (в том числе, get-запросы с шаблонизацией url):
webApi.login({
options: {"timeout": 100, {"headers": {"Content-Type": "text/plain"} }
});
Структура модуля следующая:
Вам придется работать с последними двумя директориями.
Пример #3
Допустим, разрабатывается система учета книг в каком-то университете. Большая вероятность разбиения запросов на следующие группы:
Конечно, этот список может быть расширен.
Главное - стараться максимально четко группировать запросы, чтобы потом легко было добавлять новые методы и редактировать существующие.
(function(){
angular
.module("_webApi_")
.constant("cat.account", {
"DATA": [
{ Url: '/api/acc/login', CustomOptions: false, Method: 'post', InvokeName: 'login' },
// остальные запросы
]
});
})();
Файл с запросами создан. Теперь нужно связать его с нашим webApi ядром.
(function(){
angular
.module("_webApi_")
.service("webApi.requests", webApiRequests);
function webApiRequests(catAccount){
// специальные обработчики и регистраторы
// их изменять не нужно, только добавляем новые зависимости
}
// IoC container.
webApiRequests.$inject = [
"cat.account"
];
})();
В данном случае все константы пишутся через "cat.имя константы", а подключаются в регистратор "catИмяКонстанты".
Таким образом, в webApi используется дополнительное пространство имен "cat.", чтобы не было конфликтов с другими константами в приложении.
И теперь вызываем метод согласно описанному шаблону:
webApi.login( //логин-пароль для авторизации )
Для того, чтобы не раскрывать функционал webApi внутреннему коду приложения, было принято решение использовать дополнительную абстракцию "репозиторий".
Благодаря использованию данных сущностей, мы создаем слушателя, который получает запросы из контроллеров приложения и общается с http-сервером. Связь контроллер-webApi теряется, тем самым предоставляя разработчику свободу в работе с разными репозиториями.
Допустим, у нас есть некий "FoodController" и соответствующая группа запросов foodManage. Каждый метод из данной категории отвечает за свою конкретную реализацию по управлению данными на сервере.
Объявляем репозиторий:
(function() {
"use strict";
angular
.module("app.repositories")
.factory("repository.food", foodRepository);
function foodRepository(webApi){
get: getById
getAll: getAll,
remove: removeById,
removeAll: removeAll
// остальные методы создания, обновления
function getById(id){
return webApi.getFoodItemById({
"url": { "id": id }
});
}
function getAll(){
return webApi.getAllFoodItems([]);
}
// реализация остальных методов
}
// IoC container.
foodRepository.$inject = [
"webApi"
];
})();
Таким образом, мы создали абстракную фабрику для управления данными из контроллера, не раскрывая при этом функционал webApi. И в любой момент мы сможем подменить текущую реализацию на любую другую.
Например, при получении информации об айтеме, нам необходимо теперь указывать тип: "vegetables", "fruits", "milk-made" и т.д. Благодаря наличию специального уровня абстракции, нам достаточно просто внести следующие изменения в метод:
function getById(id, category) {
return webApi.getFoodItemById({
"url": { "id": id, "category": category }
});
}
Подключение репозитория в приложение
Как уже говорилось выше, репозиторий — это сущность, предоставляющая открытые методы для управления данными через внутреннее обращение к webApi модулю.
Поэтому, нам достаточно будет вызывать конкретный метод и передать в него соответствующие параметры.
(function() {
"use strict";
angular
.module("app")
.controller("FoodController", FoodController);
function FoodController(foodRepository){
/* jshint validthis:true */
var vm = this;
// объявление переменных, инициализация данных
vm.foodItems = [];
vm.getAllFood = function(){
foodRepository.getAll().success(function(response){
vm.foodItems = response.data;
});
};
// получаем список всех айтемов при инициализации контроллера
vm.getAllFood();
}
// IoC container.
FoodController.$inject = [
"repositories.food"
];
})();
Фрагмент HTML для визуализации данных:
<div ng-controller="FoodController as fc">
<ul>
<li ng-repeat="food in fc.foodItems track by food.Id">
Title: <span class="item-title"> {{food.title}} </span>
Cost: <span class="item-cost"> {{food.cost}} </span>
</li>
</ul>
</div>
Таким образом, репозиторий возвращает данные и Angular.js автоматически их подставляет во view. И мы не можем напрямую обратиться к webApi, удалив там какой-то айтем или же добавить без согласования с центральным контроллером.
Это повышает безопасность приложения и помогает раскрывать наружу только те методы, которые мы сами считаем нужными.
Мы рассмотрели вариант создания конфигурируемого и расширяемого webApi модуля для работы с Angular.js.
Данный подход поможет вам избавиться от дублирования логики в коде, сократить время на редактирование необходимого метода, а также облегчить работу со сложными запросами.
Demo
Посмотреть исходный код модуля: github [1].
Автор: btws
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/119061
Ссылки в тексте:
[1] github: https://github.com/asduser/webApi-angularjs
[2] Источник: https://habrahabr.ru/post/282397/
Нажмите здесь для печати.