Желание разработать собственный Angular.js webApi модуль возникло при работе с большим количеством http-запросов в проекте.

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



Задачи, которые должен решать будущий webApi модуль:


  1. Предотвратить дублирование http-запросов в проекте.
  2. Группировать существующий список запросов по функциональным категориям, чтобы проще вносить правки в конкретные методы.
  3. Быть полностью независимой функциональной единицей приложения, которая подключается к любому другому Angular.js проекту простым Dependency Injection'ом.
  4. Инкапсулировать внутреннюю реализацию, чтобы избежать проблем при работе с внешними источниками.



Дальше поговорим о каждом из этих пунктов подробнее.


Дублирование http-запросов


Речь идет об использовании одного запроса в нескольких местах приложения (контроллеры, сервисы, фабрики). Когда методов 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' }

  • customOptions — использовать ли дополнительные настройки запроса. Обычно там могут указываться header'ы для конкретного запроса, значение timeout, параметр withCredentials и др.

Такой подход позволяет не только группировать несколько 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-методе, но дальше об этом всем будет рассказано.


Шаблонизация запросов


Довольно часто при разработке приложения, сервер-сайд предоставляет клиенту следующий формат запросов:


  • /api/admin/delete/profile/{id}
  • /api/admin/update/profile/{id}/block

И чтобы в при отправке запроса на сервер в нужном месте приложения не производить лишних операций с построением такого 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.


Рассмотрим каждый из вариантов генерации.


Примеры создания get-запроса
// 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"} }
});

Настройка webApi в новом проекте


Структура модуля следующая:


  • файл module.js — объявление самого модуля;
  • директория main/ — содержит в себе ядро webApi, оно не изменяется;
  • директория categories — группы запросов, одна группа — один *.js файл;
  • директория categories-handler — регистратор всех запросов в webApi модуле.

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


Пример #3


Допустим, разрабатывается система учета книг в каком-то университете. Большая вероятность разбиения запросов на следующие группы:


  • account.js — запросы на авторизацию, деавторизацию, восстановление паролей и т.д.;
  • bookManage.js — запросы на CRUD-операции с книгами;
  • studentManage.js — менеджмент студентов;
  • adminManage.js — ряд запросов по управление админской частью приложения.

Конечно, этот список может быть расширен.


Главное - стараться максимально четко группировать запросы, чтобы потом легко было добавлять новые методы и редактировать существующие.

Объявляем новую категорию запросов
(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){

          return { 
               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.

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


  1. SergeyVoyteshonok
    25.04.2016 14:31
    +1

    А почему вы не используете ngResource?

    UPD. Для классических API, где для каждой сущности, есть вся гамма методов ( GET, PUT, POST и тд) количество кода так в разы сокращается.


    1. btws
      25.04.2016 19:05

      В данном случае применялся mixed-подход: можно работать как с RESTful api, так и без него. Дополнительные уровни абстракции позволяют снизить внутренние зависимости и выполнять операции с данными через специальные связующие звенья — репозитории.

      Т.е. у нас есть webApi ядро, где описана логика взаимодействия с сервером и процессы обработки url'ов, группы запросов для простоты конфигурирования текущего api, а также репозитории — сущности, которые выступают в качестве поставщиков данных для контроллера.


  1. lastersound
    26.04.2016 08:04

    Благодарю. Как раз подобное пишу сейчас. Будет полезно посмотреть ваши решения.


  1. Re-ise
    26.04.2016 11:24

    А как вы организовываете работу с моделями?
    Также предлагаю вам ознакомиться с моей реализацией, достаточно схожие идеи.
    Endpoints конфигурируются в конфиге.
    Указываются модели для инициализации данных, также, по надобности, заголовки для чтения. (например 'X-Total-Count').
    Под капотом провайдер использует ngResource что позволяет использовать все его фишки при инициализации роутинга, работе с данными и кэшем.

    github.com/zaqqaz/ng-rest-api