Angular 2

Алексей Золотых

twitter: @zolotyh

email: aazolotyh@gmail.com



Очень большая и очень страрая кодовая база

FAQ

  • Да, мы используем Angular 2 в бою!
  • Да, мы пишем на Dart и нам очень нравится!
  • Нет, Dart не загнулся!

Dart: Language updates, use cases, discussion with Google developers

https://goo.gl/1v9R1R

20 октября 2010 года

  1. Постановление Правительства РФ от 20.10.2010 N 848 "О внесении изменений в Постановление Правительства Российской Федерации от 1 декабря 2009 г. N 982"
  2. Первый релиз AngularJS
<input type="text" ng-model="yourName" >
<h1>Hello {{yourName}}!</h1>
<input type="text">
<p>Hello</p>

<script>
$(function() {
  var $input = $('input'),
      $p = $('p'),
      startValue = $p.text();

  $input.on('keyup', function(){
    $p.text(`${startValue} ${$input.val()}`)
  })
});
</script>

— Чудо?

— Безусловно!

— За все нужно платить!

$scope и $digest цикл

Страдают все!


Думаете проблема только в производительности!?

Пример

https://goo.gl/PPOrXW
import Workout from "../../Components/WorkoutComponent/WorkoutComponent";
import Token from "../../getCSRFToken";
import animation from "css-animation";

class WorkoutContainer extends React.Component {
  constructor() {
    super();
    this.handleDeletingWorkoutItem = this.handleDeletingWorkoutItem.bind(this);
    this.toggleItemFullData = this.toggleItemFullData.bind(this);
    this.state = {
      workoutName: "",
      workoutData: []
    };
  }

  componentWillMount() {
    this.props.getParentRoute("/app");
  }

  componentDidMount() {
    this.loadWorkoutData();
  }

  componentWillUpdate(nextProps, nextState) {
    if (this.state.workoutName !== nextState.workoutName) {
      this.props.getRouteName(nextState.workoutName);
    }
  }

  componentWillReceiveProps(nextProps) {
    if (this.props.params.id !== nextProps.params.id) this.fetchData(nextProps.params.id);
  }

  loadWorkoutData() {
    this.props.setFethingData(true);
    this.fetchData(this.props.params.id);
  }

  fetchData(id) {
    fetch(`/api/v1/workout/training/${id}`)
      .then(data => {
        this.props.setFethingData(false);
        if (data.status === 404) throw Error(404);
        return data.json();
      })
      .then(data => {
        this.setState({
          workoutName: data.title,
          workoutData: data.exercises.sort((a,b) => b.priority - a.priority)
        });
      })
      .catch (error => {
        if (error.message === "404") this.props.checkIsPageExist(false);
      });
  }

  handleDeletingWorkoutItem(itemId) {
    return () => {
      const confirmDeleting = confirm("Вы действительно хотите удалить уражнение?");
      if (confirmDeleting) {
        fetch(`/api/v1/workout/exercise/${itemId}`, {
          credentials: "include",
          method: "DELETE",
          headers: {
            "Content-Type": "application/json",
            "X-CSRFToken": Token
          }
        })
        .then(data => {
          if (data.status === 204) {
            const newState = this.state.workoutData.filter(session => !(session.url === data.url));
            this.setState({
              workoutData: newState
            });
          }
        });
      }
    };
  }

  toggleItemFullData(e) {
    const allFullDataItems = [...document.querySelectorAll(".workout-item__wrapper")];
    allFullDataItems.forEach(item => {
      const isItemFullDataClose = item.classList.contains("workout-item__wrapper_closed");
      if (e.target.nextSibling === item) this.animateFullData(e.target.nextSibling, !isItemFullDataClose);
      else if (!isItemFullDataClose) this.animateFullData(item, true);
    });
  }

  animateFullData(item, isShown) {
    let height;
    item.classList.toggle("workout-item__wrapper_closed");

    animation(item, "collapse", {
      start() {
        const itemHeight = `${item.offsetHeight}px`;
        if (!isShown) {
          item.style.height = "";
          height = item.offsetHeight;
        }
        item.style.height = itemHeight;
      },
      active() {
        item.style.height = `${isShown ? 0 : height}px`;
      },
      end() {
        if (!isShown) item.style.height = "";
      }
    });
  }

  render() {
    return (
      <Workout
        workoutData={this.state.workoutData}
        toggleItemFullData={this.toggleItemFullData}
        sessionId={this.props.params.id}
        deleteItem={this.handleDeletingWorkoutItem}
      />
    );
  }
}

export default WorkoutContainer;

Выбор MVC или FLUX не очень-то и влияет на качество кода!

