Jii — JavaScript фреймворк з архітектурою від Yii 2


Вступ
Привіт всім хабровчанам, любителям Yii та Node.js.
Продалжаю серію статей про фреймворк Jii та його частини. У попередніх частинах ми розглянули частини фреймворку, які можна використовувати без ініціалізації програми, а саме — Query Builder і Active Record. З голосування (а так же листів і коментарів) стало зрозуміло, що варто продовжувати. І на цей раз ми будемо говорити про архітектурі і структурних складових фреймворку Jii.

Ідеологія

Перенесення функціоналу фреймворка з PHP JavaScript — не тривіальна задача і не може виконатися роботом. Мови мають дуже різні можливості і відмінності, головним з яких є асинхронність JavaScript.
Нижче про те, як вирішувалися ці проблеми і яких практик я дотримувався. Цих практик варто дотримуватися інших розробникам при розробці Jii і його разширений.

Promise
Кожна асинхронна операція повертає Promise об'єкт, имплементирующий стандарт ES6. Для цього використовується бібліотека when.
Методи, що починаються з префікса load (loadData()) повертають Promise, методи з приставкою get (getUser(), get('id')) — повертають дані синхронно.

Збереження API Yii 2
Початкова ідея Jii полягає в тому, щоб перенести чудовий і насичений PHP фреймворк на JavaScript. Щоб php-розробники, які бажають спробувати/перейти на node.js могли швидко і без праці освоїти фреймворк на
іншій мові програмування, навіть без читання клас референс!

Встроєний
Jii проектується з урахуванням того, що він повинен працювати скрізь де можна — браузер, сервер, phonegap, node-webkit і т. п.
Не дивлячись на те, що Yii генерує не мало глобальних констант (YII_*), клас Yii і неймспейс yii, Jii не засмічує
глобальний простір. Браузерам доступний єдиний об'єкт Jii, який можна заховати, викликавши Jii.noConflict(). У серверній частині в global нічого не пишеться, а Jii повертається як результат
виклику require('jii').

Npm пакети
Jii поширюється як набір пакетів jii-* і не має власних пакет-менеджерів (привіт, Meteor). Це означає, що разом з Jii ви можете підключити будь-які інші npm пакети.
Jii розбитий на кілька пакетів, тому його можна використовувати по частинах. Наприклад, якщо ви хочете почати використовувати тільки ActiveRecord, то ви встановлюєте jii-ar-sql і не маєте контролерів, вьюшек, http сервера і іншого непотрібного вам коду.
Якщо ви читали попередні статті, то вже убидились в цьому.
Основними пакетамы Jii на даний момент є:
Геттери і сетери
Однією з основних фішок Yii є легкість доступу до властивостей і методів об'єктів через геттери і сетери, наприклад, звертаючись через $model->user можна отримати або властивість моделі, або викликати метод getUser(), або взагалі отримати ставлення user, де відбудеться звертання до БД — магія цілковита, але всім вона подобається.
У JavaScript'е не всі браузери підтримують геттери і сетери, а в багатьох проектах потрібно ще підтримувати IE8, тому в Jii вони реалізовані у вигляді методів get('user') set('user', 'Ivan'). Такий підхід можна зустріти в багатьох бібліотеках, наприклад Backbone.
У майбутньому, коли необхідність в старих браузерах відпаде — то справжні геттери і сетери додадуться паралельно з get/set методами, які будуть через них і працювати.

Класи і неймспейсы
Як кажуть, кожен поважаючий себе програміст повинен написати свою реалізацію класів в JavaScript. Так і тут, для реалізації класів використовується моя бібліотека Neatness, яка вже добре зарекомендувала себе в інших внутрішніх проектах.

Чому не ES6 classes (і не CoffeeScript/TypeScript)?


