Debian, Dojo, Django, Python

Делюсь опытом в описанных технологиях. Блог в первую очередь выполняет роль памяток для меня самого.

Показаны сообщения с ярлыком angular. Показать все сообщения
Показаны сообщения с ярлыком angular. Показать все сообщения

Gulp для сборки приожений Angular 1.x

Немного про Gulp

Было время, когда мне очень нравился Grunt, а причина была проста - другие средства сборки Web-проектов только начинали развиваться (2013 год, если что). Вскоре я открыл для себя Gulp. В сравнении с Grunt это был действительно удобный инструмент, и я надолго остановился именно на нём.

Всё никак не найду время на изучение Angular 2 и Webpack, поэтому один из мелких проектов пишу на Angular 1.x (актуальная версия на момент написания статьи - 1.6.x). Кто писал на нём, знает, что все контроллеры, директивы, сервисы и прочие компоненты лучше размещать в отдельных файлах, однако, потом на странице нужно подключать каждый файл отдельно.

Мириться с этим было нельзя, я поискал на StackOverflow похожие запросы и выяснил, что многие сталкивались с этой проблемой. Я предлагаю своё решение, являющееся компиляцией из разных источников.

Установка пакетов

В каталоге проекта инициализируем npm, отвечаем на несколько простых вопросов, после чего ставим необходимые плагины:

npm init && npm install gulp gulp-rename gulp-uglify gulp-include gulp-watch --save
Название Описание
gulp-rename Позволяет указать имя целевого файла, отличное от исходного. Используется для создания отладочных и минимизированных версий файлов.
gulp-uglify Сжимает JS. Используется для создания минимизированных продуктовых версий.
gulp-include Позволяет вставлять код из одного файла в другой подобно тому, как это делает директива #include> в C-подобных языках. Так же позволяет делать вызов require, при этом код будет вставлен только в том случае, если не вставлялся ранее.
gulp-watch Отслеживает изменения в нужных файлах, при необходимости автоматически вызывая нужные Gulp Task

gulpfile.js

В каталоге приложения так же создадим файл по имени gulpfile.js (регистр важен) со следующим содержимым:

gulpfile.js
(function (r) {
    "use strict";

    var gulp = r("gulp"),
        uglify = r("gulp-uglify"),
        include = r("gulp-include"),
        rename = r("gulp-rename"),
        sources = {
            login: "./src/login.js",
            app: "./src/app.js"
        },
        DEST = "./todoist/static/app/"

    gulp.task("js-dev", function () {
        // Сборка без минификации, специально для отладки
        gulp.src(sources.login)
            .pipe(gulp.dest(DEST));

        gulp.src(sources.app)
            .pipe(include())
            .pipe(gulp.dest(DEST));
    });

    gulp.task("js-prod", function () {
        gulp.src(sources.login)
            .pipe(uglify())
            .pipe(rename("login.min.js"))
            .pipe(gulp.dest("./todoist/static/app/login.min.js"));

        gulp.src(sources.app)
            .pipe(include())
            .pipe(uglify())
            .pipe(rename("app.min.js"))
            .pipe(gulp.dest(DEST));
    });

    gulp.task("watch", function () {
        gulp.watch("./src/**/*.js", ["js-dev"]);
    });

    gulp.task("default", ["js-dev", "watch"]);

}(require));

Надеюсь, комментарии к тому, что тут происходит, не нужны.

Файлы приложения

Все исходные файлы приложения у меня лежат в src/, для контроллеров создан каталог controllers, для сервисов - services (директивы и фильтры пока не успел написать, поэтому и каталогов нет).

Так выглядит главный файл приложения:

app.js
(function (A) {
    "use strict";
    var app = A.module("todoist", ["ui.bootstrap", "ngRoute", "ngCookies"]);

    app.config(["$interpolateProvider", "$httpProvider", "$routeProvider", function ($interpolateProvider, $httpProvider, $routeProvider) {
        $interpolateProvider.startSymbol("{$");
        $interpolateProvider.endSymbol("$}");

        $httpProvider.defaults.xsrfCookieName = "csrftoken";
        $httpProvider.defaults.xsrfHeaderName = "X-CSRFToken";
        $httpProvider.defaults.headers.common.Authorization = "Basic";


        $routeProvider.when("/", {
            controller: "MainController",
            templateUrl: "/ui/index.html"
        }).when("/users", {
            controller: "UserListController",
            templateUrl: "/ui/users/list.html"
        }).when("/users/:id", {
            controller: "UserDetailController",
            templateUrl: "/ui/users/detail.html"
        }).otherwise({
            redirectTo: "/login"
        })
    }]);

    //=include ./controllers/*.js
    //=include ./services/*.js"
}(this.angular));

При сборке вместо комментариев //=include ./controllers/*.js будет вставлено содержимое лежащих там файлов - как раз то, чего я и хотел добиться.

