Статьи

Вступление к AngularDart: лучший Angular

AngularDart — это порт популярной платформы для платформы Dart. Он разрабатывается основной командой Angular. В этой статье я буду сравнивать версии фреймворка для Dart и JS. В частности, я рассмотрю внедрение зависимостей, директивы и переваривание.

Целевая аудитория

Статья написана для:

  • Разработчики дартс, которые имеют некоторый опыт работы с AngularJS.
  • Разработчики AngularJS думают о том, чтобы попробовать AngularDart.
  • Разработчики AngularJS, которые не собираются переходить на Dart, но хотят узнать больше о будущем фреймворка. Согласно Angular, многие функции AngularDart будут перенесены в AngularJS в какой-то момент. Поэтому изучение Dart-версии фреймворка может быть интересным, даже если вы не планируете его использовать.

Внедрение зависимости

Инъекция по имени VS Инъекция по типу

AngularDart делает интересным использование дополнительной системы типов Dart: она использует информацию о типе для настройки инжектора. Другими словами, инъекция выполняется по типу, а не по имени.

//JS:
// The name here matters, and since it will be minified in production, 
// we have to use the array syntax.
m.factory("usersRepository", ["$http", function($http){
  return {
    all: function(){/* ... */}
  }
}]);


//DART:
class UsersRepository {
  // Only the type here matters, the variable name does not affect DI.
  UsersRepository(Http h){/*...*/}
  all(){/* ... */}
}

Регистрация инъекционных объектов

В AngularJS инъекционный объект может быть зарегистрирован с угловой системой DI , используя filter, directive, controller, value, constant, service, factory, или provider.

методы Цель
фильтр Регистрация фильтров
директива Регистрация директив
контроллер Регистрация контроллеров
значение, постоянное Регистрация объектов конфигурации
сервис, фабрика, поставщик Регистрация услуг

Как видите, существует множество способов зарегистрировать инъецируемый объект, что часто смущает разработчиков. Частично это связано с тем , что filter, directiveи controllerвсе они используются для различных типов объектов, и , следовательно , не взаимозаменяемы. Функции service, factoryи provider, с другой стороны, все используются для регистрации сервисов, причем providerявляются наиболее общими.

AngularDart использует совершенно другой подход: он отделяет тип объекта от того, как он зарегистрирован в системе DI.

Любой объект может быть зарегистрирован с помощью value, typeили factory.

методы Цель
значение, тип, завод Регистрация всех объектов

Это можно сделать следующим образом.

  //DART:

  // The passed in object will be available for injection.
  value(UsersRepositoryConfig, new UsersRepositoryConfig());

  // AngularDart will resolve all the dependencies 
  // and instantiate UsersRepository. 
  type(UsersRepository);

  // AngularDart will call the factory function. 
  // You will have to resolve the dependencies using the passed in injector 
  // and then instantiate UsersRepository.
  factory(UsersRepository, (Injector inj) => new UsersRepository(inj.get(Http)));

Тот факт, что эти функции могут использоваться для регистрации любого объекта, является существенным упрощением API.

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

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

//DART:

@NgController(
    selector: '[users-ctrl]',
    publishAs: 'ctrl'
)
class UsersCtrl {
  UsersCtrl(UsersRepository repo);
}

Точно так же специальные аннотации используются для определения фильтров, компонентов и директив.

В AngularDart тип инъецируемого объекта и способ его регистрации в системе DI — это две ортогональные проблемы.

Создание модулей и начальная загрузка приложения

Ниже приведен стандартный способ создания приложения в AngularJS.

//JS:
var m = angular.module("users", ['common.errors']);
m.service("usersRepository", UsersRepository);
angular.bootstrap(document, ["users"]);

Который сопоставляется довольно близко к AngularDart.

//DART:
final users = new Module()
  ..type(UsersRepository)
  ..install(new CommonErrors());

ngBootstrap(module: users);

Еще один способ сделать это путем расширения Module.

//DART:
class Users extends Module {
  Users(){
    type(UsersRepository);
    install(new CommonErrors())
  }
}

ngBootstrap(module: new Users());

