В случае с большими проектами ручное управление синхронизацией состояния и интерфейса приложения представляет собой непростую задачу. Автор материала, перевод которого мы сегодня представляем вашему вниманию, хочет поделиться некоторыми результатами своих изысканий, направленных на сопоставление двух версий прогрессивного веб-приложения, использующего 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)
php_array
06.06.2018 19:05Я честно говоря, не очень понимаю, почему так сильно все переходят на фреймворки, забывая про обычный язык? Как же писать как тебе нужно, почему так насильно навязывают чужое видение? Да, в проектах где много людей работает, и нужно что общее и стандартное, то фреймворки, это то что доктор прописал. Но в личных проектах хотелось бы свои костыли писать...xD
Free_ze
06.06.2018 20:18Фреймворки скрывают под собой не самую удобную часть DOM-API браузера (без поддержки дата-биндинга, модуляризации интерфейса), а сам язык никуда не пропадает.
Writerim
07.06.2018 06:15Случай из практики. Была поставлена задача — написать быстрое дерево для каталога. Я думал что библиотеки имеют очень много лишнего и я напишу следовательно что-то реактивное. При больших объемах мое дерево просто вставало колом и не хотело работать. Начал искать пути решения. После прочтения хорошего такого объема информации, переписованного кода и так далее, я написал почти что я скоро напишу тот же jstree. Когда я на все плюнул и поставил jstree, все «взлетело».
Я был настолько расстроен и сделал для себя вывод, что люди, которые пишут подобные вещи не просто так это написали и квалификация в том вопросе, который они решают, несколько больше.
И фреймворки на самом деле решают очень много рутинных вопросов. Да на самом деле очень сильно упрощают разработку. Да приходится придерживаться их правил, но если опуститься на уровень чистого js, то там тоже очень много правил, которые нужно соблюдать.
faiwer
08.06.2018 07:50Если ваш личный проект достаточно велик, то писать без каких-либо удобств — это всё равно что катить квадратное колесо в гору. Нужно оптимизировать. В итоге вы либо берёте готовый фреймворк, либо… внезапно, вы пишете свой. Итог один.
smurov
Пожалуйста, не надо так говорить никогда и никому.