Последний шаг - подключить файл приложения в шаблоне Django:

<script src="{% static 'app/app.js' %}"></script>

md-autocomplete со значением по умолчанию

Задача

С помощью Angular Material создать autocomplete со значением по-умолчанию. Статья навеяна вопросом со StackOverflow.

Самому как раз сегодня понадобилось решить эту же задачу. Результат можно посмотреть здесь.

Решение

HTML-разметка

<body ng-app="app" flex layout="column" layout-margin ng-controller="Main">
  <md-content layout="column" class="md-whiteframe-z1" layout-margin>
    <md-toolbar>
      <div class="md-toolbar-tools">
        <h3>Form</h3>
      </div>
    </md-toolbar>
    <md-content class="md-whiteframe-z1">
      <div class="md-padding">
        <md-input-container>
          <label for="firstname">First name</label>
          <input type="text" name="firstname" ng-model="user.firstname" />
        </md-input-container>
        <md-input-container>
          <label for="lastname">Last name</label>
          <input type="text" name="lastname" ng-model="user.lastname" />
        </md-input-container>
        <md-autocomplete md-selected-item="user.group" md-items="item in loadGroups(filterText)" md-item-text="item.title" md-search-text="filterText">
          <md-item-template>{{ item.title }}</md-item-template>
          <md-not-found>No items.</md-not-found>
        </md-autocomplete>
      </div>
    </md-content>
  </md-content>
  <md-content class="md-whiteframe-z1" layout-margin>
    <md-toolbar>
      <div class="md-toolbar-tools">
        <h3>Model as JSON</h3>
      </div>
    </md-toolbar>
    <md-content class="md-padding">
      <p>
        {{ user | json }}
      </p>
    </md-content>
  </md-content>
</body>

JavaScript

(function(A) {
  "use strict";

  var app = A.module('app', ['ngMaterial']);

  function main(
    $q,
    $scope,
    $timeout
  ) {
    $timeout(function() {
      $scope.user = {
        firstname: "Maxim",
        lastname: "Dunaevsky",
        group: {
          id: 1,
          title: "Administrator"
        }
      };
    }, 500);

    $scope.loadGroups = function(filterText) {
      var d = $q.defer(),
        allItems = [{
          id: 1,
          title: 'Administrator'
        }, {
          id: 2,
          title: 'Manager'
        }, {
          id: 3,
          title: 'Moderator'
        }, {
          id: 4,
          title: 'VIP-User'
        }, {
          id: 5,
          title: 'Standard user'
        }];

      $timeout(function() {
        var items = [];

        A.forEach(allItems, function(item) {
          if (item.title.indexOf(filterText) > -1) {
            items.push(item);
          }
        });

        d.resolve(items);
      }, 1000);

      return d.promise;
    };
  }

  main.$inject = [
    '$q',
    '$scope',
    '$timeout'
  ];

  app.controller('Main', main);
}(this.angular));

Теперь немного о том, что происходит.

Сначала происходит инициализация модели. В примере задержка ответа эмулируется с помощью $timeout, на практике следует использовать подгрузку данных с помощью $http или $resource. При этом поле, значение которого должно быть связано с полем автодополнения, уже инициализировано, при этом в нём находится целая модель. Вопрос, как получить основную модель, содержащую вложенное поле, излишен. Этим должен заниматься сервер. Например, в Django REST Framework для таких полей используются сериализаторы объектов.

На втором этапе идёт описание способов подгрузки элементов в список. Ну, тут ничего сложного, в общем-то. С помощью того же $timeout эмулируется задержка, во время которой идёт фильтрация элементов списка. Функция возвращает promise, который разрешается списком отфильтрованных записей, когда приходит время.

Немного о Material Design Lite и Angular Material

Хочу сказать пару слов про библиотеку Material Design Lite (MDL).

TL;DR Если нужны только стили, сгодится. Если требуется качественный JS и интеграция с Angular - посмотрите в сторону Angular Material. Впрочем, в своих проектах я использую их вместе, это не так уж сложно.

Стили

Что касается стилей, тут всё довольно неплохо. Во-первых, даже при установке через bower скачиваются исходные файлы. Можно открыть нужный .scss и внести правки. Не уверен, что многие часто пользуются этой возможностью, но в любом случае она есть, и для этого даже не нужно клонировать репозиторий с GitHub.

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

Очень даже хорошо, что из коробки идёт поддержка стилей для таблиц, карточек, панели навигации и некоторых других объектов. По сравнению с Angular Material - шаг вперёд.

JS

А вот с JS не всё гладко. Во-первых, поведение меню. В Angular Material разработчики предусмотрели позиционирование таким образом, чтобы оно не уезжало за экран. В MDL приходится самому заботиться об этом и подбирать нужные стили. Впрочем, это не такая уж большая проблема. Есть кое-что ещё.

