javascript

Что будет если скрестить React и Angular?

  • вторник, 27 июня 2017 г. в 03:14:55
https://habrahabr.ru/post/331704/
  • Разработка веб-сайтов
  • JavaScript



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, я постарался раскрыть в целом какие-то технические моменты, но здесь не уместилась даже пятая часть того, что в себе содержит фреймворк. Гораздо больше информации есть в документации, ну и если будет интерес, то начну раскрывать тему глубже в других статьях.

Фреймворк пока в бете, пробуйте, смотрите )