Skip to content

jsbeans/jsbeans

Repository files navigation

JsBeans - Client-server JavaScript Fullstack Web Framework

jsBeans — изоморфный клиент-серверный фреймворк класса "full-stack" для создания комплексных программных решений с развитым веб-интерфейсом, объектной иерархией и клиент-серверным взаимодействием на языке JavaScript.

Реализован на Java 1.8 с Mozilla Rhino в качестве серверного JavaScript движка, встроенным Web сервером на Eclipse Jetty и сервисной шиной на Akka.

Ключевые особенности jsBeans:

  • Веб-интерфейс и бизнес логика приложений разрабатываются на атомарных объектно-ориентированных JavaScript-объектах - бинах (JavaScript Beans (JSB) по аналогии с JavaBeans, но легче).
  • Бины - логически законченные функциональные компоненты, которые одновременно содержат как клиентский, так и серверный код (в одном классе/файле *.jsb).
  • Прозрачное взаимодействие клиентской и серверной частей бина и автоматическая синхронизация данных поверх базовой RPC шины.
  • Динамическое разрешение зависимостей бинов
  • Обмен сообщениями между бинами в едином клиент-серверном пространстве (поверх Actor System).
  • Встроенная библиотека системных и вспомогательных средств разработки (JSB, Kernel, Cluster, Debugger, ...)
  • Встроенная библиотека клиент-серверных веб-компонентов (JSB.Widgets) и возможность подключения компонентов из репозитория.
  • Интеграция с Java "из коробки"
  • jsBeans расширяется jar модулем, содержащим: java код, системные ресурсы jsBeans, ресурсы приложений jsBeans.