коментарях та гітхабі вже є невеликий холівар з цього приводу.
Озвучу і тут причини відсутності ES6 в Jii:
  1. Для node.js потрібен препроцессинг (io.js підтримує)
  2. es6 немає неймспейсов, а вони потрібні для повторення фіч Yii. Їх можна імплементувати через модулі, але це буде милицю. Міняти одну милицю на інший — немає сенсу.
  3. ES6 захочеться використовувати геттери і сетери, але вони не підтримуються старими браузерами, як говорилося вище
  4. Багато розробники не знають формату es6/coffeescript/typescript і це буде додатковий поріг входження
  5. Складність роботи з кодом різних форматів (ES5 і ES6). Не завжди є можливість в існуючому проекті поміняти весь код на es6, а успадковувати es6 класи все одно треба, і все одно доведеться використовувати Jii.defineClass() для створення класів — милиця не йде.
Нижче я додав опитування на цю тему, голосуйте, коментуйте!

Middlewares
В Yii фреймворку компоненти доступні глобально через Yii::$app->... Проте в Jii не всі компоненти можна розташувати глобально, оскільки деякі з них (Request, Response, WebUser, Session, ...) прив'язані до контексту (запитом).
Такі компоненти будуть створюватися в контексті (Jii.base.Context) і передаватися в якості параметра в action — аналогія з передачею запиту і відповіді express.

/**
* @class app.controllers.SiteController
* @extends Jii.base.Controller
*/
Jii.defineClass('app.controllers.SiteController', /** @lends app.controllers.SiteController.prototype */{

__extends: Jii.base.Controller,

/**
*
* @param {Jii.base.Context} context
* @param {Jii.httpServer.Request} context.request
* @param {Jii.httpServer.Response} context.response
*/
actionIndex: function(context) {
context.response.data = this.render('index');
context.response.send();
}

});

Сутності

Jii додатки організовані згідно шаблону проектування модель-подання-поведінка (MVC) і мають наступні сутності:
  • програми: це глобально доступні об'єкти, які здійснюють коректну роботу різних компонентів програми та їх координацію для обробки запиту;
  • компоненти програми: це об'єкти, зареєстровані в додатку і надають різні можливості;
  • компоненти запиту: це об'єкти, зареєстровані в контексті запиту і надають різні можливості для обробки поточного запиту;
  • модулі: це самодостатні пакети, які включають в себе повністю всі засоби для MVC. Додаток може бути організовано за допомогою декількох модулів;
Приклади їх використовувати можна побачити в демо.

Програми
Додатки це об'єкти, які керують усією структурою і життєвим циклом прикладної системи Jii. Зазвичай на один воркер (процес) Node.js приходить один примірник додатка Jii, який доступний через Jii.app.
Коли вхідний скрипт створює програму, він завантажить конфігурацію і застосує її до додатка, наприклад:

var Jii = require('jii');

// завантаження конфігурації програми
var config = {
application: {
basePath: __dirname,
components: {
http: {
className: 'Jii.httpServer.HttpServer'
}
}
},
context: {
components: {
request: {
baseUrl: '/myapp'
}
}
}
};

// створення об'єкта додатка і його конфігурування
Jii.createWebApplication(config);

Важлива відмінність від Yii в тому, що конфігурація ділиться на дві частини — конфігурація (секція application) і конфігурацію контексту (запиту) (секція context). Конфігурація створює і налаштовує компоненти, модулі та конфігурує сам додаток (Jii.app) — все це відбувається при запуску воркера. У свою чергу, конфігурація контексту створює і налаштовує компоненти при кожному виклику екшену — http запиті, виклик консольної команди і т. п. Створені компоненти передаються як перший аргумент метод екшену.

З-за того, що конфігурація програми часто є дуже складною, вона виноситься у файли і розбивається на кілька конфігураційних файлів.

Компоненти програми
Програми зберігають безліч компонентів програми, які надають різні засоби для роботи програми. Наприклад, компоненти urlManager відповідальний за маршрутизацію веб запитів до потрібного контролеру; компонент db надає засоби для роботи з базою даних; тощо

Кожен компонент програми має свій унікальний ІДЕНТИФІКАТОР, який дозволяє ідентифікувати його серед інших різних компонентів в одному і тому ж додатку. Ви можете отримати доступ до компонента наступним чином:

Jii.app.ComponentID

Вбудовані компоненти програми

У Jii є кілька вбудованих компонентів програми, з фіксованими ID та конфігураціями за замовчуванням.

Нижче представлений список вбудованих компонентів додатка. Ви можете конфігурувати їх також як і інші компоненти програми. Коли ви конфигурируете вбудований компонент програми і не вказуєте клас цього компонента, то значення за замовчуванням буде використано.
  • db, Jii.sql.BaseConnection: являє собою з'єднання з базою даних, через яке ви можете виконувати запити. Зверніть увагу, що коли ви конфигурируете даний компонент, ви повинні вказати клас компонента також як і інші необхідні параметри.
  • urlManager, Jii.urlManager.UrlManager: використовується для аналізу і створення URL;
  • assetManager, Jii.assets.AssetManager: використовується для управління і опублікування ресурсів програми;
  • view, Jii.view.View: використовується для відображення уявлень.
Компоненти контексту
Набір компонентів контексту залежить від того, де він застосовується. Найпоширеніший варіант — це HTTP запит користувача, для нього ми і розглянемо вбудований набір компонентів.

Як і у компонентів програми, кожен компонент програми має свій унікальний ІДЕНТИФІКАТОР, який дозволяє ідентифікувати його серед інших різних компонентів в одному і тому ж контексті. Ви можете отримати доступ до компонента наступним чином:

actionIndex: function(context) {
context.ComponentID
}

Вбудовані компоненти запиту

Нижче представлений список вбудованих компонентів запиту. Ви можете конфігурувати їх також як і інші компоненти в секції context в конфігураційному файлі.

  • response, Jii.base.Response: являє собою дані від сервера, які буде надіслано користувачеві;
  • request, Jii.base.Request: являє собою запит, отриманий від кінцевих користувачів;
  • user, Jii.user.WebUser: являє собою інформацію аутентифицированного користувача;
Контролери
Контролери є частиною MVC архітектури. Це об'єкти класів, успадкованих від Jii.base.Controller і відповідають за обробку запиту і генерування відповіді. По суті, при обробки запиту HTTP-сервером (Jii.httpServer.HttpServer), контролери проаналізують вхідні дані, передадуть їх
у моделі, вставлять результати моделі в [представлення](structure-views), і в кінцевому підсумку згенерують вихідні відповіді.

Дії (Actions)

Контролери складаються з дії, які є основними блоками, до яких може звертатися кінцевий користувач і вимагати виконання того чи іншого
функціоналу. У контролері може бути одне або кілька дій.

Наступний приклад показує post контролер із двома діями: view create:

/**
* @class app.controllers.PostController
* @extends Jii.base.Controller
*/
Jii.defineClass('app.controllers.PostController' / * * @lends app.controllers.PostController.prototype */{

__extends: Jii.base.Controller,

actionView: function(context) {
var id = context.request.get('id');

return app.models.Post.findOne(id).then(function(model) {
if (model === null) {
context.response.setStatusCode(404);
context.response.send();
return;
}

context.response.data = this.render('view', {
model: model
});
context.response.send();
});
},

actionCreate: function(context) {
var model = new app.models.Post();

Jii.when.resolve().then(function() {
// Save user
if (context.request.isPost()) {
model.setAttributes(context.request.post());
return model.save();
}

return false;
}).then(function(isSuccess) {
if (isSuccess) {
context.request.redirect(['view', {id: model.get('id')}])
} else {
context.response.data = this.render('create', {
model: model
});
context.response.send();
}
});
}

});

В дії view (визначеному методом actionView()), код спочатку завантажує модель згідно запитаному ID моделі; Якщо модель успішно завантажено, код відобразить її подання під назвою view.

В дії create (визначеному методом actionCreate()), код аналогічний. Спочатку він намагається завантажити модель з допомогою даних із запиту і зберегти модель. Якщо все пройшло успішно, то код перенаправляє браузер на дію view з ID щойно створеної моделі. В іншому випадку він відображає уявлення create, через яке користувач може заповнити потрібні дані.

Маршрути (Routes)

Кінцеві користувачі звертаються до дій через так звані *маршрути*. Маршрут — це рядок, який складається з наступних частин:
  • ID модуля: він існує, тільки якщо контролер не належить додатком, а [модулю](structure-modules);
  • ID контролера: рядок, який унікально ідентифікує контролер серед усіх інших контролерів одного і того ж додатка (або одного і того ж модуля, якщо контролер належить модулю);
  • ID дії: рядок, який унікально ідентифікує дію серед усіх інших дії одного і того ж контролера.
Маршрути можуть мати наступний формат:

ControllerID/ActionID

або наступний формат, якщо контролер належить модулю:

ModuleID/ControllerID/ActionID

Створення дій

Створення дій не представляє складнощів також як і оголошення так званих *методів дій* у класі контролера. Метод дії це метод, ім'я якого починається зі слова action. Обчислене значення методу дії являє собою відповідні дані, які будуть вислані кінцевому користувачеві. Наведений нижче код визначає дві дії index hello-world:

/**
* @class app.controllers.SiteController
* @extends Jii.base.Controller
*/
Jii.defineClass('app.controllers.SiteController', /** @lends app.controllers.SiteController.prototype */{

__extends: Jii.base.Controller,

actionIndex: function(context) {
context.response.data = this.render('index');
context.response.send();
},

actionHelloWorld: function(context) {
context.response.data = 'Hello World';
context.response.send();
}

});

Дії-класи

Дії можуть визначаться як класів, успадкованих від Jii.base.Action або його нащадків.

Для використання такої дії, ви повинні вказати його з допомогою перевизначення методу Jii.base.Controller.actions() у вашому класі контролера, наступним чином:

actions: function() {
return {
// оголошує "error" дію за допомогою назви класу
error: 'app.actions.ErrorAction',

// оголошує "view" дію за допомогою конфігураційного об'єкта
view: {
className: 'app.actions.ViewAction',
viewPrefix: "
}
};
}

Як ви можете бачити, метод actions() повинен повернути об'єкт, ключами якого є ID дій, а значеннями — відповідні назви класу дії або [конфигурация](concept-configurations). На відміну від вбудованих дій, ID окремих дій можуть містити довільні символи, до тих пір поки вони визначені в методі actions().

Для створення окремої дії, ви повинні успадковуватися від класу Jii.base.Action або його нащадків, і реалізувати публічний метод run(). Роль методу run() аналогічна іншим методам дій. Наприклад,

/**
* @class app.components.HelloWorldAction
* @extends Jii.base.Action
*/
Jii.defineClass('app.components.HelloWorldAction', /** @lends app.components.HelloWorldAction.prototype */{

__extends: Jii.base.Action,

run: function(context) {
context.response.data = 'Hello World';
context.response.send();
}

});

Модулі
Модулі — це самодостатні програмні блоки, що складаються з моделей, уявлень, контролерів та інших допоміжних компонентів. При установці модулів в додаток, кінцевий користувач отримує доступ до їх
контролерам. З цієї причини модулі часто розглядаються як мініатюрні додатки. На відміну від додатків, модулі не можна створювати окремо, вони повинні знаходитися всередині додатків.

Створення модулів

Модуль поміщається в директорію, яка називається базовим шляхом модуля (Jii.base.Module.basePath). Так само як і в директорії програми, в цій директорії існують піддиректорії controllers, models, views та інші, в яких розміщуються контролери, моделі, подання та інші елементи. У наступному прикладі показано приблизний вміст модуля:

modules/
forum/
Module.js файл класу модуля
controllers/ містить файли класів контролерів
DefaultController.js файл класу контролера за замовчуванням
models/ містить файли класів моделей
views/ містить файли представлень контролерів і шаблонів
layouts/ містить файли представлень шаблонів
default/ містить файли подання контролера DefaultController
index.ejs файл основного подання

Класи модулів

