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


В случае с большими проектами ручное управление синхронизацией состояния и интерфейса приложения представляет собой непростую задачу. Автор материала, перевод которого мы сегодня представляем вашему вниманию, хочет поделиться некоторыми результатами своих изысканий, направленных на сопоставление двух версий прогрессивного веб-приложения, использующего Hoodie, представляющего собой список покупок. Базовая версия этого приложения написана на чистом JS (в этой статье можно найти подробности о нём). Здесь будет показан перевод приложения на Vue.js с попутным рассмотрением базовых возможностей этого фреймворка и с анализом того, что в итоге получилось.


Готовое приложение

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

Добавление товаров в список


?JavaScript


Приложение позволяет пользователю добавлять товары в свой список покупок. Делается это в файле index.html в папке public. Разметка содержится в строках 92-124 этого файла. Вот она.

<div>
  <div class="mdl-grid center-items">
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="text" id="new-item-name">
      <label class="mdl-textfield__label" for="new-item-name">Item Name</label>
  </div>
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-cost">
      <label class="mdl-textfield__label" for="new-item-cost">Item Cost</label>
  </div>
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-quantity">
      <label class="mdl-textfield__label" for="new-item-quantity">Quantity</label>
  </div>
  </div>

  <div class="mdl-grid center-items">
  <button id="add-item" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
      Add Item
  </button>
  </div>
</div>

Код для обработки и сохранения данных находится в файле public/js/src/index.js. Функция saveItems() в строке 28 ответственна за сбор значений из элементов управления, используемых для ввода данных и за сохранение этих данных. Эта функция привязана к событию click кнопки add-item. Вот код, о котором идёт речь.

function saveNewitem() {
  let name = document.getElementById("new-item-name").value;
  let cost = document.getElementById("new-item-cost").value;
  let quantity = document.getElementById("new-item-quantity").value;
  let subTotal = cost * quantity;
  if (name && cost && quantity) {
    hoodie.store.withIdPrefix("item").add({
      name: name,
      cost: cost,
      quantity: quantity,
      subTotal: subTotal
    });
    document.getElementById("new-item-name").value = "";
    document.getElementById("new-item-cost").value = "";
    document.getElementById("new-item-quantity").value = "";
  } else {
    let snackbarContainer = document.querySelector("#toast");
    snackbarContainer.MaterialSnackbar.showSnackbar({
      message: "All fields are required"
    });
  }
}

document.getElementById("add-item").addEventListener("click", saveNewitem);

?Vue


Прежде чем приступить к переработке этого проекта с использованием Vue, надо подключить фреймворк к странице. В нашем случае, в файле index.html, это сделано следующим образом:

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

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

Теперь приступим к переводу проекта на Vue. Для начала модифицируем разметку для того, чтобы использовать некоторые директивы Vue. Директивы Vue — это специальные атрибуты с префиксом v-. Вот как выглядит обновлённая разметка.

<form v-on:submit.prevent="onSubmit">
  <div class="mdl-grid center-items">
    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="text" id="new-item-name" v-model="name">
      <label class="mdl-textfield__label" for="new-item-name">Item Name</label>
    </div>
    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-cost" v-model.number="cost">
      <label class="mdl-textfield__label" for="new-item-cost">Item Cost</label>
    </div>
    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
      <input class="mdl-textfield__input" type="number" id="new-item-quantity" v-model.number="quantity">
      <label class="mdl-textfield__label" for="new-item-quantity">Quantity</label>
    </div>
  </div>

  <div class="mdl-grid center-items">
    <button id="add-item" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
      Add Item
    </button>
  </div>
</form>

Использованная здесь директива v-on применяется для прослушивания событий DOM. В вышеприведённом фрагменте кода она используется в элементе формы для прослушивания события submit. Кроме того, она задействует модификатор .prevent, который сообщает директиве v-on о необходимости вызова event.preventDefault() в вызванном событии. Мы использовали директиву v-model для элементов, используемых для ввода данных. Она нужна, в применении к элементам формы, для создания двусторонней привязки данных. Это позволяет системе автоматически выбрать правильный способ обновления элемента, основываясь на его типе. Мы использовали модификатор .number для элементов cost и quantity.

Благодаря этому выполняется автоматическое приведение типа значения из поля ввода к числу. Делается это из-за того, что даже если тип установлен в значение type=number, значение будет всегда возвращаться в виде строки. Модификаторы, используемые здесь, автоматизируют выполнение дополнительных проверок, которые ранее приходилось выполнять самостоятельно.

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

const vm = new Vue({
  el: "#app",
  data: {
    name: "",
    cost: "",
    quantity: ""
  },
  methods: {
    onSubmit: function(event) {
      if (this.name && this.cost && this.quantity) {
        hoodie.store.withIdPrefix("item").add({
          name: this.name,
          cost: this.cost,
          quantity: this.quantity,
          subTotal: this.cost * this.quantity
        });

        this.name = "";
        this.cost = "";
        this.quantity = "";
      } else {
        const snackbarContainer = document.querySelector("#toast");
        snackbarContainer.MaterialSnackbar.showSnackbar({
          message: "All fields are required"
        });
      }
    }
  }
});

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

Свойство data содержит состояние приложения. Все свойства в объекте, который здесь имеется, будут, при инициализации Vue, добавлены в систему реактивности фреймворка. Именно действия этой системы приводят к обновлению пользовательского интерфейса при изменении значений, привязанных к DOM. Например, свойство name привязано к элементу управления name с использованием директивы v-model="name". Эта директива задаёт двустороннюю привязку данных между свойством и элементом управления таким образом, что когда в поле ввода что-то добавляется (или что-то из него удаляется), производится обновление свойства name. В результате содержимое поля ввода отражает текущее состояние свойства name.

Подобным же образом привязка работает и с другими элементами управления.

Свойство methods содержит функции. В вышеприведённом коде определена функция onSubmit(), которая привязана к событию формы submit().

Вывод сохранённых данных на страницу


?JavaScript


Функция onSubmit сохраняет данные в Hoodie. После этого нам нужно вывести, в виде таблицы, добавленные в хранилище элементы. В случае с приложением, написанном на обычном JS, соответствующая разметка выглядит так:

<div class="mdl-grid center-items">
  <table id="item-table" class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
    <thead>
      <tr>
        <th class="mdl-data-table__cell--non-numeric">Item Name</th>
        <th class="mdl-data-table__cell--non-numeric">Cost</th>
        <th class="mdl-data-table__cell--non-numeric">Quantity</th>
        <th class="mdl-data-table__cell">Sub-total</th>
        <th class="mdl-data-table__cell--non-numeric">
          <button class="mdl-button mdl-js-button mdl-button--icon">
            <i class="material-icons">delete</font></i>
          </button>
        </th>
      </tr>
    </thead>
    <tbody>

    </tbody>
  </table>
</div>
<div class="mdl-grid center-items">
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
    <input class="mdl-textfield__input" type="number" id="total-cost" readonly value="0">
    <label class="mdl-textfield__label" for="cost">Total Item Cost</label>
  </div>
</div>

<script id="item-row" type="text/template">
  <tr id='{{row-id}}'>      
    <td class="mdl-data-table__cell--non-numeric">{{name}}</td>
    <td class="mdl-data-table__cell--non-numeric">{{cost}}</td>
    <td class="mdl-data-table__cell--non-numeric">{{quantity}}</td>
    <td class="mdl-data-table__cell">{{subTotal}}</td>
    <td class="mdl-data-table__cell--non-numeric">
          <button class="mdl-button mdl-js-button mdl-button--icon mdl-button--colored"
          onclick="pageEvents.deleteItem('{{item-id}}')">
          <i class="material-icons">remove</font></i>
          </button>
    </td>
  </tr>
</script>

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

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

function addItemToPage(item) {
  if (document.getElementById(item._id)) return;
  let template = document.querySelector("#item-row").innerHTML;
  template = template.replace("{{name}}", item.name);
  template = template.replace("{{cost}}", item.cost);
  template = template.replace("{{quantity}}", item.quantity);
  template = template.replace("{{subTotal}}", item.subTotal);
  template = template.replace("{{row-id}}", item._id);
  template = template.replace("{{item-id}}", item._id);
  document.getElementById("item-table").tBodies[0].innerHTML += template;
  
  let totalCost = Number.parseFloat(
    document.getElementById("total-cost").value
  );
  
  document.getElementById("total-cost").value = totalCost + item.subTotal;
}

hoodie.store.withIdPrefix("item").on("add", addItemToPage);

В этом фрагменте кода выполняется получение шаблона из DOM, замена местозаполнителей реальными данными и присоединение того, что получилось, к DOM. Затем вычисляется показатель total-cost, общая сумма по всем элементам списка, который также выводится в интерфейсе.

?Vue


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

<div class="mdl-grid center-items">
  <table id="item-table" class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
    <thead>
      <tr>
        <th class="mdl-data-table__cell--non-numeric">Item Name</th>
        <th class="mdl-data-table__cell--non-numeric">Cost</th>
        <th class="mdl-data-table__cell--non-numeric">Quantity</th>
        <th class="mdl-data-table__cell">Sub-total</th>
        <th class="mdl-data-table__cell--non-numeric">
          <button class="mdl-button mdl-js-button mdl-button--icon">
            <i class="material-icons">delete</font></i>
          </button>
        </th>
      </tr>

    </thead>
    <tbody>
      <tr v-for="item in items" :key="item._id">
        <td class="mdl-data-table__cell--non-numeric">{{ item.name}}</td>
        <td class="mdl-data-table__cell--non-numeric">{{ item.cost}}</td>
        <td class="mdl-data-table__cell--non-numeric">{{ item.quantity}}</td>
        <td class="mdl-data-table__cell">{{ item.subTotal}}</td>
        <td class="mdl-data-table__cell--non-numeric">
          <button @click="deleteRow(item._id)" class="mdl-button mdl-js-button mdl-button--icon mdl-button--colored">
            <i class="material-icons">remove</font></i>
          </button>
        </td>
      </tr>
    </tbody>
  </table>
</div>

<div class="mdl-grid center-items">
  <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
    <!-- <input class="mdl-textfield__input" type="number" id="total-cost" readonly value="0">
    <label class="mdl-textfield__label" for="cost">Total Item Cost</label> -->
    <h4>Total Cost: {{ total }}</h4>
  </div>

</div>

На самом деле, в разметку внесены не такие уж и большие изменения. Здесь скопировано содержимое ранее использованного микро-шаблона и использованы директивы и механизмы интерполяции текста Vue. Директива v-for применена для того, чтобы вывести список элементов, данные которых были взяты из свойства items. В столбцы данные выводятся с использованием возможностей конструкции Vue {{ item.name }}. Этот механизм похож на используемые в микро-шаблоне местозаполнители. Общая сумма также выводится на странице с использованием технологии интерполяции текста.

Теперь доработаем JavaScript-код в файле index-vue.js и получим следующее.

const vm = new Vue({
  el: "#app",
  data: {
    name: "",
    cost: "",
    quantity: "",
    items: []
  },
  computed: {
    // сгенерированный геттер
    total: function() {
      // `this` указывает на экземпляр vm
      return this.items.reduce(
        (accumulator, currentValue) => accumulator + currentValue.subTotal,
        0
      );
    }
  },
  methods: {
    .....
  }
});

hoodie.store.withIdPrefix("item").on("add", item => vm.items.push(item));

Вариант описываемого механизма, подготовленный средствами Vue, оказался гораздо короче и проще того, что был создан с помощью обычного JS. В этом коде было добавлено свойство данных items, именно оно используется в вышеупомянутой директиве v-for. При добавлении элемента Hoodie вызывает функцию, которая выполняет операцию vm.items.push(item) для обновления состояния приложения, а это, благодаря системе реактивности Vue, приводит к автоматическому обновлению пользовательского интерфейса. Для того чтобы вычислить общую сумму, нет нужды обращаться к элементам DOM. Здесь использовано вычисляемое свойство, которое обрабатывает набор данных items с помощью функции .reduce(). Теперь, благодаря системе реактивности Vue, пользовательский интерфейс обновляется при изменении этих значений. Хорошо тут то, что всё это позволяет программисту не беспокоиться о работе с элементами DOM в коде. В результате, с помощью кода меньшего объёма, мы решили ту же задачу, которая требовала куда больше кода в случае с обычным JS (вероятно, если бы вместо обычного JS в подобной ситуации была бы использована библиотека jQuery, кот так же вышел бы большего размера, чем тот, который получился при использовании Vue).

Сохранение элементов в виде списка


?JavaScript


После добавления элементов их нужно сохранить для последующего использования, причём так, чтобы позже можно было бы добавлять в систему и другие списки элементов. В приложении имеется кнопка Save List, которая отвечает за сбор данных, за сохранение их в виде группы элементов средствами Hoodie, и за то, чтобы у пользователя была возможность добавить в систему новый набор элементов.

При использовании обычного JavaScript соответствующая функция привязана к событию кнопки onClick. Вот разметка.

//index.html
<div class="mdl-grid center-items">
  <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored" onclick="pageEvents.saveList()">
    Save List
  </button>
</div>

Вот код функции.

//index.js
function saveList() {
  let cost = 0;

  hoodie.store
    .withIdPrefix("item")
    .findAll()
    .then(function(items) {
      for (var item of items) {
        cost += item.subTotal;
      }

      //сохраним список
      hoodie.store.withIdPrefix("list").add({
        cost: cost,
        items: items
      });

      //удалим элементы
      hoodie.store
        .withIdPrefix("item")
        .remove(items)
        .then(function() {
          //очистим таблицу
          document.getElementById("item-table").tBodies[0].innerHTML = "";

          //оповестим пользователя
          var snackbarContainer = document.querySelector("#toast");
          snackbarContainer.MaterialSnackbar.showSnackbar({
            message: "List saved succesfully"
          });
        })
        .catch(function(error) {
          //оповестим пользователя в случае ошибки
          var snackbarContainer = document.querySelector("#toast");
          snackbarContainer.MaterialSnackbar.showSnackbar({
            message: error.message
          });
        });
    });
}

window.pageEvents = {
  deleteItem: deleteItem,
  saveList: saveList
  ....
};

?Vue


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

Вот как теперь будет выглядеть разметка.

<div class="mdl-grid center-items">
  <button @click="saveList" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
    Save List
  </button>
</div>

Конструкция @click="saveList" представляет собой сокращение для v-on:click="saveList". Этот механизм используется для прослушивания событий DOM. В свойство methods объекта Vue добавляется та же функция saveList, которая использована в варианте приложения, написанном на чистом JS.

Навигационная панель


?JavaScript


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


Данные по ранее созданным спискам товаров

Разметка этой страницы находится в файле public/history.html, соответствующий ей JS-код — в файле public/js/src/history.js. У этой страницы и у index.html есть кое-что общее. Речь идёт о навигационной панели, расположенной в верхней части окна. Эта панель содержит ссылки на разные страницы, ссылки Login и Register, которые вызывают диалоговые формы для входа в систему и регистрации в ней. Здесь предусмотрена и кнопка Signout для выхода из системы.
В версии приложения, созданной с применением обычного JS, приходилось дублировать одну и ту же разметку на обеих страницах. Вот как выглядит HTML-код навигационной панели.

<header class="mdl-layout__header">
    <div class="mdl-layout__header-row">
    <!-- Title -->
    <span class="mdl-layout-title">Shopping List</span>
    <!-- Add spacer, to align navigation to the right -->
    <div class="mdl-layout-spacer"></div>
    <!-- Navigation. We hide it in small screens. -->
    <nav class="mdl-navigation mdl-layout--large-screen-only">
        <a class="mdl-navigation__link" href="index.html">Home</a>
        <a class="mdl-navigation__link" href="history.html">History</a>
        <a onclick="pageEvents.showLogin()" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
        <a onclick="pageEvents.showRegister()" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
        <a onclick="pageEvents.signout()" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
    </nav>
    </div>
</header>
<div class="mdl-layout__drawer">
    <span class="mdl-layout-title">Shopping List</span>
    <nav class="mdl-navigation">
    <a class="mdl-navigation__link" href="index.html">Home</a>
    <a class="mdl-navigation__link" href="history.html">History</a>
    <a onclick="pageEvents.showLogin()" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
    <a onclick="pageEvents.showRegister()" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
    <a onclick="pageEvents.signout()" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
    </nav>
</div>

Проанализировав этот код, можно заметить, что при щелчках по ссылкам Login, Register и Logout будут вызваны соответствующие им функции. Обработчики событий для элементов, описанных в этой разметке, определены в файле index.js.

import * as shared from "shared.js";

....

shared.updateDOMLoginStatus();
window.pageEvents = {
  showLogin: shared.showLoginDialog,
  showRegister: shared.showRegisterDialog,
  signout: shared.signOut
};

Функции, ответственные за работу с навигационной панелью, объявлены в файле shared.js.

//регистрируем диалоговые элементы
let loginDialog = document.querySelector("#login-dialog");
dialogPolyfill.registerDialog(loginDialog);
let registerDialog = document.querySelector("#register-dialog");
dialogPolyfill.registerDialog(registerDialog);

let showLoginDialog = function() {
  loginDialog.showModal();
};

let showRegisterDialog = function() {
  registerDialog.showModal();
};

let showAnonymous = function() {
  document.getElementsByClassName("login")[0].style.display = "inline";
  document.getElementsByClassName("login")[1].style.display = "inline";
  document.getElementsByClassName("register")[0].style.display = "inline";
  document.getElementsByClassName("register")[1].style.display = "inline";
  document.getElementsByClassName("logout")[0].style.display = "none";
  document.getElementsByClassName("logout")[1].style.display = "none";
};

let showLoggedIn = function() {
  document.getElementsByClassName("login")[0].style.display = "none";
  document.getElementsByClassName("login")[1].style.display = "none";
  document.getElementsByClassName("register")[0].style.display = "none";
  document.getElementsByClassName("register")[1].style.display = "none";
  document.getElementsByClassName("logout")[0].style.display = "inline";
  document.getElementsByClassName("logout")[1].style.display = "inline";
};

let updateDOMLoginStatus = () => {
  hoodie.account.get("session").then(function(session) {
    if (!session) {
      // пользователь не вошёл в систему
      showAnonymous();
    } else if (session.invalid) {
      // пользователь в системе, но сессия больше не аутентифицирована
      showAnonymous();
    } else {
      // пользователь вошёл в систему
      showLoggedIn();
    }
  });
};

let signOut = function() {
  hoodie.account
    .signOut()
    .then(function() {
      showAnonymous();
      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "You logged out"
      });
      location.href = location.origin;
    })
    .catch(function() {
      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "Could not logout"
      });
    });
};

export {
  signOut,
  showRegisterDialog,
  showLoginDialog,
  updateDOMLoginStatus
};

Этот код экспортирует функции, которые были использованы в index.js. Функции showLoginDialog() и showRegisterDialog() показывают модальные диалоговые окна, соответственно, для входа в систему и для регистрации в ней. Функция signout() позволяет пользователю выйти из системы и вызывает функцию showAnonymous(), которая скрывает ссылку Logout и показывает лишь ссылки Register и Login. Функция updateDOMLoginStatus() проверяет, аутентифицирован ли пользователь, и выводит подходящие ссылки. Эта функция вызывается при загрузке страницы.

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

?Vue


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

Переводя проект на использование компонентов Vue мы создаём новый файл, shared-vue.js. Внутри этого файла описан компонент Vue, представляющий собой навигационную панель. Вот как выглядит его код.

Vue.component("navigation", {
  props: ["isLoggedIn", "toggleLoggedIn"],
  template: `<div>
              <header class="mdl-layout__header">
        <div class="mdl-layout__header-row">
          <!-- Title -->
          <span class="mdl-layout-title">Shopping List</span>
          <!-- Add spacer, to align navigation to the right -->
          <div class="mdl-layout-spacer"></div>
          <!-- Navigation. We hide it in small screens. -->
          <nav class="mdl-navigation mdl-layout--large-screen-only">
            <a class="mdl-navigation__link" href="index.html">Home</a>
            <a class="mdl-navigation__link" href="history.html">History</a>
            <a v-show="!isLoggedIn" @click="showLogin" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
            <a v-show="!isLoggedIn" @click="showRegister" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
            <a v-show="isLoggedIn" @click="logout" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
          </nav>
        </div>
      </header>
      <div class="mdl-layout__drawer">
        <span class="mdl-layout-title">Shopping List</span>
        <nav class="mdl-navigation">
          <a class="mdl-navigation__link" href="index.html">Home</a>
          <a class="mdl-navigation__link" href="history.html">History</a>
          <a v-show="!isLoggedIn" @click="showLogin" style="cursor: pointer" class="mdl-navigation__link login">Login</a>
          <a v-show="!isLoggedIn" @click="showRegister" style="cursor: pointer" class="mdl-navigation__link register">Register</a>
          <a v-show="isLoggedIn" @click="logout" style="cursor: pointer" class="mdl-navigation__link logout">Logout</a>
        </nav>
      </div>
            </div>`,
  methods: {
    showLogin: function() {
      const loginDialog = document.querySelector("#login-dialog");
      dialogPolyfill.registerDialog(loginDialog);
      loginDialog.showModal();
    },
    showRegister: function() {
      const registerDialog = document.querySelector("#register-dialog");
      dialogPolyfill.registerDialog(registerDialog);
      registerDialog.showModal();
    },
    logout: function() {
      hoodie.account
        .signOut()
        .then(() => {
          this.toggleLoggedIn();
        })
        .catch(error => {
          alert("Could not logout");
        });
    }
  }
});

В этом коде мы зарегистрировали компонент Vue, имеющий имя navigation с объектом параметров, похожим на тот, который мы использовали, создавая экземпляр Vue. Первое свойство — это props. Свойство props — это механизм передачи данных компонентам. Компонент может определять собственные данные, но в случаях, когда фрагмент состояния приложения нужно использовать в различных компонентах, используется именно конструкция props. Свойство isLoggedIn хранит логическое значение, указывающее на статус аутентификации пользователя.

Второе свойство, template, хранит разметку, которая будет показана на странице. Эта разметка практически полностью повторяет ту, что была использована в варианте приложения, написанном на обычном JS, за исключением того, что здесь мы используем пару директив Vue — v-show и @click. Атрибут v-show используется для условного рендеринга. Здесь мы с его помощью выводим ссылку Logout в том случае, если в isLoggedIn хранится значение true, а если там хранится значение false — показываем ссылки Login и Register. Кроме тог, Vue поддерживает директивы v-if и v-else для условного рендеринга. Узнать подробности о них можно здесь. Атрибут @click — это сокращение для директивы v-on:click. Здесь функции showLogin, showRegister и logout установлены в качестве обработчиков событий для соответствующих ссылок.

Эти функции объявлены в свойстве methods. Функция logout, после успешного выхода из системы, вызывает функцию this.toggleLoggedIn(), которая передаётся в этот компонент с помощью props. Это приведёт к вызову функции, переданной посредством props, при этом ожидается, что она изменит значение свойства isLoggedin, которое этот компонент модифицировать не может. Когда оно изменится, система реактивности Vue соответствующим образом обновит DOM.
Этот компонент добавляется в index.html как элемент собственной разработки. В соответствии с этими идеями была удалена разметка навигационной панели, представленная строками 59-84. Вместо неё в файл был добавлен следующий код.

<navigation v-bind:is-logged-in="isLoggedIn" v-bind:toggle-logged-in="toggleLoggedIn"></navigation>

В JS-коде были объявлены свойства isLoggedIn и toggleLoggedIn, но, при передаче props, для этих имён использованы их эквиваленты, записанные в стиле kebab-case. Тут использована директива v-bind для динамической передачи значений этих свойств. Без этой директивы свойство передавалось бы как статическое значение и компонент получил бы строку isLoggedIn вместо логического значения. Тут так же можно использовать сокращение : для v-bind, и вышеописанную конструкцию можно было бы переписать так.

<navigation :is-logged-in="isLoggedIn" :toggle-logged-in="toggleLoggedIn"></navigation>

Значение isLoggedIn хранится в состоянии приложения, а toggleLoggedIn — это метод, объявленный в экземпляре Vue в index-vue.js. Выглядит всё это так.

const vm = new Vue({
  el: "#app",
  data: {
    name: "",
    cost: "",
    quantity: "",
    items: [],
    isLoggedIn: false
  },
  computed: {
    .....//фрагмент кода свёрнут
  },
  methods: {
    toggleLoggedIn: function() {
      this.isLoggedIn = !this.isLoggedIn;
    },
    ......//фрагмент кода свёрнут
  }
});

.....//фрагмент кода свёрнут

hoodie.account.get("session").then(function(session) {
  if (!session) {
    // пользователь вышел из системы
    vm.isLoggedIn = false;
  } else if (session.invalid) {
    vm.isLoggedIn = false;
  } else {
    // пользователь вошёл в систему
    vm.isLoggedIn = true;
  }
});

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

Диалоговое окно входа в систему


?JavaScript


Ссылки Login и Register показывают диалоговые окна, которые позволяют пользователю вводить имя пользователя и пароль для входа или регистрации в системе. Разметка, описывающая эти окна, как и разметка для навигационной панели, дублируется на различных страницах. Это можно видеть в строках 171-244 файла index.html и в строках 100-158 файла history.html.

<dialog id="login-dialog" class="mdl-dialog">
  <h4 class="mdl-dialog__title">Login</h4>
  <div class="mdl-dialog__content">
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="text" id="login-username">
        <label class="mdl-textfield__label" for="login-username">Username</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="password" id="login-password">
        <label class="mdl-textfield__label" for="login-password">Password</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <span id="login-error"></span>
      </div>
    </div>
  </div>
  <div class="mdl-dialog__actions">
    <button onclick="pageEvents.closeLogin()" type="button" class="mdl-button close">Cancel</button>
    <button onclick="pageEvents.login()" type="button" class="mdl-button">Login</button>
  </div>
</dialog>

<dialog id="register-dialog" class="mdl-dialog">
  <h4 class="mdl-dialog__title">Login</h4>
  <div class="mdl-dialog__content">
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="text" id="register-username">
        <label class="mdl-textfield__label" for="register-username">Username</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <input class="mdl-textfield__input" type="password" id="register-password">
        <label class="mdl-textfield__label" for="register-password">Password</label>
      </div>
    </div>
    <div class="mdl-grid center-items">
      <!-- Simple Textfield -->
      <div class="mdl-textfield mdl-js-textfield">
        <span id="register-error"></span>
      </div>
    </div>
  </div>
  <div class="mdl-dialog__actions">
    <button onclick="pageEvents.closeRegister()" type="button" class="mdl-button close">Cancel</button>
    <button onclick="pageEvents.register()" type="button" class="mdl-button">Register</button>
  </div>
</dialog>

Код, обеспечивающий работу внутренних механизмов этих окон, определён в файле shared.js и используется в index.js.

//shared.js

//регистрация диалоговых элементов
let loginDialog = document.querySelector("#login-dialog");
dialogPolyfill.registerDialog(loginDialog);
let registerDialog = document.querySelector("#register-dialog");
dialogPolyfill.registerDialog(registerDialog);

let closeLoginDialog = function() {
  loginDialog.close();
};

let closeRegisterDialog = function() {
  registerDialog.close();
};

let showAnonymous = function() {
  ...
};

let showLoggedIn = function() {
  ....
};

let signOut = function() {
  ....
};

let updateDOMLoginStatus = () => {
  ....
};

let login = function() {
  let username = document.querySelector("#login-username").value;
  let password = document.querySelector("#login-password").value;

  hoodie.account
    .signIn({
      username: username,
      password: password
    })
    .then(function() {
      showLoggedIn();
      closeLoginDialog();

      let snackbarContainer = document.querySelector("#toast");
      snackbarContainer.MaterialSnackbar.showSnackbar({
        message: "You logged in"
      });
    })
    .catch(function(error) {
      console.log(error);
      document.querySelector("#login-error").innerHTML = error.message;
    });
};

let register = function() {
  let username = document.querySelector("#register-username").value;
  let password = document.querySelector("#register-password").value;
  let options = { username: username, password: password };

  hoodie.account
    .signUp(options)
    .then(function(account) {
      return hoodie.account.signIn(options);
    })
    .then(account => {
      showLoggedIn();
      closeRegisterDialog();
      return account;
    })
    .catch(function(error) {
      console.log(error);
      document.querySelector("#register-error").innerHTML = error.message;
    });
};

export {
  register,
  login,
  closeRegisterDialog,
  closeLoginDialog,
  ...
};

Вот соответствующий фрагмент файла index.js.

//index.js

window.pageEvents = {
  closeLogin: shared.closeLoginDialog,
  showLogin: shared.showLoginDialog,
  closeRegister: shared.closeRegisterDialog,
  showRegister: shared.showRegisterDialog,
  login: shared.login,
  register: shared.register,
  signout: shared.signOut
};

?Vue


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

Vue.component("login-dialog", {
  data: function() {
    return {
      username: "",
      password: ""
    };
  },
  props: ["toggleLoggedIn"],
  template: `<dialog id="login-dialog" class="mdl-dialog">
      <h4 class="mdl-dialog__title">Login</h4>
      <div class="mdl-dialog__content">
        <div class="mdl-grid center-items">
          <!-- Simple Textfield -->
          <div class="mdl-textfield mdl-js-textfield">
            <input v-model="username" class="mdl-textfield__input" type="text" id="login-username">
            <label class="mdl-textfield__label" for="login-username">Username</label>
          </div>
        </div>
        <div class="mdl-grid center-items">
          <!-- Simple Textfield -->
          <div class="mdl-textfield mdl-js-textfield">
            <input v-model="password" class="mdl-textfield__input" type="password" id="login-password">
            <label class="mdl-textfield__label" for="login-password">Password</label>
          </div>
        </div>
        <div class="mdl-grid center-items">
          <!-- Simple Textfield -->
          <div class="mdl-textfield mdl-js-textfield">
            <span id="login-error"></span>
          </div>
        </div>
      </div>
      <div class="mdl-dialog__actions">
        <button @click="closeLogin" type="button" class="mdl-button close">Cancel</button>
        <button @click="login" type="button" class="mdl-button">Login</button>
      </div>
    </dialog>`,
  methods: {
    closeLogin: function() {
      const loginDialog = document.querySelector("#login-dialog");
      dialogPolyfill.registerDialog(loginDialog);
      loginDialog.close();
    },
    login: function(event) {
      hoodie.account
        .signIn({
          username: this.username,
          password: this.password
        })
        .then(() => {
          this.toggleLoggedIn();
          this.closeLogin();
        })
        .catch(error => {
          console.log(error);
          document.querySelector("#login-error").innerHTML = "Error loggin in";
        });
    }
  }
});

Этот компонент регистрируется со свойствами data, props, template и methods объекта, который передаётся методу Vue.component().

Затем, на странице, старая разметка была заменена на элемент Vue собственной разработки.

//index.html
<login-dialog v-bind:toggle-logged-in="toggleLoggedIn"></login-dialog>

Похожие действия были предприняты и при переработке механизма регистрации в системе.

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

Итоги


Мы разобрали процесс переработки веб-приложения, изначально написанного на чистом JS, с использованием возможностей фреймворка Vue.js. Если вы умеете создавать приложения с использованием HTML, CSS и JavaScript (jQuery), приступить к использованию Vue не слишком сложно. Вам не нужно знать ES6 или понимать особенности внутренних механизмов фреймворка для того, чтобы начать работу с ним.

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

Вот репозиторий с кодом описанного здесь приложения, созданного средствами чистого JS. А вот — репозиторий с его вариантом, переписанным с использованием Vue.

Уважаемые читатели! Какими фреймворками для разработки веб-приложений вы пользуетесь?

Комментарии (5)


  1. smurov
    06.06.2018 14:12

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

    Пожалуйста, не надо так говорить никогда и никому.


  1. php_array
    06.06.2018 19:05

    Я честно говоря, не очень понимаю, почему так сильно все переходят на фреймворки, забывая про обычный язык? Как же писать как тебе нужно, почему так насильно навязывают чужое видение? Да, в проектах где много людей работает, и нужно что общее и стандартное, то фреймворки, это то что доктор прописал. Но в личных проектах хотелось бы свои костыли писать...xD


    1. Free_ze
      06.06.2018 20:18

      Фреймворки скрывают под собой не самую удобную часть DOM-API браузера (без поддержки дата-биндинга, модуляризации интерфейса), а сам язык никуда не пропадает.


    1. Writerim
      07.06.2018 06:15

      Случай из практики. Была поставлена задача — написать быстрое дерево для каталога. Я думал что библиотеки имеют очень много лишнего и я напишу следовательно что-то реактивное. При больших объемах мое дерево просто вставало колом и не хотело работать. Начал искать пути решения. После прочтения хорошего такого объема информации, переписованного кода и так далее, я написал почти что я скоро напишу тот же jstree. Когда я на все плюнул и поставил jstree, все «взлетело».
      Я был настолько расстроен и сделал для себя вывод, что люди, которые пишут подобные вещи не просто так это написали и квалификация в том вопросе, который они решают, несколько больше.
      И фреймворки на самом деле решают очень много рутинных вопросов. Да на самом деле очень сильно упрощают разработку. Да приходится придерживаться их правил, но если опуститься на уровень чистого js, то там тоже очень много правил, которые нужно соблюдать.


    1. faiwer
      08.06.2018 07:50

      Если ваш личный проект достаточно велик, то писать без каких-либо удобств — это всё равно что катить квадратное колесо в гору. Нужно оптимизировать. В итоге вы либо берёте готовый фреймворк, либо… внезапно, вы пишете свой. Итог один.