Full-stack developer
0,4
рейтинг
12 февраля в 02:50

Разработка → Angular 1.5: Компоненты


Не так давно увидел свет релиз Angular 1.5, который привносит множество интересных нововведений. Важной особенностью

данной версии является то, что это первый из череды релизов, который должен сгладить концептуальный разрыв между Angular1.x и Angular2.x. Для людей, у которых есть необходимость вести проекты на Angular сейчас, но в будущем планируется постепенная миграция на Angular2, это очень радостная новость.

В данной статье я постараюсь осветить основные нововведения:

  • Компоненты!
  • Односторонние биндинги!
  • Мульти-слот трансклюды!

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

UI компоненты


Пожалуй только ленивый не слышал о концепции UI компонентов, так что эту главу можно пропустить. Или вы не слышали? А, слышали но не до конца понимаете? Тогда извиняюсь, поясню суть концепции, а так же ее преимущества.

Под компонентом мы будем подразумевать кастомный элемент, который имеет при себе какое-то дополнительное поведение и шаблон. В качестве примера, вспомним элементы video или audio. Они, конечно, не являются "кастомными" (согласно спецификации W3C) но хорошо передают суть.

Идея далеко не новая и называется она — иерархическая декомпозиция. Ее применяют для снижения сложности оочень давно. И почему бы не применить ее к UI наших WEB приложений? Мы берем UI и делим его на отдельные блоки — компоненты. Каждый компонент в свою очередь состоит из других компонентов. А те — из других, и так пока мы не дойдем до минимальной единицы — стандартных элементов, которые можно воспринимать как компоненты без поведения. Если вам доводилось работать с Qt, GTK или другими GUI фреймворками, там компонентами являются виджеты, так что назвать идею новой никак нельзя.

При выборе границы каждого компонента, мы должны стараться делать их максимально независимыми от контекста использования. В иделе UI компоненту не должно быть дела где он используется, а так же из чего состоят компоненты, которые он использует.

Декомпозиция интерфейса


Насколько нужно дробить компоненты? Это вопрос здравого смысла. Естественно, что заворачивать каждый DOM элемент в компонент глупо, потому на каком-то этапе мы должны остановиться. Все же мы делаем это для удобства, а не просто так. Для примера, давайте возьмем какое-нибудь приложение с простым UI. Вы же знаете о TodoMVC? Давайте попробуем разделить его на компоненты:



Сразу же хочу заметить, что этот вариант разделения не является единственно верным. Потому стоит пояснить, откуда у нас взялись эти компоненты, и почему я решил разделить все именно так.

У нашего приложения можно сходу выделить элементы списка, описывающие отдельные задачи. Там явно прослеживается поведение и оно явно одинаковое. Назовем их todo-item Выделив этот компонент, мы автоматически изолируем все поведение этой части UI в рамках этого компонента. Делать отдельно todo-list не имеет смысла, так как это будет компонент пустышка, зачем зря тратить время. Мы могли бы запихнуть логику фильтрации списка в этот компонент, но это проще делать на уровне сервисного слоя.

Далее мы сходу видим todo-header и todo-footer как шапку и подвал нашего приложения. Компонент todo-header будет отвечать за добавление новых задач в список. Мы конечно могли бы еще чуть раздробить этот компонент и вложить внутрь отдельный компонент, изолирующий логику добавления задач, а todo-header бы отвечал только для оформление. Или еще интереснее — пробросит добавлялку через transclude но… это как-то сложно для такого простого приложения.

todo-footer мы так же не будем дробить дальше, поскольку у нас будут слишком уж маленькие компоненты, и работать с ними будет уже не столь удобно.

У любого дерева компонентов должен быть корень, базовый элемент описывающий весь UI. У нас это todo-app. Он является своего рода точкой входа, описывающей конкретный скрин нашего приложения. Но наши приложения обычно посложнее, и имеют множество скринов, в рамках которых можно выделить дополнительные скрины и т.д. Именно по этой причине у нас есть все эти роутеры и т.д. Но вернемся к этому чуть позже.

Влияние на процесс разработки


Поскольку мы можем изолировать поведение каждого компонента, а так же имея иерархию оных, мы очень легко можем распаралелить разработку на несколько разработчиков. Разработчики смогут делать отдельные самодостаточные компоненты, покрывать их тестами и предоставлять другим членам команды.

Это сильно влияет на гибкость планирования, и фраза "9 женщин не могут родить ребенка за 1 месяц" уже не настолько хорошо описывает процесс разработки нашего приложения. Повышая эффективность наших процессов, мы можем доставлять клиенту больше фич за меньшее время, а это пожалуй самое важное в нашей работе.

