AngularJS + PHP. Заставляем $http-сервис веcти себя как jQuery.ajax()

http://victorblog.com/2012/12/20/make-angularjs-http-service-behave-like-jquery-ajax
  • Перевод
  • Tutorial
Новички в Ангуляре часто путаются из-за того, что быстрые функции $http-сервиса (напр., $http.post()) не взаимозаменяемы с эквивалентными функциями Джиквери (напр., jQuery.post()), не смотря на то, что соответствующие руководства описывают их использование схожим образом. То есть, если код в Джиквери до этого имел вид:

(function($)
{
  jQuery.post('/endpoint', { foo: 'bar' }).success(function(response)
  {
    // ...
  });
})(jQuery);

То можно обнаружить, что следующий точно такой же не работает из коробки в Ангуляре:

var MainCtrl = function($scope, $http)
{
  $http.post('/endpoint', { foo: 'bar' }).success(function(response)
  {
    // ...
  });
};

Проблема, с которой можно столкнуться, в том, что сервер не может получить параметры { foo: 'bar' } из запроса Ангуляра.

Различие в том, как Джиквери и Ангуляр сериализуют и передают данные. В основном, проблема заключается в языке, на котором написана серверная часть и который просто не понимает передачу Ангуляра с настройками по-умолчанию — чертовски обидно, потому что Ангуляр, конечно, не делает ничего плохого. По умолчанию, Джиквери передает данные с использованием Content-Type: x-www-form-urlencoded и привычной строки foo=bar&baz=moe. Ангуляр, однако, передает данные с использованием Content-Type: application/json и { "foo": "bar", "baz": "moe" } строки JSON, которую, к сожалению, некоторые серверные языки — особенно РНР — изначально не преобразуют в объект.

К счастью, разработчики Ангуляра позаботились о поддержке такого метода $http-сервисом, чтобы установить x-www-form-urlencoded для всех наших передач. Существует множество решений, предлагаемых на форумах и StackOverflow, но они не идеальны, поскольку требуют либо изменить код сервера, либо схему использования $http. Поэтому представляю вам наилучшее возможное решение, которое не требует изменений ни серверного, ни клиентского кода, а предлагает внести некоторые незначительные изменения в работу $http в настройках модуля приложения:

// Корневой модуль приложения ...
angular.module('MyModule', [], function($httpProvider)
{
  // Используем x-www-form-urlencoded Content-Type
  $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';
 
  // Переопределяем дефолтный transformRequest в $http-сервисе
  $httpProvider.defaults.transformRequest = [function(data)
  {
    /**
     * рабочая лошадка; преобразует объект в x-www-form-urlencoded строку.
     * @param {Object} obj
     * @return {String}
     */ 
    var param = function(obj)
    {
      var query = '';
      var name, value, fullSubName, subValue, innerObj, i;
      
      for(name in obj)
      {
        value = obj[name];
        
        if(value instanceof Array)
        {
          for(i=0; i<value.length; ++i)
          {
            subValue = value[i];
            fullSubName = name + '[' + i + ']';
            innerObj = {};
            innerObj[fullSubName] = subValue;
            query += param(innerObj) + '&';
          }
        }
        else if(value instanceof Object)
        {
          for(subName in value)
          {
            subValue = value[subName];
            fullSubName = name + '[' + subName + ']';
            innerObj = {};
            innerObj[fullSubName] = subValue;
            query += param(innerObj) + '&';
          }
        }
        else if(value !== undefined && value !== null)
        {
          query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';
        }
      }
      
      return query.length ? query.substr(0, query.length - 1) : query;
    };
    
    return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;
  }];
});

Примечание: Большой фрагмент выше необходимо всегда использовать с указанием функции param(). НЕ используйте вместо этого jQuery.param(); что вызовет хаос, когда попытаетесь воспользоваться $resource Ангуляра, поскольку jQuery.param() будет отклонять каждый, переданный ему, метод класса $resource! (Это особенность JQuery из-за которой свойства объекта для параметризации, содержащие функции, вызываются и возвращаемое ими значение используется в качестве параметризованного, но в случае с Ангуляром это может быть губительно, так как мы обычно передаем «реальные» экземпляры объектов с методами и т.д.)

