$mol: reactive micromodular ui-framework

Скільки потрібно часу, щоб просто вивести на екран великий список, використовуючи сучасні фреймворки?





Список на 2000 рядків ReactJS AngularJS Raw HTML SAPUI5 $mol Поява списку 170 ms 420 ms 260 ms 1200 ms 50 ms Оновлення всіх його даних 75 ms 75 ms 260 ms 1200 ms 10 ms
Напишемо нехитра додаток — особистий список завдань. Які у нього будуть характеристики?






ToDoMVC ReactJS AngularJS PolymerJS VanillaJS $mol Розмір ( html + php + css + templates ) * gzip 322 KB 326 KB 56 KB 20 KB 23 KB Час завантаження 1.4 s 1.5 s 1.0 s 1.7 s 0.7 s Час створення і видалення 100 задач 1.3 s 1.7 s 1.4 s 1.6 s 0.5 s
Невелика головоломка: перед вами синхронний код, який завантажує і обробляє вміст 4 файлів, але з сервера вони вантажаться паралельно. Як таке може бути?
Синхронна паралельна завантаження ресурсів
А тепер прошу за мною в кролячу нору, настав час дивовижних історій...

Клуб іменованих велосипедистів
Привіт, мене звати Дмитро Карлівський і я… керівник групи веб-розробки компанії SAPRUN. Наша компанія займається переважно впровадженням і підтримкою продуктів SAP в провідних компаніях Росії і ближнього зарубіжжя. Сам SAP — велика складна система, що складається з безлічі компонентів.
Один з таких компонент — веб фреймворк SAPUI5, призначений для створення односторінкових додатків. Це — типовий представник коробкових фреймворків, тобто таких, які надають вам не тільки архітектуру, а й багату бібліотеку віджетів. І як будь-коробковий фреймворк, даний піддається страшної хвороби сучасності — ожиріння.
Старий, товстий, похмурий гусак
ожиріння Проявляється у всьому: величезні обсяги коду з вишуканою німецької пасти; неповороткі віджети, ледве рухаючи списки на 100 елементів; розлогі дерева класів, в нетрях яких заблукати навіть лісовий ельф. Все це призводить до досить тривалої розробки, а час — гроші.
В результаті, досить складно вигравати тендери на розробку веб-додатків, якщо вказуєш реальні часові оцінки. А якщо виграєш, то результат, скажімо так, не вражає: додаток виходить або занадто простим, або занадто гальмівним. Особливо сумно на смартфонах, де кожен кіловат на рахунку.
Нам потрібен більш ефективний інструмент, що дозволяє малою кров'ю створювати конкурентоспроможні масштабні кросплатформені програми, тому ми зважилися на страшне — перевинайти колесо — власний веб фреймворк з промовистою назвою $mol. Розроблений з нуля, він увібрав в себе безліч свіжих ідей, про яких і піде подальше оповідання.
Реактивне програмування
Винайдене 50 років тому, воно тільки нещодавно прибуло до світу користувацьких інтерфейсів у вебі. Причому дісталося у досить куцому "push" вигляді: ви описуєте деяку послідовність дій, на вхід подаєте деякі дані, і ці дії, послідовно застосовуються до кожного елементу даних. Однак, такий підхід призводить до складнощів при реалізації ледачих і динамічно мінливих обчислень.
$mol побудований на "pull" архітектурі, де ініціатором будь-яких дій виступає споживач результату цих дій, а не джерело даних. Це дозволяє рендери лише ті частини програми, що потрапляють у видиму область; створювати лише ті об'єкти, що потрібні для рендеринга в поточний момент; запитувати з сервера лише ті дані, що потрібні для створених об'єктів.
$mol наскрізь просякнутий "ледачими обчисленнями" і автоматичним звільненням ресурсів. Ви можете всього одним рядком закешувати результат виконання функції і не турбуватися про инвалидации і очищення цього кеша — модуль $mol_atom сам відстежить всі залежності і виконає всю рутинну роботу.
const source = new $mol_atom( ( next? : number )=> next || Math.ceil( Math.random() * 1000 ) )

const middle = new $mol_atom( ()=> source.get() + 1 )

const target = new $mol_atom( ()=> middle.get() + 1 )

console.assert( target.get() === source.get() + 2 , 'Target must be calculated from source!' )
console.assert( target.get() === target.get() , 'Value must be cached!' )

source.push( 10 )

console.assert( target.get() === 12 , 'Target value must be changed after source change!' )

Тут у момент зміни source відбувається инвалидация значення middle і target, так що при запиті значення target відбувається обчислення його актуального значення, як би далеко один від одного source і target в програмі не знаходилися.
Синхронне програмування
Немає нічого простіше, ніж синхронне програмування. Код виходить коротким, зрозумілим і ви можете вільно використовувати всі можливості мови з управління потоком виконання (if, for, while, switch, case, break, continue, throw, try, catch, finally).
На жаль, JS — однопотоковий мову, тому, для забезпечення конкурентної виконання безлічі завдань, код доводиться писати асинхронний, що породжує безліч проблем: починаючи локшиною з дрібних функцій і закінчуючи ненадійною обробки виняткових ситуацій. node-fibers дозволяє писати синхронний код не блокуючи системний потік, але працює тільки в NodeJS. async/await/generators дозволяють створювати асинхронні функції, які можуть викликати один одного синхронно, але із-за несумісності з звичайними синхронними функціями, доводиться мало не всі функції робити асинхронними. Крім того, для них потрібна спеціальна підтримка з боку браузера або транспиляция в пекельну машину станів.
Модель реактивності ж, використовувана в $mol, дозволяє елегантно абстрагувати код від асинхронності. Подивіться, наприклад, на вихідний код Куайна з початку статті:
content() {

const paths = [
'/mol/app/quine/quine.view.tree' ,
'/mol/app/quine/quine.view.ts' ,
'/mol/app/quine/quine.view.css' ,
'/mol/app/quine/index.html' ,
]

const sources = paths.map( path => {
return $mol_http_resource.item( path ).text()
} )

const content = sources.map( ( source , index )=> {
const header = `# ${ paths[ index ] }\n`
const code = "`\n' + source.replace( /\n+$/ , " ) + '\n``\n'
return `${ header }\n${ code }`
} ).join( '\n' )

