[Перевод] Веб-приложение на Node и Vue, часть 5: завершение работы над проектом
Перед вами перевод пятой части руководства по разработке веб-решений на базе Node.js, Vue.js и MongoDB. В первой, второй, третьей и четвёртой частях мы рассказывали о поэтапном создании клиентской и серверной частей приложения Budget Manager. Те, кому не терпится увидеть в действии то, что в итоге получилось у автора этого материала, могут заглянуть сюда. Кроме того, вот GitHub-репозиторий проекта. Если вы — из тех, кто ценит строгую типизацию, то здесь и здесь находятся результаты переноса Budget Manager на TypeScript.

Сегодня работа над этим учебным проектом завершится. А именно, в данном материале пойдёт речь о разработке страниц по добавлению в систему записей о новых клиентах и финансовых документах, а также о создании механизмов для редактирования этих данных. Здесь же мы рассмотрим некоторые улучшения API и доведём Budget Manager до рабочего состояния.
Доработка API
Для начала перейдём в папку models и откроем файл budget.js. Добавим в него поле description для модели:
description: {
type: String,
required: true
},
Теперь перейдём в папку app/api и откроем файл budget.js, который находится в ней. Тут мы собираемся отредактировать функцию сохранения данных, store, для того, чтобы новые документы обрабатывались правильно, добавить функцию edit, которая позволит редактировать документы, добавить функцию remove, которая нужна для удаления документов, и добавить функцию getByState, которая позволит фильтровать документы. Здесь приведён полный код файла. Для того, чтобы его просмотреть, разверните соответствующий блок. В дальнейшем большие фрагменты кода будут оформлены так же.
const mongoose = require('mongoose');
const api = {};
api.store = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Client.findOne({ _id: req.body.client }, (error, client) => {
if (error) res.status(400).json(error);
if (client) {
const budget = new Budget({
client_id: req.body.client,
user_id: req.query.user_id,
client: client.name,
state: req.body.state,
description: req.body.description,
title: req.body.title,
total_price: req.body.total_price,
items: req.body.items
});
budget.save(error => {
if (error) return res.status(400).json(error)
res.status(200).json({ success: true, message: "Budget registered successfully" })
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.getAll = (User, Budget, Token) => (req, res) => {
if (Token) {
Budget.find({ user_id: req.query.user_id }, (error, budget) => {
if (error) return res.status(400).json(error);
res.status(200).json(budget);
return true;
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.getAllFromClient = (User, Budget, Token) => (req, res) => {
if (Token) {
Budget.find({ client_id: req.query.client_id }, (error, budget) => {
if (error) return res.status(400).json(error);
res.status(200).json(budget);
return true;
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.index = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.findOne({ _id: req.query._id }, (error, budget) => {
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.edit = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.findOneAndUpdate({ _id: req.body._id }, req.body, (error, budget) => {
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.getByState = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Budget.find({ state: req.query.state }, (error, budget) => {
console.log(budget)
if (error) res.status(400).json(error);
res.status(200).json(budget);
})
} else {
res.status(400).json({ success: false, message: "Invalid budget" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.remove = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Budget.remove({ _id: req.query._id }, (error, removed) => {
if (error) res.status(400).json(error);
res.status(200).json({ success: true, message: 'Removed successfully' });
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
Похожие изменения внесём в файл client.js из папки api:
const mongoose = require('mongoose');
const api = {};
api.store = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
const client = new Client({
user_id: req.query.user_id,
name: req.body.name,
email: req.body.email,
phone: req.body.phone,
});
client.save(error => {
if (error) return res.status(400).json(error);
res.status(200).json({ success: true, message: "Client registration successful" });
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.getAll = (User, Client, Token) => (req, res) => {
if (Token) {
Client.find({ user_id: req.query.user_id }, (error, client) => {
if (error) return res.status(400).json(error);
res.status(200).json(client);
return true;
})
} else return res.status(403).send({ success: false, message: 'Unauthorized' });
}
api.index = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.findOne({ _id: req.query._id }, (error, client) => {
if (error) res.status(400).json(error);
res.status(200).json(client);
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.edit = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.findOneAndUpdate({ _id: req.body._id }, req.body, (error, client) => {
if (error) res.status(400).json(error);
res.status(200).json(client);
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
api.remove = (User, Client, Token) => (req, res) => {
if (Token) {
User.findOne({ _id: req.query.user_id }, (error, user) => {
if (error) res.status(400).json(error);
if (user) {
Client.remove({ _id: req.query._id }, (error, removed) => {
if (error) res.status(400).json(error);
res.status(200).json({ success: true, message: 'Removed successfully' });
})
} else {
res.status(400).json({ success: false, message: "Invalid client" })
}
})
} else return res.status(401).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
И, наконец, добавим в систему новые маршруты. Для этого перейдём в папку routes и откроем файл budget.js:
const passport = require('passport'),
config = require('@config'),
models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.budget;
app.route('/api/v1/budget')
.post(passport.authenticate('jwt', config.session), api.store(models.User, models.Budget, models.Client, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Budget, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAllFromClient(models.User, models.Budget, app.get('budgetsecret')))
.delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Budget, models.Client, app.get('budgetsecret')))
app.route('/api/v1/budget/single')
.get(passport.authenticate('jwt', config.session), api.index(models.User, models.Budget, models.Client, app.get('budgetsecret')))
.put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Budget, models.Client, app.get('budgetsecret')))
app.route('/api/v1/budget/state')
.get(passport.authenticate('jwt', config.session), api.getByState(models.User, models.Budget, models.Client, app.get('budgetsecret')))
}
Внесём похожие изменения в файл client.js, который находится в той же папке:
const passport = require('passport'),
config = require('@config'),
models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.client;
app.route('/api/v1/client')
.post(passport.authenticate('jwt', config.session), api.store(models.User, models.Client, app.get('budgetsecret')))
.get(passport.authenticate('jwt', config.session), api.getAll(models.User, models.Client, app.get('budgetsecret')))
.delete(passport.authenticate('jwt', config.session), api.remove(models.User, models.Client, app.get('budgetsecret')))
app.route('/api/v1/client/single')
.get(passport.authenticate('jwt', config.session), api.index(models.User, models.Client, app.get('budgetsecret')))
.put(passport.authenticate('jwt', config.session), api.edit(models.User, models.Client, app.get('budgetsecret')))
}
Вот и все изменения, которые нужно внести в API.
Доработка маршрутизатора
Теперь добавим новые компоненты в маршруты. Для этого откроем файл index.js, находящийся внутри папки router.
...
// Global components
import Header from '@/components/Header'
import List from '@/components/List/List'
import Create from '@/components/pages/Create'
// Register components
Vue.component('app-header', Header)
Vue.component('list', List)
Vue.component('create', Create)
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header,
list: List,
create: Create
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
…
Здесь мы импортировали и определили компонент Create и назначили его компонентом маршрута Home (сам компонент создадим ниже).
Создание новых компонентов
▍Компонент Create
Начнём с компонента Create. Перейдём в папку components/pages и создадим там новый файл Create.vue.
Первый именованный слот — budget-creation. Он представляет компонент, который мы будем использовать для создания новых финансовых документов. Он будет виден только в том случае, когда свойство budgetCreation установлено в значение true, а editPage — в значение false, мы передаём ему всех наших клиентов и метод saveBudget.
Второй именованный слот — client-creation. Это — компонент, используемый для создания новых клиентов. Он будет видимым лишь в том случае, когда свойство budgetCreation установлено в false, и editPage так же имеет значение false. Сюда мы передаём метод saveClient.
Третий именованный слот — budget-edit. Это — компонент, который применяется для редактирования выбранного документа. Видим он только тогда, когда свойства budgetEdit и editPage установлены в true. Сюда мы передаём всех клиентов, выбранный финансовый документ и метод fixClientNameAndUpdate.
И, наконец здесь имеется, последний именованный слот, который используется для редактирования информации о клиентах. Он будет видим тогда, когда свойство budgetEdit установлено в false, а editPage — в true. Ему мы передаём выбранного клиента и метод updateClient.
▍Компонент BudgetCreation
Разработаем компонент, который используется для создания новых финансовых документов. Перейдём в папку components и создадим в ней новую папку, дав ей имя Creation. В этой папке создадим файл компонента BudgetCreation.vue.
Компонент это довольно большой, разберём его поэтапно, начиная с шаблона.
Шаблон компонента BudgetCreation
status
Remove
ITEM PRICE $ {{ item.subtotal }}
Add item
TOTAL $ {{ budget.total_price }}
Save
Тут мы сначала добавляем в шаблон элемент v-select для установки состояния документа, затем — v-select для выбора клиента, который нам нужен. Далее, у нас имеется поле v-text-field для ввода заголовка документа и v-text-field для вывода описания.
Затем мы перебираем элементы budget.items, что даёт нам возможность добавлять элементы в документ и удалять их из него. Здесь же имеется красная кнопка, которая позволяет вызывать функцию removeItem, передавая ей элемент, который нужно удалить.
Далее, здесь есть три поля v-text-fields, предназначенные, соответственно, для названия товара, цены за единицу и количества.
В конце ряда имеется простой элемент span, в котором выводится промежуточный итог по строке, subtotal, представляющий собой произведение количества и цены товара.
Ниже списка товаров имеется ещё три элемента. Это — синяя кнопка, которая используется для добавления новых элементов путём вызова функции addItem, элемент span, который показывает общую стоимость всех товаров, которые имеются в документе (сумма показателей subtotal всех элементов), и зелёная кнопка, которая используется для сохранения документа в базу данных путём вызова функции saveBudget с передачей ей, в качестве параметра, документа, который мы хотим сохранить.
Скрипт компонента BudgetCreation
В этом коде мы сначала получаем два свойства — clients и saveBudget. Источник этих свойств — компонент Home.
Затем мы определяем объект и массив, играющие роль данных. Объект имеет имя budget. Он используется для создания документа, мы можем добавлять в него значения и сохранять его в базе данных. У этого объекта есть свойства title (заголовок), description (описание), state (состояние, по умолчанию установленное в значение writing), client (клиент), total_price (общая стоимость по документу), и массив товаров items. У товаров имеются свойства title (название), quantity (количество), price (цена) и subtotal (промежуточный итог).
Здесь же определён массив состояний документа, states. Его значения используют для установки состояния документа. Вот эти состояния: writing, editing, pending, approved, denied и waiting.
Ниже, после описания структур данных, имеется пара методов: addItem (для добавления товаров) и removeItem (для их удаления).
Каждый раз, когда мы щёлкаем по синей кнопке, вызывается метод addItem, который добавляет элементы в массив items, находящийся внутри объекта budget.
Метод removeItem выполняет обратное действие. А именно — при щелчке по красной кнопке заданный элемент удаляется из массива items.
Стили компонента BudgetCreation
Теперь рассмотрим следующий компонент.
▍Компонент ClientCreation
Этот компонент, по сути, является упрощённой версией только что рассмотренного компонента BudgetCreation. Мы так же, как сделано выше, рассмотрим его по частям. Если вы разобрались с устройством компонента BudgetCreation, вы без труда поймёте и принципы работы компонента ClientCreation.
Save
Теперь пришла очередь компонента BudgetEdit.
▍Компонент BudgetEdit
Этот компонент, по сути, является модифицированной версией уже рассмотренного компонента BudgetCreation. Рассмотрим его составные части.
status
Remove
ITEM PRICE $ {{ item.subtotal }}
Add item
TOTAL $ {{ budget.total_price }}
Update
Единственное различие шаблонов компонентов BudgetEdit и BudgetCreation заключается в кнопке сохранения изменений и в связанной с ней логике. А именно, в BudgetCreation на ней написано Save, она вызывает метод saveBudget. В BudgetEdit эта кнопка несёт на себе надпись Update и вызывает метод fixClientNameAndUpdate.
Здесь всё начинается с получения трёх свойств. Это — clients, fixClientNameAndUpdate и selectedBudget. Данные тут те же самые, что и в компоненте BudgetCreation. А именно, тут имеется объект Budget и массив states.
Далее, здесь можно видеть обработчик события жизненного цикла компонента mounted, в котором мы вызываем метод parseBudget, о котором поговорим ниже. И, наконец, здесь есть объект methods, в котором присутствуют уже знакомые вам по компоненту BudgetCreation методы addItem и removeItem, а также новый метод parseBudget. Этот метод используется для того, чтобы установить значение объекта budget в то, которое передано в свойстве selectedBudget, но мы, кроме того, используем его для подсчёта промежуточных итогов по товарам документа и общей суммы по документу.
▍Компонент ClientEdit
Этот компонент, по аналогии с только что рассмотренным, похож на соответствующий компонент, используемый для создания клиентов — ClientCreation. Главное отличие заключается в том, что тут вместо метода saveClient используется метод updateClient. Рассмотрим устройство компонента ClientEdit.
Update
На этом мы завершаем создание новых компонентов и переходим к работе с компонентами, которые уже были в системе.
Доработка существующих компонентов
Теперь осталось лишь внести некоторые изменения в существующие компоненты и приложение будет готово к работе.
Начнём с компонента ListBody.
▍Компонент ListBody
Шаблон компонента ListBody
Напомним, что код этого компонента хранится в файле ListBody.vue
{{ info }}
mode_edit
delete_forever
{{ info }}
mode_edit
delete_forever
В этом компоненте надо выполнить буквально пару изменений и дополнений. Так, сначала добавим новое условие в конструкцию v-if блока md-list-item:
parsedBudgets === null
Кроме того, мы уберём первую кнопку, которую использовали для вывода документа, так как она нам больше не нужна из-за того, что увидеть документ можно, нажав на кнопку редактирования.
Тут мы добавили метод getItemAndEdit к новой первой кнопке и метод deleteItem к последней кнопке, передавая этому методу элемент, данные и переменную budgetsVisible в качестве параметров.
Ниже всего этого имеется блок md-item-list, который мы используем для вывода отфильтрованного после поиска списка документов.
В этом компоненте мы получаем множество свойств. Опишем их:
data: это либо список документов, либо список клиентов, но никогда и то и другое.budgetsVisible: используется для проверки того, просматриваем ли мы список документов или клиентов, может принимать значенияtrueилиfalse.deleteItem: функция для удаления элемента, которая принимает, в качестве параметра, некий элемент.getBudget: функция, которую мы используем для загрузки отдельного документа, который планируется редактировать.getClient: функция, используемая для загрузки карточки отдельного клиента для последующего редактирования.parsedBudgets: документы, отфильтрованные после выполнения поиска.
В компоненте есть всего один метод, getItemAndEdit. Он принимает, в качестве параметра, элемент, при этом, на основе анализа наличия у элемента свойства, содержащего телефонный номер, принимается решение о том, является ли элемент карточкой клиента или финансовым документом.
Правку кода компонента ListBody мы завершили, займёмся теперь компонентом Header.
▍Компонент Header
{{ budgetsVisible ? "Clients" : "Budgets" }}
Sign out
Здесь, в первую очередь, мы меняем свойство v-model поля поиска на searchValue.
Кроме того, мы модифицируем элемент v-select, привязывая к его событию change метод selectState.
Тут добавлены два новых свойства — selectState, представляющее собой функцию, и search, которое является строкой. В данных search теперь используется searchValue и приведённый к нижнему регистру массив элементов statusItems.
С компонентом Header мы разобрались, теперь поработаем с компонентом Home.
▍Компонент Home
Focus Budget Manager