Просто используйте приведенный выше фрагмент и всё будет в порядке!


Теперь можно двигаться дальше, используя $http.post() и другие подобные методы, предполагая, что существующий серверный код ожидает x-www-form-urlencoded данные. Вот несколько примеров конечного результата для обыденного, законченного кода (пример того, на что вы надеялись и мечтали):

HTML-шаблон
<div ng-app="MyModule" ng-controller="MainCtrl">
  <p ng-show="loading">Loading...</p>
  <p ng-hide="loading">Response: {{response}}</p>
</div>

Клиентский код (AngularJS)
var MainCtrl = function($scope, $http)
{
  $scope.loading = true;
  $http.post('/endpoint', { foo: 'bar' }).success(function(response)
  {
    $scope.response = response;
    $scope.loading = false;
  });
};

Серверный код (PHP)
< ?
header('Content-Type: application/json');
 
// Та-дам! $_POST теперь нормальный; PHP без проблем
// преобразует запросы Ангуляра в объекты
echo json_encode($_POST);
?>

Другие примечания
Напрашивается вопрос, можно ли всё-таки в PHP прочитать JSON-запрос Ангуляра? Ну, конечно, если читать входной поток PHP и декодировать JSON:

< ?
$params = json_decode(file_get_contents('php://input'));

// или так, если нужно преобразовать в PHP массив (прим. переводчика)
$params = json_decode(trim(file_get_contents('php://input')), true);
?>

Очевидный недостаток в том, что код немного менее интуитивен (мы привыкли к $_POST, в конце концов), и если серверные обработчики уже написаны с использованием $_POST, то придется переписывать серверный код. Если используется хороший фреймворк, вероятно, можно осуществить глобальные изменения, так, чтобы обработчик входа прозрачно определял JSON запросы, но я отвлекся.

Примечание переводчика: Если есть возможность, лучше настроить сервер так, чтобы он принимал Content-Type: application/json, что более логично. В статье описывается подход для крайних случаев, когда на сервере ничего нельзя поделать.

Добавка. Дополнение от nervgh

'use strict';

(function() {

    angular.extend( angular, {
        toParam: toParam
    });

/**
 * Преобразует объект, массив или массив объектов в строку,
 * которая соответствует формату передачи данных через url
 * Почти эквивалент [url]http://api.jquery.com/jQuery.param/[/url]
 * Источник [url]http://stackoverflow.com/questions/1714786/querystring-encoding-of-a-javascript-object/1714899#1714899[/url]
 *
 * @param object
 * @param [prefix]
 * @returns {string}
 */
function toParam( object, prefix ) {
    var stack = [];
    var value;
    var key;

    for( key in object ) {
        value = object[ key ];
        key = prefix ? prefix + '[' + key + ']' : key;

        if ( value === null ) {
            value = encodeURIComponent( key ) + '=';
        } else if ( typeof( value ) !== 'object' ) {
            value = encodeURIComponent( key ) + '=' + encodeURIComponent( value );
        } else {
            value = toParam( value, key );
        }

        stack.push( value );
    }

    return stack.join( '&' );
}

}());


'use strict';

// change default settings
var app = angular.module( 'app', [ ... ]);