Кожен модуль оголошується за допомогою унікального класу, який успадковується від Jii.base.Module. Цей клас повинен бути поміщений в корені базового шляху модуля. Під час запуску програми (воркера) буде
створений один екземпляр відповідного класу модуля. Як і екземпляри програми, примірники модулів потрібні, щоб код модулів міг отримати загальний доступ до даних і компонентів.

Наведемо приклад того, як може виглядати клас модуля:

/**
* @class app.modules.forum
* @extends Jii.base.Module
*/
Jii.defineClass('app.modules.forum.Module', /** @lends app.modules.forum.Module.prototype */{

__extends: Jii.base.Module,

init: function(context) {
this.params.foo = 'bar';

return this.__super();
}

});

Контролери в модулях

При створенні контролерів модуля прийнято поміщати класи контролерів у підпростір controllers простору імен класу модуля. Це також означає, що файли класів контролерів повинні розташовуватися в директорії controllers базового шляху модуля. Наприклад, щоб описати контролер post в модулі forum з попереднього прикладу, клас контролера оголошується наступним чином:

var Jii = require('jii');

/**
* @class app.modules.forum.controllers.PostController
* @extends Jii.base.Controller
*/
Jii.defineClass('app.modules.forum.controllers.PostController' / * * @lends app.modules.forum.controllers.PostController.prototype */{

__extends: Jii.base.Controller,

// ...

});

Змінити простір імен класів контролерів можна задати властивість Jii.base.Module.controllerNamespace. Якщо які-небудь контролери випадають з цього простору імен, доступ до них можна здійснити, налаштувавши властивість Jii.base.Module.controllerMap, аналогічно тому, як це робиться в додатку.

Використання модулів

Щоб задіяти модуль у додатку, достатньо включити його до властивість Jii.base.Application.modules у конфігурації програми. Наступний код у конфігурації програми задіює модуль forum:

var config = {
application: {
// ...
modules: {
forum: {
className: 'app.modules.forum.Module',
// ... інші настройки модуля ...
}
}
},
context: {
// ...
}
};

Властивості Jii.base.Application.modules присвоюється об'єкт, що містить конфігурацію модуля. Кожен ключ об'єкта являє собою *ідентифікатор модуля*, який однозначно визначає модуль серед інших модулів програми, а відповідний об'єкт — це конфігурація для створення модуля.

Доступ до моделей

Доступ до примірника модуля можна отримати наступним чином:

var module = Jii.app.getModule('forum');

Маючи екземпляр модуля можна отримати доступ до настройок і функцій, зареєстрованим у модулі. Наприклад,

var maxPostCount = module.params.maxPostCount;

В ув'язненні

Нижче я пропоную опитування про формат коду Jii. Якщо вибираєте не JavaScript-формат, то вкажіть в коментарях як би ви вирішували проблеми, описані в розділі «Чому не ES6 classes (і не CoffeeScript/TypeScript)?» у цій статті.
Нагадаю, Jii — опенсорсний проект, тому я буду дуже радий, якщо хтось приєднається до його розробки. Пишіть на affka@affka.ru.

Сайт фреймворку — jiiframework.uk
GitHub — https://github.com/jiisoft
Обговорення фіч проходить на гітхабі

Сподобалося? Постав зірку на гітхабі! :)
Який формат створення класів потрібен Jii?

/>
/>


<input type=«radio» id=«vv67629»
class=«radio js-field-data»
name=«variant[]»
value=«67629» />
Все нормально, Jii.defineClass() цілком влаштовує
<input type=«radio» id=«vv67631»
class=«radio js-field-data»
name=«variant[]»
value=«67631» />
ES6, в коментарях опишу як вирішити проблеми
<input type=«radio» id=«vv67633»
class=«radio js-field-data»
name=«variant[]»
value=«67633» />
CoffeScript
<input type=«radio» id=«vv67635»
class=«radio js-field-data»
name=«variant[]»
value=«67635» />
TypeScript
<input type=«radio» id=«vv67637»
class=«radio js-field-data»
name=«variant[]»
value=«67637» />
іншого, опишу в коментарях

Проголосувало 59 осіб. Утрималося 53 людини.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


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

0 коментарів

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