Frontend: Розробка і підтримка



Давайте уявимо, що вас перевели на новий проект. Або ви змінили роботу і про проект максимум тільки чули. Ось ви сідаєте за робоче місце, до вас приходить менеджер, тисне руку і… прямо сходу відкриває сторінку проекту, тикає пальцем в монітор і просить вставити «інформер про майбутню подію Х». На цьому ви розлучаєтеся… Що робити? З чого почати? Як створити «інформер»? Де знайти потрібний шаблон? І море інших питань.

Під катом буде розповідь, як ми намагаємося організувати ці процеси, які інструменти створюємо для препарування SPA. Крім цього, ми поговоримо про технічні подробиці реалізації Live Coding / Hot Reload і чуйний про VirtualDom і React з Angular.

Приступимо. Отже, ось ви і залишилися один на один з проектом, тимлид повідомив, де знайти репозиторій, а далі — читай README.md, і все.

README.md
Це відправна точка при зануренні в проект, вона зустрічає вас базовою інформацією:

  • Установка — тут описані всі кроки, як запустити проект, щоб навіть «дизайнер» міг це зробити;
  • Запуск — приклади базових команд для запуску проекту;
  • Опції запуску — список всіх можливих параметрів та їх опис;
  • «Перші кроки» — власне, це і є потрібний розділ:
    • швидкий пошук UI-блоку — опис інструменту для препарування програми;
    • «Що? Де? Коли?» — короткий опис структури проекту;
    • створення UI-блоку — мінімальна інформація, як створити UI-блок;
    • «Логіка програми, або де шукати обробку подій?»;
  • Приклади/скринкасты — експериментальний розділ з прикладами.
Приклад, як це виглядає в інтерфейсі gitlab


На все про все піде приблизно п'ять хвилин. Найголовніше, що ви дізнаєтеся з README: для розв'язання задачі потрібно:

  1. Встановити NodeJS/npm.
  2. Клонувати репозиторій проекту.
  3. Запустити
    npm install
    та
    npm start
    .
  4. Відкрити проект в браузері і натиснути на «піпетку» в правому нижньому куті. ;]
Але давайте по порядку.

Установка
Ми вже дуже давно використовуємо пакетну розробку, тому багато частині (grunt — і gulp-таски, утиліти, UI-компоненти тощо) розробляються як окремі npm — або jam-пакети. Такий підхід дає можливість максимально переиспользовать код між проектами, забезпечує версійність (за semver) і, крім цього, дозволяє зібрати інфраструктуру для кожного пакета саме під завдання. І головне, ніякого легасі, пакет самостійний і, хто знає, може з часом перетворитися в хороший opensource.

Крім цього, не забувайте застосовувати npm-хуки, наприклад
postinstall
. Ми його використовуємо для установки таких git-хуков, як:
  • pre-commit — перевірка стилю кодування (ESLint);
  • pre-push — запуск тестів;
  • post-merge — запуск
    npm install
    та
    jam install
    .
Останній хук може здатися дивним, але, коли ви працюєте з купою пакетів, які динамічно оновлюються, без нього ніяк. Набравши
git pull
, розробник повинен отримати актуальну версію проекту, чого можна досягти, тільки примусово запустивши
npm install
.

Якщо проект залежить від npm або іншого стороннього менеджера пакетів, подбайте про локальному registry, щоб не залежати від зовнішнього світу і його проблем (left-pad, Роскомнадзор тощо).

Запуск
npm start
— все, що потрібно знати, і неважливо, що у вас під капотом: gulp, grunt, webpack… Вище я вже писав, що в README.md є опис параметрів запуску при старті додаток читає README.md, парсити список опцій і їх опису і, якщо ви використовуєте невідому або недокументовану опцію, видає помилку. Ось таким нехитрим способом вирішується проблема документації: немає опису — немає опції.

Приклад запуску:
npm start -- --xhr --auth=oauth --build

> project-name@0.1.0 start /git/project-name/
> node ./ "--xhr" "--auth=oauth" "--build"

