Well.js – еще один подход к модульной разработке на JavaScript

в 11:05, , рубрики: Asynchronous Module Definition, javascript, require.js, Веб-разработка

По названию публикации некоторые могли подумать: «Что опять?! Еще один велосипед?» Спешу обрадовать – нет. Well.js (Github) – это обертка для существующих AMD-решений (по-умолчанию для Require.js), основная идея которой сделать работу с модулями и их зависимостями, как показалось автору, более привлекательной.

Например, возьмем модуль Require.js:

define(['views/common/basic-page', 'views/partials/sidebar', 'utils/helper', 'models/user'  ], 
	function (BasicView,SidebarView, Helper, UserModel) {  
	//тело модуля  
});  

И легким движением руки заменим на это:

wellDefine('Views:Pages:Overview', function(app, modules) {  
	this.use('Views:Common:BasicPage')  
		.use('Views:Partials:Sidebar')  
		.use('Utils:Helper', {as: 'MyHelper', autoInit: false})  
		.use('Models:User', {as: 'UserModel'})  
		.exports(function(options){  
			/* Теперь к зависимостям можно получить доступ через:  
			this.BasicPage  
			this.Sidebar  
			this.MyHelper  
			this.User  
			*/  
		});  
});  

Кому интересно, для чего все это надо, прошу под кат.

Вступление

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

Использовать для этих целей Require.js в чистом виде мне не захотелось по двум причинам: одна эстетическая – очень не нравится то, как выглядит объявление путей и перечисление всех аргументов функции. Вторая причина технологическая — не удалось быстро разобраться как, при сборке проекта быстро минифицировать и склеивать файлы в нужной мне последовательности. Эти две причины подтолкнули меня на написание обертки, которая позволила мне решить по крайней мере второй вопрос.

Работа над проектом ведется в свободное от основной работы время, поэтому я решил поделиться им с общественностью. Так же хочу отметить, что данная статья не является учебным пособием, а скорее знакомство с Well.js и примеры кода, которые я буду приводить вымышленные, но постараюсь передать через них свою мысль.

Идеология

Идеология Well.js заключается в том, чтобы разные разработчики могли писать независимые компоненты приложения, использовать их как в рамках одного проекта, так и обмениваться ими через пакетные менеджеры или другими способами. Под компонентами я понимаю не только *.js файлы, но и шаблоны, стили, и т.п.

Еще одной идеологической особенностью Well является соглашение по именованию модулей — имена модулей соответствуют их путям. Т.е. Views:Common:BasicPage соответствует файлу views/common/basic-page.js.

Применение

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

apps  
- project_one  
- project_two  
- project_three  
…  
- project_n  
build  
plugins  
vendor  
require.js  
well.js  

В папке apps находятся проекты и все их индивидуальные файлы: шаблоны, стили, изображения, скрипты и т.п.

project_one  
- styles  
- images  
- js  
-- views  
-- models  
-- collections  
-- utils  
-- index.html  

В папке build скрипты для сборки. Я для сборки использую Gulp

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

Папку vendor, так же, через nginx используют все приложения. В этой папке хранятся библиотеки, фреймворки, jquery, backbone, underscore и так далее.

vendor  
-src  
--backbone.min.js  
--handlebars.min.js  
--handlebars-runtime.min.js  
--jquery.min.js  
--underscore.min.js  
-backbone-well.js  
-handlebars-well.js  
-jquery-well.js  
-underscore-well.js  

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

wellDefine('Vendor:JqueryWell', function(app){  
	//при обертке библиотек, на всякий случай, контекст нужно установить в window  
	this.set({context: window});  
	this.exports(function(){  
		//сюда вставляется код библиотеки в том виде как она есть
	});  
});  

Естественно, все это можно делать не в ручную, а, например, с помощью Gulp. Для удобства, все исходные файлы библиотек хранятся в vendor/src, а сами модули непосредственно в vendor. Так же они получают суффикс -well для того, чтобы можно было понять, что это модуль.

Для примера, в качестве используемой в нескольких проектах компоненты я взял сущность User. Чтобы создать плагин юзера, надо все что связано с юзером, т.е. форма регистрации, авторизации, авторизации через соцсети, поместить в папку plugins/user. Получится следующая структура:

plugins  
-- user  
--- main.js  
--- model.js  
--- form.html  
--- login-view.js  
--- style.css  

Итак, прежде чем начать создавать файлы проекта, нужно сконфигурировать well. Конфигурация описывается в index.html, до подключения файла well.js.

index.html

<!DOCTYPE html>  
<html>  
<head lang="en">  
	<meta charset="UTF-8">  
	<title>Well-example(development)</title>  
	<script>  
		window.WellConfig = {  
			appRoot: '/js',  
			pluginsRoot: '/plugins',  
			vendorRoot: '/vendor', 
			strategy: 'Strategy',  
			appName: 'PluginsExample',
			//isProduction: true,
		};  
	</script>  
    <script src="require.js"></script>  
    <script src="/well/well.js"></script>  
</head>  
<body>  
	<div id="site-container"></div>  
</body>  
</html>  

appRoot — корневая папка приложения. Относительно нее будут рассчитываться пути и названия модулей приложения.

pluginsRoot — корневая папка плагинов. Относительно нее будут рассчитываться пути и названия модулей подключаемых плагинов. Напомню, что в моем случае, эта папка является общей и находится на два уровня выше корня приложения, поэтому доступ к ней осуществляется через nginx.