return content
}

Тут ви бачите цілком собі синхронну генерацію вмісту сторінки. Однак, системний потік не блокується, а завантаження всіх 4 файлів відбувається паралельно. При цьому, поки йде завантаження даних, замість них виводиться індикатор завантаження. Формується він автоматично, позбавляючи розробника і від цього головного болю теж.
Компонентне програмування
Розбиття програми на компоненти дозволяє розділяти одну велику задачу, на завдання поменше. Компоненти можуть бути реалізовані різними людьми одночасно, після чого зібрані разом. Тому важливо, щоб компоненти з одного боку були самодостатні, а з іншого — дуже гнучко настраиваемы.
Конструктор LEGO містить безліч найрізноманітніших деталей, але будь-які з них стикуються разом завдяки стандартизованому з'єднувальний інтерфейсу. $Mol в ролі такого інтерфейсу виступають властивості. Коли батьківський компонент створює дочірній, він перевизначає у того ряд властивостей, налаштовуючи його поведінка під свої вимоги. А завдяки реактивності, ризик що-небудь ненавмисно зламати в дочірньому компоненті — мінімальний.
Ви можете легко і просто замінити властивість моком, щоб перевірити логіку роботи компонента; задати всім властивостям константные значення для перевірки верстки; вказати дочірньому компоненту використовувати яке-небудь властивість батьківського, і зв'язати через нього кілька дочірніх компонент разом.
Батьківський компонент має повний контроль над дочірніми, що дозволяє робити компоненти дуже простими, без необхідності реалізації всіх можливих сценаріїв з величезними конфіг для їх налаштування.
У прикладі з Куайном використовується компонент $mol_pager, малює типову сторінку із заголовком в шапці, скроллящимся тілом і підвалом:
$mol_pager $mol_viewer
childs /
< header $mol_viewer
childs < head /
< titler $mol_viewer
childs /
< title
< bodier $mol_scroller
childs < body /
< footer $mol_viewer
childs < foot /

Але на базі нього ви можете створити новий компонент, який в шапку додає іконку, тіло замінює компонентом з батьків, а підвал взагалі видаляє:
$mol_app_quine $mol_pager
head /
< logo $mol_icon_refresh
< titler
body /
< texter $mol_texter
text < content \
footer null

З голими грудьми на амбразуру
Уявіть, що вам дістався старий проект, про який ви чули рівним рахунком нічого. Все, що у вас є — репозиторій з вихідними кодами. Документації або немає, або вона вже давно втратила колишню актуальність. А вам потрібно полагодити якийсь настирливий баг. Ще вчора.
Припустимо, перед вами ось це не хитре додаток:
Типове бізнес-додаток
Тут зліва ви бачите список заявок на закупівлі, а праворуч — подробиці з обраної заявці: кому, що, коли і на яку суму. І все б добре, та ось тільки дата поставки виводиться у форматі ISO8601 "YYYY-MM-DD", а не в звичному для цільової аудиторії "MM/DD/YYYY". Хто ми такі, щоб нав'язувати замовнику міжнародні стандарти? Ні, так справа не піде, і потрібно терміново виправити, але з чого почати, куди копати?
Єдина зачіпка — DOM елемент, куди виводиться дата. Можливо DOM інспектор зможе допомогти знайти якісь зачіпки, які дозволять вам вийти на виконавця:
Типове бізнес-додаток кишками назовні
Що за хворою психопат міг придумати настільки довгі ідентифікатори елементів? І чому вони такі дивні? Немов би є JS кодом… А що якщо..
Вміст об&#39;єкта, який витягується за кишки
Скопіювавши ідентифікатор консоль ви з подивом виявляєте, що даний код не просто робітник, але і повертає якийсь об'єкт, підозріло нагадує візуальний компонент: він є екземпляром класу
$mol_viewer
і зберігає в собі посилання на DOM елемент з якого ви і почали своє розслідування.
Тут ви помічаєте, дивну закономірність: усі поля іменуються або нормально, але зберігають в собі функції, або зберігають не функції, але іменуються дужках в кінці. Схоже, це рушницю тут теж висить не просто так — ви куштуєте викликати у об'єкта метод
objectOwner()
і отримуєте очікуваний результат — посилання на компонент вище по ієрархії:
Вміст власника знаходиться на відстані витягнутої руки
Вже краще орієнтуючись у тому, що відбувається, ви повертаєтеся до жертви неправильної локалізації і оглядає поглядом її нечисленні поля, жодне з яких не містить заповітної дати. Вашу увагу привертає поле з промовистою назвою
childs
, яка містить певну функцію її обчислене значення якої може бути саме те, що ви шукайте.
Метод повертає дату поставки
Ага! У вас є підозрюваний. Клацнувши правою кнопкою по функції ви в два рахунки знаходите місце її визначення:
Вихідний і згенерований код створення компонента
Перед вами код створення вкладеного компонента, явно згенерований роботом. На це вказує дивний шлях до файлу та короткий коментар, судячи по всьому, що послужив прототипом для генератора. А знайдена вами функція
childs
— не більше ніж посередник, передає управління функції
content
компонента-власника. Продовжуючи рух по ланцюжку доказів, ви піднімаєтеся вище, розплутуючи клубок інтриг у вищих ешелонах влади, поки, нарешті, не виходьте на справжнього злочинця під ім'ям
$mol_app_supplies.root(0).detailer().position(0).supplyDate()
:
Справжній виконавець цього звірячого злочину
Справа за малим — податися за вказаною адресою, внести необхідні зміни і перевірити їх. Але з чого почати, куди копати?
Ви выкачиваете репозиторій і виявляєте в корені проекту
package.json
. Логічно припустити, що це NodeJS проект, а значить потрібно встановити залежності:
> npm install --depth 0

