Переход с jQuery на Vue.js

https://habr.com/company/ruvds/blog/414073/
  • Разработка веб-сайтов
  • jQuery
  • VueJS
  • JavaScript
  • Блог компании RUVDS.com


Автор статьи, перевод которой мы публикуем сегодня, полагает, что в мире существует ещё много программистов, которые, когда им нужно разработать простое веб-приложение, обращаются к jQuery. Обычно это случается тогда, когда некую страницу нужно оснастить простыми интерактивными возможностями, но использование для этого какого-нибудь JavaScript-фреймворка кажется явным перебором. Ведь это — килобайты ненужного кода, шаблоны, инструменты для сборки проектов, средства для упаковки модулей… При этом подключить к странице jQuery, воспользовавшись CDN-ресурсом, проще простого.



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

Обзор проекта


Мы собираемся разработать простой электронный счёт, основанный на этом опенсорсном шаблоне от Sparksuite.

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

Вот шаблон счёта, с которым мы хотим поработать.


Электронный счёт

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

Я модифицировал шаблон, приведя HTML-код пустой строки к следующему виду:

<tr class="item">
  <td><input value="" /></td>
  <td>$<input type="number" value="0" /></td>
  <td><input type="number" value="1" /></td>
  <td>$0.00</td>
</tr>

Разработка проекта на jQuery


Для начала посмотрим на то, как решить нашу задачу с использованием jQuery.

$('table').on('mouseup keyup', 'input[type=number]', calculateTotals);

Мы подключаем прослушиватель событий к таблице. Этот прослушиватель вызовет функцию calculateTotals при изменении значений, соответствующих стоимости единицы товара или его количеству:

function calculateTotals()  {
  const subtotals = $('.item').map((idx, val)  => calculateSubtotal(val)).get();
  const total = subtotals.reduce((a, v)  => a + Number(v),  0);
  $('.total td:eq(1)').text(formatAsCurrency(total));
}

Функция берёт строки таблицы и проходится по ним, передавая каждую из строк функции calculateSubtotal и суммируя результаты. Результат вычислений попадает в константу total. В итоге общая сумма по документу подставляется в соответствующее поле счёта.

function calculateSubtotal(row) {
  const $row = $(row);
  const inputs = $row.find('input');
  const subtotal = inputs[1].value * inputs[2].value;

  $row.find('td:last').text(formatAsCurrency(subtotal));

  return subtotal;
}

В этом коде мы берём ссылки на все поля <input>, присутствующие в строке, затем умножаем значения, хранящиеся во втором и третьем полях для получения значения subtotal. Потом это значение вставляется в последнюю ячейку строки. Мы форматируем это значение с помощью функции formatAsCurrency. Вот её код:

function formatAsCurrency(amount) {
  return `$${Number(amount).toFixed(2)}`;
}

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

$('.btn-add-row').on('click', () => {
  const $lastRow = $('.item:last');
  const $newRow = $lastRow.clone();

  $newRow.find('input').val('');
  $newRow.find('td:last').text('$0.00');
  $newRow.insertAfter($lastRow);

  $newRow.find('input:first').focus();
});

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

Вот рабочий пример счёта, интерактивные возможности которого реализованы средствами jQuery.

Недостатки решения, основанного на jQuery


Зададимся вопросом о том, каковы недостатки вышеописанного подхода, или, скорее, вопросом о том, как улучшить наш проект.

Возможно, вы слышали о новых библиотеках, вроде Vue и React, о которых говорят, что они позволяют работать в декларативном, а не в императивном стиле. Если посмотреть на вышеприведённый jQuery-код, ясно, что он, в основном, читается как набор инструкций, описывающих манипуляции с DOM. Цель каждого раздела этого кода, то есть, ответ на вопрос о том, что он делает, часто довольно сложно выяснить, глядя на то, как он это делает. Конечно, можно сделать намерения используемых фрагментов кода яснее, разбив его на функции с вдумчиво подобранными именами. Однако если оставить этот код на какое-то время, а потом опять к нему вернуться, окажется, что для того, чтобы его понять, всё равно придётся приложить некоторые усилия.

Ещё одна проблема подобного кода заключается в том, что состояние приложения хранится в DOM. Информация о товарах, которые хочет заказать пользователь, существует лишь как часть HTML-разметки в пользовательском интерфейсе. Если всё, что нам надо — это выводить данные в одном месте, то эта проблема не кажется такой уж серьёзной. Однако, как только появляется необходимость выводить эти данные в нескольких местах приложения, перед нами возникает задача синхронизации данных. Не имея, как в нашем случае, единого источника достоверных данных, очень сложно поддерживать их в актуальном состоянии во всём приложении.

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

Перевод проекта на Vue


Теперь поговорим о том, как воссоздать тот же функционал на Vue. Для того чтобы воспользоваться возможностями Vue, достаточно просто подключить эту библиотеку к обычной веб-странице, так же, как это делается в случае с jQuery. То есть, нет необходимости пользоваться системой для упаковки модулей или транспилятором, не нужно разбивать приложение на компоненты и раскладывать их код по .vue-файлам.

Начнём перевод проекта на Vue с замены содержимого тега <script>:

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

На следующем шаге надо создать новый экземпляр Vue:

const app = new Vue({
  el: 'table'
});

