Статьи

Ember Components: Глубокое погружение

Ember.js — это JavaScript MVC-инфраструктура, которая позволяет разработчикам создавать амбициозные веб-приложения. Хотя чистый MVC позволяет разработчику разделять задачи, он не предоставляет вам все инструменты, и вашему приложению потребуются другие конструкции. Сегодня я собираюсь поговорить об одной из этих конструкций. Компоненты Ember — это, по сути, песочница для многократного использования. Если вы не знакомы с Ember, ознакомьтесь с разделом Начало работы с Ember.js или с курсом Let’s Learn Ember . В этом руководстве мы рассмотрим спецификацию веб-компонентов, узнаем, как написать компонент в Ember, поговорим о композиции, объясним разницу между представлением Ember и компонентом Ember, а также попрактикуемся в интеграции плагинов с компонентами Ember.


Компоненты Ember основаны на спецификации веб-компонентов W3C . Спецификация состоит из четырех меньших спецификаций; шаблоны, декораторы, теневой DOM и пользовательские элементы. Из этих четырех концепций только три из них имеют жесткие спецификации, исключение составляют декораторы. Благодаря наличию спецификаций разработчики фреймворка смогли заполнить эти новые API до того, как они будут внедрены поставщиками браузеров.

Есть несколько важных понятий, которые нужно понять при обсуждении компонентов:

  • Компоненты ничего не знают о внешнем мире, если явно не переданы в
  • Компоненты должны иметь четко определенный интерфейс с внешним миром
  • Компоненты не могут манипулировать любым JavaScript за пределами компонента
  • Компоненты могут транслировать события
  • Пользовательские элементы должны быть разделены именами с дефисом
  • Снаружи JavaScript не может манипулировать компонентами

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

Диаграмма веб-компонентов

В то время как Ember успешно заполнил много спецификаций, фреймворки, такие как AngularJS , Dart , Polymer и Xtags, имеют аналогичные решения. Единственное предостережение здесь заключается в том, что Ember и Angular в настоящее время не применяют стили к компоненту. Со временем эти решения для полифилла исчезнут, и фреймворки примут реализацию браузера. Это принципиально иной подход к разработке, поскольку мы можем использовать преимущества будущих спецификаций, не привязываясь к экспериментальным функциям в браузерах.


Зная наши знания о веб-компонентах, давайте реализуем базовый компонент my-name сверху, но на Ember. Давайте начнем с загрузки Ember Starter Kit с сайта Ember . На момент написания данного руководства версия Ember 1.3.0. После загрузки откройте файлы в вашем любимом редакторе, удалите все шаблоны в index.html (обозначается как data-template-name) и все в app.js

Первое, что мы собираемся сделать, это создать шаблон компонента. Для этого урока мы будем использовать встроенные шаблоны. Это можно сделать, написав следующее в своем файле index.html . Нам также нужно создать новое приложение Ember в нашем JavaScript.

1
2
3
4
5
6
7
8
9
<script type=»text/x-handlebars»>
   {{my-name}}
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/my-name»>
// My component template will go here
</script>
 
var App = Ember.Application.create();

Вы заметите, что data-template-name имеет путь вместо простой строки. Причина, по которой мы добавляем к имени нашего компонента "components/" заключается в том, чтобы сообщить Эмбер, что мы имеем дело с шаблоном компонента, а не с обычным шаблоном приложения. Вы также заметите, что в имени компонента есть дефис. Это пространство имен, которое я упомянул в спецификации веб-компонентов. Пространство имен сделано так, чтобы у нас не было конфликтов имен с существующими тегами.

Если мы откроем браузер, мы не увидим ничего другого. Причина этого в том, что нам еще ничего не нужно помещать в наш шаблон my-name. Давайте позаботимся об этом.

1
2
3
4
<script type=»text/x-handlebars» data-template-name=»components/my-name»>
   Hi, my name is {{name}}.
</script>
Ember Name Component

Теперь в браузере вы должны увидеть что-то похожее на изображение выше. Мы еще не закончили, как вы можете видеть, мы на самом деле не печатаем имя. Как я упоминал в первом разделе, компоненты должны предоставлять четко определенный интерфейс для внешнего мира. В данном случае нас интересует название. Итак, давайте передадим имя, поместив атрибут name в компонент my-name.