Type 'npm start' to start dev server or 'npm start {relative/path}' to build some package.
.
+-- body-parser@1.15.2
+-- compression@1.6.2
+-- concat-with-sourcemaps@1.0.4
+-- express@4.14.0
+-- mam@1.0.0
+-- portastic@1.0.1
+-- postcss@5.2.4
+-- postcss-cssnext@2.8.0
+-- source-map-support@0.4.3
`-- typescript@2.0.3

Залежностей не дуже багато, так що ставляться вони все протягом хвилини. Ви помічаєте, що у проекті активно використовується транспиляция: скрипти пишуться на typescript, стилі обробляються postcss, а для налагоджувача генеруються source-maps.
Судячи по підказці, для запуску локального сервера, потрібно виконати очевидну команду:
> npm start

22:23 Builded mol/build/-/node.deps.json
22:23 Builded mol/build/-/node.js
22:23 Builded mol/build/-/node.test.js

$mol_build.root(".").server() started at http://127.0.0.1:8080/

Подальші кроки не менш очевидні — відкриття зазначеного адреси приводить вас до списку пакетів виду:
Список файлів в корені проекту
Ви йдете всередину по просторах імен, поки не потрапляєте на потрібний додаток. Підозрюючи, що в рамках одного проекту може існувати безліч додатків, ви перевіряєте інші шляхи і переконуєтеся, що це дійсно так.
При цьому ви помічаєте, що перше відкриття програми відбувається кілька секунд, а повторні заходи вже відпрацьовують миттєво. Що за гальма на порожньому місці? Відкриття консолі прояснює ситуацію:
$mol_build.root(".").server() started at http://127.0.0.1:8080/

mol> git fetch & git log --oneline HEAD..origin/master
> git fetch & git log --oneline HEAD..origin/master
jin> git fetch & git log --oneline HEAD..origin/master

23:00:23 Builded mol/app/supplies//web.css
23:00:27 Builded mol/app/supplies/-/web.js
23:00:27 Builded mol/app/supplies//web.locale=en.json
23:00:41 Builded mol/app/todomvc//web.css
23:00:45 Builded mol/app/todomvc/-/web.js
23:00:45 Builded mol/app/todomvc//web.locale=en.json

Ага, при першому заході в додаток відбувається збірка пакетів для нього. Всі скрипти в один файл, всі стилі — в інший, тексти — в третій. Ви перезапускаєте сервер, відкриваєте браузер і перевіряєте цю теорію:
Перша завантаження 4 секунди, друга - пів секунди
Так і є — вантажаться всього 4 файлу, причому, підозріло малого обсягу в порівнянні з іншими популярними фреймворками: всі скрипти вміщаються в 30 кілобайт з урахуванням стиснення. Чорна магія, не інакше. У 30 кілобайт навіть окремо взята jQuery не поміщається, а адже ця бібліотека — основа більшості фреймворків. Ви дивіться згенерований пакет web.js і офигеваете ще сильніше, адже код навіть не минифицирован! Зовсім нічого святого!
Що ж, вистачить розважатися, пора провести виправні роботи. Ви відкриваєте
positioner.view.ts
і бачите там наступну картину:
namespace $.$mol {
export class $mol_app_supplies_positioner extends $.$mol_app_supplies_positioner {

position() {
return null as $mol_app_supplies_domain_supply_position
}

productName() {
return this.position().name()
}

price() {
return this.position().price()
}

quantity() {
return this.position().quantity().toString()
}

cost() {
return this.position().cost()
}

supplyDate() {
return this.position().supplyMoment().toString( 'YYYY-MM-DD' )
}

divisionName() {
return this.position().division().name()
}

storeName() {
return this.position().store().name()
}

}
}

якось біднувато. Де локшина? Де фрикадельки? Все, що роблять ці 8 методів — це перетворять хитросплетіння даних доменної моделі властивості моделі інтерфейсної. Щоб зрозуміти як дані виводяться, ви йдете по єдиному мабуть звідси шляху — затискаєте CTRL і клацаєте по базового класу, що приводить вас до того самого генерированному кодом, розташованому під '-/view.tree/positioner.view.tree.ts':
/// $mol_app_supplies_positioner $mol_carder
namespace $ { export class $mol_app_supplies_positioner extends $mol_carder {

/// heightMinimal 68
heightMinimal() {
return 68
}

/// productLabel @ \Product
productLabel() {
return this.text( "productLabel" )
}

/// productName \
productName() {
return ""
}

/// productItem $mol_labeler 
/// title < productLabel 
/// content < productName
@ $mol_mem()
productItem( next? : any , prev? : any ) {
return ( next !== void 0 ) ? next : new $mol_labeler().setup( __ => { 
__.title = () => this.productLabel()
__.content = () => this.productName()
} )
}
// ...

І тут купа дрібних функцій. Ні тобі обробників подій, ні створення DOM-елементів. Бачачи поруч розташовані шматки вихідного файлу і згенерований код, ви швидко починаєте розуміти цей пташиний синтаксис:
  • При оголошенні компонента спочатку вказується ім'я класу, а потім ім'я базового класу.
  • Всередині оголошення комбінація імені і значення створюють функцію, яка повертає це значення.
  • як значення можна вказати число або рядок, якщо випередити її "зворотною косою рискою". Ця риса асоціюється у вас з екрануванням даних. По всій видимості все, що йде після неї не буде розбиратися генератором, а буде вставлено рядок.
  • Якщо поставити "собачку", то текст пропаде коду, а замість нього буде вставлено отримання його по ключу. По всій видимості саме на цьому і заснована генерація файлу з текстами, яку ви підмітили, коли гралися зі складанням проекту.
  • як значення можна вказати ім'я іншого компонента і тоді функція буде повертати відповідний примірник. При цьому можна перевантажити властивості вкладеного компонента, своїми властивостями. Кутова дужка, очевидно, показує напрямок руху даних.
Начебто все просто, але не зрозуміло тільки навіщо було вводити якийсь свій формат, якщо те ж саме в typescript займає не сильно більше місця. Ви відкриваєте вихідний
positioner.view.tree
в надії побачити там щось ще.
$mol_app_supplies_positioner $mol_carder
heightMinimal 68

content < grouper $mol_viewer childs /

< mainGroup $mol_rower childs /

< productItem $mol_labeler
title < productLabel @ \Product
content < productName \

< costItem $mol_labeler
title < costlabel @ \Cost
content < coster $mol_coster
value < cost $mol_unit_money
valueOf 0
- ...

І справді суттєва відмінність у тому, що під
view.tree
ієрархія вкладених компонент представлена наочно, що дозволяє швидко в них орієнтуватися, але що генерується клас виходить цілком собі плоским, надаючи доступ до будь-якого вкладеного компоненту за один виклик методу.
Натхнений тим, як легко ви распутываете клубок внутрішньої архітектури, ви беретеся за правки. Можна було б просто поміняти формат виводу функції
supplyDate
і на цьому закрити справу:
supplyDate() {
return this.position().supplyMoment().toString( 'MM/DD/YYYY' )
}

Але це лише відтермінував вирішення цієї проблеми — формат не залежить від встановленої локалі. Адже трохи раніше ви з'ясували, що локалізація текстів вже цілком підтримується. Ви повертаєтеся до
positioner.view.tree.ts
:
/// productLabel @ \Product
productLabel() {
return this.text( "productLabel" )
}

Занурившись в
text()
ви доходите до місця, де задається мова:
export class $mol_locale extends $mol_object {

@ $mol_mem()
static lang( next? : string ) {
return $mol_state_local.value( 'locale' , next ) || 'en'
}

Ага, щоб отримати поточний мову, потрібно виконати:
$mol_locale.lang()

Ви виконуєте цей код в консолі і переконуєтеся, що він дійсно працює.
Залишилося створити функцію, яка б за ідентифікатором мови повертала формат представлення дати. Але де її розмістити? Потрібно створити окремий модуль.
За аналогією з іншими модулями ви створюєте новий за адресою
mol/dateFormat/dateFormat.ts
з наступного виду вмістом:
namespace $ {

export const $mol_dateFormat_formats : { [ key : string ] : string } = {
'en' : 'MM/DD/YYYY' ,
'ru' : 'DD.MM.YYYY' ,
}

export function $mol_dateFormat() {
return $mol_dateFormat_formats[ $mol_locale.lang() ] || 'YYYY-MM-DD'
}

}

Тільки одне не зрозуміло — ні в одному файлі немає жодного
import
ні
require
. Як же система дізнається, що цей файл потрібно включити в пакет програми? Не потрапляють ж у пакет взагалі всі файли? Щоб перевірити цю гіпотезу ви перезавантажуємо додаток і намагаєтеся викликати свежесозданную функцію з консолі:
$.$mol_dateFormat() // Uncaught TypeError: $.$mol_dateFormat is not a function

Ну не може ж воно саме розуміти який модуль потрібний, а який — ні? Або може? Ви додаєте використання функції в додаток:
supplyDate() {
return this.position().supplyMoment().toString( $mol_dateFormat() )
}

Перезавантаживши сторінку, ви з подивом виявляєте, що додаток не тільки не упали, але і вивело дату в локалізованому форматі:
Вивід дати в американському форматі
Ви перейменувати його файл
dateFormat2.ts
— все працює. Перейменувати його в директорію
dateFormat2
— знову помилка. Перейменувати його функцію
$mol_dateFormat2
— знову працює. Все стає ясно — при зверненні до глобальної функції/класу/змінної з таким дивним ім'ям відбувається пошук шляху, що відповідає частинам імені. І якщо така директорія — підключаються скрипти з неї.
Скасувавши останні перейменування, ви, з почуттям повного задоволення коммитите зміни і йдете в їдальню святкувати настільки швидке завершення завдання з кодом, який ви побачили в перший раз в житті, навіть не читаючи ніякої документації.
Нестримні чаювання
Звісно, ви могли б прочитати документацію по фреймворку і точно знати про використовуваних в ньому принципах, а не будувати теорії і перевіряти їх експериментально. Але як відомо, кращий спосіб розібратися як механізм працює — розібрати його і потикати своїми руками. Благо $mol заохочує дослідження рантайма, сповідуючи наступні принципи:
  • Всі об'єкти доступні за посиланнями з глобальній області видимості, а не заховані в замиканнях. Це дозволяє розробникам легко і просто досліджувати внутрішній стан програми.
  • Для довгоіснуючих об'єктів автоматично генеруються унікальні людинозрозумілі ідентифікатори, які одночасно є і "javascript-шляхами" до них з глобальній області видимості, що гарантує їх унікальність.
  • Зміни всіх станів логируются, із зазначенням ідентифікаторів об'єктів, що дозволяє в точності зрозуміти, що сталося. Наприклад, якщо ви включите висновок логів всіх повідомлень, ідентифікатори яких є підрядок "task", то, при завершенні завдання ToDoMVC, ви побачите таке повідомлення:
> $mol_log.filter('task')
< "task"
12:27:36 $mol_state_local.value("task=1476005250333") push Object {completed: true, title: "Hello!"} Object {completed: false, title: "Hello!"}
12:27:36 $mol_app_todomvc.root(0).taskCompleted(0) obsolete
12:27:36 $mol_app_todomvc.root(0).taskTitle(0) obsolete
12:27:36 $mol_app_todomvc.root(0).taskCompleted(0) push true false
12:27:36 $mol_app_todomvc.root(0).taskRow(0).completer().DOMTree() obsolete
12:27:36 $mol_app_todomvc.root(0).taskRow(0).DOMTree() obsolete

  • Простору імен в рантайме однозначно відповідають структурі директорій в репозиторії. Це гарантує відсутність конфліктів і дає чітке розуміння як людині, так і машині, де шукати вихідні файли.
  • Весь код псевдосинхронен і розбитий на невеликі функції, що спрощує його розуміння. В наступному прикладі відбувається неблокуючий запит файлу з текстами на потрібному мовою. Зверніть увагу на корисний стектрейс, який не доступний при використанні "обіцянок", "стримов" і тому подібних абстракцій.
Псевдосинхронный код з корисним стектрейсом
Стрибок без парашута
Уявіть, що перед вами раптово вималювалася завдання розробити багатоплатформовий додаток, який би однаково добре відчував себе як на десктопах потужних з величезними екранами, так і на мініатюрних смартфонах, де пів екрана то і справа закриває клавіатура. Ах так, і зробити це треба було ще вчора, а у вас ноутбук зламався і вам видали невинно чистий заміну.
Отже, ви працюєте в компанії ACME (а якщо не працюєте, то засновуєте свою) і вам потрібно реалізувати веб-додаток для гиковского соціального блогу HabHub. Для початку, вам потрібно просто завантажувати з гитхаба статті і показувати їх єдиною стрічкою.
Першим ділом ви встановлюєте необхідне програмне забезпечення: Git, WebStorm, NodeJS і NPM.
Далі ви выкачиваете репозиторій зі стартовим проектом MAM:
git clone https://github.com/eigenmethod/mam.git ./mam && cd mam

він Містить лише загальні для всіх пакетів конфіги:
  • .idea
    — налаштування для WebStorm: форматування коду, статичні перевірки, запуск локального сервера.
  • .editorconfig
    — налаштування для інших редакторів.
  • .gitignore
    — вказує які файли git повинен ігнорувати.
  • .pms.tree
    — вказує який пакет з якого репозиторію викачувати. Пакети викачуються складальником автоматично за необхідності.
  • package.json
    — налаштування для NPM.
  • tsconfig.json
    — налаштування для TypeScript компілятора.
Відкривши проект в WebStorm, ви запускаєте локальний сервер, кнопкою "Start" на панелі інструментів, або, якщо ви віддаєте перевагу інший редактор, виконавши в консолі:
npm start

Далі ви створюєте для програми директорію
acme/habhub
і кладете в неї
index.html
, який буде служити точкою входу в ваш додаток:
<!doctype html>
<html style=" height: 100% ">

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />

<script src="/web.js"></script>
<script src="/web.test.js"></script>
<link rel="stylesheet" href="/web.css" />

<body mol_viewer_root="$acme_habhub">

Вміст цього файлу досить типове, хіба що в атрибуті
mol_viewer_root
ви вказуєте клас компонента, який буде використаний в якості додатка. Так, компоненти на базі
$mol_viewer
настільки самодостатні, що будь-який з них може бути отрендерен ізольовано від інших, як окремий додаток.
Щоб створити згаданий компонент, ви створюєте файл
./acme/habhub/habhub.view.tree
:
$acme_habhub $mol_viewer

Після чого відкриваєте
http://localhost:8080/acme/habhub/
і переконуєтеся, що завантажується чиста сторінка, а в консолі немає ні однієї помилки — це значить, що всі необхідні файли успішно сгенерировались і завантажилися, а тести не виявили проблем.
В Багдаді все чисто
Мова опису компонент
view.tree
— потужний і лаконічний декларативний мову опису компонент, що дозволяє збирати одні компоненти з інших, як з кубиків LEGO. Вивчивши цей не хитрий мову, будь-верстальник може створювати гнучкі переиспользуемые компоненти, які легко інтегруються в інші, без традиційного "натягування верстки на логіку". Вся логіка пишеться в окремому файлі
view.ts
і як правило не вимагає змін у
view.tree
, що дозволяє програмісту і верстальщику працювати над одними і тими ж компонентами, не заважаючи один одному. Це досягається за рахунок навмисного обмеження: ви не можете просто взяти і вставити
div
в потрібному місці.
view.tree
вимагає, щоб ви використовували компоненти і (найголовніше!) кожному з них давали унікальні імена. Фактично
$mol_viewer
просто створить
div
при рендерінгу в
DOM
, але в перспективі рендеринг може бути в графічний полотно, нативні компоненти або навіть в excel файл.
Типовий сценарій створення компонента верстальником виглядає так (на прикладі компонента показує ненав'язливий лейбл над блоком):
Спочатку він пише демо-компоненти, які є прикладами використання реалізованого компоненти:
Label over simple text
$mol_labeler_demo_text $mol_labeler
title @ \Provider
content @ \ACME Provider Inc.

- Label over string form field
$mol_labeler_demo_stringer $mol_labeler
title @ \User name
content $mol_stringer
hint < hint @ \Jack Sparrow
value > userName \

Потім, власне реалізує його:
$mol_labeler $mol_viewer
childs /
< titler $mol_viewer
childs /
< title
< contenter $mol_viewer
childs /
< content null

А потім відкриває сторінку, де виводяться всі демо компоненти і додає стилі, дивлячись на всі варіанти використання компонента одночасно:
[mol_labeler_titler] {
color: var(--mol_skin_passive_text);
font-size: .75rem;
}

Багатьох бентежить незвичайний синтаксис. Те ж саме можна було б написати використовуючи більш звичний синтаксис XML:
<!-- Label over string form field -->
<component name="mol_labeler_demo_stringer" extends="mol_labeler">

<L10n name="title">User name</L10n>

<mol_stringer name="content">

<hint>
<String name="hint">Jack Sparrow</String>
</hint>

<value name="userName">
<String />
</value>

</mol_stringer>

</component>

Але він досить громіздкий; на чолі кута в нього типи компонент, а не їх імена в контексті батьківського компонента; деякі символи в рядках потребують заміни на xml-entities; велика спокуса просто скопипастить шматок верстки, без компонентної декомпозиції. Все це призводить до ускладнення роботи з кодом і його підтримки, і тому в $mol використовується саме синтаксис Tree, оптимально відповідний для завдання.
Невелика шпаргалка по view.tree:
Оголошення/використання компонента складається з 3 частин:
  1. Ім'я компонента/властивості
  2. Ім'я базового компонента
  3. Список (пере)визначаються властивостей
$
— префікс імен компонент. Даний префікс використовується скрізь, крім css, де він не допустимо.
\
з цього символу починаються сирі дані. Вони можуть містити будь-які символи (крім символу кінця рядка), без жодного екранування. Щоб встравить кілька рядків, треба додати символ
\
перед кожною.
@
— вставлений між ім'ям властивості і сирими даними, він вказує винести текст у файл з локалізованими рядками.
/
— оголошує список. Вставляйте елементи списку на окремих рядках з додатковим відступом.
*
— оголошує словник. Зіставляє текстовим ключів довільні значення. Ключ не може містити пробільні символи.
<
— односторонньо зв'язування (не плутати з одноразовим). Вказує, що властивість зліва (належить компоненту ліворуч) має брати значення властивості праворуч (належить означуваного компонента).
>
— двостороннє зв'язування (не плутати з обробниками подій). Вказує, що в якості властивості зліва, повинно бути взято властивість праворуч.
#
— довільний ключ. Вказує, що першим параметром властивість приймає деякий ключ
Числа, логічні значення і null виводяться як є, без будь-яких префіксів.
Складаємо цеглинки
Розібравшись у мові view.tree ви продовжуєте пиляти соціальний блог. Перш за все ви вирішуєте, що у вас буде типова розкладка сторінки у вигляді шапки і скроллящейся області. Для цього ви використовуєте готовий компонент $mol_pager:
$acme_habhub $mol_pager
title \HabHub
body /
\Hello HabHub!
footer null

Шапка і контент
Відмінно! В тілі сторінки повинні бути статті. Статті на GitHub пишуться у форматі markdown, тому ви додаєте кілька прикладів статей, використовуючи компонент для візуалізації markdown — $mol_texter:
$acme_habhub $mol_pager
title \HabHub
body < gisters /
< gister1 $acme_habhub_gister
text \
\# Hello markdown!
\
\*This* **is** some content.
< gister2 $acme_habhub_gister
text \
\# Some List
\
\* Hello from one!
\* Hello from two!
\* Hello from three!
footer null

$acme_habhub_gister $mol_texter

[acme_habhub_gister] {
margin: 1rem;
}

Кілька демо карток в тілі
Супер! Тепер ви прибираєте жорсткий код і залишаєте лише формулу створення картки статті за її номером:
$acme_habhub $mol_pager
title \HabHub
body < gisters /
gister# $mol_texter
text < gistContent# \
footer null

Настав час завантажити дані. Ви створюєте файл
habhub.view.ts
і пишете кілька мантр, які дозволять вам змінити поведінку вже створеного компоненти:
namespace $.$mol {

export class $acme_habhub extends $.$acme_habhub {

}

}

Перш за все ви описуєте формат в якому від сервера приходять статті:
interface Gist {
id : number
title : string
body : string
}

Потім ви визначаєте властивість, яка буде повертати посилання, по якій слід забирати дані. Надалі можна буде додати в нього логіку з обліку користувацьких переваг, але поки воно буде повертати константу:
uriSource() {
return 'https://api.github.com/search/issues?q=label:HabHub+is:open&sort=reactions'
}

А тепер ви задаєте властивість, яка буде повертати власне дані, роблячи запит до сервера через модуль $mol_http_resource_json, призначений для роботи з json-rest ресурсами:
gists() {
return $mol_http_resource_json.item<{ items : Gist[] }>( this.uriSource() ).json().items
}

Далі ви формуєте картки для показу статей з числа цих статей через властивість
gister#
, що ви оголосили ще у view.tree:
gisters() {
return this.gists().map( ( gist , index ) => this.gister( index ) )
}

gister#
звертаючись до
gistContent#
передає йому той же ключ, що переданий і йому, так що залишилося лише поставити, як за номером статті сформувати її вміст:
gistContent( index : number ) {
const gist = this.gists()[ index ]
return `#${ gist.title }\n${ gist.body }`
}

