Статьи

Разработка с AngularJS — Часть III: Сервисы

AngularJSЭто третья статья из серии о моем опыте разработки с AngularJS . Я использовал AngularJS в течение нескольких месяцев, чтобы создать для клиента функцию «Моя панель инструментов», и на этом пути научился всему, что связано с Angular. Предыдущие статьи см. В части I: Основы и в части II: диалоги и данные .

Angular предлагает несколько способов взаимодействия с данными с сервера. Самый простой способ — использовать фабрику $ resource , которая позволяет вам взаимодействовать с RESTful -источниками данных на стороне сервера. Когда мы начинали проект My Dashboard, мы надеялись взаимодействовать с REST API, но вскоре обнаружили, что в нем нет всех необходимых нам данных. Вместо того, чтобы загрузить страницу и затем сделать еще один запрос для получения ее данных, мы решили встроить JSON в страницу. Для связи с сервером мы использовали наше проверенное решение Ajax: DWR .

В Angular-речь сервисы — это синглтоны, которые выполняют определенные задачи, общие для веб-приложений . Другими словами, это любой объект $ name, который можно вставить в контроллер или директиву. Однако, как Java-разработчик, я склонен думать о сервисах как об объектах, которые взаимодействуют с сервером. Документация Angular по созданию сервисов показывает вам различные варианты регистрации сервисов. Я использовал метод API angular.Module.

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

Сервис виджетов

Служба виджетов используется для извлечения видимых виджетов для пользователя. Он имеет две функции, которые доступны для контроллеров: getUserWidgets(type)и getHiddenWidgets(type). Первая функция используется в верхней части WidgetController, а вторая — для диалога конфигурации, упомянутого в предыдущей статье .

Код для этой услуги находится в services.js . Основная часть логики заключается в ее filterData()функции, где она проходит 4-этапный процесс:

  1. Получить все виджеты по типу, гарантируя, что они уникальны.
  2. Удалить виджеты, которые скрыты по предпочтениям пользователя.
  3. Создайте массив, упорядоченный по предпочтениям пользователя.
  4. Добавьте любые новые виджеты, которые не скрыты или не упорядочены .

Код для объекта Widget выглядит следующим образом:

angular.module('dashboard.services', []).
    factory('Widget',function ($filter, Preferences) {
        var filter = $filter('filter');
        var unique = $filter('unique');
 
        function filterData(array, query) {
            // get all possible widgets for a particular type
            var data = filter(array, query);
            data = unique(data);
 
            // remove widgets that are hidden by users preference
            var hidden = Preferences.getHiddenWidgets(query.type);
            for (var i = 0; i < hidden.length; i++) {
                var w = filter(data, {id: hidden[i]});
                $.each(w, function (index, item) {
                    var itemId = item.id;
                    if (hidden.indexOf(itemId) > -1) {
                        data.splice(data.indexOf(item), 1);
                    }
                });
            }
 
            // build an array that's ordered by users preference
            var ordered = [];
            var visible = Preferences.getUserWidgets(query.type);
            for (var j = 0; j < visible.length; j++) {
                var v = filter(data, {id: visible[j]});
                $.each(v, function (index, item) {
                    var itemId = item.id;
                    if (visible.indexOf(itemId) > -1) {
                        ordered.push(item)
                    }
                });
            }
 
            // loop through data again and add any new widgets not in ordered
            $.each(data, function (index, item) {
                if (ordered.indexOf(item) === -1) {
                    ordered.push(item);
                }
            });
 
            return ordered;
        }
 
        return {
            getUserWidgets: function (type) {
                return filterData(widgetData, {type: type})
            },
 
            getHiddenWidgets: function (type) {
                var hidden = Preferences.getHiddenWidgets(type);
                var widgetsForType = filter(widgetData, {type: type});
                widgetsForType = unique(widgetsForType);
                var widgets = [];
                for (var j = 0; j < hidden.length; j++) {
                    var v = filter(widgetsForType, {id: hidden[j]});
                    $.each(v, function (index, item) {
                        if (widgetsForType.indexOf(item) > -1) {
                            widgets.push(item)
                        }
                    });
                }
                return widgets;
            }
        }
    })

Как только вы настроите сервис, подобный этому, вы можете добавить его по имени. Так , например, WidgetControllerуже Widgetвведен в его конструктор:

function WidgetController($dialog, $scope, Widget, Preferences) {

Сервис настроек

Сервис настроек используется для получения и сохранения пользовательских настроек. Это довольно просто, и большая часть его кода взаимодействует с DWR. Эта услуга имеет 5 методов:

  1. getHiddenWidgets (type) — используется сервисом Widget
  2. getUserWidgets (type) — используется сервисом Widget
  3. saveBarOrder (bars) — вызывается из WidgetController
  4. saveWidgetOrder (тип, виджеты) — вызывается из WidgetController
  5. saveWidgetPreferences (тип, виджеты) — вызывается из WidgetController

Во-первых, давайте посмотрим на save*Order()функции. Есть две части страницы, которые используют директиву ui-sortable для инициализации функции перетаскивания. Первый находится на главном <ul>, который содержит 3 бара слева.

<ul class="widgets" ui-sortable="{handle:'.heading', update: updateBars}">

Свойство update в конфигурации JSON указывает, какой метод вызывать в контроллере. Аналогично, задачи и сводные элементы вызывают updateOrderфункцию.

<ul class="summary-items" ng-model="summaryWidgets" ui-sortable="{update: updateOrder}">
...
<ul class="task-items" ng-model="taskWidgets" ui-sortable="{update: updateOrder}">

Эти функции находятся в WidgetControllerи создают массив идентификаторов виджетов для передачи в службу предпочтений.

$scope.updateBars = function(event, ui) {
    var bars = [];
    $.each($(ui.item).parent().children(), function (index, item) {
        bars.push(item.id.substring(0, item.id.indexOf('-')))
    });
    Preferences.saveBarOrder(bars);
};
 
$scope.updateOrder = function(event, ui) {
    var parentId = $(ui.item).parent().parent().attr('id');
    var type = parentId.substring(0, parentId.indexOf('-'));
    var items = [];
    $.each($(ui.item).parent().children(), function (index, item) {
        items.push(item.id.substring(item.id.indexOf('-') + 1))
    });
    Preferences.saveWidgetOrder(type, {items: items});
};

Порядок штрихов используется при загрузке страницы. Следующий код скриптлета находится внизу страницы приложения в его $ (document) .ready:

<%  String barOrder = user.getDashboardBarSortOrder();
    if (barOrder != null) { %>
    sortBars(['<%= barOrder %>']);
<% } %>

sortBars()Функция находится в dashboard.js файл (где мы помещаем все не-угловые функции):

function sortBars(barOrder) {
    // Sort bars according to user preferences
    $.each(barOrder, function(index, item) {
        var bar = $('#' + item + '-bar');
        if (bar.index() !== index) {
            if (index === 0) {
                bar.insertBefore($('.widgets>li:first-child'));
            } else if (index === (barOrder.length - 1)) {
                bar.insertAfter($('.widgets>li:last-child'));
            } else {
                bar.insertBefore($('.widgets>li:eq(' + index + ')'));
            }
        }
    });
}

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


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

factory('Preferences', function ($filter) {
    var unique = $filter('unique');
 
    return {
        // Get in-page variable: hiddenWidgets
        getHiddenWidgets: function (type) {
            var items = hiddenWidgets[type];
            return (angular.isUndefined(items) ? [] : unique(items));
        },
 
        // Get in-page variable: userWidgets
        getUserWidgets: function (type) {
            var items = userWidgets[type];
            return (angular.isUndefined(items) ? [] : unique(items));
        },
 
        // Save main bar (task, summary, chart) order
        saveBarOrder: function (bars) {
            DWRFacade.saveDashboardBarSortOrder(bars, {
                errorHandler: function (errorString) {
                    alert(errorString);
                }
            })
        },
 
        // Save order of widgets from sortable
        saveWidgetOrder: function (type, widgets) {
            userWidgets[type] = widgets.items;
            DWRFacade.saveDashboardWidgetPreference(type, widgets, {
                errorHandler: function (errorString) {
                    alert(errorString);
                }
            });
        },
 
        // Save hidden and visible (and order) widgets from config dialog
        saveWidgetPreferences: function (type, widgets) {
            // widgets is a map of hidden and visible
            var hiddenIds = [];
            $.each(widgets.hidden, function (index, item) {
                hiddenIds.push(item.id);
            });
            var visibleIds = [];
            $.each(widgets.items, function (index, item) {
                visibleIds.push(item.id);
            });
            var preferences = {
                hidden: hiddenIds,
                items: visibleIds
            };
            // reset local variables in page
            hiddenWidgets[type] = hiddenIds;
            userWidgets[type] = visibleIds;
            DWRFacade.saveDashboardWidgetPreference(type, preferences, {
                errorHandler: function (errorString) {
                    alert(errorString);
                }
            });
        }
    }
})

Использование $ http и получение данных

В этом конкретном приложении мы не делали чтения с сервера Angular. Мы просто записывали настройки на сервер и обновляли встроенные переменные при изменении данных. Функциональность приложения в реальном времени не будет заметна, если запись не удалась.

В моем текущем проекте Angular это скорее полноценное приложение, которое читает столько же, сколько и пишет. Для этого я считаю полезным либо 1) передавать обратные вызовы сервисам, либо 2) использовать систему событий Angular для публикации / подписки на события.

Первый метод является самым простым и, вероятно, наиболее знакомым для разработчиков JavaScript. Например, вот код контроллера для удаления картинки профиля:

Profile.removePhoto($scope.user, function (data) {
    // close the dialog
    $scope.close('avatar');
    // success message using toastr: http://bit.ly/14Uisgm
    Flash.pop({type: 'success', body: 'Your profile picture was removed.'});
})

И Profile.removePhoto()метод:

removePhoto: function (user, callback) {
    $http.post('/profile/removePhoto', user).success(function (response) {
        return callback(response);
    });
}

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

// controller calling code
Profile.getUser();
 
// service code
getUser: function () {
    $http.get('/profile').success(function (data) {
        if (data.username) {
            $log.info('Profile for ' + data.username + ' retrieved!');
            $rootScope.$broadcast('event:profile', data);
        }
    });
}
 
// controller receiving code
$rootScope.$on('event:profile', function (event, data) {
    $scope.user = data;
});

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

Резюме

Использование внутристраничных переменных и DWR не рекомендуется Angular Team. Тем не менее, это хорошо сработало для нас и кажется хорошим способом построения сервисов Angular. Даже если для получения всех данных станет доступен REST API, я думаю, что использование внутристраничных переменных для минимизации запросов — хорошая идея.

При получении данных вы можете использовать обратные вызовы или систему пабов / подпрограмм Angular ($ broadcast и $ on) для получения данных в ваших контроллерах. Если вы хотите узнать больше об этой технике, смотрите статью Эрик Терпстра « Общение с $ broadcast» . В своей статье Эрик упоминает паб / субмодуль Томаса Берлесона, который выступает в качестве очереди сообщений. Если вы использовали MessageQueue Томаса (или что-то подобное) с Angular, я хотел бы услышать о вашем опыте.

В следующей статье я расскажу о том, как мы изменили дизайн My Dashboard и использовали CSS3 и JavaScript для реализации новых идей.