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.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. Для зручності, всі вихідні файли бібліотек зберігаються у виробника/src, а самі модулі безпосередньо у виробника. Так само вони отримують суфікс-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 для того, щоб можна було використовувати загальні модулі як на клієнті, так і на сервері.

Також, якщо вам моя ідея здалася корисною, то для мене це буде гарною нагодою продовжити розповідати про неї.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.