В результаті у вас виходить наступного виду презентатор:
namespace $.$mol {

interface Gist {
id : number
title : string
body : string
}

export class $acme_habhub extends $.$acme_habhub {

uriSource(){
return 'https://api.github.com/search/issues?q=label:HabHub+is:open&sort=reactions'
}

gists() {
return $mol_http_resource_json.item<{ items : Gist[] }>( this.uriSource() ).json().items
}

gisters() {
return this.gists().map( ( gist , index ) => this.gister( index ) )
}

gistContent( index : number ) {
const gist = this.gists()[ index ]
return `#${ gist.title }\n${ gist.body }`
}

}

}

Код в цілому тривіальний і в тестуванні не потребує: uriSource повертає константу, правильність звернення gists до стороннього модулю перевірить typescript компілятор, gisters тривіальний і знову ж перевіряється компілятором, і тільки gistContent містить нетривіальне формування рядка, тому ви пишете на нього тест в habhub.test.ts:
namespace $.$mol {
$mol_test({

'gist content is title + body'() {

const app = new $acme_habhub

app.gists = ()=> [
{
id : 1 ,
title : 'hello' ,
body : 'world' ,
}
]

$mol_assert_equal( app.gistContent( 0 ) , '# hello\nworld' )

}

})
}

Перезавантаживши сторінку, ви виявляєте в консолі:
Неправильно формується текст
Ага, пробіл загубився. Виправивши код, ви перезавантажуємо сторінку і бачите спочатку індикатор завантаження, а потім власне статті.
Очікування статей з GitHub
Статті з GitHub
Блиск! Перевіривши, своє маленьке додаток на коректність, ви з почуттям повного задоволення коммитите зміни і йдете в їдальню святкувати настільки швидке завершення завдання, що передбачає неблокирующие запити, візуалізацію markdown і лінивий рендеринг...
Що це тут випирає?
Дещо все ж затьмарює вашу радість і вам доводиться повернутися до налагоджувача — сторінка досить довго відкривається намагаючись отрендерить відразу всі статті.
Довгий рендеринг сторінок
Кожен $mol_texter є спадкоємцем від $mol_lister, який вміє ліниво рендери вертикальні списки, дорендеривая їх по мірі прокручування. Тому при відкритті сторінки статті рендеряться не цілком, а лише деяке число перших блоків. Щоб і самі $mol_texter виключалися з рендеринга, коли точно не влазять у видиму область, достатньо їх теж засунути в $mol_lister:
$acme_habhub $mol_pager
title \HabHub
body /
< lister $mol_lister
rows < gisters /
gister# $mol_texter
text < gistContent# \
footer null