MDL выполняет обновление компонентов и установку обработчиков на них только в момент загрузки страницы. Да, он дожидается DOMReady, после чего выполняет заполнение свойств элементов. При использовании какого-либо роутинга в Angular сразу появляется проблема. Решается довольно легко, спасибо StackOverflow:

Приложение
Application.run(['$rootScope', '$location', '$timeout', function (
    $rootScope,
    $location,
    $timeout
) {
    $rootScope.$on("$viewContentLoaded", function () {
        $timeout(function () {
            componentHandler.upgradeAllRegistered();
        });
    });
}]);

Нет некоторых нужных полей, например, поле для выбора даты, которое появилось в Angular Material 0.11.0. Впрочем, обе библиотеки прекрасно работают вместе.

Если пишете SPA на Angular, для создания навбара и верхнего тулбара лучше используйте те, что идут в Angular Material. Почему? Потому что эти элементы статичны в MDL, а нам же нужно будет иногда менять их содержимое, не так ли? Да и привязка к контроллерам будет проще, если сделать один ng-view, без использования ng-include или сторонних вещей типа ui-route.

Angular Material и md-list - проблема с дополнительным действием

В Angular Material есть такой хороший компонент - md-list, и работающий с ним в паре md-list-item. Из них можно делать красивые списки, обладающие весьма важными свойствами. Во-первых, каждая строка реагирует на нажатие. Можно реализовать возможность перехода по ссылке. Во-вторых, к каждой строке можно добавить кнопку действия.

Это я всё к чему? А к тому, что сегодня почти час убил на то, чтобы разобраться, почему скопированный почти один в один пример с официальной доки работает у них и не работает у меня.

Разметка
<md-list>
    <md-list-item ng-repeat="item in items" ng-click="openDetail(item)">
        <img class="md-avatar" alt="" src=""/>
 <p>{{ item.name }}</p>
        <md-icon class="material-icons md-secondary md-warn" ng-click="remove(item)">remove</md-icon>
    </md-list-item>
</md-list>
Вся проблема заключалась в классах CSS для тега md-icon. Чтобы он превратился в кнопку, ему должен быть назначен среди прочих класс md-secondary.

Если кому интересно, то ниже код контроллера. Обратите внимание, я не отлавливаю объект события и не вызываю для него stopPropagation() и preventDefault(), это не требуется.

ctrl.js
(function (A) {
    "use strict";

    var inject = [
        '$location',
        '$mdDialog',
        '$scope'
    ];

    function Ctrl(
        $location,
        $mdDialog,
        $scope
    ){
        // Ничто не мешает загружать записи с сервера
        $scope.items = [
            {id: 1, name: 'Запись №1'},
            {id: 2, name: 'Запись №2'},
            {id: 3, name: 'Запись №3'},
            {id: 4, name: 'Запись №4'}
        ];

        function openDetail(item){
            // Переход на другой вид
            $location.path('/items/' + item.id);
        }

        function remove(item){
     // Запрос на удаление записи
            $mdDialog.show(
                $mdDialog
                    .confirm()
                    .title("Подтверждение")
                    .content('Удалить "' + item.name + '"?')
                    .ok("Да")
                    .cancel("Нет")
                ).then(function () {
                    $scope.items = $scope.items.splice($scope.items.indexOf(item), 1);
            });
        }

        $scope.openDetail = openDetail;
        $scope.remove = remove;  
    }

    Ctrl.$inject = inject;

    A.module('app').controller('Ctrl', Ctrl);
}(this.angular));

Зависимости в Angular

Внеднение зависимостей

Все знают, что Angular построен на постоянном и вездесущем внедрении зависимостей, т.н. dependency injection. Существуют как минимум три способа сделать это.

1. Implicit annotation

Зависимости просто перечисляются как аргументы нашего модуля.

Пример

(function (A){
    "use strict";
    A.module('app', []).controller('MyCtrl', function($scope, $sce){
        //Тут должен быть код контроллера
    });
}(this.angular));
Не используйте этот способ!

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

2. $inject

Хороший способ внедрения зависимостей - создать статическое свойство функции под названием $inject. В нужный момент Angular возьмёт список зависимостей оттуда.

Пример

(function (A){
    "use strict";

    function MyCtrl($scope, $sce){
        //Код контроллера
    }

    MyCtrl.$inject = [ '$scope', '$sce' ]; //Строки не сжимаются никогда

    A.module('app', []).controller('MyCtrl', MyCtrl);
}(this.angular));

Уже лучше. Такой код успешно переживёт минификацию.

3. Inline array annotation

Весьма хороший метод, не хуже предыдущего, но многие статические анализаторы кода, например, в Eclipse, считают такое поведение ошибкой (Они не правы!):

Пример

(function (A){
    "use strict";

    function MyCtrl($scope, $sce){
        //Код контроллера
    }

    A.module('app', []).controller('MyCtrl', [ '$scope', '$sce', MyCtrl ]);
}(this.angular));

Такой код тоже хорошо переживёт минификацию.

Дополнительно

Недавно узнал, что в Angular даже есть специальная директива для запрета внедрения зависимостей первым способом - ng-strict-di. Указывать её надо рядом с ng-app:

Пример

<!doctype html>
<html ng-app="app" ng-strict-di>
    <head>
        <!-- Код -->
    </head>
    <body>
        <!-- И тут тоже код -->
    </body>
</html>

После этого попытка внедрения зависимостей через Implicit Annotation будет вызывать ошибку.

CSRFToken в Angular 1.4

В новом Angular 1.4 одно из приятных изменений - возможность задавать куку и заголовок для CSRF-токена. Раньше делали так:

angular.module('app', ['ngCookies']).run(['$http', '$cookies', function($http, $cookies){
    var token = $cookies.csrftoken;
    $http.defaults.headers.common['X-CSRFToken'] = token;
    $http.defaults.headers.post['X-CSRFToken'] = token;
}]);

Сейчас на этапе конфигурирования приложения можно сделать такой финт:

angular.module('app', []).config(['$httpProvider', function($httpProvider){
    $httpProvider.defaults.xsrfCookieName = 'csrftoken';
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
}]);

Оригинальная документация - здесь. Проверял работу на Django 1.8.

Расширение контроллеров в Angular

Краткое содержание

В статье рассказывается о том, как в Angular можно реализовать наследование контроллеров.

Суть проблемы

Мне приходится писать довольно много кода на Angular, при этом заметил, что от контроллера к контроллеру меняются, порой, лишь незначительные части. Например, везде, где я использую ngInfiniteScroll, в $scope приходится помещать переменные allLoaded и loading, а так же обработчик loadMore(), выполняющий загрузку новых элементов. По незнанаю, приходилось многократно дублировать практически одинаковый код. Однако, один из пользователей StackOverflow нашёл очень хорошее решение данной проблемы.

Решение

Решение оказалось на удивление простым и заключается в использовании сервиса $controller.

BaseClass.js
(function (A){
    "use strict";
    // Базовый контроллер, который содержит код, общий для всех других контроллеров,
    // которые будут наследоваться от него
    A.module('App').controller('ListController', ['$scope', function($scope){
        $scope.allLoaded = false;
        $scope.items = [];
        $scope.loading = false;

        $scope.loadMore = function(){
            if ($scope.loading){
                return;
            } 
            // Тут какая-то работа по загрузке данных
        };

        $scope.refresh = function(noPlease){
            if (noPlease){
                return;
            }
            $scope.items.length = 0; // Более правильное решение, нежели $scope.items = [];
                                     // т.к. в этом случае сохраняется ссылка на оригинальный
                                     // объект
        };    
    }]);
}(this.angular));
ChildClass.js
(function(A){
    "use strict";
    A.module('App').controller('UserListController', [ '$controller', '$scope', function($controller, $scope){

            // Расширяем контроллер
            $controller('ListController', {
                $scope: $scope
            });

            // Теперь можно собственные свойства описывать
        }]);
}(this.angular));

Работа с REST API в Angular JS

Предупреждение! Статья устарела, часть описанных в ней вещей касательно $resource не верна, планируется к удалению.

Введение

Так как я последнее время сильно увлечён фреймворком Angular, я стараюсь писать на нём настолько хорошо, насколько могу. Одна из последних интересных задач, с которыми мне пришлось столкнуться - работа с ресурсами. Для своего проекта я решил использовать так же Django REST Framework, т.к. видел о нём много положительных отзывов.

Ресурсом в данном контексте я понимаю экземпляр объекта, созданного Angular с помощью модуля $resource. Он позволяет легко обмениваться данными с сервером, полностью реализуя клиентскую часть архитектуры REST. В статье будут рассмотрены популярные решения и моё собственное.

Обзор REST

Рассматривать решение проблемы будем на примерах. Допустим, имеется новостной сайт. Каждая новость - это объект со своими свойствами. Кроме того, на сайте есть возможность оставлять комментарии. Итак, мы уже можем создать три взаимосвязанных класса объектов:

Пользователь
  • Логин
  • Дата регистрации
  • ФИО (для удобства объединим в одно поле, но на самом деле лучше сделать 3 отдельных)
  • Дата рождения
  • Блокировка
Новость
  • Автор - ссылка на пользователя
  • Дата публикации
  • Заголовок
  • Анонс
  • Полный текст
  • Опубликована (если нет, значит, находится в состоянии черновика)
  • Количество просмотров (автоматически увеличивается с каждым уникальным пользователем)