vendorRoot — аналогично плагинам, только является хранилищем библиотек.

strategy — стратегия — это модуль который запускает приложение. В данном случае модуль так и называется Strategy, потому, что соответствует названию файла js/strategy.js.

appName — опциональный параметр, задает название приложению, которое в итоге будет доступно в объекте window.

isProduction — опциональный параметр, указывает на то, что модули минифицированы и загружены. Для продакшна достаточно склеить и минифицировать все модули в один файл. Единственное условие которое надо соблюсти — стратегия должна быть склеена самой последней.

И, наконец, JavaScript

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

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

strategy.js

wellDefine('Strategy', function (app, modules) {
	this.use('Vendor:JqueryWell');
	// При автозапуске последовательность зависимостей сохраняется, и 
	// underscore будет запущен после того как запустится jquery
	this.use('Vendor:UnderscoreWell');
	// По-умолчанию зависимые модули становятся полями объекта this
	// для того, чтобы избежать дублирования нужно использовать
	// опцию as. Она позволяет задать свойству другое имя
	// В данном случае this.Main меняетя на this.User
	// autoInit - говорит о том, зависимости надо активировать
	// автоматом, как только она будет загружена. 
	// По-умолчанию этот параметр равен true
	this.use('Plugins:User:Main', {as: 'User', autoInit: false});
	this.use('Helpers:Utils');
	this.exports(function () {
		var user = app.User = new this.User({
			onLoginSuccess: function () {
				$('#site-container').html('<h3>Hello, ' + user.model.get('name') + '</h3>')
			},
			onLoginError: function (err) {
				alert(err);
			}
		});
	});
});

Зависимости активируются в той последовательности, как они объявлены. Т.е, если Plugin:User:Main использует jQuery или Underscore, то обе эти библиотеки уже будут доступны, а вот Helpers:Utils не будет.

Под активацией понимается выполнение функции exports().

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

plugins/user/main.js

wellDefine('Plugins:User:Main', function (app, modules) {
	// well.js позволяет использовать относительные пути
	// Это удобно использовать тогда, когда зависимости хранятся
	// в той же папке что и родительский модуль
	this.use(':Model', {as: 'UserModel'});
	this.use(':LoginView');
	// well.js предоставляет возможность задавать модулям опции,
	// к которым потом можно получить доступ через this.get(<option>)
	this.set({
		template: 'form'
	});
	this.exports(function () {
		var mod = this;
		var User = function (opts) {
			this.options = opts || {};
			this.appendCss();
			this.loadTemplate(function (err, html) {
				if (err)
					throw err;
				this.onTemplateLoaded(html);
			}.bind(this));
			this.model = new mod.UserModel();
		};


		User.prototype.appendCss = function () {
			var link = document.createElement("link");
			link.type = "text/css";
			link.rel = "stylesheet";
			link.href = 'plugins/user/style.css';
			document.getElementsByTagName("head")[0].appendChild(link);
		};

		User.prototype.loadTemplate = function (next) {
			$.get('plugins/user/' + mod.get('template') + '.html', function (html) {
				next(null, html);
			}).fail(function (err) {
				next(err.statusText)
			});
		};

		User.prototype.onTemplateLoaded = function (html) {
			this.render(html);
			new mod.LoginView({
				el: $('.login-form'),
				model: this.model,
				onLoginSuccess: this.options.onLoginSuccess,
				onLoginError: this.options.onLoginError
			});
		};

		User.prototype.render = function (html) {
			$('#site-container').html(html);
		};

		return User;
	});
});

Следующие два модуля — это Model и View нашего плагина. Они были подключены выше в главном модуле.

plugins/user/model.js

wellDefine('Plugins:User:Model', function (app, modules) {
	this.exports(function (options) {
		var M = function () {
			this.attrs = {};
		};
		M.prototype.set = function (key, value) {
		  this.attrs[key] = value;
		};
		M.prototype.get = function (attr) {
			return this.attrs[attr];
		};
		return M;
	});
});

plugins/user/login-view.js

wellDefine('Plugins:User:LoginView', function (app, modules) {
	this.exports(function (options) {
		var L = function (opts) {
			_.extend(this, {
				model: opts.model,
				$el: opts.el,
				options: opts
			});
			this.$('.submit').on('click', this.submit.bind(this));
		};

		L.prototype.$ = function (selector) {
			return this.$el.find(selector);
		};

		L.prototype.auth = function (login, pass) {
			var err = 'bad username or password';
			if (login === 'demo' && pass === '1234')
				this.onLoginSuccess();
			else
				this.options.onLoginError ? this.options.onLoginError(err) : alert(err);
		};

		L.prototype.onLoginSuccess = function () {
			this.model.set('name', 'John Doe');
			if (this.options.onLoginSuccess)
				this.options.onLoginSuccess();
		};

		L.prototype.submit = function () {
			var login = this.$('input[name=name]').val();
			var pass = this.$('input[name=pass]').val();
			if (login && pass)
				this.auth(login, pass);
			else
				alert('Error: fill in all necessary fields!');
		};
		return L;
	});
});

Ввиду того, что моя цель рассказать о том, как использовать Well.js, то я не стал расписывать методы плагина. Рабочую версию примера можно посмотреть в репозитории проекта.

Пожалуй, на этом знакомство можно закончить.

Если у вас возникли замечания к публикации, прошу пишите о них в личку, все исправлю. Здравая критика, идеи и предложения по развитию проекта приветствуются.

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

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

Автор: VitaliiDel

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js