Как мы делали каркас приложения на AngularJS и Django

  • Tutorial
image

Весной нам в голову пришла идея сделать простой сервис для облачного бэкапа серверов. Поскольку в то время работа над проектом велась преимущественно по вечерам и по выходным, для ускорения процесса было решено использовать только те технологии, в которых у нас есть опыт. Для backend-части был выбран Django, а реализация клиентской части предполагалась в виде SPA на базе AngularJS. Задумка была в следующем: сделать продукт с минимальным функционалом, а затем постепенно добавлять новые возможности. Для этого необходимо было сделать достаточно гибкую и масштабируемую систему. Немного пораскинув мозгами, мы приступили.


Роутинги


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

Была утверждена следующая схема:
По пути / пользователю показывается лэндинг, который никак не связан с приложением. При переходе на /app/ сервер отдаёт файл app.html, который содержит весь head, все скрипты в конце body и один единственный div со скромным атрибутом ui-view. Именно в этот div грузится всё приложение. В зависимости от того, залогинен пользователь или нет, ему показываются разные заполнения этого div’a.

Я не буду забегать вперёд, а рассмотрю случай для аутентифицированного пользователя. Итак, в этом случае, если в URL после /app/ нет никакого хэша, то внутрь грузится следуюший слой: index.html. Этот файл содержит в себе статическую часть приложения, которая окружает всю рабочую область: хэдер, футер и боковое меню. В index.html так же есть div с атрибутом ui-view, в который будет подгружаться ещё один уровень приложения, а конкретно — различные экраны (в нашем случае это: главный экран, детальный экран сервера, экран биллинга, экран восстановления бэкапа и другие).

image

Рассмотрим, как же это всё описано с помощью ui-router:

app.config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {

    $stateProvider
        .state('index', {
            url: '/',
            templateUrl: '/static/views/index.html'
        })
        .state('index.main', {
            url: '^/main',
            templateUrl: '/static/views/pages/main.html'
        })
        .state('index.client', {
            url: '^/main/c/:id',
            templateUrl: '/static/views/pages/client.html'
        })
        .state('index.billing', {
            url: '^/billing',
            templateUrl: '/static/views/pages/billing.html'
        })
        .state('index.restore', {
            url: '^/restore',
            templateUrl: '/static/views/pages/restore.html'
        });

    $urlRouterProvider.otherwise('/main');  // Если хэш не совпадает ни с одним, то редирект на страницу /main
    
}])


Публичные и приватные страницы


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

Итак, добавим данные о публичных страницах в конфигурацию роутера:

$stateProvider
    .state('login', {
        url: '/login',
        templateUrl: '/static/views/login.html'
    })
    .state('signup', {
        url: '/signup',
        templateUrl: '/static/views/signup.html'
    })
    .state('recovery', {
        url: '/recovery',
        templateUrl: '/static/views/recovery.html'
    });


В модуле, отвечающем за авторизацию, создана фабрика, которая определяет залогинен ли пользователь:

AuthModule.factory('Auth', ['$cookieStore', function ($cookieStore) {
    var currentUser = $cookieStore.get('login') || 0,
        publicStates = ['login', 'signup', 'recovery'];

    return {
        authorize: function(state) {
            return (this.isLoggedIn() && (publicStates.indexOf(state) < 0)) || (!this.isLoggedIn() && (publicStates.indexOf(state) >= 0))
        },
        isLoggedIn: function() {
            return !!currentUser;
        }
    }

}])


Метод isLoggedIn возвращает true, если пользователь залогинен, либо false в противном случае. Метод authorize определяет для текущего состояния, имеет ли право пользователь в нём находиться.

Использование этих методов осуществляется в обработчике события $stateChangeStart, которое возникает в момент начала изменения состояния:

$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
    // Если пользователь не имеет права находиться в данном состоянии
    if (!Auth.authorize(toState.name)) {
        // Необходимо для предотвращения дальнейшего изменения состояния
        event.preventDefault();
        // Для случая первичного определения пути (при заходе на /app/ без какого-либо хэша)        
        if (fromState.url === '^') {
            if (Auth.isLoggedIn()) {
                $state.go('index.main');
            } else {
                $state.go('auth');
            }
        }
    }
});


Аутентификация


Процедура аутентификации на стороне клиента реализована с помощью функции в фабрике Auth:

login: function (user, success, error) {
    $http.post('/login/', user)
        .success(function () {
            currentUser = 1;
            success();
        })
        .error(error);
}


Вызов этой функции производится в контроллере. В качестве аргументов передаются username, password и коллбэки:

Auth.login({
    username: $scope.login.username,
    password: $scope.login.password
},
function () {
    $state.go('index.main');
},
function () {
    $scope.login.error = true;
});


На сервере с помощью стандартных django-сессий хранится информация о пользователе (его id). Для этого используются стандартные методы django.contrib.auth.

from django.contrib.auth import authenticate, login

def login_service(request):
    data = json.loads(request.body)
    user = authenticate(username=data['username'], password=data['password'])
    if user is not None:
        login(request, user)
        return HttpResponse(status=200)
    else:
        return HttpResponse('Login error', status=401)


Во время каждого http-запроса сервер проверяет, залогинен ли пользователь, и устанавливает в заголовок 'Set-Cookie' соответствующее значение. Это значение и проверяется в клиентской части с помощью $cookieStore.get('login').

Связка между моделями сервера и клиента


С целью ускорения разработки и повышения гибкости приложения, было решено использовать middleware между Django и AngularJS. Выбор пал на django-angular.

Основные его преимущества:

  • предоставляет возможность выполнять основные CRUD операции;
  • позволяет плотно связать django-формы и angular-контроллеры;
  • даёт функционал для вызова методов в django прямо из angular-контроллера.


Подробнее об установке и настройке можно прочитать в документации.

Итог


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



Эта вторая статья из цикла про то, как мы делали сервис облачного резервного копирования серверов bitcalm.com.

Первая статья: Разработка своей системы биллинга на Django
BitCalm
Company
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 10

    +1
    Не плохо, я тоже в процессе работы с AngularJS и сразу хочется поспрашивать.

    Поделитесь моментами.

    1. Где вы и как провряете права доступа к контроллерам? В каждом вызываете Auth.login?
    И кстати зачем если у вас
    function (res) { $state.go('index.main'); },
    вы ему user передаете?
    success(user);

    2. Как вы делаете предзагрузку нужных данных, например той же авторизации или данных пользователя? Через resolve? run? или еще методы
      +2
      1. Не совсем понял вопрос про права доступа к контроллерам. На каждый http-запрос в $httpProvider у нас написан interceptor (можно прочитать здесь), который отслеживает 401 ошибку и если она возникает, то разлогинивает пользователя (принудительно чистит куки и редиректит на страницу авторизации)

      Про success(user) — описка :)

      2. Предзагрузка данных делается следующим образом: в самом верху index.html находится <div ng-controller="InitCtrl"></div>. Этот контроллер отвечает только за загрузку данных. В $rootScope есть объект, показывающий, какие данные загружены (хранить это в $rootScope не очень красиво, но пока что так):

      $rootScope.loaded = {
          user: false,
          clients: false,
          tariffs: false,
          backups: false
      };
      


      В этом контроллере, при его инициализации, одновременно запускаются функции загрузки данных и в success у каждой из них выставляется флаг в $rootScope.loaded. Например:

      Backups.getBackups(function () {
          $rootScope.loaded['backups'] = true;
      });
      


      Благодаря тому, что интерфейс у нас основан на раздельных виджетах, мы можем показывать и скрывать разные виджеты в зависимости от стадии загрузки данных.
        0
        Идея с interceptor приятная, да и идея с подгрузчиком и показамо виджетов на основании флагов тоже интересная, спасибо.
      0
      Метод isLoggedIn возвращает true, если пользователь залогинен, либо false в противном случае.

      Кто бы мог подумать xD
        +2
        И первый вопрос, который возник, был связан с роутингами в клиентской части. Нам была необходима надёжная и простая система, которая поддерживала бы вложенные друг в друга шаблоны и позволяла однозначно сопоставлять определённому URL необходимый шаблон. После недолгих поисков мы выбрали ui-router.

        Если вам нужна была надежная и простая система, то, знаете, замороченный и громоздкий ui-router — это не самый очевидный выбор. route-segment спроектирован гораздо проще, и при этом предоставляет все описанные вами функции (disclaimer: я автор).
          0
          Опишу тут свой подход. А именно:
          1. Роутингом я управляю из AngularJS. Django-роутер просто парсит роутинг ангуляра.
          2. Пользователя выгружаю через inline-javascript прямо внутри django-templates и принимаю в $rootScope со всеми полями (is_superuser, is_staff и т.д.)
          3. Связь angular и django осуществляю через RPC (хотя это и не тру, и я сейчас от этого подхода уже ухожу в сторону REST)
            –3
            А не рассматривали React вместо Angular?
              0
              Нет, не рассматривали, потому что с Angular уже был опыт работы и для ускорения процесса решили остановиться на нём.
              0
              Самым полезным в этой статье оказалась картинка для привлечения внимания.
                0
                в Auth.login используйте promise а в контроллере уже делайте success

                Only users with full accounts can post comments. Log in, please.