Комментарий
  • Автор - аналогично новости, ссылка на пользователя
  • Текст комментария
  • Новость - ссылка на описанную выше модель
  • Дата написания
  • Рейтинг - обновляется пользователями (автоматически вычисляется на сервере на основе анализа данных таблицы с плюсами и минусами)

Итак, у нас уже три сущности. Спроектируем API. Тут, в принципе, ничего сложного. Весь API уместится в несколько строк:

# Пользователи
/api/user/
/api/user/:id/
/api/user/:id/news/
/api/user/:id/comments/

# Новости
/api/news/
/api/news/:id/
/api/news/:id/comments/

# Комментарии
/api/comment/
/api/comment/:id/

Как видно, API получился очень простым. Сделаем допущение, что по ссылкам без указания версии используется последняя версия, а конкретную можно использовать, например, таким образом:

/api/v1/user/

Так же отметим, что здесь под :id имеется ввиду реальный id записи. Такое обозначение вдвойне удобно, учитывая, что подобным образом формируются ссылки при использовании модуля Angular Resource.

Так же важно понять, как именно работает данный API и почему так мало URL. Нижеприведённая таблица указывает, какого типа запросы отправляются на какой адрес и к чему это приводит:

URL Метод Результат
/api/user/ GET Возвращает список всех пользователей
POST Создаёт нового пользователя, возвращает его объект
/api/user/:id/ GET Возвращает информацию об указанном пользователе
POST Выполняет обновление информации об указанном пользователе
DELETE Удаляет указанного пользователя
/api/user/:id/news/ GET Возвращает все новости, автором которых является указанный пользователь

Сразу скажу, что при использовании Django REST Framework обновление записи методом POST запрещено, вместо неё следует использовать метод PUT. Для этого нужно будет особым образом сконфигурировать $resourceManager, о чём будет сказано ниже.

Установка $resource и его настройка

Для установки зависимостей в проектах я предпочитаю использовать bower. На указанном сайте рассмотрена установка данного менеджера пакетов, здесь я на ней останавливаться не буду. Помимо angular-resource понадобится так же пакет angular-cookie. Без правильной конфигурации печенек Django будет блокировать любые обращения к URL нашего API.

Итак, ставим пакеты:

bower install angular-resource angular-cookie --save

Если Angular не был установлен ранее, он будет подтянут как зависимость. Подключите все нужные библиотеки к странице.

Теперь рассмотрим скрипт инициализации нашего приложения (как правило, это файл, имеющий название app.js)

(function (A) {
    "use strict";
    var app = A.module('MyApp', ['ngResource', 'ngCookie']);

    app.config(['$resourceProvider', function($resourceProvider){
        $resourceProvider.defaults.stripTrailingSlashes = false;
    }]);

    app.run(['$http', '$cookies', function($http, $cookies) {
        $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
        $http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
    }]);

}(this.angular));

Собственно, что происходит в этих нескольких строках.

  • Указываем модуль angular-resource как зависимость для нашего приложения. Без этого ничего не заработает.
  • Конфигурируем провайдер ресурсов. Говоря человеческим языком, делаем фундаментальные настройки, которые повлияют на всё приложение целиком.
  • На этапе запуска приложения указываем, что любые HTTP-запросы должны содержать в себе поле X-CSRFToken. Значение берётся из Cookies, которые формируются на сервере в момент первого обращения к странице.

Работа с ресурсами

Здесь рассмотрим, как разработчики модуля $resource предлагают нам работать с ресурсами.

Получить нужную запись, изменить её данные и отправить изменения на сервер:

var News, //Ресурс
    newsInstance; //Экземпляр новости

function updateNewsItem(){
    newsInstance.published = false;
    newsInstance.$save();
}

News = $resource('/api/news/:id/');
newsInstance = News.get({id: 3}, updateNewsItem); //Новость с id = 3, просто для примера

А как создать новую запись? Вот так

newsInstance = new News({text: 'Примерный текст новости'});
newsInstance.$save();

Что насчёт обновления? Метод $update в свойствах объекта? Нет, не угадали.

newsInstance = News.get({id: 3});
newsInstance.author = 4; //Какой-то другой пользователь станет автором

News.update({id: 3}, newsInstance);

Немного странно, не правда ли? Скажу лишь, что скудность документации - одна из самых многочисленных жалоб на Angular.

Но это частности. Если вкратце, бОльшая часть решений, найденных мной в интернете, предполагает создание фабрики или сервиса, возвращающих созданный объект $resource, например, так мог бы выглядеть наш ресурс для работы с новостями:

(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.factory('News', ['$resource', function ($resource) {
        return $resource('/api/news/:id/', {id: '@id'}, {
            update:{
                method: 'PUT' //Без этого не будет работать обновление объектов на стороне сервера,
                //если используется Django REST Framework
            }
        });
    }]);
}(this.angular));

Ладно, а что насчёт комментариев?


