Руководство по Vue.js для разработчиков jQuery

Введение в написание компонентов Vue.js для разработчиков с опытом работы с jQuery.

Сегодня мы собираемся создать простой компонент Vue.js на основе раскрывающейся кнопки jQuery, чтобы изучить наиболее важные различия между этими технологиями.

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

Смешивание Vue.js и jQuery

Прежде чем мы начнем, я хотел бы ответить на следующий вопрос: нельзя ли смешивать Vue.js и jQuery на одной странице?

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

Но каждая библиотека в вашем комплекте добавляет определенную стоимость. Это может показаться небольшим, jQuery занимает около 95 килобайт, а время выполнения Vue.js - около 55 килобайт. Однако все это увеличивает общий размер вашего приложения и увеличивает время, необходимое для загрузки и анализа кода.

Также у вас может возникнуть соблазн использовать jQuery для управления DOM в ваших компонентах Vue.js, потому что это может показаться проще или это может быть просто то, к чему вы привыкли.

Однако Vue.js имеет внутреннее представление всех элементов DOM, называемое виртуальным DOM. Таким образом, фактическую модель DOM не нужно анализировать каждый раз при внесении изменений. Когда вы управляете DOM с помощью jQuery (или напрямую с помощью DOM API), он больше не соответствует виртуальному представлению, используемому Vue.js. Это может привести к неожиданному поведению.

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

Простой раскрывающийся компонент

Я покажу вам, как написать наш собственный простой компонент Vue.js. Я буду использовать раскрывающийся список Boostrap в качестве примера, потому что он прост для понимания.

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

Разметка HTML раскрывающегося списка выглядит следующим образом:

<div class="dropdown">
  <button class="btn btn-default dropdown-toggle"
          type="button"
          data-toggle="dropdown"
          aria-haspopup="true"
          aria-expanded="false">
    My button
  </button>
  <ul class="dropdown-menu">
    <li><a href="#">Item 1</a></li>
    <li><a href="#">Item 2</a></li>
    <li><a href="#">Item 3</a></li>
  </ul>
</div>

Это просто кнопка с изначально невидимым раскрывающимся меню. Нажатие кнопки включает и выключает меню. Внешний <div> объединяет эти элементы вместе, чтобы обеспечить правильное расположение меню.

Если вы не знакомы с Bootstrap, он выглядит так:

Импорт стилей

Для простоты мы включим таблицу стилей Bootstrap в наш комплект, поэтому нам не нужно сосредотачиваться на написании CSS. Просто установите модуль Bootstrap с помощью npm и добавьте следующую строку в свой main.js файл:

import 'bootstrap/less/bootstrap.less'

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

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

Реализация на Vue.js

Давайте создадим файл с именем DropdownButton.vue. В нем будет шаблон нашего компонента:

<template>
  <div class="dropdown" v-bind:class="{ open }">
    <button ref="button" type="button"
            v-bind:class="[ 'btn', btnClass, 'dropdown-toggle' ]"
            aria-haspopup="true"
            v-bind:aria-expanded="open ? 'true' : 'false'"
            v-on:click="toggle"
            v-on:keydown="keyDown">
      {{ text }}
    </button>
    <div v-if="open" class="dropdown-backdrop" v-on:click="close">
    </div>
    <ul ref="menu"
        class="dropdown-menu"
        v-on:click="close"
        v-on:keydown="keyDown">
      <slot></slot>
    </ul>
  </div>
</template>

Как видите, она очень похожа на исходную разметку HTML с некоторыми важными отличиями:

  • Оболочка <div> имеет необязательный класс open. Он добавляется только в том случае, если для свойства open компонента установлено значение true.
  • Класс btn-default кнопки заменяется свойством btnClass компонента.
  • Атрибут aria-expanded устанавливается в значение true или false в зависимости от свойства open.
  • Текст кнопки заменяется свойством text.
  • Есть дополнительный элемент dropdown-backdrop, который вставляется в DOM только тогда, когда open истинно.
  • Содержимое раскрывающегося меню заменяется элементом-заполнителем <slot>.
  • Некоторые функции назначены событиям click и keydown на кнопке и элементе меню.
  • Элементы кнопки и меню имеют атрибут ref.

Чтобы зарегистрировать этот компонент в Vue.js, добавьте следующий код в main.js:

import DropdownButton from '@/components/DropdownButton'
Vue.component( 'dropdown-button', DropdownButton );

Теперь мы можем вставить его в другие компоненты Vue.js, используя следующую упрощенную разметку:

<dropdown-button text="My Button">
  <li><a href="#">Item 1</a></li>
  <li><a href="#">Item 2</a></li>
  <li><a href="#">Item 3</a></li>
</dropdown-button>

Пользовательский элемент <dropdown-button> автоматически заменяется шаблоном компонента. Элементы дочернего списка вставляются вместо элемента <slot>. Такая краткость - первое существенное преимущество использования Vue.js.

Императивное и декларативное программирование

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

