Разработка крупных JavaScript приложений

This post is also available in: Английский

Создание интерактивных Интернет сервисов http://tutrostov.ru/  подразумевает широкое применение JavaScript при их разработке. Если 10 лет назад этот язык имел скудным функционал, но в данный момент  JS существенно преобразился. Все больше логики сервисов переносится в клиентскую часть. В статье мы поделимся с вами нашим опытом проектирования и реализации крупных веб приложений.

Подход к проектированию

Код больших проектов имеет одну не самую приятную особенность. При превышении его объемом определенного порога, он становится похож на запутанный клубок. В таком случае скорость разработки падает – много времени тратиться на его распутывание.  Потому следует разделять код по логике его работы. Использование таких паттернов проектирования, как MVC, MVVP и им подобным, существенно структурирует код, сделает его более гибким и масштабируемым, систематизирует разработку приложения. Выбор конкретного паттерна может быть обусловлен как требованиями к реализации архитектуры сервиса, так и личными предпочтениями разработчиков.

В 99% случаев чтобы не изобретать велосипед, стоит использовать фреймворки, в которых уже реализованы указанные выше паттерны. Наиболее распространенными являются Backbone.js, Ember.js, Knockout.js.

Разработка типового приложения

Предположим, что перед нами встала задача по разработке одностраничного RESTful приложения. (назовем его условно “Sample App”). Залог успеха при построении масштабируемых и гибких приложений – наличие возможности отключить какой-либо функционал, либо добавить оный не влияя на общий ход работы системы. Для этого все сущности приложения разделяем на модули.

Для обеспечения модульности наших скриптов воспользуемся механизмом пространства имен. В JavaScript нет привычного, скажем для C- разработчиков, инструмента для их объявления. Их реализация заключается в создании глобального объекта, в поля которого записываются компоненты системы.

(function() {
    window.sample = {}; // Главный объект приложения
    sample.routers = {}; // Объект, содержащий контроллеры для обеспечения
                         // переходов между разделами приложения.
    sample.models = {}; // Объект для хранения моделей с данными
    sample.ui = {}; // Объект, содержащий контроллеры, отвечающие за
                    // построение интерфейсов приложения и их поведения.

    sample.core = {state: null}; // Ядро нашего приложения. Содержит в
                                 // себе объект, являющуюся «песочницей».
    sample.modules = {}; // Объект, содержащий контроллеры других
                         // модулей системы
})();

Все создаваемые сущности мы в дальнейшем будем распределять внутрь созданных объектов. Пример на  backbone.js (не обращайте внимания, что мы расширяем объект Backbone.View. На самом деле данный объект реализует не представление, а контроллер, отвечающий за логику интерфейса):

sample.ui.MainPage = Backbone.View.extend({
	anotherField: ‘’,  // public field
        initialize: function() {
       		// Здесь код, выполняющийся при инициализации объекта
		var someField = ‘’;  // private field
    	},

        render: function() {
                // рендеринг темплейтов  и т.п.
                this.renderWidjet1();
                this.renderWidjet2();
                this.renderWidjet3();
    	},

	renderWidjet1: function() {
	},
        renderWidjet2: function() {
	},
        renderWidjet3: function() {
	}
});

sample.ui.mainPage = new sample.ui.mainPage();
sample.ui.mainPage.render();

Архитектура

Модули не должны знать о существовании друг друга. Связь между ними происходит через «песочницу». Ею выступает объект, хранящий в себе состояний приложения и методы для взаимодействия между модулями.

// Объект-«песочница»
sample.core.AppState = Backbone.Model.extend({
message: null,
receiver: null,

	// Метод определения приемника
setReceiver: function(receiver) {
		this.set('receiver', receiver);
	},

	// Метод пересылки сообщения
	proceedMessage: function(message, success, error) {
            	var receiver = this.get('receiver');

		if (receiver) {
			this.set('message', message) ;
			receiver.receiveMessage(message);

                        if (success) {
				success.call();
			}
		}  else if (error) {
			error.call();
		}
	}
});

// Объект-отправитель
sample.modules.Sender = Backbone.View.extend({
	message: 'text to send',

        initialize: function() {
       		this.model = sample.core.state;
    	},

	sendMessage: function() {
		// отправляем сообщения в «песочницу»
		this.model.proceedMessage(this.message,
                            this.successMessage, this.errorMessage);
	},

	successMessage: function() {
		alert('OK!');
	},

        errorMessage: function() {
		alert('THERE IS NO RECEIVER!');
	}
});

// Объект-приемник сообщений
sample.modules.Receiver = Backbone.View.extend({
        initialize: function() {
                this.model = sample.core.state;

                // Сообщаем «песочнице», что пересылать сообщения нужно нам
                this.model.setReceiver(this);
    	},

	receiveMessage: function(message) {
		alert(message);
	}
});