(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.factory('Comment', ['$resource', function ($resource) {
        return $resource('/api/comment/:id/', {id: '@id'}, {
            update:{
                method: 'PUT' //Без этого не будет работать обновление объектов на стороне сервера,
                //если используется Django REST Framework
            }
        });
    }]);
}(this.angular));

Итак, налицо уже идёт дублирование кода! Значит, можно написать нечто такое:

(function (A){
    "use strict";
    var app = A.module('MyApp'),
        resources = {
            'User': '/api/user/:id/',
            'UserComments': '/api/user/:id/comments/',
            'UserNews': '/api/user/:id/news/',
            'News': '/api/news/:id/',
            'NewsCimments': '/api/news/:id/comments/',
            'Comment': '/api/comment/:id/'
        },
        idConfig = {id: '@id'},
        putSettings = {
            update: {
                method: 'PUT'
            }
        },
        i;

    for (i in resources){
        if (resources.hasOwnProperty(i)){ //Стандартная проверка, что это свойство собственное,
                                          //а не унаследовано от прототипа
            app.factory(i, ['$resource', function ($resource){
                return $resource(resources[i], idConfig, putSettings);
            }]);
        }
    }
}(this.angular));

Ну что ж, неплохо, если не считать предупреждения статического анализатора, что опасно создавать функции в цикле. Но по-прежнему остаётся несколько проблем:

  • Размножение сущностей. Почему для получения новости и комментариев к ней используются два разных ресурса?
  • Фабрика. Внутри Angular работает так, что при каждом вызове фабрики создаётся новый объект. Конечно, в JavaScript работает автоматический сборщик мусора, но сам подход не очень хорош.
  • В чём разница между $save и $update? В том, что во втором случае передаётся значение id? Почему бы тогда не вызывать нужный метод в зависимости от того, есть в объекте это свойство или нет?
  • Так и не решилась проблема с обновлением записей. Конечно, у созданного объекта есть все нужные методы, но выглядят они уродливо (это моё личное мнение), плюс приходится работать на уровне, достаточно близком к Pure JS, а хотелось бы чего-то более абстрагированного.
  • Что, если нам потребуются какие-то дополнительные методы для каждого из ресурсов? Например, автоматически выполнять локализацию дату публикации каждого комментария и каждой новости?
  • Безобразие с реализацией обещаний. В некоторых случаях методы ресурсов возвращают объект, а в некоторых - promise. Опять же, путаница.

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

В процессе написания модуля пришлось прибегнуть к ряду различных ресурсов, самым полезным из которых оказался MDN, конкретно - вот эта статья. Для имитации классического наследования я использовал описанный в стандарте ECMAScript 5 метод Object.create.

(function (A){
    "use strict";
    var app = A.module('MyApp'),
        idParams = {id: '@id'}, //Эта часть без изменений
        updParams = {
            update: {
                method: 'PUT'
            }
        };

    app.service('Managers', [
        '$q',
        '$resource',
        function($q, $resource){

            /*
             * Функция создаёт отложенный объект, т.е. тот, который
             * имеет свойство promise и может находиться только в
             * трех состояниях - разрешён, отклонён, в работе.
             * Как правило, такие объекты используются вместе с AJAX
             */
            function defer(){
                return $q.defer();
            }

            /*
             * Функция выполняет обращение к указанному ресурсу
             * указанным методом. При необходимости передаются
             * нужные параметры, например, экземпляр объекта.
             */
            function execQuery(method, data, resource){
                var d = defer();

                // Ниже выполняется обращение к нужному методу ресурса
                resource[method](
                    data,
                    function(response){ //Успешное выполнение запроса
                        d.resolve(response);
                    },
                    function (response){ //Запрос отклонён (ошибки)
                        d.reject(response);
                    }
                );

                return d.promise;
            }

            /*
             * Универсальная функция для создания ресурсов. Принимает
             * на вход два параметра - URL для обращения к API и
             * объект конфигурации. Если он не указан, используется
             * объект по-умолчанию, который будет брать id из
             * свойств самого объекта, передаваемого на вход ресурса.
             */
            function createResource(url, params){
                return $resource(url, params || idParams, updParams);
            }

            function BaseManager(url, params){
                this.sources = {
                    main: createResource(url, params);
                };
            }

            // А теперь к прототипу добавим нужные методы

            // Получить объект по id
            BaseManager.prototype.getById = function(id){
                return execQuery('get', {id: id}, this.sources.main);
            };

            // Получить все объекты
            BaseManager.prototype.getAll = function(){
                return execQuery('query', {}, this.sources.main);
            };

            //Удалить объект
            BaseManager.prototype.remove = function(id){
                return execQuery('remove', {id: id}, this.sources.main);
            };

            //Сохранить или обновить объект
            BaseManager.prototype.save = function(item){
                var method = item.hasOwnProperty('id') ? 'update' : 'save';

                return execQuery(method, item, this.sources.main);
            };



            // Создадим объект новостей, используя шаблон классического
            // наследования средствами ECMAScript 5

            function News(){
                //Вызов "предка" применительно к нашему "классу"
                BaseManager.apply(this, ['/api/news/:id/']);

                //Создадим дополнительный источник данных
                this.sources.comments = createResource('/api/news/:id/comments/');
            }

            // В статье на MDN описано, что здесь происходит
            News.prototype = Object.create(BaseManager.prototype);
            News.prototype.constructor = News;

            // А теперь - собственный метод
            News.prototype.getComments = function (id){
                return execQuery('query', {id: id}, this.sources.comments);
            };


            // Комментарии
            function Comment(){
                BaseManager.apply(this, ['/api/comment/:id/']);
            }

            Comment.prototype = Object.create(BaseManager);
            Comment.prototype.constructor = Comment;


            // Создаём объект, который будет хранить по одному экземпляру
            // Описанных выше классов

            return {
                News: new News(),
                Comment: new Comment()
            };
    }]);
}(this.angular));