Этот способ предпочтителен, если вы хотите подключить компоненты по-разному, например, в зависимости от платформы, на которой работает приложение.

Настройка объектов для инъекций

AngularJS предоставляет несколько опций для настройки объектов для инъекций. Самый простой — ввести объект конфигурации с помощью value.

//JS:
m.value("usersRepositoryConfig", {login: 'jim', password: 'password'});
m.service("usersRepository", function (usersRepositoryConfig){ 
  //...
});

То же самое можно сделать в дартс.

//DART:
class UsersRepositoryConfig {
  String login;
  String password;
}

class UsersRepository {
  UsersRepository(UsersRepositoryConfig config){/* ... */}
}

type(UsersRepository);
value(UsersRepositoryConfig, new UsersRepositoryConfig()..login="Jim"..password="password");

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

//JS:
m.value("usersRepositoryConfig", {login: 'jim', password: 'password'});
m.factory("usersRepository", function (usersRepositoryConfig){ 
  return new UsersRepository(usersRepositoryConfig.login, usersRepositoryConfig.password);
});

Версия AngularDart, опять же, очень похожа.

//DART:
value(UsersRepositoryConfig, new UsersRepositoryConfig()..login="Jim"..password="password");

factory(UsersRepository, (Injector inj){
  final c = inj.get(UsersRepositoryConfig);
  return new UsersRepository(c.login, c.password);
});

Некоторые предпочитают определять поставщика для этой цели.

//JS:
m.provider("usersRepository", function(){
  var configuration;

  return {
    setConfiguration: function(config){
      configuration = config;
    },

    $get: function($modal){
      return function(){
        return new UsersRepository(configuration);
      }
    }
  };
});

setConfigurationМетод должен быть вызван на этапе конфигурации приложения.

//JS:
m.config(
  function(usersRepositoryProvider){
    usersRepositoryProvider.setConfiguration({login: 'Jim', password: 'password'});
  }
);

Поскольку AngularDart не имеет ни провайдеров, ни явного этапа настройки, пример не может быть напрямую переведен в Dart. Это самое близкое, что я придумал.

//DART:
final users = new Module()..type(UsersRepositoryConfig)
                          ..type(UsersRepository);

Injector inj = ngBootstrap(module: users);

inj.get(UsersRepositoryConfig)..login = "jim"
                              ..password = "password";

Директивы, контроллеры и компоненты

Теперь давайте переключимся и поговорим о другом столпе основы — директивах.

Хотя директивы AngularJS являются чрезвычайно мощными и, как правило, простыми в использовании, определение новой директивы может ввести в заблуждение. Я думаю, что команда Angular осознала это, и поэтому API версии Dart-фреймворка сильно отличается.

В AngularJS есть два типа объектов, используемых для организации взаимодействий пользовательского интерфейса:

  • Директивы инкапсулируют все взаимодействия с DOM. Они декларативны и могут рассматриваться как способ расширения html.
  • Контроллеры являются обязательными. Они не знают о DOM и могут содержать логику приложения.

В AngularJS эти два типа объектов различны: для их регистрации используются разные помощники, а для их определения используются совершенно разные API.

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

Второе изменение — новый тип объекта — component. В AngularDart директивы в основном используются для расширения элементов DOM. Когда вы хотите определить новый пользовательский элемент, вы используете компоненты.

Давайте посмотрим на несколько примеров.

Директивы

vs-matchДиректива может быть применена ко входному элементу. Он слушает изменения в этом элементе, и когда значение соответствует предоставленному шаблону, директива добавит matchкласс к элементу.

Может использоваться следующим образом:

<input type="text" vs-match="^\d\d$">

Это очень простая реализация описанной директивы AngularJS:

//JS:
directive("vsMatch", function(){
  return {
    restrict: 'A',

    scope: {pattern: '@vsMatch'},

    link: function(scope, element){
      var exp = new RegExp(scope.pattern);
      element.on("keyup", function(){
        exp.test(element.val()) ?  
          element.addClass('match') : 
          element.removeClass('match');
      });
    }
  };
});

