По названию публикации некоторые могли подумать: «Что опять?! Еще один велосипед?» Спешу обрадовать – нет. 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