// Объект-подписчик на событие
sample.modules.Listener = Backbone.View.extend({
        initialize: function() {
                var self = this;
       		this.model = sample.core.state;

		// Подписываемся
                this.model.bind(‘change:message’: function() {
	                 self.listenMessage(self.model.get(‘message’));
                });
    	},

        listenMessage: function(message) {
		console.log(message);
	}
});

sample.core.state = new sample.core.AppState();
sample.modules.receiver = new sample.modules.Receiver();
sample.modules.sender  = new sample.modules.Sender();
sample.modules.listener = new sample.modules.Listener();

В приведенном примере представлены 3 модуля: отправитель, получатель и подписчик, а также «песочница» для их связи. Несложно заметить, что каждый из объектов представляет собой применение паттерна проектирования Singletone. В тоже время «песочница» построена на основе шаблона Mediator, выступая посредником меду отправителем и получателем. Шаблон подписчика – Observer, который отслеживает изменения поля посредника. Это реализовано через механизм событий. Данный инструмент является ключевой особенностью языка JavaScript.  При разработке сложного приложения стоит как можно чаще использовать его. Стандартные события ограничиваются действиями пользователя и манипуляцией DOM, но различные библиотеки и фреймворки расширяют данный инструмент, позволяя подписываться на изменения данных в моделях и т.п.

Построение масштабируемого JS приложения подразумевает широкое использование callback-методов.  Функции обратного вызова – один из основных инструментов построения асинхронного кода. В примере выше использование callbacks позволяет привязать обработчики ошибок отправки сообщений к объекту-отправителю.

Backend и модель данных

В RESTful приложениях связь с бэкэндом осуществляется через API, реализующий выдачу требуемых данных в определенном формате. Как правило это JSON. Идея построения связи фронтэнд-бэкэнда заключается в привязке модели на клиенте к определенному серверному методу.

sample.models.SomeModel = Backbone.Model.extend({
        initialize: function() {
	},

	url: function() {
		return ‘some/url’;
        }
});

sample.models.someModel = new sample.models.SomeModel;
sample.models.someModel.fetch(); //забираем данные с бэкэнда
sample.models.someModel.save(); //отправляем данные на бэкэнд

Маршрутизация запросов

Переход между разделами одностраничного приложения реализуется через специальный объект-маршрутизатор. Такие объекты также часто называют «Машинами состояний» (State Machines). В Backbone.js такой объект также реализует HTM5 History API.

sample.routers.App = Backbone.Router.extend({
    routes: {
        "": "initPage",
        "!": "initPage",
        "!/": "initPage",

        "!/about": "aboutPage",
        "!/chat": "chatPage",
        "!/player": "playerPage"
    },

    initialize: function() {
        // Создаем объекты модулей
    },

    initPage: function() {
    },

    aboutPage: function() {
    },

    chatPage: function() {
    },

    playerPage: function() {
    }
});

// Запускаем маршрутизацию
$(document).ready(function() {
    sample.routers.app = new sample.routers.App();
    Backbone.history.start();
});

Другие особенности

Разделы приложения могу содержать общие для всех них элементы. Такие объекты не должны перегружаться при переходе с одной страницы на другую (например видеоплеер). Для этого необходимо для каждого виджета создать на странице отдельный контейнер.  Все отображения элементов производятся через манипуляции с деревом DOM. Это даст нам возможность менять лишь нужные блоки раздела. Но помните, что работа с DOM –процесс ресурсоемкий. Старайтесь максимально оптимизировать эти действия.

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

Структура размещения кода

При разработке крупного приложения предельно важно четко структурировать разрабатываемый код.  Поддерживать проект, в котором все находится в одном файле, крайне тяжело. Нужно принять за правило следующий подход: каждая сущность (объект, модель, контроллер и т.п.) = отдельный файл.  Объедините сущности по их назначению и сложите в директории.  Например все модели в директорию ‘models’, представления – в ‘templates’ и т.д.

Сборка всех файлов в единый может происходить как простым объединением bash-скриптом, так и использованием  специальных библиотек (например require.js).

Пример готового приложения

Для демонстрации возможностей мы создали типовое приложение, которое включает в себя 4 раздела:  2 текстовых, HTML5 видеоплеер и радел чата, привязанного к текущему контенту в плеере.  Перемещения по разделам не будут закрывать проигрыватель, так что всегда присутствует возможность перехода к нему без перезагрузки видео. За отображение пользователю нужного раздела отвечает отдельный модуль системы, реализующий плавные переходы между «страницами».  Выбор ролика на главной странице приведет к переходу в другую комнату чата, замену проигрывателя и смену фона страницы.

Демо приложение

Заключение

В статье мы рассмотрели особенности построение гибких и масштабируемых JS приложений с использование Backbone.js. Вы можете использовать другой фреймворк, однако принципы, приведенные выше останутся неизменными.

Полезные ссылки