Есть так же еще один аспект, который не очень любят обсуждать. Это сегрегация обязанностей между фронтэнд-разработчиками и верстальщиками. Может прозвучать грубо, но намного эффективнее взять парочку дешевых верстальщиков и одного дорогого фронтэнд разработчика, нежели двух дорогих фронтэнд разработчиков. А с учетом того, что штуки вроде БЭМ не вчера появились, добиться модульной верстки не составляет особых проблем. Нужно просто добиться определенного уровня ответственности и взаимопонимания от разработчиков.

Мы так же можем вынести все шаблоны компонентов из JS файлов, и дать верстальщикам возможность работать как можно ближе к реальному месту применения шаблонов, что уменьшает риски, связанные с взаимодействием команды. Причем верстальщиу не нужно особо знать Angular для того, что бы доделать оформление. Или же фронтэнд разработчик может предоставлять верстальщикам уже готовые компоненты с примитивной разметкой и без стилей заранее. А верстальщики уже будут заниматься доводкой.

Так причем тут Angular 1.5?


По своей сути компоненты, это директивы, определяющие новый элемент со своим поведением (изолированным в контроллере) и шаблоном. А как всем известно, у Angular дико переусложенное API директив. Да, оно очень гибкое и позволяет делать много того, что обычно не стоит делать. А так как у нас есть необходимость сохранять обратную совместимость, упростить его не представляется возможным.

Именно по этому для объявления компонентов в Angular 1.5 мы получили новое API:

class MyComponentController {
  // поведение компонента определяет контроллер
}

// вместо фабрики мы используем обычные объекты
const myComponentDefinition = {
    // вместо scope + bindToController: true
    // у нас появился более удобный способ объявлять биндинги
    bindings: {
       'name': '='
    },
    // так же как и для директив, мы можем либо применить шаблон
    // либо воспользоваться `templateUrl`
    // мы так же можем использовать функции
    // для динамического объявления шаблонов
    template: `
       <div class="my-component">{{ $ctrl.name }}</div>
    `,
    // тут примерно так же как и в случае с директивами
    // единственное что `controllerAs` используется всегда
    // в случае если вы явно не прописали элиас для контроллера
    // будет использовано значение `$ctrl`.
    controller: MyComponentController
}

// спецификация HTML5 требует наличия хотя бы одного дефиса 
// в имени кастомных элементов. В нашем случае это my-component
angular.module('app').component('myComponent', myComponentDefinition);

Полное описание API, а так же сравнение с API директив, доступно в документации. Все же полезно туда иногда поглядывать.

Итак, мы теперь можем разделить UI на отдельные компоненты, осталось только разобраться с их состоянием. Что такое состояние компонента? Грубо говоря, это какие-то данные, которые компонент использует для формирования представления, наполнения биндингов и т.д. Откуда компонент берет эти данные? Запрашивает их из сервиса, или же получает через биндинги, или на основе данных из биндингов запрашивает у сервиса. Словом, вариантов куча. Но как лучше?

Stateless vs Stateful компоненты


Ребята из Facebook считают, что компоненты должны быть подобны чистым функциям. А UI в этом случае будет лишь композицией этих функций. То есть формула счастья от Facebook:

UI = сomponents(state)

Что это означает? Это означает, что состояние данных должно получаться извне директив и прокидываться внутрь через биндинги. Таким образом мы делаем компоненты более предсказуемыми. В компоненте верхнего уровня будет все состояние для скрина, он будет прокидывать нужную часть состояния в дочерние компоненты и так далее.

Делая компоненты независимыми от источника состояния, мы развязываем себе руки в том, каким образом мы будем получать и хранить состояние. Мы можем использовать redux, rx.js, можем использовать обычный подход с мутированием состояния, можем кешировать промежуточные данные, словом… нас ограничивает только наша фантазия. В этом изначально и была суть MVC, который придумали в далеком 79-ом году. Полное отделение логики обработки и хранения данных от логики формирования их представления. Сделав это разделение, у нас не будет никаких проблем с тем, что бы независимо менять и то и то. И в качестве бонуса, тестированием каждого отдельного компонента или сервиса становится очень простым.

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

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

Компоненты и маршрутизация


Если рассматривать UI как иерархию компонентов, то каждый скрин будет веткой нашего дерева, и каждый вложенный скрин — еще одним ответвлением и т.д. И в рамках каждой новой ветки мы можем определить корень, и создать для него компонент. Решать же какой именно компонент будет отображаться будет решать наш раутер.

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

Ребята из команды uiRouter попытались решить эту проблему введя вложенные вьюшки, что можно воспринимать как дробление UI на отдельные компоненты, просто не явное. Мы так же можем использовать ресолвы для отделения логики получения данных, а так же можем форсить обновление отдельных вьюшек.

Пойдем дальше! Уберем поведение из контроллера стэйта (по сути уберем контролер), заменим темплейт на один одинешенек компонент, прокинем в него состояние из ресолверов через биндинг атрибутов, и вуаля — все у нас теперь предсказуемо и легко.

Помимо ngRoute и uiRouter так же стоит посмотреть на angular-router, который является адаптацией роутера из angular2 для ветки 1.x. В целом же не стоит забывать что вскоре мир увидет релиз uiRouter 1.0, в котором так же много вкусностей.

$scope не нужен!


Пойдем еще дальше! $scope не нужен! Ну как, в контексте директив, он все еще бывает нужен. В особенности, что бы подчищать за собой. Но в контроллерах/сервисах использовать его не рекомендуется. Мне очень нравится идея добавить правило в eslint, которое будет ругаться на наличие $scope где-то кроме link-функций директив. Вместо использования $scope мы можем использовать старый добрый javascript и биндинг на атрибуты контроллеров.

Это не то что бы что-то новое, возможность биндить значения на атрибуты контроллера это не новость, эта возможность появилась еще в angular 1.3, но так как большинство примеров в документации, а так же статей используют $scope, я думаю было бы неплохо обсудить как можно жить без него.

Вопрос биндинга на атрибуты контроллера мы уже рассмотрели, когда обсуждали API компонентов. Теперь перейдем к остальным кейсам, когда нам очень хочется использовать $scope. Первым из них, пожалуй, будет являться использование системы событий $emit/$broadcast/$on. Просто не используете их. Они не случайно привязаны к иерархии скоупов, и служат именно для нотификации отдельных элементов о том, что что-то произошло. В частности, обычно использование листенеров ограничивается отслеживанием события $destroy, на котором мы должны убирать все, что мы оставили после себя, и что не будет прибито сборщиком мусора. Например хэндлеры ивентов на document.

Использовать события скоупов для организации pub/sub в сервисах, или еще хуже, завязывать какую-то логику приложения на них, это очень плохо. И хоть в оочень редких случаях это может быть полезным, я рекомендую 10 раз подумать прежде чем использовать $scope или $rootScope для реализации системы событий в вашем приложении, лучше воспользоваться отдельными библиотеками предназначенными для этого.

Кто у нас там дальше на очереди? $apply и $digest. Эти методы предоставляют нам возможность синхронизировать состояние после асинхронных операций. Они запускают $digest цикл, который собирает изменения и запускает обработчики. Использовать эти методы нужно только там, где непосредственно происходит асинхронная операция. И обычно у нас уже все это завернуто в сервисы. Делать же что-то эдакое в компонентах, просто неразумно. В крайнем случае используйте сервис $timeout. Если же вы работаете с событиями DOM — то опять же для этого есть директивы, компоненты ничего не должны знать о DOM.

Ну и на сладкое — $watch. Ох как это прекрасно, когда разработчик решает отслеживать изменения состояния в контроллерах, а еще слаже это потом отлаживать. Но как быть, если нам сверху через биндинги может придти обновление данных? Вдруг мы хотим отфильтровать коллекцию, или еще чего специфичного. Ну… давайте подумаем. Значения мэпятся на свойства нашего контроллера. В отличии от $scope, который является частью фреймворка, у нас есть вся власть над нашим контроллером. Продолжая размышлять… у нас же есть геттеры/сеттеры! А это значит, что мы можем точно определить момент, когда наши данные поменялись.

class MyComponent {
    get bindedValue() {
        return this.$value;
    }

    set bindedValue(value) {
       this.$value = value;
       // а теперь вызовем метод, который должен обновить что-то
       // вместо того, что бы вешать неявный ватчер
       this.makeSomethingWithNewValue();
    }
}

angular.module('app').component('myComponent', {
   bindings: {
       "bindedValue": "="
   },
   template: `<div>Какой-то шаблон</div>`,
   controller: MyComponent
});

Вот так вот просто. Собственно именно по этой причине такая интересная концепция как Object.observe была исключена черновиков стандарта. Ну что, все еще думаете что нам так уж нужен $scope?

Односторонние биндинги и изоляция


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

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

class MyComponent {
    constructor() {
        this.myValue = 'take my value'; 
    }
}
angular.module('app').component('myComponent', {
    template: `
        <my-nasty-component passed-value="$ctrl.myValue"></my-nasty-component>
        <p>My Value: {{ $ctrl.myValue }}</p>
    `,
    controller: MyComponent
});

class MyNastyComponent {
    constructor() {
        this.passedValue = 'I\'m touching myself tonight!';
    }
}
angular.module('app').component('myNastyComponent', {
    bindings: {
        passedValue: '='
    },
    template: `<span>Mhahaa!</span>`,
    controller: MyNastyComponent
});

Как вы думаете, какое значение будет выведено? Явно не то что мы хотели. Да, конечно же пример надуманный, но мы можем сделать подобное случайно и потом долго искать виновника. Иногда все же стоит ограничивать наши возможности.

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

bindings: {
    passedValue: '<' // вот так вот
},

И все, теперь когда контроллер нашего вредного компонента меняет значение свойства passedValue, оно не уходит вверх и остается на этом уровне.

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

class MyNastyComponent {
    constructor() {
        this.passedValue.message = 'I\'m touching myself tonight!';
    }
}

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

Итак, мы научились делать декомпозицию UI на отдельные реюзабельные элементы, а так же разобрались с односторонними биндингами. Так же не забудем что нам больше не нужен $scope. Теперь было бы нелпохо это потыкать вживую. Или нет?

Мульти-слот трасклюд


Наши реюзабельные компоненты еще не полностью реюзабельные. Есть еще целый класс компонентов, которые отличаются по своему содержанию, и их можно описать как однотипные "обертки" для разных компонентов. Однако эти обертки так же могут иметь поведение. Давайте рассмотрим пример из material design. Предположим что у нас есть необходимость сделать много однотипных скринов, отличающихся по содержанию, но имеющих общую структуру:



Как бы мы устранили дублирование при помощи компонентов? Мы видим что у нас есть два "слота", в которые бы мы хотели поместить каке-то содержание. Интересно, можно ли поместить компоненты не в шаблоне, а вместе использования компонента? Конечно можно, у нас же есть трансклюды

Для начала вспомним что такое трансклюды (transclude или включения) и вспомним почему они вообще так называтюся. Как говорит нам википедия, включение, это внедрение части, или целого электронного документа в один или более других документов через гипертекстовые ссылки. Сложно, да? Посмотрим картинку:



Надеюсь теперь понятно. Поскольку в ангуляре мы работаем с представлением как с шаблонами, то этот механизм предоставляет нам декларативный способ включения других шаблонов. Для того, что бы компонент мог прокидывать кусок документа в свой шаблон, в определении компонента нам надо указать transclude: true, и затем поместить директиву ngTransclude на нужный элемент. Еще можно воспользоваться transcludeFn, но это уже тянет на отдельную статью.

Однако до недавнего времени, мы могли работать только с одним стотом. С версии angular 1.5 нам стали доступны мульти-слот трансклюды. Они действуют довольно просто. В рамках компонента мы должны вместо true у свойства transclude определить объект, описывающий слоты:

transclude: {
   // при такой записи ангуляр будет искать элемент, 
   // подподающий под заданный селектор 
   // и выкинет ошибку в случае его отсутствия
   slotName: 'elementSelector',
   // но нам не всегда нужно, что бы элемент был обязательным
   // иногда нам нужно дать опциональную возможность переопределить слот
   optionalSlotName: '?optionalElementSelector'
}

Вот так вот, далее же мы можем при помощи директивы ngTransclude разместить элементы наших слотов:

<div class="component">
   <div class="component-main" ng-transclude="slotName"></div>
   <div class="component-main" ng-transclude="optionalSlotName">
      Если вы читаете это, значит для слота <em>optionalSlotName</em> не нашлось элемента.
   </div>
</div>

Еще одной причиной, почему разработчики недолюбливали трансклюды, является проблема производительности. С введением мульти-слот трансклюдов пришлось решить и эту проблему, введя ленивую компиляцию вложенных шаблонов. Все вместе это открывает нам довоьно много возможностей в плане реализации реюзабельных компонентов. Например мы можем реализовать компонент listView, который будет изолировать логику вывода списка элементов. Причем мы можем добавить этому списку поведение, вроде… показывать спиннер пока данные загружаются, или же отображать сообщение о том что данных нет. А за счет мульти-слот трансклюдов мы можем подменять части шаблонов и делать компонент еще более гибким. Спойлер: в конце статьи я дам ссылку на простенькую реализацию такого компонента.

А как же директивы?


Директивы можно расценивать как декораторы для компонентов. Их задача — инкапсулировать дополнительное поведение, и применять их к определенным компонентам. Таким образом мы можем уменьшить специфичность компонентов и добавлять ее декорацией.

Такие директивы как ngRepeat, ngShow или ngIf можно рассмаривать как универсальные декораторы, которые можно применять для любых элементов. Они инкапсулируют DOM операции, и предоставляют декларативный способ управления представлением.

Классическими примерами декораторов для компонентов могут служить директивы для форм, которые добавляют компоненту input дополнительное поведение, в виде специфичной валидации или трансформации значений. Делается это при помощи указания зависимости от другой директивы, указываемой в свойстве require, которая в нашем случае будет компонентом.

В нашем примере с компонентом listView, мы могли бы написать директиву-декоратор, которая бы позволяла при помощи одного атрибута добавить функциональность pull-to-refresh. Или же бесконечный скрол с подгрузкой данных. При всем при этом нам не нужно вносить изменения в сам компонент, что значит что мы сможем проще поддерживать наши решения!

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


Если вы начинающий разработчик, то вам смертельно важно научиться покрывать тестами ваш код. Как минимум для того, что бы иметь возможность продолжать экспериментировать с архитектурой вашего приложения, чистить его, пробовать новое с минимальными рисками того, что вас побьют за регрессии. Для этого так же важно, что бы написание тестов не отнимало много времени и не создавало оверхэд (иначе придут злые менеджеры и будут кричать что вы медленно работаете). А еще тесты должны быть относительно быстрыми, что бы иметь возможность запускать как можно чаще (проблемы решать проще когда вы знаете что изменилось с последнего прогона тестов). Итак, что могут предложить в этом плане компоненты?

Тестировать маленькие вещи проще чем большие, потому декомпозиция на компоненты уже должна упрощать тестирование. Stateless компоненты тестировать еще проще, как как нам не нужно заботиться о внутреннем состоянии компонента. Мы просто создаем экземпляр компонента, пробрасывая необходимое для теста состояние, и все. Но давайте подумаем что еще у нас упрощает жизнь.

Тот факт, что представление в Angular является полностью декларативным, существенно упрщает юнит тестирование UI компонентов. Отдельные директивы, предоставляющие нам строительные блоки для построения декларативного UI, и инкапсулирующие императивную логику по работе с DOM, уже должны быть покрыты тестами, нам нет смысла тестировать их еще раз.

Максимум что нам стоит проверить — так это состояние, которое наш компонент предоставляет шаблонам. Конечно было бы неплохо еще проверить, что бы в шаблоне биндинги были верные, но это проще делать в рамках e2e тестов. В Angular2 с этим будет проще, так как в случае опечаток в выражениях у нас появится возможность ловить ошибки. В ветке 1.x с этим немного грустно.

Итого, тест типичного компонента может быть примерно таким:

describe('my component', () => {
    let component;

    beforeEach(() => {
        component = new MyComponent();
        component.someState = [1, 3, 2];
    });

    it('sorts collection before render', () => {
        expect(component.sortedCollection).toBe.equal([1, 2, 3]);
    });
});

Согласитесь, это очень просто. Никакой специфичной Angular фигни. Только javascript! В этом основное преимущество dirty checking-а между данными и представлением, а не между представлением и DOM, как например в React. Мы полностью отделяем UI от состояния приложения, а такие вещи банально проще тестировать.

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

Светлое будущее?


Конечно же у нас все еще есть проблемы, вроде необходимости в принципе объявлять биндинги на уровне компонента, или же необходимость понимать разницу между аж 4-мя видами изоляции атрибутов скоупов… Но это уже намного лучше, нежели в былые времена. А главное разрыв между Angular 1.5 и Angular2 сводится уже к минимуму. Если вы хотите еще больше сократить этот разрыв, можно воспользоваться различными решениями, почитать ngUpdate. Так же есть масса пакетов, добавляющих декораторы из Angular2 для регистрации компонентов, сервисов и прочего, делая ваши приложения еще ближе к Angular2.

А пример?


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

мой маленький листвью

В примере использованы компоненты, односторонние биндинги, реакция на изменения значений без $scope.$watch а так же мульти-слот трансклюды для стилизации. К сожалению я не нашел в себе сил добавить туда пример использования директив в качестве декораторов, а так же, хоть мне и стыдно, не добавил тестов. Так же не могу сказать что этот пример идеален и готов к использованию в бою. Это просто пример, который должен подтолкнуть вас к новым идеям.