Швидкий рендеринг сторінок
Ледачий рендеринг
Працює ледачий рендеринг просто і надійно. Будь-який компонент може надати інформацію про свою мінімальній висоті через властивість
heightMinimal
. Наприклад,
$mol_texter_rower
вказує мінімальну висоту в 40 пікселів, менше яких він займати не зможе, незалежно від вмісту, css правил і ширини батьківського елемента. Компонент $mol_scroller відстежує позицію скролінгу і встановлює властивість
$mol_viewer_heightLimit
контексту візуалізації таким чином, щоб гарантовано накрити видиму область (позиція скролінгу плюс висота вікна). Контекст автоматично передається всім отрендеренным всередині компонентів і доступний в них через
this.context().$mol_viewer_heightLimit()
. Використовуючи всю цю інформацію, компонент $mol_lister розраховує скільки елементів списку потрібно отрендерить, щоб гарантовано накрити видиму область. Так як всі згадані властивості реактивні, то при зміні складу елементів, позиції скролінгу і розмірів вікна, відбувається автоматичний дорендеринг відсутніх або видалення зайвих елементів списку.
Саме за рахунок ледачого візуалізації $mol і виявляється лідером в тестах продуктивності. Без нього, продуктивність $mol була б на рівні Angular. Хтось може заперечити, що це не чесно. Однак, це не менш чесно, ніж Virtual DOM в React, що дозволяє не робити те, що можна не робити. При цьому прискорення в обох випадках дістається майже безкоштовно, без кілометрів тендітної логіки, що описує коли і що треба робити, а коли й не потрібно.
Ледачий рендеринг дозволяє швидко показувати користувачеві екран, практично незалежно від обсягів виведених даних. Так як частина компонент не рендерится, то і дані для них не запитуються, що дозволяє і вантажити їх лише в міру потреби, нічого не міняючи для цього в шарі відображення. Єдиний слизький момент — якщо прокрутити сторінку відразу в самий кінець списку, то таки доведеться почекати повного візуалізації всіх елементів. Але це досить рідкісний випадок в повсякденному використанні, адже завдяки фільтрам і сортування, куди простіше зробити так, щоб потрібні дані виявилися нагорі, ніж вручну шукати їх величезному списку.
Виняткові ситуації
Все б добре, але після останніх оптимізацій кудись пропав індикатор завантаження. Ви відкриваєте DOM-інспектор і бачите там наступну картину:
Список нульової висоти
Блок
$acme_habhub.root(0).lister()
в який виводиться список статей не зміг отрендериться так як список статей ще не завантажений, тому для нього автоматично був встановлений атрибут
mol_viewer_error
з типом помилки в якості значення. Для типу помилки
$mol_atom_wait
за замовчуванням малюються біжать смужки. Але от біда, у відсутності вмісту цей блок схлопнувся до нульової висоти, і тому не видно індикатора завантаження. Найпростіше рішення — встановити для цього блоку мінімальну висоту:
[acme_habhub_lister] {
min-height: 1rem;
}