1
2
3
4
<script type=»text/x-handlebars»>
   {{my-name name=»Chad»}}
</script>

Когда вы обновите страницу, вы должны увидеть «Привет, меня зовут Чад» . Все это с написанием одной строки JavaScript. Теперь, когда у нас есть чувство написания базового компонента, давайте поговорим о разнице между компонентами Ember и представлениями Ember.


Ember — это MVC, поэтому некоторые могут подумать: «Почему бы просто не использовать представление для этого?» Это законный вопрос. Компоненты на самом деле являются подклассом Ember.View, самое большое отличие здесь заключается в том, что представления обычно находятся в контексте контроллера. Возьмите пример ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
App.IndexController = Ember.Controller.extend({
  myState: ‘on’
});
 
App.IndexView = Ember.View.extend({
 
  click: function () {
    var controller = this.get( ‘controller’ ),
    myState = controller.get( ‘myState’ );
 
    console.log( controller ) // The controller instance
    console.log( myState ) // The string «on»
  }
 
});
1
2
3
<script type=»text/x-handlebars» data-template-name=»index»>
  {{myState}}
</script>

Представления обычно располагаются за шаблоном и превращают необработанный ввод (click, mouseEnter, mouseMove и т. Д.) В семантическое действие (openMenu, editName, hideModal и т. Д.) В контроллере или маршруте. Еще одна вещь, на которую следует обратить внимание: шаблонам также нужен контекст. Таким образом, в конечном итоге происходит то, что Ember выводит контекст через соглашения об именах и URL. Смотрите схему ниже.

Тлеющая Иерархия

Как вы можете видеть, существует уровень иерархии, основанный на URL-адресе, и каждый уровень этой иерархии имеет свой собственный контекст, который определяется соглашениями об именах.

У компонентов Ember нет контекста, они знают только интерфейс, который они определяют. Это позволяет компоненту быть представленным в любом контексте, делая его отделенным и многократно используемым. Если компонент предоставляет интерфейс, работа контекста заключается в том, чтобы выполнить этот интерфейс. Другими словами, если вы хотите, чтобы компонент отображался правильно, вы должны предоставить ему данные, которые он ожидает. Важно отметить, что эти передаваемые значения могут быть как строками, так и связанными свойствами.

Ember иерархия с компонентами

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


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

Ember Group Chat Component

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

Групповой чат

Давайте начнем с создания нового html-файла с именем chat.html и настройки всех зависимостей для Ember. Далее создайте все шаблоны.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<script type=»text/x-handlebars» data-template-name=»application»>
  {{outlet}}
</script>
 