Теперь давайте сравним это с версией AngularDart.

//DART:
@NgDirective(selector: '[vs-match]')
class Match implements NgAttachAware{
  @NgAttr("vs-match") 
  String pattern;

  Element el;

  Match(this.el);

  attach(){
    final exp = new RegExp(pattern);
    el.onKeyUp.listen((_) =>
      exp.hasMatch(el.value) ? 
        el.classes.add("match") : 
        el.classes.remove("match"));
  }
}

Позвольте мне провести вас через это:

  • NgDirective говорит Angular, что этот класс является директивой.
  • selectorСвойство определяет , когда эта директива включена. В этом случае это когда элемент имеет vs-match атрибут.
  • В дополнение к возможности внедрения любого сервиса в директиву, вы также можете внедрить элемент, к которому применяется директива. Это то, что Match(this.el)делает.
  • Привязки могут быть установлены путем передачи карты, аналогичной AngularJS. Но вы также можете сделать это, используя аннотации, которые я лично нахожу намного проще для чтения и понимания.
  • Когда вызывается конструктор директивы, значение шаблона еще не было привязано. Решение заключается в реализации NgAttachAware. Он предоставляет attachметод, который будет вызван, когда digestпроизойдет следующее . В этот момент все сопоставления атрибутов обрабатываются, поэтому я могу безопасно построить регулярное выражение.
  • Наконец, нет никаких ссылок или функций компиляции.

Компоненты

Компонент, который мы рассмотрим далее, переключает видимость его содержимого, и его можно использовать следующим образом:

<toggle button="Toggle">
  <p>Inside</p>
</toggle>

Это AngularJS реализация этого компонента:

//JS:
directive("toggle", function(){
  return {
    restrict: 'E',

    replace: true,
    transclude: true,

    scope: {button: '@'},

    template: "<div><button ng-click='toggle()'>{{button}}</button><div ng-transclude ng-if='showContent'/></div>",

    controller: function($scope){
      $scope.showContent  = false;
      $scope.toggle = function(){
        $scope.showContent  = !$scope.showContent ;
      };
    }
  }
})

Теперь давайте сопоставим это с версией Dart:

//DART:
@NgComponent(
  selector: "toggle",
  publishAs: 't',
  template: "<button ng-click='t.toggle()'>{{t.button}}</button><content ng-if='t.showContent'/>"
)
class Toggle {
  @NgAttr("button")
  String button;

  bool showContent = false;
  toggle() => showContent = !showContent;
}
  • NgComponent говорит Angular, что этот класс является компонентом.
  • publishAsэто имя, которое мы можем использовать в шаблоне для доступа к toggleобъекту. Стоит отметить, что tдоступно только в шаблоне этого компонента, а не во вставленном контенте.
  • templateНе удивительно, определяет, как этот пользовательский элемент отображается.

Хотя версии JS и Dart выглядят одинаково, под капотом есть важные различия.

Компонент AngularDart использует теневой DOM для визуализации своего шаблона.

AngularJS:

AngularDart:

Shadow DOM предоставляет нам инкапсуляцию DOM и CSS, которая отлично подходит для создания повторно используемых компонентов. Кроме того, API был изменен в соответствии со спецификациями веб-компонентов (например, ng-transcludeбыл заменен на content).

Компонент AngularDart использует элемент шаблона для хранения своего шаблона.

Это устраняет необходимость таких взломов, как ng-src.

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

Контроллеры

В следующем примере показан очень простой контроллер, реализованный в AngularJS.

//JS:
<div ng-controller="CompareCtrl as ctrl">
  First <input type="text" ng-model="ctrl.firstValue">
  Second <input type="text" ng-model="ctrl.secondValue">

  {{ctrl.valuesAreEqual()}}
</div>


controller("CompareCtrl", function(){
  this.firstValue = "";
  this.secondValue = "";

  this.valuesAreEqual = function(){
    return this.firstValue == this.secondValue;
  };
});

Версия Dart совсем другая.

//DART:
<div compare-ctrl>
  First <input type="text" ng-model="ctrl.firstValue">
  Second <input type="text" ng-model="ctrl.secondValue">

  {{ctrl.valuesAreEqual}}
