habrahabr

Нетрадиционный обзор AngularJS

  • пятница, 6 февраля 2015 г. в 02:12:37
http://habrahabr.ru/company/hexlet/blog/249793/

Привет, Хабр!

Наш прошлый перевод нетрадиционного обзора React многим понравился, и, конечно, люди стали сравнивать Реакт с популярным AngularJS. Сегодня мы публикуем перевод статьи «An Unconventional Review of AngularJS» от того же автора (Джеймса Шора, ведущего проекта Let’s Code: Test-Driven JavaScript). Поклонникам Angular просьба сохранять спокойствие.



AngularJS это все, что я ожидаю от фреймворка. И это не хорошо.

В ноябре, декабре и январе я обозревал AngularJS для серии «front-end frameworks» в рамках проекта Let’s Code JavaScript. Суммарно я провел 40 часов изучая, программируя и решая задачи. Как обычно, моей целью было изучить AngularJS создавая приложение.

Angular это, наверное, самый популярный фронт-энд фреймворк сейчас. Его разрабатывает команда из Google, что сразу внушает доверие. Он настолько популярен, что входит в акроним. Angular это часть так называемого стека «MEAN»: MongoDB, Express, AngularJS, Node.JS. Самая что ни на есть передовая технология.

Angular описывает себя как инструмент для улучшения ХТМЛ. Он позволяет расширить ХТМЛ новыми определениями — директивами — которые превращают статичный ХТМЛ-документ в динамический шаблон. Директивы могут быть атрибутами или тегами (или даже комментариями или классами, но это уже не совсем обычная история), и они превращают статичный ХТМЛ-документ во что-то живое и дышащее, на первый взгляд без добавления JavaScript.

Лучший пример это знаменитая двухсторонняя привязка данных (two-way binding). ХТМЛ-шаблон может содержать переменные, как большинство языков шаблонирования, но в случае с Angular ваша страница автоматически обновляется при каждом обновлении переменной.

Например, в приложении, которое я писал для обзора, есть таблица, которая изменяется при изменении полей конфигурации. Вот фрагмент кода, который рендерит строку таблицы. Заметьте, тут нет никакой работы с событиями или мониторинга состояния… Просто шаблон, который описывает ячейки в строке. Angular делает все автоматически.

// Copyright (c) 2014-2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
(function() {
  "use strict";

  var StockMarketCell = require("./stock_market_cell.js");

  var stockMarketRow = module.exports = angular.module("stockMarketRow", [StockMarketCell.name]);

  stockMarketRow.directive("stockMarketRow", function() {
    return {
      restrict: "A",
      transclude: false,
      scope: {
        value: "="
      },
      template:
        '<tr>' +
          '<td stock-market-cell value="value.year()"></td>' +
          '<td stock-market-cell value="value.startingBalance()"></td>' +
          '<td stock-market-cell value="value.startingCostBasis()"></td>' +
          '<td stock-market-cell value="value.totalSellOrders().flipSign()"></td>' +
          '<td stock-market-cell value="value.capitalGainsTaxIncurred().flipSign()"></td>' +
          '<td stock-market-cell value="value.growth()"></td>' +
          '<td stock-market-cell value="value.endingBalance()"></td>' +
        '</tr>',
      replace: true
    };
  });
})();

Магия.

С такими примерами легко понять, почему Angular стал таким популярным. С ним тяжелые проблемы кажутся тривиальными. Но выдержит ли он проверку временем?

Нетрадиционный обзор


Слишком много фреймворков заманивают в ловушку: с ними легко начать, что очень круто, но в последствии очень сложно поддерживать и расширять свой код. Это уже не очень круто.

Так что когда я обозреваю фреймворк, я не смотрю на обычные критерии производительности, популярности или размера. Нет, я хочу знать ответ на один важный вопрос:

В течение следующих 5-10+ лет, когда я буду поддерживать свой продукт, этот код принесет больше пользы или страданий?

Большинство фреймворков сделаны таким образом, что они экономят время на первичное создание продукта. Но эти затраты тривиальны по сравнению со стоимостью поддержки вашего приложения в на протяжении нескольких лет. Перед тем как рекомендовать фреймворк, я должен знать, что он выдержит проверку временем. Будет ли он расти и изменяться вместе со мной? Или я окажусь в оковах еле-пригодного для работы легаси через три года?

Я смотрю на пять распространенных опасностей.

1. Замкнутость (Lock-in). Когда я решу перейти на новый или более лучший фреймворк (или библиотеку), насколько сложно будет переключиться?

2. Жёсткая архитектура (Opinionated Architecture). Могу ли я решать задачи так, как нужно моему приложению, или я должен повиноваться неким идеям, разработанным авторами фреймворка?

3. Побочная сложность (Accidental Complexity). Я трачу время на решение своей проблемы или борюсь с фреймворком?

4. Тестирование (Testability). Могу ли я просто тестировать свой код, используя стандартные инструменты и без лишней мороки с mock-объектами?

5. Рендер на сервере (Server-Side Rendering). Будут ли люди ждать выполнения JavaScript чтобы увидеть что-нибудь полезное? Придется ли танцевать с бубном чтобы заставить поисковики индексировать мой сайт?

Я оценил Angular в каждой категории с помощью a ☺ (уиии!), ☹ (буэээ!), or ⚇ (ни туда, ни сюда).

1. Замкнутость — ☹ (буэээ!)


Без вопросов: Angular запирает вас в свои рамки. Вы задаете интерфейс специфичными Angular-директивами, в специфичных Angular-шаблонах, используя специфичные Angular-определения и код. Нет способа абстрагироваться от него. Если решите переключиться — будете все переписывать с нуля.

Это обычная практика. Настолько обычная, что она обычно дает повод поставить meh-рожицу «ни туда ни сюда». Но Angular постарался заработать именно «буэээ».

Во-первых, Angular хочет быть хозяином всего вашего клиентского кода. Если вы пишите приложение с Angular, это означает что вы должны использовать специфичные для Angular валидации, писать бизнес-логику в виде специфичных для Angular сервисов, и соединяться с бэк-эндом через специфичные для Angular сервисы.

Во-вторых, команда Angular дает нам понять, что стоимость поддержки для них не приоритет. В Angular 1.3 пропала поддержка IE 8. Angular 2 сильно переписали, забыв про несколько ключевых концепций текущей версии. Это скорее всего будет причиной переписать все приложение.

Повторюсь: весь ваш фронт-энд заперт, и даже если ничего не менять, то скорее всего придется переписывать что-нибудь. Переписывать это ужасная идея. Вы потратите кучу денег и времени чтобы повторить то, что у вас уже есть. Фреймворк, в roadmap которого входит идея «переписать приложение» — неприемлем.

2. Жёсткая архитектура — ⚇ (ни туда, ни сюда)


Angular хочет чтобы вы строили свое приложение в определенном формате, но делает это не очень явно. Это скорее «пассивно-агрессивная архитектура».

Жёсткая архитектура это хорошая идея в краткосрочной перспективе, плохая в долгосрочной. В краткосрочной перспективе жесткий фреймворк позволяет начать быстро, не задумываясь о структуре приложения. Но в долгосрочной перспективе слишком жесткая архитектура ограничивает возможности. С ростом ваших требований, стандарты фреймворка начинают мешать вам все больше и больше.

Пассивно-агрессивная архитектура Angular включает худшее из обоих миров. Она делает допущения о дизайне вашего приложения, но она не помогает вам понять эти допущения. Я не уверен что понимаю это даже сейчас, но вот что мне удалось выудить:

«На фундаментальном уровне, Angular считает, что вы используете stateless „сервис“-объекты для логики и простые объекты для структур данных (объекты без методов) для сохранения состояния. Сервисы по сути — глобальные переменные; большинство функций могут использовать любой сервис, обращаясь к ним по особому имени. Структуры данных хранятся в $scope, они связаны с шаблонами и директивами. Объекты структур данных управляются „контроллерами“ („клей“, связанный с шаблонами и директивами) и сервисами.»

Я не фанат такой архитектуры. Отделяя состояние от логики, Angular ломает инкапсуляцию и разделяет тесно связанные концепции. Вместо того, чтобы поместить логику рядом с данными, с которыми она и работает, Angular хочет чтобы вы разнесли логику по всему приложению. Появляется риск «оперирования жертвы дробовика»: любое изменение выливается в кучу мелких исправлений.

Приложение из туториала демонстрирует эту проблему. Приложение выводит на экран список смартфонов, и объекты «телефон» являются ключевой концепцией. В идеале, изменение внутренней структуры объекта-телефона не должно ни на что влиять. Но они просто объекты с данными, так что их изменение повлечет за собой изменения в других частях приложения: шаблон вывода списка телефонов, шаблон вывода информации о телефоне, и оба контроллера для этих шаблонов.

Я предпочитаю rich domain объекты, которые содержат в себе состояние и бизнес-логику. Так я могу делать изменения ничего не ломая. Для моего приложения-примера я использовал rich domain слой, который полагается на неизменяемый объект со значением. Пассивно-агрессивная архитектура Angular не поддерживает этот подход — иногда приходилось деформировать мой код чтобы удовлетворить допущения Angular’а, но это не было совсем уж невозможной задачей. Могло бы быть хуже.

3. Побочная сложность — ☹ (буэээ!)


Angular знаменит своей крутой кривой обучения и плохой документацией. Мне кажется это симптомы более крупной проблемы. Это не проблема документации, это проблема Angular. У него просто плохой дизайн. Вот лишь несколько недостатков что я заметил:

Дырявые абстракции. Чтобы использовать Angular в нетривиальном проекте, вам придется влезть глубоко и понять как он работает под капотом. Вам придется понять области видимости и как они работают в рамках прототипного наследования: digest loop; $watch, $watchCollection, и $apply; и еще кучу других штук.

Волшебные строки это попытка обойти плохое единство дизайна. Зачастую у вас будут появляться куски кода, которые связаны между собой, но находятся в разных файлах.

Непонятные символы везде. В Angular есть несколько крохотных языков, которые придется вставлять в строки в своем приложении. Готовьтесь изучать разницу между «=», «&», «=*», и «@»; «E», «A», и «EA»; оператором «|»; и другими иероглифами.

Неуловимые несовместимые разногласия. Проблемы можно решать несколькими путями, каждый путь имеет мелкие, но важные несовместимости. Например, то, как вы создадите контроллер будет влиять на синтаксис в шаблоне и на способ хранения переменных в $scope.

Склонность к тихому фейлу. Легко ошибиться, сделать что-то не так и не получить никакой индикации причины. Написал «Е» там, где стоило написать «А»? Приложение просто перестало работать.

Когда я писал такое же приложение в первые раз на Реакте, мне понадобилось 28¾ часов. То же самое приложение на Angular я писал 39½ часа, не смотря на то, что я мог использовать часть кода из первой попытки на Реакте. Десять часов сверху. Причина — излишняя сложность Angular.

4. Тестирование — ⚇ (ни туда, ни сюда)


Angular считает тестирование очень важным. Одна из его главных фич — внедрение зависимостей (dependency injection) сделана специально для упрощения тестирования.

Учитывая этот фокус, я был удивлен тем, как все плохо с тестированием в Angular. Оно акцентирует внимание на логике тестирования в контроллерах и сервисах, но не дает почти ничего для тестирования поведения пользовательского интерфейса. Нет поддержки симуляции событий браузера и нет никакой возможности юнит-тестировать ХТМЛ-шаблоны. Кастомные директивы можно тестировать, но тестировать директиву, внутри которой есть другая директива — страшно.

Angular сфокусирован на возможности юнит-тестирования бизнес-логики. Но причина лишь в том, что сама архитектура провоцирует помещать бизнес-логику в UI (в частности, в контроллеры и сервисы). Хорошая архитектура помещала бы бизнес-логику в независимые от UI объекты.

Многие аспекты Angular ощущаются как пластыри поверх нанесенных самому себе порезов.

Убрав бизнес-логику, как это было в случае с моим приложением, остается лишь тестировать как Angular рендерит ХТМЛ и реагирует на события, и Angular не поддержал моего желания сделать это. Команда Angular советует использовать специально сделанный ими end-to-end фреймворк для тестирования Protractor.

End-to-end тесты медленные и хрупкие. Их количество нужно минимизировать, а не рассчитывать на них как на центральную идею стратегии тестирования. К счастью, поместив UI моего приложения в кастомные директивы, я получил возможность юнит-тестировать Angular. Так что он тут заработал нейтральную морду «ни туда ни сюда», но если приглядеться, то можно увидеть на ней одну маленькую слезу.

5. Рендер на сервере — ☹ (буэээ!)


AngularJS не рассчитан на работу на сервере. Никаких сюрпризов, но стоит это учесть.

Итог: избегайте его.

Работа с Angular — мучение. Каждый шаг это новая особенность или испытание, и к концу моего обзора он меня реально достал. Возможно, если бы я делал все, как хочет Angular, а не пытался внедрить свой дизайн, мне было бы легче, но моей целью было изучить возможности поддержки проекта в долгосрочной перспективе, а не сделать все как можно быстрее.

И в этом отношении все плохо. Angular это сложный фреймворк, и он вырос во что-то странное. Он популярен, но не хорош, и я подозреваю, что он быстро забудется когда появятся более хорошие альтернативы. Angular 2 уже на горизонте, так что принимая Angular сегодня, вы приговариваете себя (скорее всего) к переписыванию приложения через пару лет. И хотя будущие версии могут исправить эти недостатки, сегодняшний Angular это плохой выбор. Избегайте его.