<script type=»text/x-handlebars» data-template-name=»index»>
  {{ group-chat messages=model action=»sendMessage» }}
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/group-chat»>
  <div class=»chat-component»>
    <ul class=»conversation»>
      {{#each message in messages}}
        <li class=»txt»>{{chat-message username=message.twitterUserName message=message.text time=message.timeStamp }}</li>
      {{/each}}
    </ul>
 
    <form class=»new-message» {{action submit on=»submit»}}>
      {{input type=»text» placeholder=»Send new message» value=message class=»txt-field»}}
      {{input type=»submit» class=»send-btn» value=»Send»}}
    </form>
  </div>
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/chat-message»>
  <div class=»message media»>
    <div class=»img»>
      {{user-avatar username=username service=»twitter»}}
    </div>
    <div class=»bd»>
      {{user-message message=message}}
      {{time-stamp time=time}}
    </div>
  </div>
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/user-avatar»>
  <img {{bind-attr src=avatarUrl alt=username}} class=»avatar»>
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/user-message»>
  <div class=»user-message»>{{message}}</div>
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/time-stamp»>
  <div class=»time-stamp»>
    <span class=»clock» role=»presentation»>
    <span class=»time»>{{format-date time}}
  </div>
</script>

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

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

Групповой чат без данных

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

1
2
3
<script type=»text/x-handlebars» data-template-name=»index»>
  {{ group-chat messages=model action=»sendMessage» }}
</script>

В этом случае мы передаем модель из контекста IndexRoute как «messages», и мы установили строку sendMessage в качестве действия над компонентом. Действие будет использоваться для трансляции, когда пользователь хочет отправить новое сообщение. Мы расскажем об этом позже в уроке. Еще одна вещь, которую вы заметите, это то, что мы устанавливаем строгие интерфейсы для вложенных компонентов, все из которых используют данные, передаваемые из интерфейса группового чата.

1
2
3
4
5
6
7
<ul class=»conversation»>
  {{#each message in messages}}
    <li class=»txt»>{{chat-message username=message.twitterUserName message=message.text time=message.timeStamp }}</li>
  {{/each}}
</ul>

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
App = Ember.Application.create();
 
App.IndexRoute = Ember.Route.extend({
  model: function() {
    return [
      {
        id: 1,
        firstName: ‘Tom’,
        lastName: ‘Dale’,
        twitterUserName: ‘tomdale’,
        text: ‘I think we should back old Tomster.
        timeStamp: Date.now() — 400000,
      },
      {
        id: 2,
        firstName: ‘Yehuda’,
        lastName: ‘Katz’,
        twitterUserName: ‘wycats’,
        text: ‘That\’sa good idea.’,
        timeStamp: Date.now() — 300000,
      }
    ];
  }
});

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

Групповой чат частично заполнен данными

С нашим компонентом user-avatar мы хотим использовать сервис под названием Avatars.io для получения аватара пользователя Twitter на основе его имени пользователя в Twitter. Давайте посмотрим, как компонент user-image используется в шаблоне.

1
2
3
4
5
6
7
8
9
<script type=»text/x-handlebars» data-template-name=»components/chat-message»>
{{ user-avatar username=username service=»twitter» }}
</script>
 
<script type=»text/x-handlebars» data-template-name=»components/user-avatar»>
  <img {{bind-attr src=avatarUrl alt=username}} class=»avatar»>
</script>

Это довольно простой компонент, но вы заметите, что у нас есть связанное свойство с именем avatarUrl . Нам нужно создать это свойство в нашем JavaScript для этого компонента. Еще одна вещь, которую вы заметите, это то, что мы указываем сервис, с которого мы хотим получить аватар. Avatars.io позволяет вам получать социальные аватары из Twitter, Facebook и Instagram. Мы можем сделать этот компонент чрезвычайно гибким. Давайте напишем компонент.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
App.UserAvatarComponent = Ember.Component.extend({
  avatarUrl: function () {
    var username = this.get( ‘username’ ),
          service = this.get( ‘service’ ),
          availableServices = [ ‘twitter’, ‘facebook’, ‘instagram’ ];
 
    if ( availableServices.indexOf( service ) > -1 ) {
       return ‘http://avatars.io/’ + service + ‘/’ + username;
    }
    return ‘images/cat.png’;
 
  }.property( ‘username’ , ‘service’ )
 
});

Как видите, для создания нового компонента мы просто следуем соглашению об именах NAMEOFCOMPONENTComponent и расширяем Ember.Component . Теперь, если мы вернемся к браузеру, мы должны увидеть наши аватары.

Групповой чат без форматированной даты

Чтобы позаботиться о форматировании даты, давайте использовать moment.js и напишем помощник Handlebars для форматирования даты для нас.

1
2
3
Ember.Handlebars.helper(‘format-date’, function( date ) {
  return moment( date ).fromNow();
});

Теперь все, что нам нужно сделать, это применить помощник к нашему компоненту отметки времени.

1
2
3
4
5
6
<script type=»text/x-handlebars» data-template-name=»components/time-stamp»>
  <div class=»time-stamp»>
    <span class=»clock» role=»presentation»>
    <span class=»time»>{{format-date time}}
  </div>
</script>

Теперь у нас должен быть компонент, который форматирует даты, а не временные метки эпохи Unix.

Групповой чат с датами

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
App.TimeStampComponent = Ember.Component.extend({
 
  startTimer: function () {
    var currentTime = this.get(‘time’);
    this.set(‘time’, currentTime — 6000 );
    this.scheduleStartTimer();
  },
 
  scheduleStartTimer: function(){
    this._timer = Ember.run.later(this, ‘startTimer’, 6000);
  }.on(‘didInsertElement’),
 
  killTimer: function () {
    Ember.run.cancel( this._timer );
  }.on( ‘willDestroyElement’ )
 
});

Здесь нужно отметить пару моментов. Одним из них является синтаксис обработчика событий on() . Это было введено в Ember до релиза 1.0. Он делает именно то, что вы думаете, когда компонент метки времени вставляется в DOM, вызывается scheduleStartTime . Когда элемент собирается уничтожить и очистить, будет killTimer метод killTimer . Остальная часть компонента просто сообщает время для обновления каждую минуту.

Ember.run вещь, которую вы заметите, это то, что есть несколько звонков на Ember.run . В Ember есть система очередей, обычно называемая циклом выполнения, которая сбрасывается при изменении данных. Это делается для того, чтобы объединить изменения и сделать их один раз. В нашем примере мы будем использовать Ember.run.later для запуска метода startTimer каждую минуту. Мы также будем использовать Ember.run.cancel для отключения таймера. По сути, это собственные методы начала и окончания интервалов Ember. Они необходимы для синхронизации системы очередей. Для получения дополнительной информации о цикле выполнения я предлагаю прочитать статью Алекса Матчнера «Все, что вы никогда не хотели знать о Ember Run Loop» .

Следующее, что нам нужно сделать, это настроить действие так, чтобы, когда пользователь нажимает «Отправить», было создано новое сообщение. Наш компонент не должен заботиться о том, как создаются данные, он должен просто сообщить, что пользователь попытался отправить сообщение. Наш IndexRoute будет нести ответственность за принятие этого действия и превращение во что-то значимое.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
App.GroupChatComponent = Ember.Component.extend({
  message: »,
  actions: {
    submit: function () {
      var message = this.get( ‘message’ ).trim(),
          conversation = this.$( ‘ul’ )[ 0 ];
 
      // Fetches the value of ‘action’
      // and sends the action with the message
      this.sendAction( ‘action’, message );
 
      // When the Ember run loop is done
      // scroll to the bottom
      Ember.run.schedule( ‘afterRender’, function () {
        conversation.scrollTop = conversation.scrollHeight;
      });
 
      // Reset the text message field
      this.set( ‘message’, » );
    }
  }
});
1
2
3
4
<form class=»new-message» {{action submit on=»submit»}}>
  {{input type=»text» placeholder=»Send new message» value=message class=»txt-field»}}
  {{input type=»submit» class=»send-btn» value=»Send»}}
</form>

Поскольку компонент группового чата владеет кнопкой ввода и отправки, нам нужно реагировать на нажатие кнопки «Отправить» на этом уровне абстракции. Когда пользователь нажимает кнопку отправки, он выполняет действие отправки в реализации нашего компонента. В обработчике действия отправки мы собираемся получить значение сообщения, которое задается вводом текста. Затем мы отправим действие вместе с сообщением. Наконец, мы сбросим сообщение до пустой строки.

Другая странная вещь, которую вы видите здесь, это Ember.run.schedule метод Ember.run.schedule . Еще раз, это цикл выполнения Ember в действии. Вы заметите, что расписание принимает строку в качестве первого аргумента, в данном случае «afterRender». У Ember на самом деле есть несколько разных очередей, которыми он управляет, и это одна из них. Таким образом, в нашем случае мы говорим, что когда отправка сообщения завершена, выполняются какие-либо манипуляции, и после того, как очередь рендеринга очищена, вызовите наш обратный вызов. Это прокрутит нашу ul to the bottom so the user can see the new message after any manipulations. For more on the run loop, I suggest reading Alex Matchneer's article "Everything You Never Wanted to Know About the Ember Run Loop" . ul to the bottom so the user can see the new message after any manipulations. For more on the run loop, I suggest reading Alex Matchneer's article "Everything You Never Wanted to Know About the Ember Run Loop" .

Если мы перейдем в браузер и нажмем кнопку отправки, мы получим действительно приятную ошибку от Ember: «Uncaught Error: Ничто не обработало событие« sendMessage ». Это то, что мы ожидаем, потому что мы не сообщили нашему приложению о том, как к реакции на эти типы событий. Давайте это исправим.

01
02
03
04
05
06
07
08
09
10
App.IndexRoute = Ember.Route.extend({
 /* … */
  actions: {
   sendMessage: function ( message ) {
      if ( message !== ») {
    console.log( message );
      }
   }
 }
});

Теперь, если мы вернемся в браузер, наберем что-нибудь в поле ввода сообщения и нажмем кнопку «отправить», мы должны увидеть сообщение в консоли. Таким образом, в этот момент наш компонент слабо связан и взаимодействует с остальным нашим приложением. Давайте сделаем что-то более интересное с этим. Сначала давайте создадим новый Ember.Object будет служить моделью для нового сообщения.

1
2
3
4
5
6
7
8
App.Message = Ember.Object.extend({
  id: 3,
  firstName: ‘Chad’,
  lastName: ‘Hietala’,
  twitterUserName: ‘chadhietala’,
  text: null,
  timeStamp: null
});

Поэтому, когда sendMessage действие sendMessage мы хотим заполнить поле text и timeStamp нашей модели Message, создать новый его экземпляр, а затем вставить этот экземпляр в существующую коллекцию сообщений.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
App.IndexRoute = Ember.Route.extend({
/* … */
  actions: {
    sendMessage: function ( message ) {
      var user, messages, newMessage;
 
      if ( message !== » ) {
 
        messages = this.modelFor( ‘index’ ),
        newMessage = App.Message.create({
          text: message,
          timeStamp: Date.now()
        })
 
        messages.pushObject( newMessage );
      }
    }
  }
});

Когда мы вернемся к браузеру, мы сможем создавать новые сообщения.

Групповой чат Создание сообщений

Теперь у нас есть несколько различных многократно используемых блоков пользовательского интерфейса, которые мы можем разместить где угодно. Например, если вам нужно было использовать аватар где-то еще в приложении Ember, мы можем повторно использовать компонент user-avatar.

1
2
3
4
5
6
<script type=»text/x-handlebars» data-template-name=»index»>
{{user-avatar username=»horse_js» service=»twitter» }}
{{user-avatar username=»detroitlionsnfl» service=»instagram» }}
{{user-avatar username=»KarlTheFog» service=»twitter» }}
</script>
Аватары пользователей из Twitter и Instagram

В этот момент вы, вероятно, задаетесь вопросом «Что если я захочу использовать какой-нибудь плагин jQuery в своем компоненте?» Нет проблем. Для краткости давайте изменим наш компонент user-avatar, чтобы отображать подсказку при наведении курсора на аватар. Я решил использовать всплывающую подсказку плагина jQuery для обработки всплывающей подсказки. Давайте изменим существующий код, чтобы использовать всплывающую подсказку.

Сначала добавим правильные файлы в наш chat.html и chat.html существующий компонент аватара пользователя.

1
2
3
4
5
6
7
<link href=»css/tooltipster.css» rel=»stylesheet» />
 
<script type=»text/JavaScript» src=»js/libs/jquery.tooltipster.min.js»></script>
<script type=»text/JavaScript» src=»js/app.js»></script>

И тогда наш JavaScript:

01
02
03
04
05
06
07
08
09
10
11
12
13
App.UserAvatarComponent = Ember.Component.extend({
  /*…*/
  setupTooltip: function () {
    this.$( ‘.avatar’ ).tooltipster({
      animation: ‘fade’
    });
  }.on( ‘didInsertElement’ ),
 
  destroyTooltip: function () {
    this.$( ‘.avatar’ ).tooltipster( ‘destroy’ );
  }.on( ‘willDestroyElement’ )
 
)};

Еще раз мы видим синтаксис декларативного прослушивателя событий, но впервые видим this.$ . Если вы знакомы с jQuery, вы ожидаете, что мы будем запрашивать все элементы с классом «аватар». В Ember это не так, потому что применяется контекст. В нашем случае мы ищем только элементы с классом ‘avatar’ в компоненте user-avatar. Это сопоставимо с методом поиска jQuery. После уничтожения элемента мы должны отменить привязку к аватору на аватаре и очистить любую функциональность, это делается передачей ‘destroy’ подсказке инструмента. Если мы зайдем в браузер, обновим и наведем курсор на изображение, то увидим имя пользователя.

Подсказки аватара

В этом руководстве мы подробно рассмотрели компоненты Ember и показали, как можно использовать повторно используемые блоки пользовательского интерфейса для создания более крупных композитов и интеграции плагинов jQuery. Мы смотрели на то, как компоненты отличаются от представлений в Ember. Мы также рассмотрели идею интерфейсного программирования, когда речь заходит о компонентах. Надеюсь, мне удалось пролить некоторый свет не только на Ember Components, но и на Web-компоненты и на то, куда движется сеть.