Приятной разработки!
Протько Сергей @Fesor
карма
97,2
рейтинг 0,4
Full-stack developer
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (18)

  • 0
    Вот так вот просто. Собственно именно по этой причине такая интересная концепция как Object.observe была исключена черновиков стандарта. Ну что, все еще думаете что нам так уж нужен $scope?
    Фактически вы убрали реактивное взаимодействие, заменив его на интерактивное. Это работает в простейших линейных случаях: делаем то, потом это, а затем ещё кое что. Но сложность экспоненциально растёт с каждым ветвлением логики. Статья про атомы иллюстрирует этот процесс. Реактивное программирование — это ж чуть ли не единственное достоинство ангуляра, по сравнению с остальными фреймворками. Зачем вы так жестоко с ним? :-)

    Как вы думаете, какое значение будет выведено? Явно не то что мы хотели.
    Как раз именно то. Не понимаю, почему вы хотите тут что-то другое. Другое дело, что инициатором любого биндинга должен быть внешний компонент, а не внутренний. И именно эту проблему надо решать, а не вводить сомнительные односторонние биндинги, которые приводят лишь к такого рода тавтологиям: on-item-select="$ctrl.selectedItem = selectedItem" selected-item="$ctrl.selectedItem"

    Мульти-слот трасклюд
    Ну каконец-то. Хотя, в итоге рендерится слишком много элементов. Если мне надо поместить в слот другой компонент, то у меня будет 1 элемент для слота, 1 элемент для параметра и только внутри него будет вложенный компонент. И эти промежуточные элементы придётся дополнительно стилизовать, чтобы прокидывать ширину и высоту.

    Только javascript! В этом основное преимущество dirty checking-а между данными и представлением, а не между представлением и DOM, как например в React. Мы полностью отделяем UI от состояния приложения, а такие вещи банально проще тестировать.
    Это преимущество реактивного программирования, а не грязных проверок. У грязных проверок одно преимущество — простой код. Но они крайне не эффективны. Но вообще говоря, декларативно разделять надо не только view-model и dom, но и model и view-model.
    • +1
      Реактивное программирование — это ж чуть ли не единственное достоинство ангуляра, по сравнению с остальными фреймворками.


      Так, я может чего-то не понимаю, но с каких пор в ангуляре есть реактивное программирование? Мы в каком-то из топиков уже это обсуждали и выяснили, что для FRP ангуляр не очень подходит. В Angular2 для реактивщины используется rx.js. Но ладно, даже если допустить что под «реактивностью» мы тут подразумеваем реакцию на изменение данных, то ничего же не поменялось.

      Просто вместо двух ватчеров (один создает ангуляр при изоляции скоупов и другой — вы, когда хотите отслеживать изменения внутри директивы) мы теперь используем явный вызов метода при изменении свойства, и остается один ватчер. Инициатором вызова этой функции остается ангуляр, и вызываться он будет исключительно по изменению значения.

      Как раз именно то. Не понимаю, почему вы хотите тут что-то другое.

      Ну вы дальше в том же абзаце и написали, почему я хочу что-то другое. Компонент верхнего уровня должен спокойно передавать часть состояния вниз и не беспокоиться что его поменяют. Меня не должно парить что происходит внутри компонентов, которые я использую.

      которые приводят лишь к такого рода тавтологиям


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

      И эти промежуточные элементы придётся дополнительно стилизовать, чтобы прокидывать ширину и высоту.


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

      Это преимущество реактивного программирования, а не грязных проверок.

      Вот опять… я уже начинаю сомневаться в своем понимании «реактивного программирования». Это же просто биндинги, MVVM и т.д. FRP тут причем?
      • 0
        Так, я может чего-то не понимаю, но с каких пор в ангуляре есть реактивное программирование? Мы в каком-то из топиков уже это обсуждали и выяснили, что для FRP ангуляр не очень подходит.
        Ну, он там корявый, но всё же есть.

        В Angular2 для реактивщины используется rx.js.
        Это даже хуже вотчеров в плане поддержки :-)

        Просто вместо двух ватчеров (один создает ангуляр при изоляции скоупов и другой — вы, когда хотите отслеживать изменения внутри директивы) мы теперь используем явный вызов метода при изменении свойства, и остается один ватчер. Инициатором вызова этой функции остается ангуляр, и вызываться он будет исключительно по изменению значения.
        Представьте себе ситуацию: у вас 3 свойства А1, А2, А3, каждое из которых зависит от каждого (или почти каждого) из 3 других свойств Б1, Б2, Б3. Когда устанавливается значение А1, вам нужно пройтись по всем зависимым свойствам и сказать им обновиться. Поддерживать эти «обратные зависимости» приходится вручную, что неизбежно приводит к ошибкам и неэффективности. Именно с этой проблемой и борется реактивная архитектура. Только в случае FRP (knockout, атомы) зависимость прямая (от зависимого к зависимостям), а в случае стримов (rx.js, bacon.js) обратная (от зависимости к зависимым).

        Ну вы дальше в том же абзаце и написали, почему я хочу что-то другое. Компонент верхнего уровня должен спокойно передавать часть состояния вниз и не беспокоиться что его поменяют. Меня не должно парить что происходит внутри компонентов, которые я использую.
        Только корень проблемы не в двустороннем биндинге, а в месте его объявления. А то, что вложенный компонент _может_ использовать односторонний биндинг, вместо двустороннего, это не решает проблему того, что он _может_ использовать и двусторонний биндинг и напортачить не у себя. К тому же, он с тем же успехом _может_ и не изменять значение, которое пришло ему извне, оставаясь в рамках старого доброго двустороннего биндинга.

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

        Ну это не особо большая проблема. В этом даже можно найти свои плюсы, в том плане, что это более гибкий вариант для стилизации.
        Это очень большая проблема, потому как механизмы лейаута в хтмл далеки от идеала и нельзя просто вставить обёртку ничего не сломав. Типичные проблемы: не работающий height:100%, разваливающийся flexbox, чем больше элементов, тем менее отзывчивый интерфейс получается (типичный пример — эксельчик).
        • 0
          Ну, он там корявый, но всё же есть.


          Ну если так рассуждать, то FRP есть везде где есть биндинги и обзерверы, что уже смахивает на чушь.

          Это даже хуже вотчеров в плане поддержки :-)

          Аргументируйте. Ну и опять же rx.js не залязит в компоненты (обычно, хотя у меня в списке «попробовать» еще и это значится). Мне очень понравилась реализация работы с HTTP в Angular2 с использованием Observable.

          Представьте себе ситуацию: у вас 3 свойства А1, А2, А3, каждое из которых зависит от каждого (или почти каждого) из 3 других свойств Б1, Б2, Б3.


          Чем сеттеры отличаются от $watch? ваша задача решается либо через deep watch, либо кастомные ватчеры, либо через 5 отдельных ватчеров которые запускают именно реакцию.

          const a1 = Symbol('a1'); 
          // ...
          class MyComponent {
              set A1 (val) {
                  // ...
                  this[a2] = calcA2(this[a1]);
              }
              set A2 (val) {
                  // ...
                  this[a3] = calcA3(this[a2]);
              }
          
              // ну или можно было бы одну функцию описывающую переходы замутить
          }
          


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

          Меня смущает такая избыточность кода на ровном месте.

          А вы выпишите на листик юзкейсы, где вы не хотели бы что бы компонент за нас решал что выделено а что нет. У меня вот есть несколько проектов, где использование двустороннего биндинга и решения «что выделять» вылилось бы в проблемы, и там я как-раз таки использую прмерно схожий подход, и все хорошо.

          А избыточным это кажется, потому что это пример. Я очень надеюсь что в конце месяца у меня найдется время, что бы выложить на гитхаб кое-какие компоненты, которые я использую для админок. List-view — один из них, и он в реальности чуть-чуть по интереснее.

          типичный пример — эксельчик


          Судя по вашим комментариям в каждом топике, сказывается специфика вашей работы. У вас эксельчики — это типичные примеры. И там да, нужны и FRP и прочие вещи. Но это далеко не «типичные» задачи для большинства разработчиков. Мне кажется что именно это является основной причиной, по которой нам так сложно достичь взаимопонимания.
  • –1
    Сравним реализуцию компонент на Ангуляре и на Сферическом Фреймворке в Вакууме…

    Объявление компоненты

    Ангуляр (скрипт + вёрстка + конфиг):
    angular.module( 'app' ).component( 'myPanel' , {
    	transclude : {
    		myPanelHead : '?head',
    		myPanelBody : 'body'
    	},
    	template: `
    		<div class="my-panel">
    			<div class="my-panel-header" ng-transclude="head"></div>
    			<div class="my-panel-bodier" ng-transclude="body">No data</div>
    		</div>
    	`
    } )
    

    Сферический Фреймворк (ничего лишнего, только декларативное описание):
    $my_panel : $mol_block child
    	< header : $mol_block child < head : null
    	< bodier : $mol_block child < body =No data
    


    Использование компоненты

    Ангуляр (куча тэгов, разделение на приложение и компоненту):
    <body ng-app="app">
    	<my-panel>
    		<my-panel-head>My tasks</my-panel-header>
    		<my-panel-body>
    			<my-task-list
    				assignee="me"
    				status="todo"
    			/>
    		</my-panel-body>
    	</my-panel>
    </body>
    

    Сферический Фреймворк (ничего лишнего, приложение — тоже компонента):
    $my_app : $my_panel 
    	head : =My tasks
    	body : $my_task_list
    		assignee : =me
    		status : =todo
    


    Кастомизация компоненты

    Ангуляр (нет кастомизации, только копипаста):
    angular.module( 'app' ).component( 'myPanelExt' , {
    	transclude : {
    		myPanelHead : '?head',
    		myPanelBody : 'body',
    		myPanelFoot : '?foot'
    	},
    	template: `
    		<div class="my-panel">
    			<div class="my-panel-header" ng-transclude="head"></div>
    			<div class="my-panel-bodier" ng-transclude="body">No data</div>
    			<div class="my-panel-header" ng-transclude="foot"></div>
    		</div>
    	`
    } )
    

    Сферический Фреймворк (наследуемся и подстраиваем под себя):
    $my_panelExt : $my_panel child
    	< header
    	< bodier
    	< footer : $mol_block child < foot : null
    
    • +1
      Мы уже как-то в одном из топиков спорили на тему «читабельности» и экономической эффективности. Вы привели довод мол, что вы можете объяснить новичку идеологию вашего фреймворка и он может быстро приступать к работе. Но как на счет таких случаев:

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

      В целом же людей, которые могут написать вменяемую систему с использованием таких подходов не особо то и много. Достаточно посмотреть на количество вакансий и зарплаты хаскель/кложа программистов.

      Ваш «сферический в вакууме» фреймворк не будет мэйнстримом, во всяком случае ближайшие лет пять, пока индустрия не подтянется к этому уровню.
      • –1
        — Вас банально не будет рядом, что бы объяснить новому человеку на проекте, как с этим всем жить.

        Значит будет кто-то другой, кто поможет разобраться. Ну и документацию, разумеется, почитать не помешает.

        — У проекта не такой большой бюджет что бы нанимать кого-то вашего уровня.

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

        — Опять же отсутствие возможности сегрегации обязанностей между людьми.
        Наоборот, декларативное описание компонент позволяет разделить зоны ответственности верстальщика и программиста. А хорошая компонентная модель позволяет одного верстальщика разделить на несколько, как и программиста.

        В целом же людей, которые могут написать вменяемую систему с использованием таких подходов не особо то и много. Достаточно посмотреть на количество вакансий и зарплаты хаскель/кложа программистов.
        Это не какой-то новый язык программирования, это ближе к haml-подобному шаблонизатору. А вот логика описывается одноимёнными классами на тайпскрипте.

        Ваш «сферический в вакууме» фреймворк не будет мэйнстримом, во всяком случае ближайшие лет пять, пока индустрия не подтянется к этому уровню.
        Скорее пока какая-нибудь компания не начнёт его продвигать. К сожалению, современная индустрия очень тенденциозна. Сейчас все вакансии по фронтенду пестряд Ангуляром. Но уже появились и первые ласточки, которые наелись ангуляра и ищут что-то другое. Обычно этим другим сейчас выступает Реакт, на котором хоть компоненты можно делать полноценные.
  • 0
    Использовать события скоупов для организации pub/sub в сервисах, или еще хуже, завязывать какую-то логику приложения на них, это очень плохо.

    Почему?
    • 0
      Это внутренний механизм ангуляра, который предназначен только для того, что бы обеспечить уведомление директив о их жизненном цикле. В рамках сервисного слоя намного удобнее использовать обычный диспатчер событий. А в рамках компонентов вам просто не нужны события. Конечно есть и исключения, потому я и написал что если очень хочется то можно, просто лучше «10 раз подумать».
  • 0
    Angular Component Router, который во втором ангуляре (но также доступен и в первой версии, где он пока что не слишком стабилен), работает с компонентами. Получается чтобы "по канонам" сварганить страницу нужно как минимум 3 сущности?

    • внешний компонент который будет взаимодействовать с Router
    • затем директива которая будет обеспечивать некоторую логику получения данных и тд
    • затем как миниуму один компонент который будет это логику "рисовать"
    • 0
      Формально за все это отвечает один компоннет, просто он может состоять из других компонентов. В целом же они собираются добавить что-то типа ресолверов, и тогда логику по получению данных можно будет вынести наверх.
      • 0
        Нужно больше абстракций :-)
        Кстати, ждать ли критику Сферического Фреймворка или всё настолько идеально, что и придраться не к чему?
        • 0
          Нужно больше абстракций :-)

          Тут нет "абстракций", тут есть "разделение обязанностей". Для апликачек явный поток данных сверху вниз очень упрощает дело (именно по этому я использую ресолверы для получения данных из сервисов, и сервисы для мутации состояния). По сути для счастья нам надо только сервис, ресолвер и компонент.

          Кстати, ждать ли критику Сферического Фреймворка или всё настолько идеально, что и придраться не к чему?

          Для объективной критики мне надо побольше времени его посмотреть, и время на это у меня будет только в пятницу, к сожалению. Пока же субъективная оценка превалирует просто по стилю ML и т.д.
          • 0
            Но можно и проще: компоненты, сервисы и инъекция зависимостей.
            • 0
              Ресолвер это тот же сервис, просто уточнение его зоны ответственности. Так что да.
  • 0
    Какие есть механизмы управления компонентом из вне (из контроллера), например у меня есть несколько компонентов «медиа-проигрыватель», с методами старт/пауза/стоп, Ангуляр 2 (1.5) предлагает какие-нибудь официальные/правильные подходы для этого?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.