Список невеликої висоти
Але що якщо завантаження обірветься або відбудеться її якась помилка?
Індикатор помилки
Так справа не піде! Треба повідомити користувачу що пішло не так. Ви могли б просто перехопити виняток
gisters()
і намалювати замість списку статей повідомлення про помилку. Але це досить типовий код, який зручніше винести в окремий компонент, який би брав деяку властивість, і якщо при його обчисленні відбувалася б помилка — не просто падав, а показував повідомлення користувачеві. Саме так і працює
$mol_statuser
:
$acme_habhub $mol_pager
title \HabHub
body /
< statuser $mol_statuser
status < gisters /
< lister $mol_lister
rows < gisters /
gister# $mol_texter
text < gistContent# \
footer null

Гламурне повідомлення про помилку
Як можна помітити, особливу увагу в $mol приділено толерантності до помилок. Якщо якийсь компонент впав, то тільки він вийде з ладу, не ламаючи все інше, не залежне від нього, додаток. А якщо джерело проблеми усунуто, то і компонент слідом повертається до нормальної роботи.
Так як код на $mol в переважній більшості випадків синхронний, то й try-catch працює як належить. Але що якщо дані ще не завантажені і за ними потрібно сходити на сервер? Це сама натуральна виняткова ситуація для синхронного коду. Тому, модуль завантаження даних робить як годиться асинхронний запит, але замість негайного повернення даних (яких ще немає), кидає спеціальне виключення
$mol_atom_wait
, яка розкручує стек до найближчого реактивного властивості, яка його переймає і запам'ятовує в собі. А коли дані прийдуть, то це властивість буде обчислено повторно, але на цей раз замість винятку, будуть вже синхронно повернуті дані. Таким не хитрим способом досягається абстрагування коду всього додатки від асинхронності окремих операцій, без необхідності вибудовувати ланцюжка обіцянок і перетворення половини функцій в "асинхронні" (async-await).
Тут же варто відзначити елегантну магію, доступну у всіх сучасних браузерах завдяки Proxy API. В загальному випадку, при зверненні до реактивного властивості, в якому збережений об'єкт виключення, це саме виключення кидається негайно. Але якщо підтримується Proxy API, то повертається лише проксі, який кидає виняток, при спробі взаємодії з результатом. Це дозволяє продовжити виконання коду, відклавши "синхронізацію" до моменту, коли повертаються дані реально знадобляться.
Наприклад, вам потрібно вивести вітальне повідомлення, але саме повідомлення взяти з конфига, а ім'я користувача з його профілю. При першому обчисленні властивості greeting, буде наступна картина:
@ $mol_mem()
greeting() {

const config = $mol_http_resource_json.item( './config.json' ).json()
// Запущений асинхронний запит, а в config поміщений Proxy

const profile = $mol_http_resource_json.item( './profile.json' ).json()
// Запущений асинхронний запит, а в profile поміщений Proxy

// В цей момент виконання буде зупинено, а властивість greeting буде поміщено виняток $mol_atom_wait
const greeting = config.greeting.replace( '{name}' , profile.name ) 

// Сюди виконання вже не дійде
return greeting
}

