Использование RESTful контроллеров для ресурсов AngularJS

    Как упоминалось ранее, Ангуляр предоставляет класс $resource для повышения уровня абстракции между кодом на стороне клиента и серверным API. Поэтому теперь выполнять операции CRUD по сети довольно легко. Но что происходит, когда необходимо выполнить команду для RESTful ресурса, что выходит за пределы стандартных методов CRUD (т. е. создания, чтения, обновления, удаления)? К счастью, Ангуляр достаточно хорошо работает с RESTful «контроллерами».

    В соответствии со Сводом правил по REST API Марка Массе, есть четыре архетипа ресурсов:

    • Документ — представление ресурса.
    • Коллекция — набор данных на сервере.
    • Хранилище — набор данных на клиенте.
    • Контроллер — выполняемое действие.

    Документ, Коллекция и Хранилище представляют ресурсы. Контроллеры, с другой стороны, отвечают за изменение ресурсов. Можно использовать глаголы в HTTP для обозначения действия над ресурсом, но для заурядных CRUD-операций это не всегда имеет смысл. Возьмем в качестве примера следующие ресурсы:

    • /messages
    • /messages/4

    Имея Коллекцию сообщений (первый ресурс) и отдельное сообщение (второй ресурс), как сообщить об очистке всех сообщений? Или, как переместить данное сообщение в архив? CRUD не вполне отвечает таким требованиям. Но контроллеры отлично подходят для операций подобного типа:

    • /messages/clear-all
    • /messages/4/archive

    Здесь мы используем Контроллеры «clear-all» и «archive» для изменения Коллекции и архивирования Документа, соответственно.

    Начав использовать контроллеры для наших ресурсов, мы пришли к схеме URL, имеющей большую вариативность. $resource Ангуляра позволяет проделывать такое, но придется использовать несколько «умных» особенностей чтобы настроить связывание данных.

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

    • /messages/:id/:controller

    Проблема в том, что, в настоящее время, возникает некоторая неоднозначность вокруг первого URL-параметра. Является ли «:id» ссылкой на, основывающийся на Коллекции, Контроллер? Или это ссылка ID Документа?

    Чтобы устранить эту неопределенность, Ангуляр позволяет определить несколько параметров в одной и той же части URL шаблона:

    • /messages/:listController:id/:docController

    Обратите внимание, что вторая часть шаблона URL содержит два параметра:

    • :listController
    • :id

    Пока использую только один из них, в то время, как Ангуляр построит RESTful-ресурс должным образом. Чтобы продемонстрировать это, написал демку, в которой определяется, а затем задействуется ресурс сообщения как было показано выше:

    <!doctype html>
    <html ng-app="Demo">
    <head>
        <meta charset="utf-8" />
        <title> Использование RESTful контроллеров для ресурсов AngularJS</title>
     
        <!--
            Т. к. работаем с ресурсами, то должны загрузить модули
            AngularJS и ngResource.
        -->
        <script type="text/javascript" src="../angular-1.0.2/angular.js"></script>
        <script type="text/javascript" src="../angular-1.0.2/angular-resource.js"></script>
        <script type="text/javascript">
     
     
            // Говорим Ангуляру загрузить ngResource перед загрузкой 
            // основного модуля приложения.
            var app = angular.module( "Demo", [ "ngResource" ] );
     
            // Запускаем, когда app готово.
            app.run(
                function( $resource ) {
     
     
                    // При определении ресурса, мы получаем несколько действий
                    // из коробки, таких как как get() и query(), основанных на
                    // стандартных командах HTTP. Но можем также использовать RESTful
                    // контроллер для изменения состояния ресурса с помощью
                    // действия, которое выходит за рамки обычных операций CRUD.
                    var messages = $resource(
                        "./api.cfm/messages/:listController:id/:docController",
                        {
                            id: "@id",
                            listController: "@listController",
                            docController: "@docController"
                        },
                        {
                            clear: {
                                method: "POST",
                                params: {
                                    listController: "clear-all"
                                }
                            },
                            archive: {
                                method: "POST",
                                params: {
                                    docController: "archive"
                                }
                            }
                        }
                    );
     
                    // Теперь наш ресурс определен, давайте вызовем
                    // его с различными параметрами.
     
                    // GET без ID.
                    messages.query();
     
                    // POST с контроллером списка.
                    messages.clear();
     
                    // GET с ID.
                    messages.get(
                        {
                            id: 4
                        }
                    );
     
                    // POST с контроллером документа.
                    messages.archive(
                        {
                            id: 8
                        }
                    );
     
                }
            );
     
        </script>
    </head>
    <body>
        <!--  Намеренно оставлена пустой. -->
    </body>
    </html>
    

    Обратите внимание, что мой ангуляровский ресурс рассматривается двумя различными контроллерами:

    • :ListController – действует на коллекцию сообщений.
    • :DocController – действует на конкретное сообщение.

    Когда выше задействовались методы ресурса, то, в конечном итоге, было сделано четыре запроса по следующим адресам:


    Как можно увидеть Ангуляр правильно определил URL-адреса в соответствии с REST. Кайф!

    Если интересно, вот файл тестового API:

    <!--- Получение сырого ресурсного пути, который был запрошен. --->
    <cfset resourcePath = cgi.path_info />
     
     
    <!---
        ПРИМЕЧАНИЕ: Кажется, что ColdFusion 10 подвис, пока я не сделал запрос для получения тела POST. Не знаю, почему.
    --->
    <cfif ( cgi.request_method neq "GET" )>
     
        <cfset requestBody = getHTTPRequestData().content />
     
    </cfif>
     
     
    <!---
       Определяем тип запроса, который пришел, на основе шаблона  запрошенного ресурсного пути.
    --->
    <cfif reFind( "^/messages$", resourcePath )>
     
        <cfset response = "GET without ID." />
     
    <cfelseif reFind( "^/messages/clear-all$", resourcePath )>
     
        <cfset response = "POST with clear-all Controller." />
     
    <cfelseif reFind( "^/messages/\d+$", resourcePath )>
     
        <cfset response = "GET with ID." />
     
    <cfelseif reFind( "^/messages/\d+/archive+$", resourcePath )>
     
        <cfset response = "POST with archive controller" />
     
    <cfelse>
     
        <cfset response = "Hmm, couldn't match resource." />
     
    </cfif>
     
     
    <!---
         Преобразуем ответ в JSON. Ангуляр будет знать, как его распарсить.
    --->
    <cfset serializedResponse = serializeJSON( response ) />
     
    <!--- Добавляем заголовок для отладки пути. --->
    <cfheader
        name="X-Debug-Path"
        value="#cgi.path_info#"
        />
     
    <!--- Возврат потока ответов обратно к клиенту. --->
    <cfcontent
        type="application/json"
        variable="#charsetDecode( serializedResponse, 'utf-8' )#"
        />
    


    По некоторым причинам, ColdFusion 10 зависал при POST запросах, до тех пор, пока я не получал доступ к телу запроса (через функцию getHttpRequestData()). Такое происходило только, если тело запроса содержало JSON (JavaScript Object Notation) данные. Когда оно было пустым, запрос не вис. Не разбираюсь досконально в обработке запросов и не знаю, имеет ли это смысл, однако, замечу, что ColdFusion 9 такого не вытворяет.

    Оригинал
    Поделиться публикацией

    Комментарии 25

      0
      А-а! ColdFusion.
      Неужели его кто-то использует?

      А за статью по AngularJS спасибо.
        0
        Статья с осени прошлого года :-) По ресурсам больше ничего не смог найти.
        +1
        Поправьте, если я ошибаюсь, но RESTful подразумевает использование методов GET, POST, PUT, DELETE и указание действия в параметрах запроса, а не в адресе ресурса.
        DELETE /api.cfm/messages
        POST /api.cfm/messages/8?action=archive
          0
          Можно подробнее об этом. И желательно ссылку на правила (укажу в примечании). Т.к. это перевод, то что было в оригинале, то было
            +1
            Я погорячился, сказав слово «подразумевает» — как справедливо заметил marapper: REST — это принцип построения архитектуры, строгой спецификации не существует. Зато существует куча руководств в стиле best practices — сегодня только ленивый (вроде меня) не написал собственный гайд по «правильному» REST.

            Мне по душе пришлись ресурсы restapitutorial.com (и их RESTful Best Practices), и Thoughts on RESTful API Design.
            Как указывать действие в запросе — руководства оставляют решение этого вопроса на плечах разработчика.
              0
              Ох уж все эти бест практисы. Выбрали бы уже один, пусть не бест, а норм и все бы им пользовались. А так только голову забивают))
            0
            Не совсем, REST — просто архитектура, она определяет использование возможностей протокола, т.е.

            Метод как указание типа действия (фетчинг, изменение имеющегося, создание нового, удаление). Есть еще PATCH, HEAD — их также можно использовать.

            Ресурс как объектное отражение сущностей, над которым можно произвести какие-то действия.

            Коды и прочие заголоки запросов и ответов как дополнительная возможность управления и доп.информация.

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

            Также есть такая вещь, как HateOAS, штука которая, кхе-кхе, гомогенизирует такой REST-интерфейс — restcookbook.com/Basics/hateoas/
              0
              Как понял, они (HateOAS) просто-напросто все действия в GET'е проводят. И все равно называют это REST'ом :-)
                0
                Наверное, у меня не совсем верное представление о понятии «RESTful» — я думаю, что это просто набор лучших практик реализации REST-архитектуры, и в первую очередь — использование по полной возможностей протокола HTTP.

                На самом деле, я считаю, что указывать действие стоит вообще в http-заголовках, а не параметрах URI — но уж лучше в параметрах, чем в пути.
                Но, если над сущностью можно сделать больше операций, чем обычно, то куда логичнее это сделать отдельным ресурсом, т.к. смешение в POST редактирования и, например, привязки другой сущности — странно.
                Вы не хотите смешивать в одном методе привязку сущности и редактирование, а я не хочу смешивать в ресурсе сущность и действия. Метод POST просто подразумевает отправку данных, ничего более, на то он и метод. Кто-то вообще отождествляет метод POST с созданием сущности — не думаю, что это правильно. А для привязки сущности есть http-метод LINK :)
                  0
                  А что по этому поводу говорят реализации REST в популярных серверных фреймворках? Они же предлагают какой-то путь? Или просто расчленяют запрос на параметры, а дальше сам думай?
                    +1
                    Не уверен, что совсем понял вопрос, но:
                    есть python-фреймворки flask и bottle — каждый из них позволяет повесить на один маршрут обработчики для разных http-методов: bottle с помощью декораторов, flask — c помощью декораторов, либо с помощью отдельных методов объекта, представляющего ресурс.
                    Ну а работа с http-заголовками и аргументами запроса не представляет труда во многих популярных фреймворках.
                +1
                > POST /api.cfm/messages/8?action=archive
                Я читал где то, что если Вы делаете POST и передаете
                параметр action это почти всегда плохо. И в целом
                уже несколько лет такое правило мне здорово помогает.

                Лучше делать POST /api.cfm/archive и в теле запроса message=8
                В ответ сервер возвращает ID транзакции по которому можно
                опрашивать состояние операции.
                  +1
                  Я читал где то, что если Вы делаете POST и передаете
                  параметр action это почти всегда плохо.
                  Речь о параметре в URI, или вообще в запросе?
                  И в целом
                  уже несколько лет такое правило мне здорово помогает.
                  Помогает чем?
                  Лучше делать POST /api.cfm/archive и в теле запроса message=8
                  В случае с добавлением в архив это может быть уместно, но что делать если нужно просто произвести манипуляцию над документом? Например, конвертировать все изображения документа в формат png, указав при этом необходимые характеристики конечного изображения.
                  В ответ сервер возвращает ID транзакции по которому можно
                  опрашивать состояние операции.
                  Такой подход актуален для операций, которые могут затянуться по времени. Для быстрых операций, на мой взгляд, это избыточное решение.
                    +1
                    > Помогает чем?
                    Помогает поддерживать API в постоянно изменяющихся условиях.
                    Смотришь на решения которые принимал несколько лет назад и
                    видишь — такой подход хорош. С точки зрения клиенской библиотеки
                    и с точки зрения структуры исходного кода. Позольте объяснить подробней.

                    Очень редко операции по изменению данных касаются одного ресурса.
                    Обычно изменения происходят в рамках одной транзакции.
                    Вот мы эту транзакцию и создаем делая POST.

                    Ведь если нужно изменить одно поле — делаем PUT, не так ли?

                    > Речь о параметре в URI, или вообще в запросе?
                    В запросе. Хотя даже /item/id/do-something я тоже стараюсь избегать.
                    Наверное потому что потратив много времени в борьбе с чужими API
                    и на разработку своих, считаю Computation REST наиболее близким к идеалу.

                    Но нужно понимать что правило «don't do POST with action» это не догма, а способ
                    сделать жизнь простого разработчика проще. Который работает для меня.

                    PS. www.erenkrantz.com/CREST/
                      0
                      Потихоньку начинаю понимать плюсы такого подхода.
                      Спасибо за информацию для размышления.
                        +1
                        Спасибо за понимание. Больше хороших API!
                          0
                          И Вам :)
                0
                Зачем использовать два «контроллера» в url?
                На сколько я помню, достаточно вот такого:
                ......
                "./api.cfm/messages/:id/:docController"
                ......
                                clear: {
                                            method: "POST",
                                            params: {
                                                docController: "clear-all"
                                            }
                                        },
                

                И anular Отлично отследит отсутвие :id, и составит правильный url
                  0
                  Хотите сказать, что команду «arhive» так же передавать в docController, а Ангуляр просто уберет лишний слеш (возникающий из-за отсутствующего айди)?
                    0
                    Именно так.
                      +1
                      И при этом получается более логично.
                      message.archive({id: 8}); // архивация сообщения с id=8
                      message.archive(); // архивация всех сообщений
                      
                        0
                        Вообще, да. В статье есть фраза, которую перевел как «Пока использую только один из них, в то время, как Ангуляр построит RESTful-ресурс должным образом» Возможно, автор имел в виду такой подход… Просто затруднился правильно подобрать смысл.
                          0
                          Если я правильно понял, то в оригинале говорится о вот этих двух параметрах ":listController:id", из которых angular построит правильный запрос.
                          Не понимаю почему автор оригинала выбрал такой подход… но всё равнно узнал что-то новое. Я до этого даже и не думал, что можно использовать совместно несколько плейсхолдеров, в одной части url.
                  0
                  На сколько я понял REST, то смысл в том, что бы манипулировать самими данными, без вызова функций обработки на стороне сервера.

                  archiveMessage = function(id) {
                    message.id = id;
                    message.$save();
                  }
                  archiveAll = function() {
                    //Update field loop
                    ....
                    // end loop
                    var MyRequest = $resource('/messages', {}, {saveData: {method:'POST', isArray: true}}); 
                    $scope.doSubmit = function() { MyRequest.saveData({}, $scope.data); //scope.data is array of objects
                  }
                  

                  Да согласен, чрезмерная передача данных будет (+ еще нужно учесть, что результат сохранения в REST должен вернуть объект измененный/массив объектов)
                    0
                    /messages/:listController:id/:docController не будет работать если hostname или часть пути содержит `id`.

                    Например:

                    $resource('http://foo-id.com/messages/:listController:id/:docController', {id: 2});

                    Выдаст

                    foo-id.com/messages:listController2

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