Одним из самых ожидаемых событий 2015 года, для фронт-енд разработчиков, помимо выхода финальной спецификации ES6, является появление новой версии одного из самых популярных фреймворков, AngularJS. Анонсированные изменения, настолько значительны, что существует мнение о том, что это по сути новый фреймворк написанный с нуля.
В связи с этим, я позволю себе представить вам перевод большой статьи “All About Angular 2.0”, одного из разработчиков фреймворка Роба Ейзенберга, в которой он раскрывает что ждет нас в следующей версии.
Все, кому это может быть интересным, добро пожаловать под кат. И запаситесь временем… :-)
Предпосылки для возникновения Angular 2.0
AtScript
Внедрение зависимостей (Dependency Injection, DI)
Шаблонизация и связывание данных (Templating and Databinding)
Маршрутизатор (The Router)
Преамбула
Хотите понять как стратегия будет реализована в Angular 2.0? Вы в нужном месте. В следующей статье я объясню основные возможности Angular 2.0 включая мотивацию которая стоит за этими изменениями. По мере чтения, я постараюсь раскрыть свою точку зрения и идеи о процессе создания Angular 2.0, включая важные области в которых, как я думаю, все еще необходимы улучшения.
Angular 1.3
Прежде чем начать говорить о будущем AngularJS, давайте на минутку посмотрим на его текущую версию. Angular 1.3 является лучшей версией AngularJS доступной на данный момент. И она была выпущена в релиз совсем недавно. С множеством багфиксов, расширением возможностей и увеличением производительности. Это один из сильных и зрелых фреймворков доступных сегодня.
Но у вас вероятно имеется множество вопросов о будущем AngularJS. Когда выйдет версия 2.0? Что случится с 1.х? Будет ли возможно миграция с 1.х на 2.0?
Команда Angular работает гораздо лучше отвечая на подобные вопросы и вы можете помочь им в этом.
Могу вам сказать что более 1600 проектов в Google были построены с использованием Angular. Как вы можете видеть Google много инвестировал в текущую версию и в любом случае будет вынужден поддерживать ее. Отвечая на вопросы на ngEurope Brad Green сказал что после выхода релиза Angular 2.0 можно ожидать что Angular 1.3 будет поддерживаться еще в течении 1.5 — 2 лет. Мы так же предприняли соответствующие изменения в команде и руководстве, направленные на поддержку Angular 1.3. И поэтому даже несмотря на то что мы интенсивно работаем над Angular 2.0, остается команда преданная Angular 1.3. Этой командой руководит Pete Bacon Darwin, которого, я уверен вы знаете по его значительному вкладу в AngularJS. Я бы хотел воодушевить вас на обращение к руководителям проекта, с просьбой о более значительных действий в этом направлении, включая официальную поддержку старой версии.
Тоже самое касается вопроса миграции с Angular 1.х на Angular 2.0. Я думаю мы должны и будем работать в этой области. Если это так же важно для вас, дайте об этом знать. Команде AngularJS должна знать насколько это важно для вас, что бы они думали об этом и планировали все наперед.
Мотивация для возникновения Angular 2.0
Возможно вы думаете: — А зачем делать Angular 2.0 вообще? Зачем сразу прыгать к 2.0 и вносить столько ключевых изменений? Не является ли это капризом? Ну можно еще понять небольшие изменения, но к чему все эти принципиальные изменения в Angular 2.0. Это оправдано? Оно того стоит?
Я бы хотел остановиться на нескольких моментах, прежде чем погружусь в детали. Я надеюсь это предоставит основу понимания последующих деталей и базу для осмысленной критики (часть из которой я намерен предложить сам).
Производительность
Когда AngularJS был впервые создан, более 5 лет назад, он по сути не был предназначен для программистов. Это был инструмент больше нацеленный на верстальщиков, которые нуждались в быстром создании HTML форм. В течении времени произошли изменения основанные на различных требованиях и разработчики взяв это инструмент стали создавать все более и более сложные приложения. Команда Angular 1.х много работала над изменениями позволяющими удовлетворять потребности современных вебприложений. Однако существует ряд ограничений влияющий на возможные улучшения. Часть этих ограничений связаны с производительностью и являются результатом используемой модели связывания данных и инфраструктуры шаблонов. Для решения данных проблем требуется совершенно новый подход.
Изменения в Интернете
За прошедшие пять лет, с момента выхода AngularJS, в интернете произошли значительные изменения. Например 5 лет назад было возможно создать приличный кросс-браузерный сайт без помощи jQuery. Однако сегодняшние браузеры не только интенсивно вносят различные изменения в DOM, но и часто эти изменения очень актуальных для фреймворков.
и интернет продолжает меняться…
Все те массовые изменения произошедшие за последние несколько лет, только бледная тень того что нас ждет в ближайшие 1-3 года. В течении нескольких месяцев ES6 будет завершен. И есть все основания думать что в браузеры в 2015 году обеспечат его полную поддержку. Текущие браузеры уже поддерживают некоторые функции такие как модули, классы, лямбды, генераторы и т.д. Эти возможности фундаментально изменят опыт программирования на JavaScript. Но большие изменения не ограничены только JavaScript. На горизонте Web Components. Это термин обычно связан с коллекцией следующих W3C спецификаций:
— Custom Elements — возможность расширять HTML настраиваемыми тегами.
— HTML Imports — возможность упаковки различных ресурсов (HTML, JS, CSS...).
— Template Element — возможность вставки инертного HTML в документ.
— Shadow DOM — возможность инкапсуляции DOM и CSS.
Комбинация этих четырех возможностей позволяет разработчикам создавать декларативные компоненты (Custom Elements) которые полностью инкапсулируются (Shadow DOM). Эти компоненты могут описывать их собственные виды (Template Element) и могут легко паковаться для передачи другим разработчикам (HTML Imports). Когда спецификации будут поддерживаться всеми основными бразуреми, мы вероятно увидим взрыв креативных решений в области создания собственных компонентов, решающих основные проблемы и недостатки в стандартном HTML (взято из внутренней документации проекта Databinding whit Web Components).
Это уже сегодня возможно в Chrome, и другие браузеры так же внедряют эти изменения. Отличное будущие, не правда ли?
Здесь есть только одна проблема: большинство из сегодняшних фреймворков не готовы к этому, включая Angular 1.х. Большинство фреймворков имеют систему связывания данных основанную на небольшом количестве HTML элементов с хорошо известными эвентами и поведением. AngularJS должен позволить разработчикам использовать все преимущества Web Components, а для этого требуется новая реализация связывания данных (databinding).
Мобильные устройства
Говоря о пяти прошедших пяти годах… боже мой, насколько изменился компьютерный пейзаж. Мобильные телефоны и планшеты повсюду! Несмотря на то что AngularJS может использоваться для создания мобильных аппликаций, он не создавался с этой целью. Это включает в себя все, начиная от проблем с производительностью, недостающие возможностью маршрутизации, отсутствия кеширования предкомпилированных видов и даже не блещущая поддержка эвентов основанных на прикосновениях.
Простота использования
Давайте будем честными… AngularJS не самая легкая вещь в изучении. Да когда вы начинаете вы думаете: Да это прекрасно. Это реально легко и волшебно!!!" Потом вы начинаете создавать свою аппликацию и говорите: «Боже… что это!!?? Я не понимаю!». Я слышу эту историю снова и снова. Даже есть забавный график иллюстрирующий это.
Большинство из сложностей произрастает из оригинального дизайна фреймворка. Первоначально, не было кастомных директив. Для их реализации писали хардкод. И уже позже, они появились в API. Первоначально не было контроллеров, потом… ну в общем взгляните на график.
Итог
Что бы привести AngularJS в соответствие с текущим положением дел в интернете, стало абсолютно понятно что необходимо его принципиальное изменение. Фактически, без этих изменений, AngularJS рискует стать устаревшим в течении года. И именно поэтому команда AngularJS отвечая на вызовы времени, создает новую версию Angular. Речь идет, по существу, о переосмыслении AngularJS с позиций современно интернета.
Примечание
Даже несмотря на то что я не могу раскрыть всех деталей, я могу определенно сказать что Angular 2.0 сильно отличается от 1.х. Кто то даже может спросить если это тот же самый фреймворк. Думаю это хороший вопрос. Как я уже упоминал ранее, я уверен что команде AngularJS необходимо взять на себя бремя поддержки версии 1.х, а так же подготовить механизм миграции на 2.0 и руководство для компаний принимающих решение об использовании фреймворка сегодня и всех кто планирует двигаться дальше с 2.0. И хотя сейчас не существует подобных задач у команды сосредоточенной на технологической стороне вопроса, я думаю это необходимо и будет полезно и уважительно по отношению к сообществу AngularJS.
Теперь, когда понятен бэкграунд изменений, давайте посмотрим на ключевые моменты.
AtScript
AtScript язык расширяющий ES6, который начали использовать авторы Angular 2.0. Он имеет синтаксис подобный TypeScript, для представления дополнительных типов данных, который может проверятся на стадии выполнения, что лучше чем на стадии компиляции. Он так же расширяет язык аннотаций метаданных (metadata annotations).
Вот примет кода:
import {Component} from 'angular';
import {Server} from './server';
@Component({selector: 'foo'})
export class MyComponent {
constructor(server:Server) {
this.server = server;
}
}
Здесь мы видим ES6 базовый код с небольшим добавлением AtScript. Объявления Import и class соответствуют точному синтаксису ES6. Ничего особенного. Но взгляните на функцию конструктор. Обратите внимание на то что параметр server определен вместе с типом. В AtScript типы используются для создания проверок в момент выполнения (runtime). Обратите так же внимание на синтаксис @Component, над декларацией класса. Это и есть аннотация метаданных. Component является обычным классом, как любой другой. Когда мы его создаем с использованием аннотации, компилятор создает код являющийся экземпляром аннотации и сохраняет его в область доступную для фреймворка. Помня это, давайте посмотрим на пример в чистом ES6:
import * as rtts from 'rtts';
import {Component} from 'angular';
import {Server} from './server';
export class MyComponent {
constructor(server) {
rtts.types(server, Server);
this.server = server;
}
}
MyComponent.parameters = [{is:Server}];
MyComponent.annotate = [
new Component({selector: 'foo'})
];
RTTS аббревиатура для RunTime Type System. Это небольшая библиотека нацеленная на проверку типов на стадии выполнения скрипта. В момент обработки кода компилятором, переменная server будет объявлена типом Server. Вы так же можете создавать собственные типы, для соответствия структурной типизации, или использовать специальные правила. Но когда вам нужно будет создать готовый релиз, компилятор может исключить эти объявления типов из финального кода для повышения производительности.
Прекрасно то, что типизация является независимой, объявление типа и аннотации могут отслеживаться. Оба вида объявлений могут транслироваться в очень простую ES5 совместимую структуру данных, хранящуюся внутри функции MyComponent. Это позволяет любому фреймворку или библиотеке находить и использовать эти данные. На протяжении нескольких лет это было весьма полезно на таких платформах как .NET и Java. Это, так же, чем то напоминает возможности мета-программирования имеющиеся в Ruby. Конечно же, аннотации могут использоваться для мета-программирования в Angular 2.0, для более простого создания директив. Подробности позже.
Примечание
Мне лично нравится AtScript, но я так же старый фанат TypeScript. Так что можно сказать, что я был уже подготовлен к AtScript. Я знаю что многие разработчики противятся использованию типов в JavaScript. Не могу их винить. Я имею богатый опыт с различными система типизации. Иногда хороший, иногда не очень. Интересным моментом тут является то, что в AtScript вы можете использовать синтаксис типов, как простой путь для предоставления метаданных другим библиотекам. Я думаю что это одна из самых мощных возможностей AtScript, позволяющая хранить информацию о типе в момент выполнения, для предоставления фреймворку или мета-программирования. Я не буду удивлен если и в других транслируемых языках (в которых перед компиляцией происходит трансляция в другой язык. Например: TypeScript, CoffeeScript транслируются в JavaScript), так же появятся подобные возможности.
Тем не менее у меня есть пара замечаний.
Я бы хотел видеть более официальное признание AtScript. Это значит что этот язык должен быть не только внутри команды работающей над AngularJS. Он должен жить собственной жизнью, в которой команда AngularJS только важный пользователь. Это значит что несколько разработчиков должны фултайм работать над развитием AtScript, исправлением багов, улучшению генерируемого кода, созданию инструментов и т.п., и это должен быть долговременный план. Когда разработчики выбирают язык, это важное решение. Я бы хотел видеть что Google серьезно выбирает AtScript для будущей работы.
Другим вопросом, который возникает в связи с этим, является Dart. Dart другой язык так же разработанный в Google. И это не просто транслируемый в JavaScript язык. Он имеет собственную среду исполнения и библиотеки классов. Как результат Dart имеет собственный API для манипуляций с DOM, коллекции, события, регулярные выражения и другое. Хотя эти API хороши сами по себе, они не совместимы с существующим JavaScript кодом. Из-за этого несовпадения, между Dart и другим миром, он общается через специальный ограниченный интерфейс. И несмотря на техническую возможность использования JavaScript библиотек, чаще всего это непрактично. В случае с AngularJS, это приведет к неприемлемо низкой производительности. Ну, Google взяли и создал Angular Dart. Версию Angular повторенную в Dart.
Проблем решена… да уж скорее нет.
Теперь есть две версии AngularJS, в которых нужно исправлять баги, добавлять возможности, выпускать релизы… написанные в разных языках и обслуживаемые разными командами. В общем одну проблему решили за счет появления другой.
Возможно вы сейчас удивлены: Какое все это имеет отношение к AtScript?
Для Angular 2 есть идея объединить Angular и Angular Dart. Вместо двух команд, работающих над двумя разными версиями одного и того же, лучше иметь одну команду делающую работу в одном направлении. AtScript поможет здесь потому что он имплементирован на вершине #Traceur, который легко расширяется. Тем самым команды получат возможность использовать AtScript для Javascript и Dart.
Прекрасно! И в чем проблема?
Помните я писал что Dart имеет другую модель DOM и прочие. Не так то просто транслировать подобный код. Как результат, процесс сборки в Angular 2.0, будет в реальности сложнее чем можно подумать. Потребуется создать различные версии API для Javascript и Dart, которые потом будет использовать компилятор. Это еще та задача.
И это повышает барьер для тех кто захочет внести свой вклад в Angular 2.0. Хотя следует отметить, что возможно это только проблема текущей, ранней стадии. И возможно будут найдены другие решения. Я знаю что многие из вас внесли свой большой вклад в развитие AngularJS. Команда Angular это ценит и думает над тем как это может быть улучшено. Однако, в настоящие время, это далеко от идеала.
Важное замечание!
То что Angular 2.0 пишется с использованием AtScript не значит что вы тоже будете вынуждены его использовать. Свои Angular 2.0 приложения, вы можете легко писать на TypeScript, ES6, ES5, CoffeeScript… или том что вы предпочитаете. Конечно лучше всего использовать AtScript для разработки на Angular 2.0, из-за его способности автоматически создавать метаданные из примитивов языка, но в конце концов они транслируются и в простой ES5. Финальный ES5, в этом случае, концептуально подобен DDO из Angular 1.x, но он все равно лучше чем просто directive-specific технология.
Внедрение зависимостей (Dependency Injection, DI)
Базовой возможностью Angular 1.х было внедрение зависимостей. Через DI вы могли легко увидеть принцип «разделяй и властвуй» в разработке ПО. Сложные задачи могут быть концептуализированны с точки зрения их роли и обязанностей. Потом они могут быть представлены в виде объектов, совместное использование которых приводит к достижению конечной цели. Большие (или маленькие) системы, которые следуют этому пути, могут монтироваться на лету, путем использование DI фреймворка. Такие системы обычно проще тестировать, поскольку финальный дизайн получается более модульным и позволяет проще изолировать компоненты. Все это было возможно в Angular 1.х, но тем не менее было несколько проблем.
Первая проблема, мучающая Angular 1.х связана с минификацией. В момент когда минификатор производил замену имен на более короткие, он искажал название зависимости, в результате чего она переставала работать. Изменения API добавили возможность сделать код боле дружественным для минификации, но при этом потерялась исходная элегантность.
Другие проблемы Angular 1.х, связаны с отсутствующими возможностями, обычными для серверных DI фреймворков, доступных в мире .NET и Java. Например, lifetime/scope control и child injection.
Аннотации
В AtScript мы представили основной механизм для связывания метаданных с функцией. Также, AtScript формат для метаданных устойчив к минификации и легко пишется и в ES5. Это делает его идеальным кандидатом для поддержи DI библиотек, с информацией необходимой для создания экземпляров объектов.
Когда DI нужен экземпляру класса (или при вызове функции), он исследует их для получения ассоциированных метаданных. Вот пример в AtScript:
MyComponent.parameters = [{is:Server}];
В новом DI ищется значения свойства parametrs, которое будет использовано для определения зависимостей и попытки из вызвать. В данном случае, это один параметр типа Server. В результате будет создан экземпляр объекта Server и помещен в функцию перед ее вызовом. Вы так же можете использовать специальную Inject аннотацию для использования вместе с DI. Это отменит использование parametrs. Это так же легко поддерживается, даже если вы используете язык который автоматически не создает метаданные. Вот пример на чистом ES5:
MyComponent.annotate = [new Inject(Server)];
Среда выполнения воспринимает это как параметры. Нужно отметить, что сейчас вы можете использовать что угодно как injection token. Например, вот так:
MyComponent.annotate = [new Inject('my-string-token')];
Сразу как вы сконфигурируете DI, с чем то что можно связать с 'my-string-token', все это будет работать прекрасно. Тем не менее рекомендуется использовать экземпляр конструктора как было показано выше.
Экземпляр области видимости (Instance Scope)
В Angular 1.х все экземпляры класса в DI были синглтонами. Это осталось дефолтным и в Angular 2.0.
Для того что бы получить другое поведение в 1.х, нужно было использовать Services, Providers, Constants… Это приводило к путанице.
К счастью, новый DI имеет новые, более мощные, возможности. Появился контроль области видимости. Итак, если вам нужно, DI создаст новый экземпляр объекта. Всякий раз когда вам потребуется это, нужно всего лишь сделать следующие:
@TransientScope
export class MyClass { ... }
Это становится еще более мощным, когда вы создаете ваш собственный идентификатор области видимости для использования в комбинации с child injectors…
Вставка наследника (Child injectors)
Child injectors это большая новая возможность. Child injectors наследует от родителя все сервисы, но имеет возможность переопределять их на своем уровне. Когда это возможность комбинируется с собственной областью видимости вы можете легко вызывать некоторые типы объектов в вашей системе, что автоматически переопределит их в разных областях видимости. Это очень мощная возможность. Как пример этого, новый маршрутизатор имеет Child Routers. Каждый Child Router, внутри создает собственный Child injector. Это позволяет каждой части маршрутизатора наследовать сервисы от родительского маршрутизатора или переопределять эти сервисы на основании разных сценариев навигации.
Примечание
Настраиваемые области видимости и Child injectors, предполагаются как довольно сложные и продвинутые в использовании возможности. Я не ожидаю что большинство приложений будет это использовать. Однако, поскольку это уже внутри Angular, это доступно и вам, если вам потребуются подобные возможности.
и еще…
Некоторые другие возможности в новом DI, такие как провайдеры (providers) (кастомные функции которые обеспечивают значения для вставки), ленивая вставка (lazy injection) (определяет что вы хотите что-то вставить, но не сейчас, а позже) и promise-based async injection(вставки и обещания к которым вы можете получить доступ асинхронно запросив соответствующую зависимость)
Комментарий
Мне лично нравиться новый DI. Опять же, я немного предвзят здесь, поскольку использую DI уже много лет, и это всегда было центральным компонентом в UI фреймворках которые я создавал. Новый DI играет важную роль в Angular 2.0. Возможности, такие как Child injectors, дают огромную разницу в сравнении со старыми решениями. Теперь когда это возможность есть, она может использоваться и для шаблонизации и для маршрутизации. И там и там есть потребность создавать собственную область видимости и изолировать различные сервисы.
Одной из важнейших вещей которые будут удалены в Angular 2.0 является: $scope. Однако, несмотря на то что $scope будет удален, некоторые его возможности сохранятся. Эти возможности будут перемещены и улучшены став частью дизайна фреймворка. Вы возможно будете застигнуты в врасплох удалением $scope, но новый дизайн упростит вещи и внутри Angular и для разработчиков. Я пишу это здесь, поскольку новые возможности DI, такие как Child injectors, пересекаются с предыдущими возможностями из $scope. В этом случае, новый DI выкладывает на стол лучшее решение. В общем, это не только решит внутренние потребности самого Angular, но и откроет новые возможности для вас.
К сожалению, жизнь, это не прогулка в розовом саду, с молодой любовницей. Давайте поговорим от проблемах. Речь пойдет о модулях. Вы можете удивится, каким боком здесь это. Но в планы для Angular 2.0 входит адаптация ES6 стандартов для модулей. В текущей версии Angular был использован свой специфичный подход для обработки модулей. Пять лет назад, когда Angular разрабатывался, не было никаких стандартов на это счет. Сегодня вещи изменились и есть более чистый подход к решению этой задачи. Это ключевое изменение и требует переработки кода от каждого кто решить мигрировать на новую версию Angular. Это конечно тяжело, что такое критическое изменение должно быть сделано, но Angular может стать неуместным, если эта проблема не будет решена сейчас.
Есть еще один камень преткновения связанный с DI, особенно если вы пишите на ES5. Angular 2.0 предпочитает дизайн основанный на классах с аннотациями и метаданными. Синтаксис классов и аннотаций не особо хорош в ES5. В общем то, его там вообще нет. Вы можете реализовать все используя прототипы и другое, но это не так чисто как в AtScript или даже в ES6 или TypeScript, которые поддерживают классы и статичные члены классов. Я удивлюсь если это может быть улучшено для разработчиков не готовых перейти на ES6. Возможно, дополнительная библиотека даст вам простой путь создания классов с метаданными? Возможно это будет немного похоже на DDO объект в Angular 1.х, но более общее, что позволит создать класс с метаданными. Я бы хотел услышать соображения об этой идее и другие идеи которые у вас возможно есть, как сгладить разработку в ES5 и улучшить процесс миграции.
Шаблонизация и связывание данных (Templating and Databinding)
Мы подошли к действительно интересным вещам: шаблонизации и связыванию данных. Я собираюсь обсудить их в тандеме. Хотя система связывания данных технически стоит отдельно от шаблонизации, в реальном опыте, при создании приложения они используются совместно. Таким образом рассматривать их бок-о-бок имеет больший смысл.
Давайте начнем с понимания как вид (View) попадает на экран. По существу вы начинаете с фрагмента HTML кода. Он будет находится внутри элемента. Этот фрагмент обрабатывается компилятором шаблонизатора. Компилятор анализирует фрагмент, определяет в нем директивы, связывает выражения, события и т.п. Все эти данные извлекаются из DOM в структуру данных, которая может быть использована в конце концов для создания экземпляра шаблона. Как часть данной фазы происходит некоторая обработка данных, такая как обработка выражений например. Каждый узел содержащий специальные инструкции связан со соответствующим классом. Результат данного процесса кешируется так что бы не возникала необходимость повторной обработки. Мы называет это результат: ProtoView. После того как вы получили ProtoView вы можете использовать его для создания View. Когда ProtoView создает View, для всех директив, идентифицированных на предыдущем шаге, создаются их экземпляры для вставки в DOM. Watchers устанавливаются на связанных выражениях. Конфигурируются перехватчики событий. Структуры данных, которые были получены в предыдущем процессе, в фазе компиляции, позволяют нам делать все очень быстро. После того как вы получили View, вы можете отобразить его добавив во ViewPort. Как разработчик, вы не увидите большую часть всего этого, вы просто создадите шаблон и он будет работать. Но я хотел расписать этот процесс на более высоком уровне, прежде чем перейти к деталям.
Динамическая загрузка (Dynamic Loading)
Одной из очень недостающих возможностей в Angular 1.х была динамическая загрузка кода. Если вы хотели добавить новые директивы или контроллеры на лету, это было очень сложно или даже невозможно. Это просто не поддерживалось. В Angular 2.0, мы создаем некоторые вещи с нуля, помня об асинхронности. И когда мы компилируем шаблон, это по сути асинхронный процесс.
Теперь, мне нужно упомянуть о деталях компиляции шаблона и я оттолкнусь от моего упрощенного объяснения выше. Когда вы компилирует шаблон вы не только обеспечиваете компилятору шаблон, но и вы так же предоставляете определение для компонентов (Component definition). Component definition содержит метаданные о том что именно, директивы, фильтры и тому подобное использовано в шаблоне. Это гарантирует то, что все необходимые зависимости, будут загружены прежде чем они будут обработаны в процессе компиляции. Из-за того что мы базируем наш код на ES6 спецификации, простое упоминание зависимости в Component definition вызовет модуль загрузки и загрузит ее, если она еще не была загружена. Таким образом, используя возможности ES6 для модулей, мы бесплатно получаем динамическую загрузку всего что нам понадобится.
Директивы (Directives)
Прежде чем мы копнем синтаксис шаблонов, мы должны взглянуть на директивы как на способ расширения HTML. В Angular 1.х DDO (Directive Definition Object) использовался для создания директив. Это кажется был один из самых больших источников для страданий разработчиков.
Что если мы сделаем директивы проще?
Мы поговорили о модулях, классах и аннотациях. Что если мы используем эти основные конструкции для создания директив? И конечно же мы это сделали.
В Angular 2.0 существует три основных типа директив.
- Component Directive — создает кастомный компонент объединяющий View и Controller. Мы можем использовать его как собственных HTML элемент. А так же маршрутизатор может создавать маршрут к компонентам.
- Decorator Directive — расширение существующих HTML элементов дополнительным провидением. Классический пример ng-sh-ow.
- Template Directive — Трансформация HTML в многократно используемый шаблон. Создатель директивы может контролировать где и как шаблон будет обработан и вставлен в DOM. Примером могут служить ng-if и ng-repeat.
Вы возможно уже слышали о смерти контроллеров в Angular 2.0. Ну, это не совсем правда. В реальности контроллеры стали частью того что мы называем Component. Component состоит из вида (View) и контроллера (Controller). Вид это ваш HTML шаблон и контроллер определяющий JavaScript логику. Вместо того что бы определять контроллер через API как это сделано в Angular 1.х, в 2.0 вы можете создать обычный класс с некоторой аннотацией. Вот пример части компоненты реализующей контроллер (как выгляди Вид рассмотрим немного позже):
@ComponentDirective({
selector:'tab-container',
directives:[NgRepeat]
})
export class TabContainer {
constructor(panes:Query<Pane>) {
this.panes = panes;
}
select(selectedPane:Pane) { ... }
}
Можно отметить здесь несколько моментов.
Первый, контроллер компоненты это просто класс. Конструктор автоматически вставит зависимости. Благодаря тому что здесь использован Child injector, класс может получить доступ к любому сервису выше в DOM иерархии, а так же службам локальным для данного элемента. Например, в данном случае, Query injector. Это специальная коллекция, которая автоматически синхронизируется с наследниками Pane (панель) элемента и дает вам знать когда что-то добавляется или удаляется.
Это позволит вам обрабатывать подобную логику как в $link коллбек из Angular 1.х, но там это обрабатывается иным способом чем через конструктор класса.
Теперь, взгляните на @ComponentDirective аннотацию. Это идентифицирует класс как компоненту и предоставляет метаданные которые нужны компилятору для его вставки. Например selector:'tab-container', это CSS селектор который будет использован для поиска в HTML. Элемент совпавший с этим селектором будет возвращен в TabContainer. Так же, directives:[NgReapet] указывает на зависимость используемую в шаблоне. Я пока это не демонстрировал. Чуть позже мы увидим это когда поговорим о синтаксисе.
Важной деталью является то что шаблон будет прямо связан с классом. Это значит что любые свойства и методы класса могут быть доступны прямо в шаблоне. Это подобно «controller as» синтаксису из Angular 1.2. Но при этом нет необходимости установки $scope между классом и шаблоном. Как результат упрощение Angular изнутри и упрощение синтаксиса для разработчиков на нем.
Давайте теперь взглянем на Decorator Directive. Как насчет NgShow?
@DecoratorDirective({
selector:'[ng-show]',
bind: { 'ngShow': 'ngShow' },
observe: {'ngShow': 'ngShowChanged'}
})
export class NgShow {
constructor(element:Element) {
this.element = element;
}
ngShowChanged(newValue){
if(newValue){
this.element.style.display = 'block';
}else{
this.element.style.display = 'none';
}
}
}
Здесь мы можем видеть немного больше аспектов. Прежде всего, снова использован класс с аннотацией. Конструктор связывает класс с декодируемым HTML элементом. Компилятор понимает что речь идет о декорации потому что это Decorator Directive и понимает что нужно это применить к любому элементу который совпадает с CSS селектором selector:'[ng-show]'.
Так же есть несколько других любопытных свойств у данной аннотации.
bind: {'ngShow': 'ngShow'} используется для связывания свойства класса с HTML атрибутом. Не все свойства класса проявятся в виде HTML атрибутов. Если вы хотите что бы свойство было связываемым с HTML, вы определяете это в bind метаданных.
observe: {'ngShow': 'ngShowChanged'} говорит системе связывания что вы хотите получать уведомления когда ngShow свойство изменится и будет вызван метод ngShowChanged. Обратите внимание что ngShowChanged реагирует на изменения так, что в свою очередь изменят отображение HTML элемента к которому он присоединен. (Заметим что это очень простоя реализация, только для демонстрационных целей).
ОК, как же выглядит Tеmplate Directive? Как насчет того чтобы взглянуть на NgIf?
@TemplateDirective({
selector: '[ng-if]',
bind: {'ngIf': 'ngIf'},
observe: {'ngIf': 'ngIfChanged'}
})
export class NgIf {
constructor(viewFactory:BoundViewFactory, viewPort:ViewPort) {
this.viewFactory = viewFactory;
this.viewPort = viewPort;
this.view = null;
}
ngIfChanged(value) {
if (!value && this.view) {
this.view.remove();
this.view = null;
}
if (value) {
this.view = this.viewFactory.createView();
this.view.appendTo(this.viewPort);
}
}
}
Надеюсь вам понятен смысл этой Tеmplate Directive аннотации. Прежде всего идет регистрация директивы и предоставление необходимых метаданных для установки свойств и наблюдателей, точно так как в предыдущем примере для NgShow. Tеmplate Directive имеет доступ к определенным специальным службам которые могут быть вставлены в конструкторе. Первой является ViewFactory. Как упоминалось раньше Tеmplate Directive преобразует HTML указанный в шаблоне. Шаблон автоматически компилируется и вы получаете доступ к ViewFactory в Tеmplate Directive. Вызов createView API создает экземпляр шаблона. Вы так же получаете доступ к ViewPort который представляет область DOM из которого был извлечен шаблон. Вы можете использовать это для добавления или удаления экземпляра шаблона из DOM. Обратите внимание как ngIfChanged реагирует на изменения в обрабатываемом шаблоне и добавляет или удаляет это из ViewPort. Если вы реализуете вместо это NgRepeat вы должны многократно создать экземпляр шаблона и так же предоставить специфические данные в createView API и затем вы сможете добавить множество экземпляров во ViewPort. Это основы.
Теперь вы ознакомились с каноническими примерами трех типов директив. Я надеюсь что это разъяснило вещи в плане того как вы можете расширить HTML новым поведением.
Тем не менее, есть важная вещь, которую я до сих пор не полностью разъяснил. Это контроллеры.
Как вы создаете контроллер для вашей аппликации? Допустим, вы хотите настроить маршрутизатор так чтобы происходил вызов контроллера и отображался Вид. Как вы это сделаете? Простой ответ, это использовать Component Directive.
В Angular 1.х директивы и контроллеры были разными вещами. С разным API и возможностями. В Angular 2.0 мы убрали DDO и сделали директивы основными на классах, где мы смогли объединить директивы и контроллеры внутри модели компоненты. Теперь когда вы настраиваете маршрутизатор, вы просто указываете маршрут к Component Directive (которая содержит вид и контроллер, по существу, так же как и раньше).
Например если вы создаете гипотетический контроллер для редактирования клиентов, это может выглядеть так:
@ComponentDirective
export class CustomerEditController {
constructor(server:Server) {
this.server = server;
this.customer = null;
}
activate(customerId) {
return this.server.loadCustomer(customerId)
.then(response => this.customer = response.customer);
}
}
Здесь нет реально ничего нового. Мы просто вставили гипотетическую службу server и используем ее для загрузки данных о клиенте в момент когда мы активировали маршрутизатор. Единственное, что может быть тут интересно, то что вам не нужен селектор или другие метаданные. Причиной того является что этот компонент не используется как элемент. Он был динамически создан маршрутизатором и динамически вставлен в DOM. Как результат мы опустили ненужные детали.
Итак, вы знаете как создать Component Directive, как создать эквивалент Angular 1.х контроллера для использования с маршрутизатором. Это было очень непросто в Angular 1.х, но теперь у нас есть классы и метаданные и работа с директивами значительно упрощается и становится значительно проще создавать свои «контроллеры» таким способом.
Примечание
Я бы хотел отметить, что примеры кода директив показанных выше основаны на комбинации предварительной версии кода и черновых спецификаций. Они должны рассматривать скорее как демонстрация, но не как точный синтаксис директив который еще находится в разработке. Компилятор шаблонов и язык связывания очень изменчивая часть Angular 2.0 в данный момент, с довольно частыми изменениями дизайна.
Синтаксис шаблона (Template Syntax)
Итак, мы поняли как работает основной процесс компиляции, что он может загружать код асинхронно, как писать директивы и как их подключать, как контроллеры вписываются в этот пазл, но мы до сих пор не рассмотрели актуальный шаблон. Давайте сделаем это сейчас, рассмотрев шаблон для гипотетического TabContainer, пример которого я показывал раньше. Я включил здесь опять код директивы для удобства:
@ComponentDirective({
selector:'tab-container',
directives:[NgRepeat]
})
export class TabContainer {
constructor(panes:Query<Pane>) {
this.panes = panes;
}
select(selectedPane:Pane) { ... }
}
<template>
<div class="border">
<div class="tabs">
<div [ng-repeat|pane]="panes" class="tab" (^click)="select(pane)">
<img [src]="pane.icon"><span>${pane.name}</span>
</div>
</div>
<content></content>
</div>
</template>
Пожалуйста, сдерживайте ужас, который вы испытываете глядя на этот код. Да, это валидный HTML соответствующий спецификации. Но нет, это еще не наш финальный синтаксис связывания. Но давайте используем это, как пример для начальной точки, для более широкой дискуссии.
Ключом понимания синтаксиса связывания данных, является левая часть атрибута декларации. Помня это, давайте взглянем на тег изображения.
<img [src]="pane.icon"><span>${pane.name}</span>
Когда мы видим имя атрибута окруженное [], это говорит нам, что правая часть, значение атрибута, является связанным выражением (binding expression).
Когда мы видим выражение окруженное ${}, это говорит нам, что данное выражение должно интерполироваться в контент как строка. (Это подобный синтаксис который использован в ES6 для интерполяции строк)
Оба эти связывания одно-направленные от model/controller к view.
Теперь посмотрим на этот жуткий див:
<div [ng-repeat|pane]="panes" class="tab" (^click)="select(pane)">
ng-repeate это TemplateDirective. Можно сказать что мы связываем его с выражением поскольку он окружен []. Кроме того, так же имеется | и слово «pane». Это означает что локальное имя переменной которое используется в шаблоне является «pane».
Теперь посмотрим на (^click). Круглые скобки служат для обозначения выражения обработчика событий. Если внутри скобок находится ^, это значит что вы прикрепляете разработчик прямо к DOM узлу, чем мы позволим событию всплыть и оно будет обработано на верхнем уровне документа.
Я сейчас воздержусь от выражения собственного мнения по поводу всего это и выскажу его ниже, когда буду комментировать эту секцию. Давайте пока проигнорируем, что вы или я думаем о том, почему был выбран подобный синтаксис.
Web Components изменили все. Это еще один пример изменения выходящих за привычные рамки. Большинство фреймворков, основанных на связанных данных, используют ограниченный набор HTML элементов и понимают специальное поведение только у некоторых элементов, таких как input и подобных. Однако, в мире Web Components, не может быть сделано никаких предположений. Разработчик, никак не связанный с Angular, может создать свой собственный с любым числом свойств и событий который ему будут необходимы. К сожалению, не существует способа проанализировать Web Component и извлечь какие либо метаданные которые можно использовать для связывания данных.
Нет никакого способа узнать какие события актуальны на данный момент. Взгляните на это:
<x-foo bar="..." baz="..."></x-foo>
Глядя на bar и baz можете ли вы определить является это событием или свойством? Нет… и к сожалению Angular тоже, потому что спецификация Web Components не включает понятие для само-описания (self-describing) компонентов. Это печально, потому что система связывания данных не может сказать что именно нуждается в связывании, а что нет. Для решения этой проблемы, нам нужно общая система связывания данных с синтаксисом который позволит разработчику сказать что является событием, а что свойством для связывания.
Это не единственная трудность. Так же необходимо, что бы эта информация была представлена так что бы она не нарушала сам Web Component. Это значит что Web Component нельзя позволить увидеть это выражение. Это может сломать его. Можно только позволить увидеть результат определения выражения. Это актуально не только для Web Components, но также и для любых вставок. Рассмотрим это:
<img src="{{some.expression}}">
Этот код вызовет ошибочный запрос пытающийся найти "{{some.expression}}" изображение. Это не то, чего бы мы ожидали. Нам не нужно чтобы img видел само выражение, а только результат его определения. Angular 1.х решает это используя ng-src, кастомную директиву.
Теперь вернемся к Web Components… это было бы катастрофой если бы для каждого атрибута Web Component вы были вынуждены создавать собственную директиву. Я не думаю что мы хотим это. Нам нужно решить проблему более общим способом.
Для достижения этой цели, имеется две возможности. Первая, удалить атрибут из DOM в момент компиляции шаблона. Это предотвратит реагирование Web Component на содержимое выражения. Однако, сделав это, инспектор DOM не покажет никаких следов привязки к атрибуту. Это может сделать отладку более сложной.
Другой возможностью является кодирование атрибута так, что Web Component не распознает его. Это позволит Angular видеть выражения, но предотвратит их распознание Web Component. Это так же позволит оставить нам атрибут в элементе после компиляции, что позволит видеть его инспектору DOM. Это безусловно лучше для отладки.
Как вы можете видеть выше, наша команда в настоящие время выступает за второй подход, а именно кодирование имен атрибутов. Такое кодирование так же необходимо, что бы была видна разница между свойствами и событиями. Синтаксис показанный выше, является одной из нескольких точек зрения как сделать это.
Это часть вызывала сильную дискуссию на протяжении месяцев внутри команды. Вышеприведенный синтаксис не был единодушно принят всеми, но это было решение большинства. Есть очень много соображений при разработке синтаксиса связывания. Если это вам интересно, я рекомендую прочитать весьма обширный документ на эту тему здесь.
Теперь, когда вы ознакомились с тем как создавать шаблоны, связывание и директивы, все объединилось в одно целое.
Комментарий
Я хочу сказать очень многое относительно того, что я только что вам рассказал. Тяжело даже решить, с чего начать.Давайте начнем с компилятора.
В начале мы рассмотрели поверхностный процесс компиляции шаблона. Хотя еще куча вещей делается в этой области, я, в целом, очень доволен разработанным компилятором.Там использовано довольно много клевых вещей, позволяющий сохранить использование небольшого объема памяти, уменьшить кол-во мусора и предоставить супер быстрый экземпляр шаблона. Это великолепно. Конечно остались еще некоторые возможности для улучшений, но то что сделано уже солидно. Хотя мы не говорили о dirty checking (механизме, посредством которого происходит обновление связанных данных) его реализация так же содержит ряд новых идей, которые, вероятно, приведут к улучшению производительности и в экземплярах шаблонов и в самом механизме dirty checking. Все это хорошо. А возможность динамической загрузки, просто прекрасна. Это очень недостающая возможность в Angular 1.х, критичная для больших приложений. И я конечно счастлив что это является одним из основных требований нового дизайна фреймворка.
Теперь, поговорим о директивах.
Новый механизм создает директивы используя классы, лучшая обработка зависимостей и аннотации очень приятные вещи. Этот путь проще чем тот, который требовался в Angular 1.х. К сожалению, это очень значительное изменение для разработчиков 1.х. Это так же может быть немного сложнее для тех, кто ограничен ES5 и не хочет или не может использовать ES6, TypeScript или AtScript. Раньше, в этой статье, я упоминал возможность использовать небольших библиотек для работы с ES5 поддерживающих создание аннотированных классов. Подобный API может выглядеть немного лучше чем DDO объект. Возможно это так же облегчит процесс переноса кода с Angular 1.х на 2.0. Вы можете попытаться сделать это сейчас в 1.3 и потом это поможет потом проще перейти на 2.0… представьте это разновидность абстрактного слоя при миграции. Хотя я и не уверен. Я бы хотел услышать ваше мнение об этом. Я знаю что для многих из вас, это может быть проблемой.
Другая вещь, которая беспокоит меня в директивах, то что аннотации немного многословны. Посмотрите еще раз на NgShow директиву. Вы видите как много раз NgShow или вариант этого повторяется? Это выглядит немного глупым на мой взгляд. Так же посмотрите на CustomerEditControler. Почему нам нужно все это в аннотации ComponentDirective? Почему нам просто не использовать обычный класс, поскольку маршрутизатор уже знает что это.
Я много спорил внутри команды, относительно предпочтения конвенций над конфигурациями. Это то что сделало Rails популярным и оказало влияние на многие современные фреймворки. Поэтому я думаю что это правильный путь. Я бы хотел видеть как конвенции при создании директивы ликвидируют шаблоны. Без этого я бы не чувствовал что новая система создания директив на упрощена настолько, насколько это требуется. Есть ощущение что сложности имеющие с DDO просто переместились в другое место и находятся сейчас в аннотациях. Что думает вы? Вам нравятся конвенции? Как вы думаете они могут сделать директивы проще (при условии что вы всегда можете переопределить конвенцию аннотацией)?
К сожалению, это не единственная реально серьезная из имеющихся проблем. Серьезную проблему так же можно увидеть в ComponentDirective. Интересно, вы видите ее? Вы обратили внимание, что здесь нарушен принцип Separation Presentation? Давайте еще раз взглянем на пример с TabContainer:
@ComponentDirective({ selector:'tab-container', directives:[NgRepeat] }) export class TabContainer { constructor(panes:Query<Pane>) { this.panes = panes; } select(selectedPane:Pane) { ... } }
Вы видите что TabContainer обязан, в его метаданных, перечислить все директивы используемые в шаблоне? Это прямая связь между TabContainer (контроллером) и деталями реализации View. Ранее я упоминал что это необходим для того что бы компилятор знал что необходимо загрузить прежде чем компилировать шаблон. Но это разрушает основную выгоду от концепции использования MVC, MVVM или другого подхода разделяющего уровни презентации и логики. Что бы вы не думали, что это просто теория, позвольте мне отметить некоторые из последствий этого:
- больше не будет возможно использовать ng-include. Компилятор обработает ComponentDirective в порядке компиляции HTML. Это не позволит включить ваш собственный HTML во View.
- будет больно если вам потребуется несколько потенциальных презентаций (views) для одного компонента. Представьте что у вас есть компонент и вы хотите иметь для него различные презентации (views) для телефона и для компьютера. Вы должны объединить все директивы, фильтры и т.д., которые вы будете использовать в обоих презентациях и удостоверится что они все представлены в метаданных одного компонента. Это ночной кошмар для последующей поддержки кода. Вы больше не можете ничего удалить из списка зависимостей, не проверив все презентации.
- невозможно иметь множество runtime презентаций для одного компонента. Представьте что вы настроили маршрутизатор с набором путей. Несколько путей могут использовать один контроллер, но вам необходимы разные презентации. Вы просто не можете сделать это. Уж извините.
- совершено невозможно получить специальную композицию экранов. Это делает UI основанным на управление данными, более сложным в будущем. Вы не можете сделать различные комбинации презентаций и контроллеров (view models). Это ограничивает возможности повторного использования и препятствует композиционным подходам при создании UI. Это заставляет вас создавать подклассы контролера для того что бы получить различные его презентации.
На счастье, дизайн еще находится в стадии множества изменений и решить данную проблему пока довольно просто. Позволить шаблонам быть полностью автономными разрешив им определять собственный импорт. Убрать метаданные из директивы и поместив их в шаблон.
Вы можете использовать собственный элемент как например:<ng-import src="ngRepeat"></ng-import>
Компилятор найдет здесь все необходимое, что требуется загрузить, прежде чем он обработает шаблон. Это так. Все выше упомянутые проблемы будут решены.
ОК, теперь когда мы рассмотрели компилятор и директивы…
Я думаю, что это привело нас к синтаксису шаблонов.
Давайте будем честными. Когда многие из вас увидели синтаксис шаблонов, вас вероятно вырвало. Не всех, но многих из вас. Я лично был против этого синтаксиса, но это было решением большинства. (Что бы понять почему так стало, вам нужно прочитать вот этот документ) Не бойтесь! Там было несколько технических проблем, описание которых заставило синтаксис указанный выше стать фактическим синтаксисом. Сейчас команда вернулась к живой дискуссии о лучшем синтаксисе. Многие члены сообщества присоединились и предложили собственные идеи. Я представлю мои собственные рекомендации здесь для того что вы могли их прокомментировать.
Вот какой базовый синтаксис я предлагаю:
- property=”{{expression}}” — односторонние связывание данных от модели к свойству элемента, обозначенному двойными фигурными скобками {{}}
- on-event=”{{expression}}” — добавляет обработчик события который выполнит выражение, обозначен on- префиксом.
- ${expression} — строка интерполируется в HTML контент и атрибуты (основанная на ES6 синтаксисе)
Это все. Затем, под капотом, нам нужно удалить выражения из DOM что бы предотвратить различные проблемы с WebComponents. И только в режиме отладки мы должны вернуть их обратно используя префикс такой как bind- таким образом они будут обнаружены при инспектировании DOM без влияния на WebComponents. И хотя здесь возникает разница между тем что вы пишите и что видите в DOM, но я думаю это разумный подход для чистого и более стандартизированного синтаксиса связывания.
Не все согласятся со мной. Что думаете вы?Это предложение помогает решить проблемы связывания и так же помогает с обратной совместимостью. Возможно мы должны немного больше думать об обратной совместимости. Мы могли бы разрешить использовать {{expression}} для интерполяции строк в HTML, но вы должны подписаться под этим. Это устареет только в следующем тысячелетии. Предпочитаемым же методом будет всегда ${expression}, но это потребует более подготовленного пути для миграции. Так же возможно потребуется создать набор дополнительных директив, которые могут потеряться, для того что бы были доступны ng-click и подобные вещи, которые тоже не скоро устареют. Дополнительно, мы должны представить документацию, которая представит старое и новое бок по боку и поможет людям перевести шаблоны вовремя.
Это базис моего предложения. Хотя есть и другие предложения. Я конечно же пристрастен. Хотелось бы знать что думаете вы. Обратная совместимость важна для вас? Будете ли вы предпочитать синтаксис с {{}} или вы предпочтете синтаксис который кодирует имена атрибутов. Здесь много возможностей для дискуссии.
Ну, теперь то все наши проблемы решены? Нет. Остался еще слон в посудной лавке.
Речь идет о направлении связывания!
Я не знаю, если вы обратили внимание на это, но здесь не прозвучало ни одного слова о двустороннем связывании (two-way databinding), в целой статье. В самом деле, ни один из примеров приведенных выше не включал способ спецификации различных опций связывания, таких как направленность, триггеры, debouce и т.п. Ну и как теперь связать элемент и передать данные обратно в модель? Как создать собственный WebComponent который нуждается в обновлении вашей модели?
Существует яростная дискуссия внутри Angular команды, о том если 2.0 нуждается в двустороннем связывании или нет. Если вы читали публичные документ описывающие дизайн или следили за презентацией на ngEurope или Q&A, вы возможно поняли это. Я сильный сторонник двустороннего связывания. Для меня это выглядит как часть души Angular. Я пока не видел элегантной альтернативы и пока его не увижу, до тех пор буду выступать за сохранение двустороннего связывания данных.
Вы даже может быть удивлены почему это вообще рассматривается.
Я слышал некоторые объяснения связанные с соблюдением DAG потоками данных. Это идея стала недавно популярной благодаря ReactJS. Но, по правде говоря, вы не можете полностью обеспечить это. Я могу нарушить это просто используя агрегатор событий (event aggregator). Этот паттерн очень распространен в композитных аппликациях. Я думаю можно научить людей что такое DAG и помочь им использовать это, но нельзя их заставлять. Это только делает их работу более сложной.
Я думаю единственной большая проблема относительно связывания в Angular это dirty checking. С момента когда dirty checking использован, каждое изменение должно проверятся дважды. Причина в том что если произошло изменение оно может привести в свою очередь к другим связанным изменениям, как побочному эффекту. Поэтому нужна повторная проверка того что все в порядке. Теперь, если есть изменения после повторной проверки, то должна быть третья… и так далее. Это то, что называют стабилизацией модели. Да, это мучение для системы. Но отказ от двустороннего связывания не решит этой проблемы. Связывание данных мощный инструмент и люди здесь продолжают делать ошибки. Но я думаю с ними можно справиться. Я знаю это правда для многих из вас.
Возможно вы не согласны со мной и думаете “наконец то избавление от двустороннего связывания”. Люди думающие так безусловно есть. Однако, я подозреваю, что многие пользователи Angular, Durandal, Knockuot, Ember и других подобных фреймворков, согласятся со мной. К счастью команда не зациклилась на одном. Она пытается рассмотреть все возможности. Таким образом, не стоит особо беспокоится. Тем не менее, если вам нравится двустороннее связывание, вы должно помочь мне. Я думаю будет отлично, если и другая часть команды Angular узнает как сильно вы любите двустороннее связывание.
С другой стороны, если вы думаете что двустороннее связывание дурацкая идея, приглашаю вас помочь нам найти альтернативу. До сих пор я не видел хороших альтернатив, но возможно у вас есть какая то отличная идея. В этом случае я приглашаю поделится ей с нами. Если мы работая вместе найдем что то действительно лучшее… это будет чудесно.
Маршрутизатор (The Router)
Давайте теперь поговорим о маршрутизаторе…
Примечание. Если вы устали от чтения и хотели бы лучше посмотреть видео о маршрутизаторе, вы можете найти его на презентации на ngEurope.
Основы
Для того что бы Angular 2.0 был достойным фреймворком он нуждается в хорошем решении для маршрутизации. В начале 2014 года Brian Ford начал процесс сбора и о имеющихся решениях в области маршрутизации как внутри так и за пределами Angular сообщества. Мы изучили огромное множество готовых решений и объединили это с запросами поступающими из сообщества. После того как был составлен и обсужден соответствующий документ, я взял на себя попытку реализовать это. После определенного числа итераций мы думаем у нас получилось что-то клёвое.
Естественно, новый маршрутизатор обрабатывает все основные сценарии, которые бы вы ожидали от какого либо маршрутизатора.
Вот быстрый список основных возможностей…
- Простая основанная на JSON конфигурация
- Дополнительные соглашения по конфигурации
- Статичные, параметризованные и splat шаблоны
- поддержка строки запросов
- Использование пуш состояний (push state) и изменений хеша (hashchange)
- Навигационная модель (для генерации навигационного UI)
- Обновление Title у документов
- обработка 404 ошибки
- манипуляции с историей
и другое…
Вложенный маршрутизатор (Child Routers)
Возможно вы привыкли к тому, что бы настраивать карту маршрутизации для всего вашего приложения наперед. Но, с новым маршрутизатором вы получили дополнительную гибкость в этом вопросе. Фактически вы можете иметь свой маршрутизатор у каждого компонента к которому нужно перейти. Мы назвали это Child Routers и это позволяет вам скрыть (инкапсулировать) различные области вашего приложения. Если вы работаете над большим проектом с несколькими командами или вы универсал-одиночка который любит все держать по полочкам, вы полюбите эту возможность. Теперь вы может разрабатывать каждую часть вашего приложения как мини-приложение, с собственным маршрутизатором. Потом, вы просто подключаете его к основному приложению и указываете относительный путь к компоненту и это все будет работать. Если вы хотите увидеть что то веселое, посмотрите на демонстрацию рекурсивного роутера на моей презентации.
Окно активации (Screen Activation)
Иногда, во врем навигации, у вас возникает необходимость получить контроль над процессом. Например, пользователь покидает страницу с не сохраненными данными и вам нужно убедится что это так и должно быть. Или, прежде чем перейти на третий шаг, вам нужно убедится что имеются данные с предыдущих двух шагов и сделать редирект назад при необходимости. Для обработки такого типа сценариев мы реализовали жизненный цикл навигации в котором ваши контроллеры могут влиять на процесс навигации. Вот лист перехватываемых событий (hooks) жизненного цикла:
- canActivate — позволяет/запрещает навигацию в новый контроллер.
- activate — ответ на успешный переход к новому контроллеру.
- canDeactivate — позволяет/запрещает навигацию из текущего контроллера.
- deactivate — ответ на успешный выходе из старого контроллера.
Функции обратного вызова с префиксом can* позволяют вам контролировать навигацию, возвращая логические значения. Вы так же можете вернуть обещание (promice) как значение, что позволит вам выполнять асинхронные операции как часть этого процесса. Кроме того вы может возвращать специальные навигационные команды (например Redirect) которые позволят вам контролировать процесс на низком уровне.
Как и можно предположить это все работает и с вложенным маршрутизатором.
Дизайн (Design)
Мы много работает что бы сделать дизайн подключаемым насколько это вообще возможно. Вся логика процесса навигации построена по конвейерному принципу. Это значит что вы можете добавить ваш собственный шаг в любое звено конвейера или даже удалить дефолтные шаги. Например, если вам не нравится Screen Activation, вы можете удалить это. Все смоделировано как четырехшаговый конвейер, по одному шагу на каждую фазу жизненного цикла. Другой важной возможностью конвейера является то, что каждый шаг асинхронный. Соответственно, если вам нужно сделать запрос к серверу для авторизации пользователя или загрузить предварительные данные для контроллера, вы может сделать это как шаг конвейера и исключить таким образом этот код из контроллера, за ненадобностью.
Комментарий
Для меня трудно в данном случае быть не предвзятым, поскольку я занимался реализацией маршрутизатора. Я думаю что это “хороший первый пас” для нового маршрутизатора. Там еще есть некоторые недостающие возможности, но в целом я думаю что общий дизайн маршрутизатора является очень хорошим.
Как бонус мы так же собираемся портировать его и в Angular 1.3
Заключение
Спасибо за ваше время потраченное на чтение этой статьи. На момент публикации (ноябрь 2014) это самая обширная статья об Angular 2.0.
Я попытался изложить соображения об основных особенностях и дизайне новой версии. Что так же включает значительную долю моего личного мнения. Дизайн все еще развивается и мы пока находимся на ранней стадии развития. Таким образом, я надеюсь основная часть планируемых изменений будет сделана до релиза. Есть конечно “неизвестные”, такие как двустороннее связывание данных, где команда еще не в каком направлении правильно двигаться. Мы стараемся рассмотреть все точки зрения. Возможно вы придумали что-то новое и удивительное в этом направлении!? Пожалуйста, будьте терпеливы и помните что как участники сообщества вы приглашены принять участие в решении этих проблем. Я умоляю вас, будьте добры и вежливы когда делает это, но, пожалуйста, поделитесь с нами мыслями, идеями и мнениями.
Спасибо!
Rob Eisenberg
Широко признанный эксперт в области разработки UI. Создатель Caliburn.Micro и Durandal фреймворка и член команды Angular 2.0
Автор: non4me