[Из песочницы] Well.js – еще один подход к модульной разработке на JavaScript

По названию публикации некоторые могли подумать: «Что опять?! Еще один велосипед?» Спешу обрадовать — нет. 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.UserModel */ }); }); Кому интересно, для чего все это надо, прошу под кат.

Вступление Я работаю в компании, у которой довольно много разных проектов и часто появляются новые идеи, которые порождают новые проекты. У некоторых проектов есть повторяющиеся компоненты, или они используют один и тот же набор библиотек, которые логично не копипастить, а вынести в доступное для всех проектов хранилище и запрашивать оттуда по мере необходимости.Использовать для этих целей 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

Well-example (development)

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 ('

Hello, ' + user.model.get ('name') + '

') }, 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 (

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 для того, чтобы можно было использовать общие модули как на клиенте, так и на сервере.

Хочу принести благодарность своему коллеге-перловику szcheh, некоторые рекомендации которого, нашли применение в данном проекте.

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

© Habrahabr.ru