if ( parentEl.hasClass( 'open' ) ) {
  el.attr( 'aria-expanded', 'false' );
  parentEl.removeClass( 'open' );
  $( '.dropdown-backdrop' ).remove();
} else {
  el.attr( 'aria-expanded', 'true' );
  parentEl.addClass( 'open' );
  $( '<div>' ).addClass( 'dropdown-backdrop' ).insertAfter( el );
}

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

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

В Vue.js свойства данных компонента представляют его фактическое состояние, поэтому их иногда называют «единственным источником истины». Компонент - это просто виртуальный объект в памяти. Свойства различных элементов DOM выводятся из этих исходных свойств. Всякий раз, когда вы меняете свойство компонента, вся DOM автоматически обновляется.

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

Итак, в Vue.js весь код, который переключает состояние меню, можно записать так:

this.open = !this.open;

Код компонента

Давайте добавим код в наш компонент:

<script>
export default {
  props: {
    btnClass: { type: String, default: 'btn-default' },
    text: String
  },
  data() {
    return {
      open: false
    }
  },
  methods: {
    toggle() {
      this.open = !this.open;
    },
    close() {
      this.open = false;
    }
  }
}
</script>

Компонент имеет два свойства. btnClass по умолчанию 'btn-default' и text по умолчанию неопределенное значение. Значения этих свойств указываются как атрибуты при создании компонента, например:

<dropdown-button text="My Button">
  ... items ...
</dropdown-button>

Это создает кнопку с текстом 'My Button'. Он также использует класс кнопки по умолчанию, 'btn-default', потому что нет атрибута btn-class.

Функция data() возвращает начальное состояние компонента. Состояние состоит из одного свойства open. Изначально для него установлено значение false, и его можно изменить, просто присвоив другое значение. Это то, что делают два метода, toggle() и close().

Методы вызываются событиями щелчка, поэтому меню можно переключать, щелкая по кнопке. Когда меню открыто, создается элемент фона, который обнаруживает щелчки мыши вне раскрывающегося списка и закрывает меню. Фон - это просто полностью прозрачный <div> элемент, заполняющий всю страницу.

Обращение с клавиатурой

Добавить поддержку клавиатуры немного сложнее. Обработка фокуса элементов DOM требует непосредственного взаимодействия с элементами DOM, потому что фокус может быть изменен не только нашим кодом, но и пользователем, например, с помощью клавиши Tab. Может возникнуть соблазн использовать для этого jQuery, но я покажу вам, что писать код без него не так уж и сложно.

Вот реализация обработчика клавиатуры, которая имитирует поведение раскрывающегося списка Bootstrap:

keyDown( e ) {
  if ( e.keyCode == 38 || e.keyCode == 40 ) { // up and down keys
    if ( !this.open ) {
      this.open = true;
    } else {
      const items = this.$refs.menu.querySelectorAll( 'li a' );
      if ( items.length > 0 ) {
        let index = Array.prototype.indexOf.call( items, e.target );
        if ( e.keyCode == 38 && index > 0 )
          index--;
        else if ( e.keyCode == 40 && index < items.length - 1 )
          index++;
        if ( index < 0 )
          index = 0;
        items[ index ].focus();
      }
    }
    e.preventDefault();
  } else if ( e.keyCode == 27 ) { // Esc key
    this.$refs.button.focus();
    this.close();
  }
}

Некоторые элементы в шаблоне нашего компонента имеют атрибут ref. Мы можем получить к ним доступ напрямую, используя объект $refs.

Нам нужно найти все дочерние элементы ссылки в раскрывающемся списке. Вместо jQuery мы можем использовать стандартный метод querySelectorAll(). Он работает во всех современных браузерах и поддерживает селекторы CSS, известные из jQuery.

Нам также нужно найти в списке индекс текущего элемента с фокусом (который вызвал событие клавиатуры). В jQuery мы будем использовать метод index(), который можно заменить методом indexOf() из Array.

Коллекция, возвращаемая querySelectorAll(), - это NodeList, а не Array. Но мы все еще можем вызвать этот метод, используя прототип Array. Вы также можете вызвать другие методы массива, используя этот простой трюк.

Код меняет фокус на предыдущий или следующий элемент списка при нажатии клавиши со стрелкой вверх или вниз или открывает меню, если оно все еще закрыто. Нажатие Esc закрывает меню и снова устанавливает фокус на кнопку. Мы также вызываем e.preventDefault(), чтобы предотвратить прокрутку страницы вверх или вниз.

Динамическое создание компонентов Vue.js

Когда мы регистрируем наш компонент с помощью Vue.component(), мы можем использовать его где угодно в шаблонах других компонентов Vue.js.

Но что, если мы хотим использовать его на странице, которая создается на стороне сервера, например, с помощью кода PHP? Это может быть полезно, если вы хотите использовать компоненты Vue.js так же, как традиционные компоненты jQuery.

Проблема в том, что Vue.js не обрабатывает теги <dropdown-button> в теле страницы. Это работает только с другими компонентами Vue.js. Вместо этого браузер попытается отобразить его как обычный элемент <div>, и вы просто увидите список элементов:

Чтобы заменить <dropdown-button> элементов на странице фактическими компонентами, мы должны найти все вхождения этих элементов, извлечь из них информацию и создать соответствующие компоненты. Есть несколько способов сделать это.