- Гілка: master (Sun Aug 29 2016 10:28:06 GMT+0300 (MSK))
- Додаткові опції
- xhr: true (завантаження статики через `XMLHttpRequest`)
- auth: oauth (авторизація через `proxy`, `oauth`, `account`)
- build: true (використовувати складання)
- Складання проекту
- Запуск сервера на 3000
- Сервер запущено: localhost:3000


Перші кроки

Повернемося до задачі. Отже, README.md прочитаний, проект встановлений і запущений, переходимо до пункту «швидкий пошук блоку, або „піпетка“ — наше все».

«Піпетка» — це інструмент для аналізу структури компонентів і їх параметрів. Щоб їй скористатися, відкриваємо браузер, клікаємо на «піпетку» і вибираємо місце, куди «менеджер тицьнув пальцем».
ПрикладПеретягування
image

Інспектор
image

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

Тепер клікаємо на назву файлу, і… відкривається IDE, а курсор встановлений в потрібну строчку. Поруч є «око», якщо натиснути на нього — відкриється GUI/переглядач з обраним блоком.



Всі основні точки входу знайшли, тепер приступимо до додавання «інформера».

Створення UI-блоку
Є два шляхи створення блоку (обидва описані в README):

  • через консоль;
  • використовуючи GUI.
Консольний інструмент потрібен, коли немає можливості використовувати GUI, у всіх інших випадках зручніше і наочніше вдаватися до GUI.

GUI
Це веб-інтерфейс для перегляду, а головне, розробки UI-блоків проекту.
Що він уміє:

  • перегляд списку всіх блоків;
  • простий пошук за назвою і ключовими словами;
  • висновок всіх варіантів використання для конкретного блоку;
  • створення нового блоку;
  • перейменування блоку.


image

Першим ділом потрібно дізнатися, чи немає в проекті подібних інформерів. Скориставшись пошуком, знаходимо подібний блок, знову використовуємо «піпетку» для вивчення його структури і натискаємо «+», вводимо назву нового блоку, натискаємо «ОК», після чого GUI відкриває перегляд створеного блоку. Знову використовуємо піпетку і відкриваємо IDE для редагування css/шаблон/js.



Отже, що ж сталося? Після натискання кнопки «ОК» GUI створює папку з типовим блоком, який в нашій архітектурі складається мінімум з чотирьох файлів:

  • block-name.html — шаблон блоку
    <div></div>
  • block-name.scss — стилі
    .block-name {
    padding: 10px;
    background: red;
    }
    
  • block-name.js — опис поведінки
    import feast from 'feast';
    import from template 'feast-tpl!./block-name';
    import styleSheet from 'feast-css!./block-name';
    
    /**
    * @class UIBlockName
    * @extends feast.Block
    */
    export default feast.Block.extend({
    name: 'block-name',
    template,
    styleSheet
    });
    
  • block-name.spec.js — специфікація, на основі якої будуються приклади використання
    export default {
    'base': {
    attrs: {}
    }
    };
    

При редагуванні будь-якого з цих файлів всі зміни застосовуються без перезавантаження сторінки. Це не просто модна забава, а величезна економія часу. Блоки можуть мати логіку, а Hot Reload дозволяє не втрачати поточний стан, що відбувається при F5 / cmd + r. Ще під час редагування шаблону підключені блоки автоматично оновлюються. Іншими словами, GUI трохи програмують за вас. ;]



Ось так, майже нічого не знаючи про проект, можна додати новий блок. Вам не потрібно читати кілометри документації, щоб виконати звичайну задачу. Але це не означає, що «кілометри» не потрібні: ще й як потрібні — для поглиблення знань і життя проекту без його основних мейнтейнерів. Наприклад, для роботи з API і бізнес-логікою у нас є внутрішня JSSDK, документація якої генерується на основі JSDoc3.

Міні-підсумок
Вивчати документацію і кодову базу проекту потрібно і правильно, але вже на стадії глибинного занурення, спочатку ж досить описати сценарії виконання типових завдань. Такі інструкції повинні бути легкими і інтуїтивно зрозумілими. Автоматизуйте все, що можна автоматизувати. Як бачите, у нашому випадку це не просто створення блоку: автоматизація починається з установки проекту, хуков, оновлення пакетів і т. д. Вхід в проект повинен бути легкий і веселий ;]