Поскольку service реализует в Angular паттерн Одиночка, объект с экземплярами наших классов будет создан всего лишь один раз. Затем при каждом следующем вызове будет возвращено ранее сформированное значение. Посмотрим на практический пример применения описанного выше сервиса:

(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.controller('NewsListController', ['$scope', 'Managers', function ($scope, Managers){

        // Загрузка всех новостей
        function loadNews(){
            $scope.loading = true;
            $scope.news = [];
            Managers.News.getAll().then(
                function (rows){
                    $scope.news = rows;
                },
                function (response){
                    $scope.errors = response.data; // В DRF - именно так
                }
            );
        }

        loadNews();

        // Загрузка комментариев при нажатии на ссылку "Комментарии" для
        // выбранной новости
        $scope.loadNewsComments = function(newsInstance){
            newsInstance.commentsLoading = true;

            Managers.News.getComments(newsInstance.id).then(
                function(rows){
                    newsInstance.commentsLoading = false;
                    newsInstance.comments = rows;
                },
                function (response){
                    newsInstance.commentsLoading = false;
                    newsInstance.errors = response.data;
                }
            );
        }
    }]);
}(this.angular));

Как тот же код мог бы работать в личном кабинете администратора:

(function (A){
    "use strict";
    var app = A.module('MyApp');

    app.controller('NewsCreateController', ['$scope', 'Managers', function ($scope, Managers){
        $scope.save = function(){
            Managers.News.save($scope.news_data).then(
                function (response){
                    $scope.news_data = response; //Теперь у нашей новости
                    // появился id, и вообще все данные теперь - с сервера
                },
                function (response){
                    $scope.errors = response.data;
                }
            );
        };
    }]);

    app.controller('NewsEditController', [
        '$scope',
        '$routeParams', // Взят для примера, для работы требует angular-route
        'Managers',
        function ($scope, $routeParams, Managers){
            var newsId = $routeParams.id;

            $scope.loaging = true;
            Managers.News.getById(newsId).then(
                function (response){
                    $scope.loading = false;
                    $scope.news_item = response;
                },
                function (response){
                    $scope.loading = false;
                    $scope.errors = response.data;
                }
            );

            $scope.save = function(){
                $scope.saving = true;
                Managers.News.save($scope.news_item).then(
                    function (response){
                        $scope.saving = false;
                        $scope.news_item = response; //Строго говоря, это делать
                        //необязательно
                    },
                    function (response){
                        $scope.saving = false;
                        $scope.errors = response.data;
                    }
                );
            };

    }]);
}(this.angular));

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

Преветствуется конструктивная критика рассмотренных решений

P. S. Далее пойдёт описание того, как я создавал REST API на сервере.

Анонс статьи по Django REST Framework и Angular

В сети крайне мало информации по работе с Django REST Framework, а на русском можно сказать что нет вообще. Учитывая сложность вопросов на Toster и устарелость статей на Хабре, решил написать довольно объёмную статью по работе с данным фреймворком. Сам не являюсь профессионалом в нём и WEB-разработке вообще, но поделиться накопленными знаниями будет не лишне.

Примерное оглавление

  • Зачем нужен DRF?
  • Установка, подключение к проекту.
  • Сериализаторы
  • Виды-функции и виды-классы
  • Типовые задачи
  • Подключение Angular Resource
  • Несколько советов по API
  • Исходный код моего модуля Angular для работы с API с объяснением написанного

Пишите в комментариях, какие вопросы дополнительно следует включить в статью, постараюсь рассмотреть их все.

Django и $resource

Суть проблемы

Решил использовать для реализации RESTful API своего приложения такой модуль Angular, как angular-resource. Проблем с установной и подключением, как обычно, не было. Создал свой сервис:

(function (A, U) { //U === undefined
    "use strict";
        
    A.module('DesktopApplication').factory('Category', function ($resource) {
        var instance = {
            selectedRow: U, //Используется в моём проекте для обмена данными между контроллерами,
                            //не является обязательным элементом
            resource: $resource('/category/rows/:id', {id: '@id'})
        };
        return instance;        
    });
}(this.angular));

Казалось бы, всё должно работать. И ведь правда, вот такое работает (GET-запрос на получение данных):

//Где-то в коде, использующем сервис Category
Category.resource.query(function (items){
    $scope.rows = items;
});

А вот такое уже нет:

//Чуть ниже
Category.save({'id': 4, 'name': 'Username'}, function (result) {
    $scope.success = result.success;
});

Проблема оказалась в том, что даже явно указав слэш в конце строки ресурса, я получил POST-запросы, идущие к адресам без слэша, и Django такие запросы тут же банит. Начался поиск и чтение StackOverflow.

Решение

Решение оказалось довольно простым. Нужно указать в настройках приложения, что удалять концевой слэш для URL в AJAX-запросах не надо (а по-умолчанию включено). Кроме того, для корректной работы запросов методом PUT и DELETE нужно дополнительно конфигурировать $http. Иначе декоратор @csrf_protect из комплекта Django будет резать такие запросы. С учётом вышесказанного, скрипт настройки приложения стал выглядеть так (для сокращения размера выброшены прочие модули, которые я на самом деле использую в своём проекте):

(function(A) {
    "use strict";
    A.module('DesktopApplication', [ 'ngCookies', 'ngResource')
     .config(function ($interpolateProvider, $resourceProvider) {
         $interpolateProvider.startSymbol('{$');
         $interpolateProvider.endSymbol('$}');
         $resourceProvider.defaults.stripTrailingSlashes = false;
      })
     .run(function ($http, $cookies) {
          $http.defaults.headers.common['X-CSRFToken'] = $cookies.csrftoken;
          $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
          $http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
     });
}(this.angular, this.jQuery));

AngularJS, Django и POST

Введение

Начал писать приложение на Angular. Стало сразу две проблемы.

  • Как передавать CSRF-Token Django при каждом запросе?
  • Почему данные, отправляемые через POST, не видны в объекте request Django?
  • CSRF-Token

    Для CSRF-Token нашлось очень простое решение. Достаточно подключить модуль angular-cookies и при старте приложения задать параметры сервиса $http. С учётом того, что скобки для Angular у меня заменены с {{ и }} на {$ и $}, заготовка главного модуля приложения у меня выглядит так:

    (function(A){
        "use strict";
        A.module('DesktopApplication', ['ngCookies']).config(function($interpolateProvider){
            $interpolateProvider.startSymbol('{$');
            $interpolateProvider.endSymbol('$}');
    
        }).run(['$http', '$cookies', function ($http, $cookies){
            $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
            $http.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
            }]);
    }(this.angular));

    Естественно, на сервере при отдаче шаблонов нужно, чтобы соответствующие Cookies были установлены. Делается очень просто:

    return render_to_response('template.html', RequestContext(request))

    Angular и POST

    Отдельной болью стала отправка данных на сервер через POST. Было потрачено несколько часов на чтение Тостера, официальной документации по Angular и Django, пока наконец я не наткнулся на решение одного из пользователей StackOverflow. Суть решения такова: Angular при отправке данных не превращает их в ожидаемый на бекэнде формат, а передаёт их как строку.

    Допустим, у нас имеется такой объект:

    userdata = {
        login: 'userlogin',
        password: '123456'
    };

    Попробуем отправить эти данные:

    $http.post('/login/', userdata).success(function(data){
        //Что-то делаем с полученными в ответ данными
    });

    В консоли Firebug будет выведена строка запроса:

    "{"login": "userlogin", "password": "123456"}"

    Попробуем получить эти данные:

    def login_view(request):
        if request.method == 'POST' and request.is_ajax():
            username = request.POST.get('login', '')
            password = request.POST.get('password', '')

    Мы ожидаем, что в третьей и четвёртой строках будет примерно следующее:

    username = 'username'
    password = '123456'

    На самом деле этого не происходит, т.к. данные должны быть переданы в таком виде:

    login=userlogin&password=123456

    Проблема в том, что в текущей версии Angular (1.3.4 на момент написания статьи) нет функции для приведения объекта в такой вид. Поэтому автор посоветовал использовать старый добрый jQuery, точнее его функцию param:

    $http.post('/login/', $.param(userdata)).success(function(data){
        //Обработка ответа сервера
    });

    Вот такой код будет работать. Так же пользователи предлагали самописные функции, выполняющие ту же работу, но я советую использовать проверенные решения. Кроме этого обязательно присутствие в заголовке пост параметра Content-Type, равного application/x-www-form-urlencoded, соответствующий код приведён выше.