Не має значення в якій послідовності прийдуть відповіді від сервера, тому що код функції є идемпотентным, тобто допускає множинні перезапуски. Після отримання кожної відповіді, властивість буде перевычисляться і знову зупинятися на відсутніх даних, поки всі необхідні дані не будуть завантажені:
@ $mol_mem()
greeting() {

const config = $mol_http_resource_json.item( './config.json' ).json()
// Config поміщений json, отриманий з сервера

const profile = $mol_http_resource_json.item( './profile.json' ).json()
// Profile поміщений json, отриманий з сервера

const greeting = config.greeting.replace( '{name}' , profile.name ) 
// greeting буде обчислено на основі config і profile 

// Нарешті, дійшли до кінця і повернули актуальне значення
return greeting
}

Свистілки і блестелки
Як можна було помітити, $mol містить все необхідне, щоб просто взяти і почати робити додаток. Нічого Не потрібно конфігурувати, а в комплекті йде бібліотека стандартних адаптивних компонент, що містить як тривіальні компоненти типу $mol_filler, який виводить відомий "Lorem ipsum", так і комплексні компоненти, типу $mol_grider, який призначений для відображення величезних таблиць з плаваючими заголовками.
При рендерінгу DOM-елементу встановлюється атрибут з ім'ям класу компонента, а також іменами всіх класів-предків. Це дозволяє, наприклад, встановити для всіх кнопок загальні стилі:
[mol_clicker] {
cursor: pointer;
}