Единственным параметром, который надо передать конструктору нового экземпляра Vue, является el. Это — селектор (такой же, какими пользуются и в jQuery), который идентифицирует ту часть документа, которой мы хотим управлять с помощью Vue.

Vue можно поручить задачи разного масштаба — от управления целой страницей (в случае, например, с одностраничным приложением), до небольшого фрагмента, заключённого в тег <div>. В нашем примере Vue будет отвечать за работу с HTML-таблицей.

Данные


Добавим в экземпляр Vue условные данные для трёх строк документа:

const app = new Vue({
  el: 'table',
  data: {
    items: [
      { description: 'Website design', quantity: 1, price: 300 },
      { description: 'Hosting (3 months)', quantity: 1, price: 75 },
      { description: 'Domain name (1 year)', quantity: 1, price: 10 },
    ]
  }
});

Свойство data — это то место, где мы храним состояние приложения. Состояние включает не только данные, с которыми должно работать наше приложение, но и информацию о состоянии пользовательского интерфейса (например — о том, какой из разделов приложения, состоящего из нескольких вкладок, активен в настоящий момент, или о том, свёрнут ли некий виджет, вроде «аккордеона», или развёрнут).

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

Модификация шаблона


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

Используя атрибут v-for, мы можем вывести блок HTML-кода для каждого элемента из массива items:

<tr class="item" v-for="item in items">

</tr>

Vue повторит эту разметку для каждого элемента массива (или объекта), который передаётся v-for. Это позволяет нам обратиться к каждому элементу в цикле. В данном случае этот элемент представлен переменной item. Так как Vue наблюдает за свойствами объекта data, фреймворк будет динамически обновлять разметку при изменении содержимого массива items. Всё, что нам надо сделать — это модифицировать состояние, добавив или удалив элементы, а Vue будет автоматически обновлять пользовательский интерфейс.

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

<td><input v-model="item.description" /></td>
<td>$<input type="number" v-model="item.price" /></td>
<td><input type="number" v-model="item.quantity" /></td>
<td>${{ item.price * item.quantity }}</td>

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

В последней ячейке мы используем фигурные скобки {{}}, их мы применяем для вывода текста. В скобках можно использовать любое рабочее JavaScript-выражение. В данном случае мы умножаем два свойства элемента и выводим то, что получилось. Опять же, Vue наблюдает за моделью данных, изменение любого свойства приведёт к автоматическому пересчёту выражения.

События и методы


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

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

const app = new Vue({
  // ...
  methods: {
    myMethod() {}
  },
  // ...
})

Объявим метод addRow, который можно будет вызывать для добавления нового элемента в массив items:

methods: {
  addRow() {
    this.items.push({ description: '', quantity: 1, price: 0 });
  },
},

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

Итак, метод теперь у нас есть. Как его вызывать по нажатию кнопки Add row? Для добавления прослушивателей событий к элементам управления в шаблоне в Vue используется синтаксическая конструкция v-on:event-name.

<button class="btn-add-row" @click="addRow">Add row</button>

В Vue, кроме того, предусмотрено сокращение для конструкции v-on:, которое выглядит как @. Оно использовано в вышеприведённом фрагменте кода. В качестве обработчика события можно использовать любой метод из экземпляра Vue.

Вычисляемые свойства


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

Для этой цели мы можем использовать обычный метод, но я полагаю, что в данном случае нам лучше всего подойдёт так называемое вычисляемое свойство. Работа с такими свойствами напоминает вышеописанную работу с методами. А именно, для создания таких свойств экземпляру Vue передают объект computed, содержащий функции, результаты выполнения которых мы хотим использовать в шаблоне:

const app = new Vue({
  // ...
  computed: {
    total() {
      return this.items.reduce((acc, item) => acc + (item.price * item.quantity), 0);
    }
  }
});

Теперь на вычисляемое свойство можно сослаться из шаблона:

<tr class="total">
  <td colspan="3"></td>
  <td>Total: ${{ total }}</td>

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

Если бы мы использовали метод для подсчёта общей суммы по документу, вычисления выполнялись бы каждый раз при выводе шаблона. Но, так как мы используем вычисляемое свойство, общая сумма пересчитывается только при изменении значений quantity или price в строках таблицы.

Фильтры


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

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

Как вы уже, наверное, поняли, для того, чтобы создать фильтр, достаточно передать экземпляру Vue объект (filters в данном случае) с соответствующим ключом:

const app = new Vue({
  // ...
  filters: {
    currency(value) {
      return value.toFixed(2);
    }
  }
});

Здесь мы создали очень простой фильтр, который называется currency. Он вызывает функцию toFixed(2) для переданного ему числового значения и возвращает полученный результат. Использовать этот фильтр в шаблоне можно так:

<td>Total: ${{ total | currency }}</td>

Вот готовая реализация нашего проекта на Vue.

Итоги


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

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

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

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

Если тема разработки веб-приложений на Vue вам интересна — взгляните на этот материал. Кроме того, если вы подумываете о переводе проектов с чистого JS на Vue — вот наша недавняя публикация на эту тему.

Уважаемые читатели! Пользуетесь ли вы jQuery? И, если пользуетесь, планируете ли менять эту библиотеку на что-то другое, например — на Vue?

Ещё не оценен

Последние записи

Архив

2018
2017
2016
2015
2014

Категории

Авторы

Ленты

RSS / Atom