А также

  • динамически генерируемый скрипт-загрузчик (Web Page Bootstrap)
  • динамическая подзагрузка бинов на клиент
  • Встроенные шаблонизаторы *.jsb файлов, в частности `#dot ...`
  • Поддержка вызовв методов бина через HTTP (http://my.server.com/index.jsb)
  • Расширения бинов (traits), позволяющие навесить на бины дополнительный функционал.
  • Минификатор и упаковщик с управлением зависимостями
  • (в планах) Аннотирование полей и методов /** @annotation {...} */
  • TODO

Использование

  1. В виде самостоятельного сервера приложений с развертыванием прикладных приложений "из папки".
  2. В виде подключаемой Java библиотеки. Прикладные модули, как правило, также подключаются в общий classpath основного приложения

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

Основные принципы

Что такое бины?

В соответствии с концепцией jsBeans вся логика приложений (как серверная, так и клиентская) строится в виде иерархий объектов на языке JavaScript, напоминающих EJB-компоненты в Java EE. Они изоморфны, соответствуют основным принципам ООП, могут порождаться и удаляться, могут вызываться удаленно и мигрировать с одного сервера на другой или в браузер. Такие объекты мы называем - бинами.

Бины описываются в виде JS объектов. Они содержат системные поля, отражающие специфику функционирования бина, а также ряд пользовательских полей и методов для описания бизнес логики. Все системные поля начинаются на $, например: $name , $parent , $require и т.п.

Декларация бина осуществляется с помощью функции JSB(beanDescriptor); и выглядит следующим образом:

JSB({
    $name: 'Foo',
    
    myField: 123,

    myMethod: function(){
        return this.myField;
    }
};

Функция JSB(beanDescriptor) непосредственно формирует сам бин по описанию в beanDescriptor и помещает его в специальный контейнер бинов (JSB-контейнер) с целью дальнейшего управления его жизненным циклом. JSB-контейнер изоморфен и присутствует как на стороне сервера так и на стороне клиента.

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

JSB.lookup('Foo', function(FooClass){
	var foo = new FooClass();
	foo.myMethod();
    foo.myField = ', World!';
	foo.hello('Hello');
});

Все бины размещаются в файлах *.jsb в файловой системе сервера. В момент инициализации сервер рекурсивно сканирует папку с бинами и загружает их в свой репозиторий, так же поддерживается динамическая загрузка бинов. Каждый файл *.jsb содержит только декларацию бина без функции JSB(...), например: Файл test.jsb:

{
    $name: 'MyBean',
    
    myField: 123,
    
    myMethod: function(){
        return this.myField;
    }    
}

Контейнер бинов

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

Клиент-серверное взаимодействие

Клиент-серверные бины, как правило, содержат несколько секций (в частности – $server и $client).

  • В секции $client, в основном, располагаются поля и методы для взаимодействия с DOM моделью браузера.
  • В секции $server – серверные поля и методы, отвечающие за бизнес логику, работу с СУБД, файловой системой и другими ресурсами операционной системы.

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

Взаимодействие между кодом из разных секций осуществляется прозрачно для разработчика, путем вызова автоматически генерируемых асинхронных proxy-методов.

Файл foo.jsb:

{
    /** пакет и имя бина */
    $name: 'my.examples.Foo',
    
    /** бин может наследовать свойства и методы родительского */
    $parent: 'my.examples.FooParent',
     
    /** бин может импортировать другие бины (например, для создания экземпляров), 
    ссылки на классы импортированных бинов будут интегрированы в методы как локальные переменные через встроенное замыкание*/
    $require: {MyWorld: 'my.examples.MyWorld'},

    /** методы из общей секции могут использоваться (копируются) в $client и $server*/
    formatMessageText: function(data){
        var text = '';
        for (let p in data) if (data.hasOwnProperty(p)) {
            text += p + ' = ' + data[p] + '\n';
        }
        return text;
    },

    /** секция клиентского кода (исполняется в браузере, в контексте Web страницы)*/
    $client: {
        $constructor: function(){
            this.scheduleTimestampMessage();
        },

		scheduleTimestampMessage: function(){
			window.setInterval(function(){
                /** асинхронный вызов метода из "противоположной секции" осуществляется
                через адаптер, получаемый путем вызова метода remote() у бина.
                */
                $this.remote().getTimestamp(function(result, error){                    					/** результат вызова удаленного метода будет возвращен в коллбэк */
                    
                    /** для удобства в scope всех методов автоматически добавляется
                    	локальная переменная $this равная this при вызове метода бина
                    	(избавляет от  `var self = this`) */
                    if (!error) {
                        var data = {timestamp: result};
                        alert($this.formatMessageText(data));
                    }
                });                
            }, 1000);
		}
    },

    /** секция серверного кода (исполняется на сервере)*/
    $server: {
        /** В серверной секции поддерживается импорт классов и методов Java*/
        $require: {System: 'java:java.lang.System'},
            
        /** Серверный конструктор вызывается при порождении серверной части экземпляра бина */
        $constructor: function(){
            /** создание экземпляра импортированного бина, см `$require` */
            this.myWorld = new MyWorld();                        
        },
        
        /** методы, объявленные в серверной секции могут вызываться с клиента и наоборот */
        getTimestamp: function (){
            return 0 + this.getSystemTimestamp(); // '0 + ' - Java Int -> JS integer
        },
        
        getSystemTimestamp: function() {
            /** Вызываем метод класса Java из пакета java.lang.System для получения
             	текущего времени */
            return System.currentTimeMillis();
        }
    }    
}

Создание web компонентов

Web компонент

Файл myWebControl.jsb:

{
	$name: 'my.examples.MyWebControl',

	/** Унаследуем наш компонент от бина Control из библиотеки JSB.Widgets */
	$parent: 'JSB.Widgets.Control', 

	/** Наш компонент будет использовать ComboBox из библиотеки JSB.Widgets */	
	$require: ['JSB.Widgets.ComboBox'],

	/** Клиентская секция (код будет выполняться в браузере) */
	$client: {
		$constructor: function(opts){
			$base(opts); // Вызываем родительский конструктор
			
			/** Загружаем стили (путь задается относительно места расположения 
			файла с текущим бином) */
			$jsb.loadCss('myWebControl.css');

			/** Создадим экземпляр бина ComboBox из библиотеки JSB.Widgets */
			var cb = new ComboBox({
				cssClass: 'myCombo',
				dropDown: true,
				items: [{
					key: 'first',
					element: 'Первый'
				},{
					key: 'second',
					element: 'Второй'
				},{ 
					key: 'third',
					element: '<div class="cool">Третий</div>'
				}],

				/* инициализируем комбо значением, переданным через параметр 
				конструктора */
				value: opts.initialValue,
				
				onChange: function(key, obj){
					$this.updateMyData();
				}
			});
			
			/** Добавим ComboBox в DOM элемент нашего компонента */
			this.append(cb); 

			/** Создадим DOM элемент для помещения туда данных с сервера */
			this.append($('<div class="myContainer"></div>'));
			
			/** Загрузим/обновим данные с сервера и выведем их */
			this.updateMyData();
		},

		updateMyData: function(){
			/** Получим текущее значение из комбо-бокса */
			var cbVal = this.find('.myCombo').jsb().getData();
			
			/** Вызываем серверную функцию для получения данных с сервера.
			Первым аргументом передаем значение из комбо, а вторым - колбэк 
			функцию, которая будет вызвана сразу как только серверный метод 
			вернет данные */
			this.server().loadMyData(cbVal, function(res){
				$this.drawData(res); // отрисуем данные
			});
            /** Для удаленного вызова серверного кода можно также использовать
            	функцию remote(), которая осуществляет удаленный вызов на 
            	противоположной стороне. Но если мы знаем, что вызывающий метод
                находится на стороне клиента и удаленно вызывает серверную
                функцию, то можно явно сразу использовать метод server().
                Аналогична и обратная ситуация - находясь в теле серверного метода
                можно удаленно вызвать клиентский метод, путем использования
                функции client()*/
		},
		
		drawData: function(data){
			/** Предварительно очистим контейнерный элемент */
			$('.myContainer').empty(); // вызов jQuery

			/** Добавим данные в контейнерный элемент при помощи встроенного
			шаблонизатора doT */
			$('.myContainer').append(`#dot
				<ul class="myTags">
				{{ for(var i in data) { }}
					<li class="myTag">{{=data[i]}}</li>
				{{ } }}
				</ul>
			`);
		}
	},

	/** Серверная секция (код будет выполняться на стороне сервера) */
	$server: {
		myDictionary: {
			first: ['Анна', 'Мария'],
			second: ['Вера', 'Лариса'],
			third: ['Вероника','Петр']
		},
		
		loadMyData: function(key){
			return this.myDictionary[key];
		}
	}
}

Встраивание компонента в HTML при помощи JavaScript

<html>
	<head>
		<script type="text/javascript" src="jsbeans.jsb"></script>
	</head>
	<body>
		<div id="myGlobalContainer"></div>
		
		<script type="text/javascript">
			JSB.create('my.examples.MyWebControl', {
				container: '#myGlobalContainer',
				initialValue: 'second'
			}, function(myCtrl){
				/** place code here if you want to do anything after control
				has been created */
			});
		</script>
	</body>
</html>

Автоматическое встраивание компонента в HTML

<html>
	<head>
		<script type="text/javascript" src="jsbeans.jsb"></script>
	</head>
	<body>
		<div id="myGlobalContainer" 
			jsb="my.examples.MyWebControl"
			initialvalue="second"
		></div>
	</body>
</html>

Синхронизация общих полей

Поля и методы, объявленные в общей секции бина (вне $server и $client) являются общими и для сервера и для клиента и могут использоваться в коде обеих секций. JsBeans позволяет синхронизировать общие поля таким образом, что поле измененное кодом на одной стороне автоматически обновится и на другой. Политика синхронизации полей гибко настраивается опцией $sync, которая может задаваться в разных секциях бина. Синхронизация может быть однонаправленной и двунаправленной, может осуществляться автоматически, а может управляться программно.

Автоматическая однонаправленная синхронизация

Для включения автоматической однонаправленной синхронизации необходимо в общей секции бина установить опцию $sync: true. В этом случае все общие поля, изменяемые кодом на стороне сервера, будут автоматически изменяться и на стороне клиента, но не наоборот.

{
	$name: 'my.examples.MySyncBean',
	$sync: true,

	/** Общее поле, подлежащее синхронизации */
	myCommonField: 'starting...',

	$client: {
		$constructor: function(){
			console.log(this.myCommonField);
		},
		
		/** Метод $onSyncAfter будет вызван сразу как только одно или несколько
		синхронизируемых полей будут обновлены */
		$onSyncAfter: function(syncInfo){
			console.log(this.myCommonField);
		}
	},

	$server: {
		$constructor: function(){
			/** Будем обновлять поле на стороне сервера каждую минуту */
			JSB.interval(function(){
				$this.myCommonField = new Date().toString();
			}, 60000);
		}
	}
}

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

Настройка политики синхронизации

Опция $sync:true устанавливает политику синхронизации по умолчанию, которая эквивалентна следующей настройке:

$sync: {
	updateClient: true,
	updateServer: false,
	updateCheckInterval: 1000,
	include: [],
	exclude: []
}

Установка значений для updateClient и updateServer позволяет задать направление синхронизации, при этом синхронизация может быть как однонаправленной, так и двунаправленной (оба значения установлены в true). При помощи опций include и exclude можно указать имена полей, подлежащих синхронизации. Опция updateCheckInterval позволяет установить интервал проверки полей на необходимость их синхронизации. Надо отметить, что в случае большого количества синхронизируемых полей, автоматическая проверка может негативно сказываться на производительности. В таких случаях рекомендуется выполнять проверку изменений только по мере необходимости и желательно в тех местах, где эти изменения происходят. Для этого нужно отключить автоматическую проверку, установив опцию updateCheckInterval: 0 и запускать эту процедуру программно, при помощи вызова метода doSync, который имеется у каждого бина.

{
	$name: 'my.examples.MySyncBean2',
	$sync: {
		updateCheckInterval: 0 // отключим автоматическую синхронизацию
	},

	/** Общее поле, подлежащее синхронизации */
	myCommonField: 'starting...',

	$client: {
		$constructor: function(){
			console.log(this.myCommonField);
		},
		
		/** Метод onAfterSync будет вызван сразу как только одно или несколько
		синхронизируемых полей будут обновлены */
		onAfterSync: function(syncInfo){
			console.log(this.myCommonField);
		}
	},

	$server: {
		$constructor: function(){
			/** Будем обновлять поле на стороне сервера каждую минуту */
			JSB.interval(function(){
				$this.myCommonField = new Date().toString();

				/** Вызовем процедуру синхронизации программно*/
				$this.doSync();
			}, 60000);
		}
	}
}

Управление логикой синхронизации

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

{
	$name: 'my.examples.SyncBeforeAfterTest'

	myField1: 'test',
	myField2: {},

	$client: {
		// ...
		/** onBeforeSync вызывается после получения синхропакета, 
		но перед обновлением полей. 
		Здесь мы можем принять решение - стоит	ли применять синхропакет или нет */
		onBeforeSync: function(syncInfo){
			if(syncInfo.isChanged('myField1')) {
				return false; // не будем принимать изменения
			}
			return true; // примем изменения
		}
		// ...
	}
}

После обновления полей фреймворк вызывает метод onAfterSync в целях уведомления о произведенной синхронизации.

{
	$name: 'my.examples.SyncBeforeAfterTest2'

	myField1: 'test',
	myField2: {},

	$client: {
		// ...
		/** onAfterSync вызывается после синхронизации.
		Здесь мы можем выполнить какие-либо действия узнав, что поле обновилось*/
		onAfterSync: function(syncInfo){
			if(syncInfo.isChanged('myField1')) {
				console.log('Ура, теперь myField1 = ' + $this.myField1);
			}
		}
		// ...
	}
}

Обмен сообщениями

  • TODO

Рутовый контекст в сессионных бинах - $root

только для $singleton и $fixedId

  • TODO

Привязка к текущему контексту при вызове из чужих потоков - JSB.wrap()

  • TODO

Управление жизненным циклом

Бин является составным объектом, разные его части (клиентская и серверная) имеют различные JavaScript объекты, у каждого может быть свой жизненный цикл и область видимости. Разработчик может привязать жизненный цикл бина к сесии пользователя, управлять автопорождением экземпляров или управлять жизненным циклом вручную. Ниже приведено несколько часто используемых случаев.

Синглтон

  • TODO

Синглтон в рамках сессии

  • TODO

Автопорождение серверной части с клиента

  • TODO

Один бин: один клиентский - один серверный

  • TODO

Один бин: много клиентских - один серверный

  • TODO

Один бин: много клиентских - много серверных

  • TODO

.

About

JavaScript Full-stack Web Framework on JVM

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published