Технічна частина
Почну трохи здалеку. На початку 2012 року ми створили свій шаблонизатор Fest. Він перетворював XML в js-функцію, яку можна використовувати на клієнті і сервері. Функція приймала об'єкт параметрів і видавала рядок: класичний js-шаблонизатор. Тільки, на відміну від побратимів, функція на той момент була супероптимизирована, ми могли запускати її на чистому V8, домігшись продуктивності Сі-шаблонизатора, що застосовували раніше.

[XML -> JSFUNC -> STRING -> DOM]


За цей час на базі Fest ми розробили внутрішню бібліотеку блоків, яка використовується відразу на декількох проектах (Пошта, Хмара та ін). Тобто кнопки, инпуты, форми, списки і т. д. у нас спільні. Власне, це і були перші кроки по структуруванню верстки і компонентів.

Час минав, і все гостріше поставало питання «Як нам жити далі?», адже Fest повертає рядок, оновити стан можна двома способами: або «змінити все», або «точково впливати на DOM з JS».

Звичайно, доводиться використовувати обидва підходи: де-то простіше і швидше перемалювати все, десь потрібно змінити тільки один css-клас. В цілому при роботі з шаблонизатором, який видає рядок, є свої плюси/мінуси, і це аж ніяк не продуктивність, як багато зараз думають. Основних проблем кілька:

  • Перемалювати все — переинициализация подій, повторне одержання посилань на потрібні DOM-фрагменти, «миготіння» картинок і т. п.
  • Точкове вплив — розмиває і дублює логіку, ускладнюючи розробку.
Тому ми почали рухатися далі, але з можливістю мінімального переписування вже готових компонентів.

Експериментів було багато. Пробували ввести data binding, дуже схожий на ангуляровский, але, на відміну від нього, Fest все так же видавав рядок, а data binding накладався вже після вставки в DOM. Це дозволило зберегти початкову швидкість і роботу через V8. На жаль, на великих списках у нас залишилися ті ж проблеми з аля-$digest, що і у ангуляра, хоч наша реалізація і була трохи швидше (у рамках наших завдань).

З часом на ринок вийшов React і подарував нам VirtualDom. Провівши бенчмарки, я трохи впав у смуток: базовий «список листів» вийшов приблизно в три рази повільніше, ніж у нас (і це з спрощеною реалізацією). До того ж ми не хотіли переписати свій код, а тільки замінити принцип оновлення шаблону. Але немає лиха без добра: React дав поштовх всьому js-спільноти, і незабаром, як гриби, почали зростати альтернативні реалізації vdom: Incremental DOM, morphdom, Deku, mithril Bobril та багато інших.

Справа залишалася за малим: провести бенчмарки на наших завданнях, вибрати підходящий і написати для наших шаблонів транспилер.

[XHTML -> JSFUNC -> VDOM? -> DOM]


Але головною метою було отримати максимально комфортну розробку блоків, а саме:

  • Інтерфейс для створення, перегляду і тестування блоків.
  • Live coding (CSS/HTML/JS).
  • Автоматизація створення/редагування блоків.
  • Інструменти для інспектування компонентів.
  • Візуалізація зв'язків між компонентами.
Крім цього, у нас вже були GUI/веб-інтерфейс до бібліотеки блоків, залишилося лише уніфікувати ідею, щоб кожен проект міг без особливого болю розгорнути GUI для себе.

Розробка
Live Coding
Думаю, не помилюся, якщо скажу: всі знають, що таке Webpack і BrowserSync. Про них написано багато, так що загострювати увагу на них не буду, а покажу альтернативний шлях: як бути, коли вам не підходять коробкові рішення. Тільки не думайте, що я закликаю вас винаходити велосипед: аж ніяк, це просто більш низькорівневий варіант, про який багато хто забуває і витрачають багато часу на «прикручування» того ж Webpack.

Якщо це так, то node-watch + socket.io — все, що вам потрібно. Два готових інструменту, які ви можете легко інтегрувати в свій проект.
const fs = require('fs');
const http = require('http');
const watch = require('node-watch');
const socket = require('socket.io');
cosnt PORT = 1234;

const app = http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'html/text'});
res.end();
});

const io = socket(app);

app.слухати(PORT, () => {
watch('path/to', {recursive: true}, (file) => {
fs.readFile(file, (err, content) => {
const ext = file.split('.').pop();
io.emit(`file-changed:${ext}`, {file, content});
});
});
});


<script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.5/socket.io"></script>
<script>
const io = io(location.protocol + '//' + location.host)
socket.on('file-changed:html, function (data) {
// data.file, data.content
});
</script>

Ось і все, тепер на клієнті можна отримувати зміни.

В реальності у нас все приблизно так і виглядає, основне відміну від наведеного лістингу — це препроцессинг JS і CSS при віддачі на клієнт. Так, саме так; на відміну від Webpack, у нас в dev-середовищі не використовуються банди, файли перетворюються на вимогу.

Гаряче оновлення блоків
Щоб вдихнути нове життя в fest, знадобилося вибрати бібліотеку для роботи з vdom і написати транспилер для xhtml/xml, врахувати проблеми реалізації та вирішити їх.

Які проблеми? Наприклад, щоб додати новий функціонал (обробку конструкції/тега), доводилося вносити зміни в бібліотеку і піднімати версію. Крім цього, шаблони можна було побудувати тільки на сервері.

Так і з'явився Feast. ;]

Ще він перетворює xml/xhtml у JSFUNC, але ця функція повертає рядок, а JSON, яка далі передається citojs (це дуже спритна і простенька бібліотека для роботи з vdom), а citojs вже будує або оновлює vdom.

Крім цього, тепер компіляція шаблонів відбувається прямо на клієнта, тому шаблони віддаються «як є» і на клієнті перетворюються спочатку в AST, а потім, згідно з правилами трансформації, JSFUNC.
Наприклад, ось так виглядають правила для перетворення тега `fn:for`
// <fn:for data="attrs.items" as="key" value="item">...</fn:for>
'fn:for': {
scope: true,
required: ['data'],
expressions: ['data'],
prepare: (node, {as, key, data}) => ({
as: attrs.as || '$value',
key: attrs.key || '$index',
data
}),
toCode: () => ['EACH($data, @@.children, function (&as, &key) {', '});']);
}

Це дозволило вирішити відразу кілька проблем:

  • більше не потрібен сервер для компіляції;
  • розмір навіть надлишкового html менше, ніж скомпільованого JS;
  • правила трансформації можна доповнювати на льоту;
  • максимум метаінформації.
Тому при отриманні нового html на клієнті він заново транслюється в JS-функцію і викликається перерендер всіх блоків, створених на основі цього шаблону:
socket.on('file-changed:html', (data) => {
const updatedFile = data.file;
feast.Block.all.some(Block => {
if (updatedFile === Block.prototype.template.file) {
const template = feast.parse(data.content, updatedFile);
Block.setTemplate(template);
Block.getInstances().forEach(block => block.render());
return true; 
}
});
});

Для CSS приблизно така ж логіка, головною зміною було запровадження CSS-модульності, щоб раз і назавжди розпрощатися з main.css і доставляти css разом з кодом компонента, а також захистити селектори від перетину і можливості обфускации.

CSS Modules
Як би голосно це не звучало, але сам процес досить простий і вже був відомий (наприклад), але мало поширений через відсутність зручних інструментів. Все змінилося з появою postcss і webpack. Перш ніж перейти до нашої реалізації, поглянемо, як це працює в інших, наприклад у React і Angular2.

React + webpack
import React from 'react';
import styles from './button.css';

export default class Button extends React.Component {
render () {
return <button className={styles.btn}>
<span className={styles.icon}><Icon name={this.props.icon}/></span>
<span className={styles.text}>{this.props.value}</span>
</button>;
}
}

React + webpack + react-css-modules
import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './button.css';

class Button extends React.Component {
render () {
return <button styleName='btn'>
<span styleName='icon'><Icon name={this.props.icon}/></span>
<span styleName='text'>{this.props.value}</span>
</button>;
}
}

export default CSSModules(Button, styles);

@CSSModules(styles)
export default class Button extends React.Component {
// ...
}

Angular2
На відміну від React, Ангуляр підтримує подобу модульності з коробки. За замовчуванням він до всіх селекторам додає специфічності у вигляді унікального атрибута, але, якщо виставити певний «прапорець», буде використовувати shadow dom.
@Component({
selector: `my-app`,
template: `<div class="app">{{text}}</div>`,
styles: [`.app { ... }`] // .app[_ngcontent-mjn-1] { }
});
export class App {
// ...
}

Наш варіант — щось середнє, для нього не потрібно спеціально готувати шаблон, досить просто довантажити css і додати його в опис блоку:
import feast from 'feast';
import from template 'feast-tpl!./button.html';
import styleSheet from 'feast-css!./button.css';

export default feast.Block.extend({
name: 'button',
template,
styleSheet,
});

Крім цього, є ще експериментальна гілка не просто з заміною класів, а з повноцінним inline стилів. Це може стати в нагоді для роботи на слабких пристроях (телевізори тощо).

Власне, сама гілка виглядає так:
const file = "path/to/file.css";

fetch(file)
.then(res => res.text())
.then(cssText => toCSSModule(file, cssText))
.then(updateCSSModuleAndRerenderBlocks)
;

function toModule(file, cssText) {
const exports = {};

cssText = cssText.replace(R_CSS_SELECTOR, (_, name) => {
exports[name] = simpleHash(url + name);
return '.' + exports[name];
});

return {file, cssText, exports};
}

Як бачите, абсолютно ніякої магії, все дуже банально: отримуємо css як текст, знаходимо всі селектори, за допомогою простого алгоритму вважаємо hash і зберігаємо в об'єкт експорту [оригінальна назва] => [нове].

Ну і найцікавіше: JS, що з ним?

JS / Hot Reload

Розглянемо приклад. Припустимо, у нас є клас
Foo
:
class Foo {
constructor(value) {
this.value = value;
}
log() {
console.log(`Foo: ${this.value}`, this instanceof Foo);
}
}


Далі десь у коді:
var foo = new Foo(123);
foo.log(); // "Foo: 123", true


Після чого ми вирішуємо оновити реалізацію на
NewFoo
:
class NewFoo {
constructor(value) {
this.value = value;
}
log() {
console.log(`NewFoo: ${this.value}`, this instanceof NewFoo);
}
});

Та так, щоб вже створені примірники продовжили працювати коректно.
foo.log(); // "NewFoo: 123", true
foo instanceof Foo; // true


Щоб такий фокус, не потрібен препроцессинг, досить чистого JS:
function replaceClass(OldClass, NewClass) {
const newProto = NewClass.prototype;
OldClass.prototype.__proto__ = newProto;
// Оновлюємо статичні методи
Object.keys(NewClass).forEach(name => {
OldClass[name] = NewClass[name];
});
// Переносимо методи прототипу
Object.getOwnPropertyNames(newProto).forEach(name => {
OldClass.prototype[name] = newProto[name];
});
}

Так, ось і вся функція, десять рядків — і JS Hot Reload готовий. Майже. Я спеціально не став перевантажувати цю функцію, а показав тільки суть. По-хорошому потрібно ще помітити старі методи, яких більше немає, як невидалені.

Але тут є проблема :]
replaceClass(Foo, class NewFoo { /* ... */});
foo.constructor === Foo; // false (!!!)


Вирішити її можна декількома способами:
  1. Все ж використовувати Webpack, він обертає створення класу в спеціальний врапперов, який повертає і оновлює створюваний клас.
  2. Застосовувати обв'язку для створення класів, наприклад
    createClass('MyClassName', {...});
    .
  3. Ще можна звернутися до Proxy, але тут теж знадобиться препроцессинг


В підсумку наша схема виглядає так:
socket.on('file-changed:js', (data) => {
const updatedFile = data.file;
new Function('define', data.content)(hotDefine);
});


hotDefine
займається всій магією: замість запитуваного об'єкта (наприклад, feast) повертає не оригінальний, а спеціальний
FeastHotUpdater
, який і оновлює реалізацію.

Інструменти для аналізу коду
Як я показав у прикладі, на даний момент основний інструмент, який дозволяє інспектувати елементи прямо з браузера, — це «піпетка». Одна з приємних особливостей — відкриття потрібного файлу в IDE. Для цього використовується чудова бібліотека Романа Дворнова lahmatiy/open-in-editor:
const openInEditor = require('open-in-editor');
const editor = openInEditor.configure(
{editor: 'phpstorm'},
(err) => console.error('Something went wrong:' + err)
);

editor.open('path/to/file.js:3:10')
.catch(err => {
console.error('[open-in-editor] Ooops:', err);
});


Ще у Романа є подібний компонент для інспекції React і Backbone, який вміє набагато більше мого, та й виглядає суперськи. ;]
Приклад роботи component-inspector від Романаimage

Ті, хто добре знайомий з React, Ember, Angular, Backbone, прекрасно знають і про таких рішеннях, як React Developer Tools, Ember Inspect, Batarand, Backbone Debugger та ін Все це розширення DevTools для препарування положення.

Спочатку у мене в планах був саме экстеншен, благо API Хрому має до цього + є приклади, а всі вище перераховані розширення лежать на github, так що ви завжди можете подивитися реалізацію.

Але на жаль, не можна поставити розширення користувачеві, а нам дуже часто доводиться вивчати проблеми на машинах не тільки колег. Тому поки я сконцентрувався на інструментах, через які можна отримати максимум інформації в браузері без її перезавантаження. Тут і розкривається вся краса компіляції шаблонів на клієнті: вам не потрібні дві збірки (бойова і dev), збірка завжди одна, при дебаге ви завжди отримаєте всю можливу метаінформацію про компоненті.

Що ще?
Логування
Баги завжди бувають — це не біда, біда, якщо ви не можете зрозуміти, що сталося раніше. Тому ми приділяємо багато уваги логированию. Ідеальна ситуація, якщо ви в будь-який момент можете відкрити консоль в бою і зрозуміти, що сталося після ваших дій.

image

Покриття коду
Здебільшого це тільки експеримент, але його цілком можна використовувати для перевірки якості ручних тестів. Беремо istanbul, проганяємо через нього код і розкачуємо на тестові машини, далі разів у N секунд скидаємо в лог покриття. Ось таким нехитрим способом можна побачити, наскільки добре написані у вас сценарії для тестерів, покривають чи вони функціонал.

Приклад відображенняimage
image


Аналіз структури програми

Чим далі, тим більше додаток зростає, галузиться, і одного разу його структура стає незрозумілою. Так виглядала перша спроба ;]

Перша спроба візуалізації структури програмиimage


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

Олюднений результатimage
image


Зараз це інтерактивне дерево містить відомості, як блоки вкладені один в одного, під якими умовами і циклами; іншими словами, можна окинути поглядом все відразу. Але основна мета цього інструменту — перегляд активних вузлів в залежності від вхідних параметрів і знаходження мертвих зон (на жаль, поки не дороблено, так що показати не зможу).

Timeline
Голосно буде сказано, але найближчий аналог, тільки дуже простий, — це DevTools Timeline. На сьогодні він вміє відображати процеси, що відбуваються всередині додатка (рендер, події, зміни моделей тощо). Це дозволяє швидко зрозуміти, що саме і в якій послідовності змінилося, скільки часу це зайняло. Крім того timeline вже не раз допоміг виявити аномальну поведінку (зайвий перерендер або підозрілі зміни моделей).

Приклад запуску програми в dev-режиміimage
image


Висновок
Не важливо, який фреймворк ви використовуєте, під капотом все одно буде код, написаний вами, зі своєю специфічною логікою і стилем, – ось про що потрібно пам'ятати. Тому документуйте, описуйте і автоматизуйте цю «специфіку». Не бійтеся створювати інструменти під себе, навіть маленький bash-скрипт може істотно спростити ваше життя. Крім цього, обов'язково шукайте готові інструменти, навіть якщо вам здається, що таких немає. Чим популярніший інструмент, який ви використовуєте, тим більше його спільнота. У таких рішень, як React, Vue, Ember, Angular, – хороша підтримка Live Coding, розширення для Dev Tools і багато іншого. Наприклад, для React нещодавно вийшла вже друга версія react-storybook.

P. S. А тепер невеличке опитування.

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

0 коментарів

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