Я опасаюсь, что мы потерпели поражение в борьбе со сложностью систем

— Эдсгер Вибе Дейкстра

— Что сделано в Angular2 чтобы помочь разработчикам?

— Давайте сравним!

AngularJS vs Angular2

  • Прощай $scope и $digest цикл!
  • Больше не будет контроллеров
  • Компонентный подход
  • Однонаправленный поток данных
  • ES6, Dart, Typescript
  • Dependency Injection
  • Можно прикрутить Redux, MobX, RxJS

Анатомия для компонента

Декораторы

function superhero(target) {
        target.isSuperhero = true;
        target.power = 'flight';
        }

@superhero
class MySuperHero {}

console.log(MySuperHero.isSuperhero);

<widget />

Готовый код

https://goo.gl/jN1DyA

<widget />

  • index.html
  • Map.ts
  • TabBar.ts
  • Country.ts
  • Widget.ts

Модель для страны Country.ts

export class Country {
  constructor(
    public id: string,
    public label: string,
    public coords: string) { }
}

Код для виджета Widget.ts

@Component({
  selector: 'widget',
  template: `<tab-bar [data]="list"
                      (onTabSelect)="select($event)"></tab-bar>
             <map [item]="country"></map>`,
  directives: [TabBar, StaticMap]
})
export class Widget {
  list: Country[] = countries;
  country: Country = new Country();

  select(c:Country) {
    this.country = c;
  }
}

<map [item]="country">

Код для Map.ts

@Component({
  selector: 'map',
  template: `
  <img *ngIf="item.coords"
    src="https://maps.googleapis.com/maps/api/staticmap
    ?center={{item.coords}}
    &zoom=5&size=400x400&
    key=AIzaSyBAyMH-A99yD5fHQPz7uzqk8glNJYGEqus" />`,
})
export class StaticMap {
  @Input()
  item: Country;
}
<tab-bar [data]="list"(onTabSelect)="select($event)"></tab-bar>
export class TabBar {
    ...
    @Output();
    onTabSelect: EventEmitter<Country>;
    ...
    onClick(tab:Country) {
      this.active = tab;
      this.onTabSelect.emit(tab);
    }
  }
}

Вниз - @Input

Вверх - @Output

  • для обновления можно исполозовать RxJS
  • или что-то похожее
  • или что-то самописное

Языки программирования

Было: ES5, Dart

Cтало: ES6, ES5, Typescript, Dart

Было: ES5, Dart

Cтало: ES6, ES5, Typescript , Dart

У нас появилась опциональная типизация

Зачем мне типизация!?

  • поддержка IDE
  • опыт из других языков
  • ошибки на этапе компиляции

Типизация на уровне фреймворка

Типизированные компоненты

@Component({
  selector: 'countdown-parent-vc',
  template: `<countdown-timer></countdown-timer>`,
})
export class CountdownComponent {
  @ViewChild(CountdownTimerComponent)
  private timerComponent: CountdownTimerComponent;

            ...
}
            

Можно вызывать методы прямо у таймера из кода класса

Angular1 — filters

{{movie.title | uppercase}}

Angular2 — pipes

{{movie.title | uppercase}}

Angular1

<input ng-model="vm.favoriteHero"/>

Angular2 — pipes

<input [(ngModel)]="favoriteHero" />

Angular1

<div ng-class="{active: isActive,
                   shazam: isImportant}">

Angular2

<div [ngClass]="{active: isActive,
                 shazam: isImportant}">
<div [class.active]="isActive">
export class Widget {
    constructor() {
      this.api = new APIWrapper();
    }
}
export class Widget {
    constructor() {
      this.api = new APIWrapper(this.key);
    }
}

Искать по всему коду?

Инверсия управления (Inversion of Control, IoC)

  • Factory pattern
  • Service locator
  • Dependency injection

Сервис локатор

angular.module('myModule', [])
.factory('serviceId', ['depService', function(depService) {
  // ...
}])

Dependency injection

interface API {
    url:string;
}


@Injectable()
class APIImpl implements API {
    url: string;
}
@NgModule({
    providers: [{ provide: API, useClass: APIImpl }],
    bootstrap: [Widget]
})
export class AppModule { }



@Component({
    selector: 'my-heroes',
    providers: [API],
})
export class Widget { }
  • Удобно для тестирования
  • Удобно для разработки

Shadow DOM

Эмуляция Shadow DOM

[_nghost-pmm-5] {
  display: block;
  border: 1px solid black;
}

h3[_ngcontent-pmm-6] {
  background-color: white;
  border: 1px solid #777;
}

Изолирование стилей можно отключить

Формы
Общее мнение

Спасибо за внимание!