А потім для якогось конкретного типу кнопки перевантажити їх:
$mol_clicker_major $mol_clicker

[mol_clicker_major] {
background: var(--mol_skin_accent);
color: var(--mol_skin_accent_text);
}

Крім того, відповідно до методологією БЕМ для всіх вкладених компонент встановлюються контекстно-залежні атрибути виду
my_signup_submitter
,
my_signup
— ім'я класу власника, а
submitter
— ім'я властивості, в який об'єкт збережений:
$my_signup $mol_pager
body /
< submitter $mol_clicker_major
childs /
< submitterLabel @ \Submit

[mol_signup_submitter] {
font-size: 2em;
}

Така логіка роботи дозволяє позбавити розробника від необхідності вручну дописувати до кожного dom-елементу "css-класи" і підтримувати порядок в їх іменуванні. У той же час, вона дає високу гнучкість при композиції компонент — завжди можна якось по особливому стилізувати конкретний компонент в конкретному контексті його використання, без ризику зламати що-то в інших місцях.
Так як один і той же компонент може використовуватися в абсолютно різних місцях, в різних додатках, виконаних в абсолютно різних дизайнах, критично важливо, щоб компонент міг адекватно мімікрувати під загальний дизайн додатка. Основним аспектом цієї мімікрії є кольори. Тому, як мінімум стандартні компоненти, які не містять в собі ніякої інформації кольорів, замість цього беручи її з глобальних констант. $Mol ці константи згруповані у модулі $mol_skin. Реалізуючи свою програму, ви можете перевизначити ці константи і всі компоненти перефарбуються у відповідності з ними:
:root {
--mol_skin_base: #c85bad;
}

Гламурний дизайн
Якщо вам не вистачить стандартних констант — ви завжди можете завести свої. Це дозволить тим, хто буде використовувати ваші компоненти, так само просто інтегрувати їх у свій дизайн, як і стандартні.
Екстрене гальмування
$mol досить простий і гибкок, однак, як і будь-фреймворк, він має і деякі жорсткі рамки, які заохочують "хороші" практики і перешкоджають "поганим". Що таке "добре", а що таке "погано", залежить від цілей. $Mol вони ставилися такі:
  • Створення швидких додатків. З швидким додатком працювати — одне задоволення. Як кінцевому користувачеві, так і початкового розробнику.
  • Швидке створення додатків. Це дає не тільки здешевлення виробництва, але і більше часу на інші етапи: від узгодження, до впровадження.
  • Створення кроссплатформенних додатків. Веб платформа як ніщо краще підходить для цих цілей.
  • Довгострокова підтримка створених додатків. Вона не повинна перетворюватися в сніжний ком з милиць і латочок.
  • Мінімізація помилок у створених додатках. Вони і боляче б'ють по репутації, та впровадження затягують.
  • Створення компактних додатків. Чим менше коду, тим швидше він стартує, тим менше в ньому багів, тим швидше його писати.
  • Створення міжпроектної кодової бази. Це і професіонала прискорює, і новачкові дозволяє швидше влитися в процес.
  • Расспараллеливание розробки додатків. Горизонтальна і вертикальна декомпозиція, дозволяють більшій кількості людей працювати над одним додатком використовуючи наработоки один одного, що призводить до прискорення постачання нових версій.
За підсумком, на поточний момент можна виділити наступні властивості $mol, які в інших фреймворках або не зустрічаються взагалі, або зустрічаються, але в кілька куцому вигляді:
  • Мінімум конфігурування — тільки кілька простих угод і максимальна автоматизація.
  • Микромодульность — додаток збирається з безлічі маленьких модулів. Немає строго виділеного ядра.
  • Автоматичні залежності між модулями — детектування залежності за фактом використання та автоматичне включення залежностей в пакет при збірці.
  • Багатомовні модулі — ні якогось виділеного мови, всі мови рівноправні при пошуку залежностей і складанні пакету.
  • Статична типізація — використовується TypeScript для вихідних і проміжних файлів.
  • Безліч додатків і бібліотек в одній кодовій базі — складання будь-якого модуля як незалежного пакета для деплоя куди б то не було.
  • Повна реактивність — автоматичне виявлення та ефективна актуалізація залежностей між станами.
  • Синхронний код, але неблокирующие запити — в тому числі і паралельні запити, коли це можливо.
  • Повна ленивость — лінива відтворення, лінива ініціалізація властивостей, лінива завантаження даних, лінива складання.
  • Контроль життєвого циклу об'єктів — автоматичне знищення при втраті залежностей від нього.
  • Висока компонуемость — легко з'єднувати навіть ті компоненти, які написані без оглядки на настроюваність.
  • Толерантність до помилок — виняткові ситуації не приводять до нестабільної роботи програми.
  • Кросплатформеність — модуль може містити різні версії коду під різні оточення і для різних оточень збираються окремі пакети.
  • Орієнтація на дослідження рантайма — скрізь є "хлібні крихти" допомагають знайти кінці.
  • Людинозрозумілі ідентифікатори об'єктів — генеруються автоматично на підставі імен властивостей, які ними володіють.
  • Логування зміни всіх станів — підтримується фільтрація вмісту ідентифікатора.
  • Простору імен замість ізоляції — простий доступ до консолі до будь-якого стану, простору імен відповідають розташуванню модулів у файловій системі.
  • Автогенерація BEM-атрибутів не потрібно вручну прописувати класи, імена в CSS гарантовано відповідають іменам в JS/TS/view.tree, підтримується спадкування.
Болванс Чик
На цьому наша історія… не закінчується. Можливо саме ви станете співавтором її продовження, привнесе з собою свіжих ідей, а може навіть і соковитих запитів на злиття. На цьому роздоріжжі у вас є кілька шляхів:
Джерело: Хабрахабр

0 коментарів

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