</div>

@NgController(
  selector: "[compare-ctrl]",
  publishAs: 'ctrl'
)
class CompareCtlr {
  String firstValue = "";
  String secondValue = "";

  get valuesAreEqual => firstValue == secondValue;
}

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

фильтры

Наконец, давайте посмотрим, как вы можете определить фильтр.

//JS:
filter("isBlank", function(){
  return function(value){
    return value.length == 0;
  };
});

и версия Dart:

//DART:
@NgFilter(name: 'isBlank')
class IsBlank {
  call(value) => value.isEmpty;
}

Зоны и $ scope. $ Apply

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

Нет необходимости звонить $scope.$applyпри интеграции со сторонними компонентами.

Позвольте мне проиллюстрировать это следующим примером.

<div ng-controller="CountCtrl as ctrl">
  {{ctrl.count}}
</div>

CountCtrl это контроллер, который просто увеличивает countпеременную.

//JS:
controller("CountCtrl", function(){
  var c = this;
  this.count = 1;

  setInterval(function(){
    c.count ++;
  }, 1000);
})

Опытный разработчик AngularJS сразу заметит, что код не работает. Angular просто не может видеть, что countпеременная была изменена в обратном вызове. Чтобы исправить эту проблему, вы должны обернуть ее $scope.$applyследующим образом:

//JS:
controller("CountCtrl", function($scope){
  var c = this;
  this.count = 1;

  setInterval(function(){
    $scope.$apply(function(){
      c.count ++;
    });
  }, 1000);
})

Это фундаментальное ограничение AngularJS — вам нужно сообщить Angular для проверки изменений. Фреймворки стараются свести к минимуму количество мест, где вам нужно это сделать, связав библиотеку фьючерсов и предоставив $intervalсервис. Но в тот момент, когда вы начнете использовать какую-то другую библиотеку фьючерсов или, в общем случае, будете интегрироваться с асинхронными сторонними компонентами, вам придется это использовать $scope.$apply.

Теперь давайте сопоставим это с версией Dart.

//DART:
<div count-ctrl>
  {{ctrl.count}}
</div>

@NgController(
    selector: "[count-ctrl]",
    publishAs: 'ctrl'
)
class CountCtrl {
  num count = 0;

  CountCtrl(){
    new Timer.periodic(new Duration(seconds: 1), (_) => count++);
  }
}

Версия Dart работает, хотя ее нет $apply, и Timerничего не знает об Angular. Это фантастика! Чтобы понять, как это работает, нам нужно узнать о концепции зон.

Dart Docs: A Zone represents the asynchronous version of a dynamic extent. Asynchronous callbacks are executed in the zone they have been queued in. For example, the callback of a future.then is executed in the same zone as the one where the then was invoked.

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

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

Завершение

  • API AngularDart основаны на классах.
  • Фреймворк использует инъекцию по типу вместо инъекции по имени.
  • Типы объектов отделены от того, как они зарегистрированы в системе DI.
  • Аннотации, такие как NgControllerи NgDirective, используются для настройки объектов для инъекций.
  • Директивы, фильтры, компоненты, контроллеры и услуги все могут быть зарегистрированы с помощью value, typeи factory.
  • Директивы используются для расширения элементов DOM.
  • Компоненты — это облегченная версия веб-компонентов, и они используются для создания пользовательских элементов.
  • Компоненты используют теневой DOM для визуализации своих шаблонов.
  • Контроллеры — это директивы, которые создают новую область видимости для элемента, к которому они применяются.
  • Прицел автоматически переваривается через зоны дротиков, что исключает необходимость scope.$apply.

Что будет перенесено на JS

Основываясь на выступлении Миско и Игоря в Devoxx, похоже, что большинство изменений будут перенесены в AngularJS, в частности:

  • Тип на основе инъекций
  • Использование аннотаций для определения объектов
  • Использование теневого DOM
  • зон

Учить больше

Я надеюсь, что эта статья даст вам достаточно информации, чтобы начать. Если вы хотите узнать больше, проверьте: