Статьи

WebSocket с WebMotion и AngularJS

В этой статье описывается новая функция в WebMotion: управление WebSockets. Мы будем использовать Javascript-фреймворк AngularJS для клиентской части.

Чтобы проиллюстрировать эту функцию, мы создадим панель управления задачами с их состояниями (todo, выполняется или завершено). Эта панель должна быть доступна нескольким пользователям и автоматически обновляется.

Напомним, WebMotion — это веб-фреймворк Java. Он использует файл сопоставления для описания связи между сервером и клиентом. Он основан на JEE API с сервлетом 3.

AngularJS — это инфраструктура MVC Javascript. Это позволяет добавлять директивы в ваш HTML-документ, чтобы ваши страницы были динамичными. Одной из особенностей является то, что он автоматически обновляет модель между вашим контроллером и вашей HTML-страницей.

Для получения более подробной информации, вы можете посетить веб-сайты http://www.webmotion-framework.org и http://angularjs.org .

Демонстрация окончательного образца панели инструментов доступна здесь: http://www.webmotion-framework.org/dashboard/ . и исходный код доступен по следующему адресу: http://svn.debux.org/webmotion-ext/dashboard/ .

Создать проект

Maven используется в качестве менеджера Buid для примера. WebMotion предлагает архетип для первоначального проекта. Чтобы использовать его, вам просто нужно ввести следующую команду:

$ mvn archetype:generate \
    -DarchetypeGroupId=org.debux.webmotion \
    -DarchetypeArtifactId=webmotion-archetype \
    -DarchetypeVersion=2.3.3 \
    -DgroupId=org.debux.webmotion \
    -DartifactId=dashboard \
    -Dpackage=org.debux.webmotion.dashboard \
    -Dversion=1.0-SNAPSHOT \
    -DusesExtras=N

Что касается AngularJS, вам просто нужно включить скрипт на своих страницах и объявить приложение. Пример состоит из одной страницы.

<html ng-app="DashboardApp">
    <head>
        <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
    </head>
</html>

Модельная часть

Что касается сохранения данных (задач и их состояний), сервер сохранит их в списке памяти.

public class Task {
    protected String id;
    protected String name;
    protected String status;

    public Task(String name) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.status = "todoTasks";
    }
}

Сервер вернет навигатору задачи в виде трех списков, по одному для каждого состояния.

public class SortedTasks {
    List<Task> todoTasks = new ArrayList<Task>();
    List<Task> progressTasks = new ArrayList<Task>();
    List<Task> doneTasks = new ArrayList<Task>();
}

public static SortedTasks getSortedTasks(List<Task> tasks) {
    SortedTasks sortedTasks = new SortedTasks();

    for (Task task : tasks) {
        String status = task.getStatus();

        if ("todoTasks".equals(status)) {
            sortedTasks.todoTasks.add(task);

        } else if ("progressTasks".equals(status)) {
            sortedTasks.progressTasks.add(task);

        } else if ("doneTasks".equals(status)) {
            sortedTasks.doneTasks.add(task);
        }
    }
    return sortedTasks;
}

Последний шаг — инициализация модели с помощью вызова слушателя при запуске сервера.

public class StartupListenner implements WebMotionServerListener {

    public void onStart(Mapping mapping, ServerContext serverContext) {
        List<Task> tasks = Arrays.asList(
            new Task("Task 0"),
            new Task("Task 1"),
            new Task("Task 10"),
            new Task("Task 11")
        );
        serverContext.setAttribute("tasks", new ArrayList<Task>(tasks));
    }

    public void onStop(ServerContext serverContext) { // do nothing }

}

Чтобы включить этот прослушиватель, мы не должны забывать объявление прослушивателя в файле отображения WebMotion. Этот файл находится в src/main/resourcesпапке. Файл структурирован по нескольким разделам. Раздел «[config]» позволяет настроить приложение WebMotion.

[config]
server.listener.class=org.debux.webmotion.dashboard.StartupListenner

Посмотреть часть

Текущее приложение будет одностраничным веб-приложением. На этой странице будут отображаться созданные задачи на панели инструментов. Контроллер AngularJS связан с телом HTML для управления DOM, соответствующим модели, благодаря AngularJS.

<body ng-controller="MainCtrl">...</body>

Что касается части Javascript, контроллер является функцией. Это контроллер, который получает события и манипулирует моделью.

function MainCtrl($scope) {
}

Страница будет разделена на две части. Первым будет форма создания с полем ввода. addTaskФункция будет вызываться в то время как форма создания представляется.

<form ng-submit="addTask()">
    <input type="text" ng-model="taskName" size="30" required placeholder="add new task here">
    <input >
</form>

Во второй области отображаются все созданные задачи, отсортированные по их статусу. Модель получается из контроллера и просматривается с помощью директивы «ng-repeat» для создания тега div для каждой задачи. В случае удаления или перемещения задачи на контроллере вызывается метод, т.е. delTask ​​и updateTask.

<div >
    <div >
        <h1>Todo</h1>
        <div >
            <button >×</button>
            {{task.name}}
        </div>
    </div>
    <div >
        <h1>In progress</h1>
        <div >
            <button >×</button>
            {{task.name}}
        </div>
    </div>
    <div >
        <h1>Done</h1>
        <div >
            <button >×</button>
            {{task.name}}
        </div>
    </div>
</div>

Директива AngularJS позволяет добавить поведение в DOM. Директивы drag-event и drop-event являются специальными директивами для проекта. Это позволяет легко обрабатывать перетаскивание HTML5, добавляя необходимые события на элементы и обратный вызов на текущем контроллере.

angular.module('components', [])
    .directive('dragEvent', ['$parse', function($parse) {
        return function(scope, element, attrs) {
            element.bind("dragstart", function (evt) {
                var id = element.attr("id");
                evt.dataTransfer.setData("drag-id", id);

                var fn = $parse(attrs.dragEvent);
                fn(scope, {$element : element});
            });
            element.attr("draggable", true);
        }
    }])
    .directive('dropEvent', ['$parse', function($parse) {
        return function(scope, element, attrs) {
            element.bind("dragover dragenter", function (evt) {
                evt.stopPropagation();
                evt.preventDefault();

                return false;
            });

            element.bind("drop", function (evt) {
                var id = evt.dataTransfer.getData("drag-id");
                var elementTransfer = angular.element(document.getElementById(id));
                element.append(elementTransfer);

                evt.stopPropagation();
                evt.preventDefault();

                var fn = $parse(attrs.dropEvent);
                fn(scope, {$element : elementTransfer, $to : element});
            });
        }
    }]);

angular.module('DashboardApp', ['components']);

Осталось добавить стиль в задачи благодаря Twitter Boostrap:

<link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css">
<style>
    html, body, .row, .span4 {
        height: 100%;
    }
    .task {
        width: 110px;
        height: 100px;
        float: left;
        background: #ffff66;
        padding: 10px;
        margin: 10px;
        border-radius: 3px;
    }
</style>

В конце концов, URL для доступа к странице добавляется в разделе «Действие» в WebMotion. Действие в файле сопоставления состоит из метода HTTP, пути и действия, которое нужно выполнить. В следующем правиле действие заключается в том, чтобы вернуть страницу index.html пользователю.

[actions]
GET       /                           view:index.html

Часть контроллера

Для управления WebSocket на сервере мы используем функцию отправки сообщений JSON, предоставляемую WebMotion. Этот протокол позволяет легко вызывать с клиента метод в WebSocket. Объект JSON содержит имя метода и параметры.

Чтобы использовать его, вы должны создать, как классическое действие в WebMotion, контроллер, который возвращает визуализацию клиенту при вызове метода. Рендерер позволяет вам возвращать страницы, данные, перенаправления, … в навигатор. В этом случае рендеринг является типом RenderWebSocket.

public class TasksManager extends WebMotionController {

    private static final Logger log = LoggerFactory.getLogger(TasksManager.class);

    public Render createWebsocket() {
        TasksManagerWebSocket socket = new TasksManagerWebSocket();
        return new RenderWebSocket(socket);
    }