Самый простой способ - извлечь разметку элемента и преобразовать ее в динамически скомпилированный компонент Vue.js:

const elements = document.querySelectorAll( 'dropdown-button' );
for ( let i = 0; i < elements.length; i++ ) {
  const element = elements[ i ];
  const compiled = Vue.compile( element.outerHTML );
  new Vue( {
    el: element,
    render: compiled.render
  } );
}

Цикл перебирает все <dropdown-button> элементов. Код извлекает разметку outerHTML из каждого элемента. Он компилирует его в функцию рендеринга и создает новый компонент Vue.js, который использует эту функцию и заменяет исходный элемент.

Это очень мощный механизм, но нужно быть очень осторожным. Компилятор интерпретирует HTML как шаблон Vue.js, что означает, что, например, фигурные скобки {{ }} можно использовать для вставки любого выражения JavaScript. Это может привести к неожиданному поведению, и даже возможна XSS-атака (Vue-инъекция?), Когда пользователь намеренно вставляет такое выражение.

Кроме того, для использования Vue.compile() требуется полная версия сценария Vue.js, которая немного больше, чем среда выполнения Vue.js.

Другое решение - вручную проанализировать содержимое элемента, извлечь всю необходимую нам информацию и передать ее функции рендеринга:

const elements = document.querySelectorAll( 'dropdown-button' );
for ( let i = 0; i < elements.length; i++ ) {
  const element = elements[ i ];
  const text = element.getAttribute( 'text' );
  const links = element.querySelectorAll( 'li a' );
  const items = [];
  for ( let j = 0; j < links.length; j++ ) {
    items.push( {
      text: links[ j ].innerText,
      href: links[ j ].getAttribute( 'href' )
    } );
  }
  new Vue( {
    el: element,
    render( h ) {
      return h( DropdownButton, {
        props: { text },
        scopedSlots: {
          default: props => items.map( item => h( 'li', [
            h( 'a', {
              attrs: { href: item.href }
            }, item.text )
          ] ) )
        }
      } );
    }
  } );
}

Код извлекает значение атрибута text каждого элемента. Затем он находит все дочерние ссылки, и внутренний цикл заполняет массив items, содержащий текст и URL-адрес каждой ссылки.

Затем создается новый компонент Vue.js, который заменяет исходный элемент. Наша вручную созданная функция рендеринга создает компонент DropdownButton, передавая извлеченный текст как его свойство text. Он также использует слот по умолчанию для вставки элементов списка со ссылками, которые соответствуют извлеченным элементам.

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

Преобразование HTML в компоненты Vue.js

В большинстве случаев HTML-код, отображаемый на стороне сервера, будет содержать обычную разметку с кнопкой и меню, а не настраиваемый элемент <dropdown-button>.

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

Простота Bootstrap заключается в том, что сценарий просто добавляет некоторое динамическое поведение к существующей разметке, а не полностью заменяет ее. Однако аналогичного эффекта можно добиться и при использовании Vue.js.

Мы можем немного изменить код, показанный выше, чтобы преобразовать стандартную разметку выпадающего меню в наш компонент Vue.js:

const elements = document.querySelectorAll(
                   '[data-toggle="dropdown"]' );
for ( let i = 0; i < elements.length; i++ ) {
  const element = elements[ i ];
  const text = element.innerText;
  const links = element.parentElement.querySelectorAll(
                  'ul.dropdown-menu li a' );
  const items = [];
  for ( let j = 0; j < links.length; j++ ) {
    items.push( {
      text: links[ j ].innerText,
      href: links[ j ].getAttribute( 'href' )
    } );
  }
  new Vue( {
    el: element.parentElement,
    render( h ) {
      // ... same as above ...
    }
  } );
}

Мы изменили селектор на [data-toggle="dropdown"]. Он возвращает все элементы кнопки, которые имеют этот специальный атрибут. Стандартный сценарий Bootstrap использует тот же селектор.

Извлекаем текст из каждой кнопки. Затем мы запрашиваем родительский элемент <div>, чтобы найти все дочерние ссылки в соответствующем раскрывающемся меню, и заполняем список элементов так же, как в предыдущем примере.

Наконец, компонент Vue.js монтируется к родительскому элементу <div> кнопки. При этом исходный элемент заменяется тем же содержимым, поэтому изменение невидимо для пользователя, но компонент теперь «активен», и им можно управлять с помощью мыши и клавиатуры.

Обратите внимание, что техника, которую я здесь показываю, похожа на рендеринг на стороне сервера, но это не то же самое. При использовании SSR компоненты Vue.js фактически запускаются на сервере, что требует использования Node.js. Здесь я предполагаю, что у нас есть просто традиционное веб-приложение, написанное на PHP или другом языке.

Если вы до сих пор читали, реализация раскрывающейся кнопки в Vue.js может показаться излишней, потому что при использовании jQuery она просто работает «из коробки». Но помните, это очень простой пример. Как только вы начнете писать и использовать более сложные компоненты, написанные на Vue.js, вы оцените, насколько это мощно и просто.