Akili — javascript фреймворк, который появился под влиянием таких решений как React, Angular, Aurelia и в меньшей степени некоторых других. Целью было объединить все лучшее, что я вижу в них и максимально все упростить.
Нравится React, но отталкивает JSX? Любите Angular, но надоела всякая магия?
Тогда вам стоит попробовать это.
Я убежден, что наилучший способ в чем-то разобраться это практика. Поэтому начну описание сразу же с примеров. Они написаны так, как если бы мы компилировали код с помощью Babel (es2015 + stage-0).
Первые шаги
import Akili from 'akili';
class MyComponent extends Akili.Component {
constructor(el, scope) {
super(el, scope);
scope.example = 'Hello World';
}
}
Akili.component('my-component', MyComponent); // регистрируем компонент
document.addEventListener('DOMContentLoaded', () => {
Akili.init(); // инициализируем фреймворк
});
<body>
<my-component>${ this.example }</my-component>
</body>
Здесь мы создали свой первый компонент, зарегистрировали его и инициализировали приложение. Обычный компонентный подход на первый взгляд, но сразу хотел бы отметить пару моментов.
Во-первых, область видимости компонента разделена от области видимости разметки. То есть, можно спокойно наследовать компоненты и это никак не отразиться на этой самой разметке.
class MySecondComponent extends MyComponent {
constructor(...args) {
super(...args);
this.scope.example = 'Goodbye World';
}
myOwnMethod() {}
}
Akili.component('my-second-component', MySecondComponent)
<body>
<my-component>${ this.example }</my-component>
<my-second-component>${ this.example }</my-second-component>
</body>
За область видимости разметки отвечает свойство компонента scope. Это специальный объект, который вы можете заполнить необходимыми данными и отображать их в шаблонах с помощью выражений вида ${ this.example }
, где this и есть этот самый scope. На самом деле в скобках может быть любое javascript выражение.
Во-вторых, области видимости разметки также наследуются. Добавим в scope первого компонента новое значение:
class MyComponent extends Akili.Component {
constructor(el, scope) {
super(el, scope);
scope.example = 'Hello World';
scope.test = 'Test';
}
}
Тогда разметка ниже:
<body>
<my-component>
<b>${ this.example }</b>
<my-second-component>${ this.example } - ${ this.test }</my-second-component>
</my-component>
</body>
После компиляции будет выглядеть как:
<body>
<my-component>
<b>Hello World</b>
<my-second-component>Goodbye World - Test</my-second-component>
</my-component>
</body>
В-третьих, синхронизация логики компонента с его шаблоном происходит путем лишь изменения переменной scope в любой момент времени.
class MyComponent extends Akili.Component {
constructor(...args) {
super(...args);
this.scope.example = 'Hello World';
setTimeout(() => {
this.scope.example = 'Goodbye World';
}, 1000);
}
}
Через секунду значение переменной изменится и в объекте и в шаблоне.
Lifecycle в двух словах, в сравнении с React
.constructor(el, scope)
Прежде всего, поскольку любой компонент это простой javascript класс, будет вызван конструктор. Он получает в аргументы html элемент, к которому будет привязан компонент и объект scope. Здесь вы можете делать с элементом любые изменения, либо отменить компиляцию, в случаи необходимости, вызовом метода .cancel().
.created()
Если компиляция компонента не была отменена, то вы попадаете сюда. Этот метод фактически ничем не отличается от конструктора. В React, похожую функцию выполняет componentWillMount.
.compiled()
Здесь компонент скомпилирован, в шаблонах вместо выражений уже соответствующие значения.
В React это componentDidMount. Вы имеете доступ ко всем родительским элементам, которые к этому моменту гарантированно скомпилированы тоже.
.resolved()
Этот метод, насколько я знаю, не имеет аналогов в известных мне фреймфорках.
Дело в том, что Akili позволяет использовать при компиляции асинхронные операции, если это нужно. К ним относятся некоторые системные и любые кастомные операции. Например, загрузка шаблона компонента из файла:
class MyComponent extends Akili.Component {
static templateUrl = '/my-component.html';
constructor(...args) {
super(...args);
this.scope.example = 'Hello World';
}
}
Или любая асинхронная операция, которую мы выполним сами:
class MyComponent extends Akili.Component {
static templateUrl = '/my-component.html';
constructor(...args) {
super(...args);
this.scope.example = 'Hello World';
}
compiled() {
return new Promise((res) => setTimeout(res, 1000));
}
}
В методе compiled вы можете вернуть промис, тогда resolved будет ждать выполнения ВСЕХ асинхронных операций. При этом сама компиляции будет происходить синхронно.
Другими словами в методе resolved вы можете быть уверены, что скомпилированы абсолютно все дочерние элементы, любого уровня вложенности, в том, числе содержащие какие-либо асинхронные операции.
.removed()
Вызывается при удалении компонента. Аналог — componentDidUnmount.
.changed(key, value)
Вызывается при изменении любого атрибута компонента. Аналог — componentWillReceiveProps.
Это очень важная часть фреймфорка, поэтому опишу ее более подробно в отдельной секции ниже.
Универсальность, изоляция, модульность компонентов
Очень важно, чтобы компонент мог быть полностью изолирован и вообще не зависел от внешних условий. Вот пример такого компонента:
import Akili from 'akili';
class NineComponent extends Akili.Component {
static template = '${ this.str }';
static define() {
Akili.component('nine', NineComponent);
}
constructor(...args) {
super(...args);
this.scope.str = '';
}
compiled() {
this.attrs.hasOwnProperty('str') && this.addNine(this.attrs.str);
}
changed(key, value) {
if(key == 'str') {
this.addNine(value);
}
}
addNine(value) {
this.scope.str = value + '9';
}
}
Добавим его к предыдущим примерам:
import NineComponent from './nine-component';
NineComponent.define();
Akili.component('my-component', MyComponent);
document.addEventListener('DOMContentLoaded', () => {
Akili.init();
});
<body>
<my-component>
<nine str="${ this.example }"></nine>
</my-component>
</body>
Итак, вот что мы получим после компиляции:
<body>
<my-component>
<nine str="Hello World">Hello World9</nine>
</my-component>
</body>
Обратите внимание, NineComponent получился абсолютно обособленным. Он похож на функцию, которая может принимать какие-то аргументы и что-то с ними делать. В данном случаи просто добавляет цифру 9 в конец переданной строки и отображает ее.
Можно провести аналогию между атрибутами в Akili и свойствами в React.
this.attrs => this.props
. Они выполняют одну и туже роль, но есть мелкие различия:
В Akili свойство attrs как и scope является Proxy, то есть можно добавить, изменить или удалить атрибут html элемента, делая соответствующие операции с каким-то свойством данного объекта. Свойства объекта attrs синхронизируются с атрибутами элемента.
Вы можете использовать атрибуты для биндинга. В примере выше, если переменная области видимости this.example компонента MyComponent изменится, то будет вызван метод changed у NineComponent. Обратите внимание, мы не сделали для этого ничего особенного. Выражение в атрибуте str ничем не отличается от примеров в начале, где мы просто отображали значение в шаблоне.
Для удобства можно использовать сокращенную версию changed.
class NineComponent extends Akili.Component {
changed(key, value) {
if(key == 'str') {
this.addNine(value);
}
}
}
class NineComponent extends Akili.Component {
changedStr(value) {
this.addNine(value);
}
}
Примеры выше эквиваленты. Чтобы не плодить гору ифов или кэйсов, проще писать сразу нужный метод. Принцип именования прост: changed + название атрибута кэмел кейсом с заглавной буквы.
События
Здесь все просто, добавляем тире после on, а дальше все как обычно. Изменим наш первоначальный пример:
class MyComponent extends Akili.Component {
static events = ['timeout'];
constructor(...args) {
super(...args);
this.scope.example = 'HelloWorld';
this.scope.sayGoodbye = this.sayGoodbye;
}
compiled() {
setTimeout(() => this.attrs.onTimeout.trigger(9), 5000);
}
sayGoodbye(event) {
console.log(event instanceof Event); // true
this.scope.example = 'Goodbye World';
}
}
<body>
<my-component on-timeout="${ console.log(event.detail); // 9 }">
<button on-click="${ this.sayGoodbye(event) }">say goodbye</button>
${ this.example }
</my-component>
</body>
Система событий основана на нативной. В примере выше видно, что вы также можете создавать и вызывать свои кастомные события.
Работа с массивами
class MyComponent extends Akili.Component {
constructor(...args) {
super(...args);
this.scope.data = [];
for (let i = 1; i <= 10; i++) {
this.scope.data.push({ title: 'value' + i });
}
}
}
<my-component>
<for in="${ this.data }">
<loop>${ this.loopIndex } => ${ this.loopKey} => ${ this.loopValue.title }</loop>
</for>
</my-component>
<my-component>
<ul in="${ this.data }">
<li>${ this.loopValue }</li>
</ul>
</my-component>
Дополнительно
Из коробки Akili также имеет роутер, библиотечку для совершения ajax запросов, множество системных компонентов для работы с циклами, формами, возможность прикрутить серверный рендеринг и.т.д, в документации вы можете найти подробное описание.
Данная статься написана чтобы познакомить вас с Akili, я постарался раскрыть в целом какие-то технические моменты, но здесь не уместилась даже пятая часть того, что в себе содержит фреймворк. Гораздо больше информации есть в документации, ну и если будет интерес, то начну раскрывать тему глубже в других статьях.
Фреймворк пока в бете, пробуйте, смотрите )
Автор: IsOrtex