    public class TasksManagerWebSocket extends WebMotionWebSocketJson {
    }
}

Не забудьте объявить сопоставление. Действие объявляется с именем classe и вызываемым методом:

[actions]
GET       /tasksManager               TasksManager.createWebsocket

По умолчанию возвращаемый метод (через сокет) передается только соединению вызывающей стороны, которое отправило объект JSON. В этом примере мы будем транслировать все соединения, чтобы сообщить модификацию. Для этого вы должны сохранить все соединения в контексте и переопределить метод, который отправляет результат следующим образом:

@Override
public void onOpen() {
    // Store all connections
    ServerContext serverContext = getServerContext();
    List<TasksManagerWebSocket> connections = (List<TasksManagerWebSocket>) serverContext.getAttribute("connections");
    if (connections == null) {
        connections = new ArrayList<TasksManagerWebSocket>();
        serverContext.setAttribute("connections", connections);
    }
    connections.add(this);
}

@Override
public void onClose() {
    ServerContext serverContext = getServerContext();
    List<TasksManagerWebSocket> connections = (List<TasksManagerWebSocket>) serverContext.getAttribute("connections");
    connections.remove(this);
}

@Override
public void sendObjectMessage(String methodName, Object message) {
    ServerContext serverContext = getServerContext();
    List<TasksManagerWebSocket> connections = (List<TasksManagerWebSocket>) serverContext.getAttribute("connections");
    for (TasksManagerWebSocket socket : connections) {
        socket.superSendObjectMessage(methodName, message);
    }
}

public void superSendObjectMessage(String methodName, Object message) {
    super.sendObjectMessage(methodName, message);
}

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

public SortedTasks getTasks() {
    ServerContext serverContext = getServerContext();
    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    SortedTasks sortedTasks = Task.getSortedTasks(tasks);
    return sortedTasks;
}

public SortedTasks addTask(String name) {
    ServerContext serverContext = getServerContext();

    Task task = new Task(name);
    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    tasks.add(task);

    return getTasks();
}

public SortedTasks updateTask(final String id, String newStatus) {
    ServerContext serverContext = getServerContext();

    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    Task task = (Task) CollectionUtils.find(tasks, new Predicate() {
        public boolean evaluate(Object object) {
            Task other = (Task) object;
            return other.getId().equals(id);
        }
    });

    task.setStatus(newStatus);
    tasks.remove(task);
    tasks.add(task);

    return getTasks();
}

public SortedTasks delTask(final String id) {
    ServerContext serverContext = getServerContext();

    List<Task> tasks = (List<Task>) serverContext.getAttribute("tasks");
    CollectionUtils.filter(tasks, new Predicate() {
        public boolean evaluate(Object object) {
            Task other = (Task) object;
            return !other.getId().equals(id);
        }
    });

    return getTasks();
}

Фабрика помогает создать утилиту для передачи вызова на веб-сокет. Для управления веб-сокетами в AngularJS вы можете использовать общую фабрику:

angular.module('components', [])
    .factory('WebSocket', function() {
        return {
            connect : function(url) {
                var self = this;
                this.connection = new WebSocket(url);

                this.connection.onopen = function() {
                    if (this.onopen) {
                        self.onopen();
                    }
                }
                this.connection.onclose = function() {
                    if (this.onclose) {
                        self.onclose();
                    }
                }
                this.connection.onerror = function (error) {
                    if (this.onerror) {
                        self.onerror(error);
                    }
                }
                this.connection.onmessage = function(event) {
                    if (this.onmessage) {
                        self.onmessage(event);
                    }
                }
            },

            send : function(message) {
                this.connection.send(message);
            },

            close : function() {
                this.connection.onclose = function () {};
                this.connection.close()
            }
        }
    });

В противном случае вы можете использовать конкретную фабрику для вашего звонка. Метод в фабрике создается с объектом JSON для возврата на сервер для каждого вызова. Обратный вызов при получении сообщения позволяет обновить данные на стороне клиента.

angular.module('components', [])
    .factory('TasksManager', function() {
        var url = "ws://localhost:8080/Dashboard/tasksManager";
        return {
            init : function() {
                var self = this;
                this.connection = new WebSocket(url);

                this.connection.onopen = function() {
                    console.log("connected");
                    self.getTasks();
                }
                this.connection.onclose = function() {
                    console.log("onclose");
                }
                this.connection.onerror = function (error) {
                    console.log(error);
                }
                this.connection.onmessage = function(event) {
                    console.log("refresh");
                    var data = angular.fromJson(event.data);
                    self.refresh(data.result);
                }
            },

            getTasks : function() {
                this.sendMessage({
                    method : "getTasks",
                    params : {}
                });
            },

            addTask : function(name) {
                this.sendMessage({
                    method : "addTask",
                    params : {
                        name : name
                    }
                });
            },

            updateTask : function(id, status) {
                this.sendMessage({
                    method : "updateTask",
                    params : {
                        id : id,
                        newStatus : status
                    }
                });
            },

            delTask : function(id) {
                this.sendMessage({
                    method : "delTask",
                    params : {
                        id : id
                    }
                });
            },

            sendMessage : function(event) {
                this.connection.send(JSON.stringify(event));
            }
        }
    });

Наконец, просто введите фабрику в контроллер и подключите события из представления.

function MainCtrl($scope, TasksManager) {

    $scope.addTask = function() {
        TasksManager.addTask($scope.taskName);
        $scope.taskName = "";
    }

    $scope.updateTask = function(element, status) {
        var id = element.attr("id");
        TasksManager.updateTask(id, status);
    }

    $scope.delTask = function(task) {
        TasksManager.delTask(task.id);
    }

    TasksManager.refresh = function(tasks) {
        $scope.tasks = tasks;
        $scope.$digest();
    }

    TasksManager.init();
}

Запустите приложение

Теперь вы можете запустить приложение с Jetty, введя следующую командную строку:

$ mvn jetty:run

Развернуть приложение можно на выделенном сервере Jetty, Tomcat или Glassfish.

Вы можете визуализировать результат в избранных навигаторах, чтобы связаться с эффектом обновления, чтобы ввести следующий адрес http: // localhost: 8080 / Dashboard / .