app

    .config( function( $httpProvider ) {    // [url]http://habrahabr.ru/post/181009/[/url]
        $httpProvider.defaults.headers.post[ 'Content-Type' ] = 'application/x-www-form-urlencoded;charset=utf-8';
        $httpProvider.defaults.transformRequest = function( data ) {
            return angular.isObject( data ) && String( data ) !== '[object File]' ? angular.toParam( data ) : data;
        };
    });
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 27
  • +2
    Можете меня пнуть, но я не понимаю почему все так любят Ангуляр, если даже таки элементарные вещи делаются там костылями.
    • 0
      Думаю, что эта та вещь, которую однажды сделал и забыл. А рутинный код он ускоряет в разы.
      • 0
        Ну во всех других фреймврках почему-то этот bolierplate не нужен.
        Просто при работе с фреймворком я обычно хочу видеть работу из коробки. А не поковырять там, допилить здесь.
      • +5
        А в чем костыль? Костыль это использовать application/x-www-form-urlencoded там, где нет формы и писать 57 строк на клиенте, вместо одной на сервере.
        • 0
          Ну вот тут пишутся эти 57 строк, а даже в том же Backbone они не пишутся.
          Чем не костыль?
          • +2
            Я о том, что x-www-form это не элементарная вещь, а костыль. В том же Backbone:
            If you're working with a legacy web server that can't handle requests encoded as application/json, setting Backbone.emulateJSON = true; will cause… the request to be made with a application/x-www-form-urlencoded MIME type, as if from an HTML form.

            AngularJS, видимо, выбрал не поддерживать legacy web servers.

            Вот что хотелось бы в AngularJS, это чтобы ресурсы возвращали promises по аналогии с $http, и в promises реализовали бы always.

            • –1
              Видно не такие уж они legacy, раз такая проблема возникла
              • +1
                Понятно, спасибо. А можно узнать какие серверные фреймворки поддерживают application/json, а какие нет?
                • +1
                  Насколько знаю, все популярные поддерживают (yii, zend, symfony). Да и PHP сам по себе поддерживает, только вместо $_POST нужно json_decode(file_get_contents('php://input'), true) писать.
                  • +1
                    Тогда можно и:

                    $_POST = json_decode(file_get_contents('php://input'), true);
                    
                    • 0
                      Главное, чтобы потом не понадобилось данные формы получать :-)
                • 0
                  Так уже есть в новой версии habrahabr.ru/post/180767/ См. сервис $q
                  • 0
                    Круто, а есть экспиренс применения unstable 1.1.х? Часто ломается код, написанный под эту версию?
                    • 0
                      Ни разу не замечал никаких багов. Правда, не особо сложные вещи делаю. В любом случае, разработчики обещают осенью довести эти версии до стабильной 1.2
                  • +1
                    В ветке 1.1 и promises для ресурсов, и always уже есть — осталось дождаться стабильного релиза (1.2).
                    Упс, выше уже ответили наполовину.
            • 0
              Мне кажется в любом проекте должна быть обертка вокруг глобальных переменных, и такую дают вроде как все фреймворки. А если есть обертка — зачем такие костыли?
              • 0
                Согласен. Тоже буду использовать json_decode(file_get_contents('php://input')), вместо предложенного решения. Но факт, что столкнулся с проблемой и пол дня потратил на поиски решения. Так что, надеюсь, кому-то сэкономлю время!
              • 0
                Благо Flask это обрабатывает как положено.
                • 0
                  Для Silex есть решение в Cookbook. silex.sensiolabs.org/doc/cookbook/json_request_body.html
                  • 0
                    По моему надо все это вытащить в ангуляровский сервис, а не размазывать по контроллеру и конфигурации модуля.
                    • 0
                      Было бы интересно узнать, как цивилизованно обрабатывать формы с input type=file в ангуляре.
                      • 0
                        Будет у меня такая задача. Расскажу, когда разберусь
                      • 0
                        Как я понимаю рассчитано на использование с сервером типа nodejs.
                        • +1
                          Практически все ПХП-фреймворки понимают application/json, У раби он рейлс таких проблем, вообще, нет. Не знаю, правда, как с Питоном. Тут, мне кажется, ПХП отстает…
                          • 0
                            раби… джиквери… ппц.
                        • 0
                          Б-га ради, не пишите слово «Джиквери» :-)
                          • 0
                            А что, хорошее русское слово :-)

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