[Перевод] Веб-приложение на Node и Vue, часть 3: развитие клиента и сервера
Сегодня публикуем третью часть из серии материалов, посвящённой разработке приложения Budget Manager с использованием Node.js, Vue.js и MongoDB. В первой и второй частях мы создавали сервер, настраивали механизмы аутентификации и занимались обустройством фронтенда. В этом материале продолжим работать над клиентской и серверной частями системы. То, что уже создано, пока почти не касается логики самого приложения, которое предназначено для работы с финансовыми документами. Поэтому, кроме прочего, мы займёмся и этим аспектом проекта.
Несколько исправлений
Для начала мне хотелось бы поблагодарить пользователя @OmgImAlexis за указание на проблему с фоновым изображением, на то, что у меня нет прав на его использование, и за рекомендацию по поводу этого ресурса со свободно распространяемыми картинками.
Поэтому сегодня мы начнём с замены фонового изображения, используемого в проекте, на это (не забудьте уменьшить изображение, если вы собираетесь разворачивать приложение). Если вы хотите сразу использовать уменьшенное изображение — можете взять его из моего репозитория.
После загрузки изображения перейдём к файлу компонента App.vue
и заменим то изображение, что было раньше. Кроме того, отредактируем стили:
Тут мы добавили свойство background-size: cover
и следующую конструкцию:
.application {
background: none;
}
Сделано это из-за того, что Vuetify использует белый фон для страниц приложения. Теперь, всё ещё находясь в файле App.vue
, выполним некоторые изменения шаблона:
Тут мы поменяли div id="app"
на a v-app
, это — главный компонент из Vuetify.
Теперь откроем файл компонента Authentication.vue
и внесём некоторые изменения в стили:
Здесь мы переопределили несколько стилей Vuetify, причина этого — в особенностях работы v-app
. Кроме того, мы расширили класс l-auth
, так как наш класс l-signup
в точности такой же, различия заключаются лишь в анимации. В результате приложение будет выглядеть так:
Теперь переходим к файлу index.js
, который расположен в папке Authentication
. Для начала внесём изменения в метод authenticate
:
authenticate (context, credentials, redirect) {
Axios.post(`${BudgetManagerAPI}/api/v1/auth`, credentials)
.then(({data}) => {
context.$cookie.set('token', data.token, '1D')
context.$cookie.set('user_id', data.user._id, '1D')
context.validLogin = true
this.user.authenticated = true
if (redirect) router.push(redirect)
}).catch(({response: {data}}) => {
context.snackbar = true
context.message = data.message
})
},
Тут мы изменили промис таким образом, чтобы, разобрав объект data
, извлечь из него идентификатор пользователя, так как мы намереваемся хранить этот id
.
Далее, отредактируем метод signup
:
signup (context, credentials, redirect) {
Axios.post(`${BudgetManagerAPI}/api/v1/signup`, credentials)
.then(() => {
context.validSignUp = true
this.authenticate(context, credentials, redirect)
}).catch(({response: {data}}) => {
context.snackbar = true
context.message = data.message
})
},
Первый промис мы заменили стрелочной функцией, так как ответа от POST-запроса мы не получаем. Кроме того, тут мы больше не задаём токен. Вместо этого вызываем метод authenticate
.
Мы внесли в проект эти исправления, так как, в противном случае, после регистрации в системе, пользователь будет перенаправлен таким образом, будто он аутентифицирован, но мы его при этом не аутентифицируем, в результате система будет работать не так, как ожидается.
Теперь, сразу под методом signup
, добавляем метод signout
:
signout (context, redirect) {
context.$cookie.delete('token')
context.$cookie.delete('user_id')
this.user.authenticated = false
if (redirect) router.push(redirect)
},
Далее, сразу после метода signout
внесём небольшие изменения в метод checkAuthentication
:
checkAuthentication () {
const token = document.cookie
this.user.authenticated = !!token
},
Тут можно оставить всё как есть, либо, для преобразования константы token
в логическое значение, воспользоваться тернарным оператором сравнения.
Распространённый недочёт JS-кода заключается в использовании логических выражений для приведения неких значений к логическому типу вместо применения конструкции с восклицательным знаком. Обычно этот вариант выглядит так:
this.user.authenticated = token ? true : false
Разработка компонента Header
Прежде чем заняться компонентом домашней страницы, создадим шапку для неё. Для этого перейдём в папку components
и создадим файл Header.vue
:
Clients
Sign out
Сейчас перед нами довольно простая заготовка компонента. Тут имеется лишь поле для ввода поискового запроса, привязанное к данным из search
, кнопка для перехода к странице клиентов, которой мы займёмся позже, переключатель для фильтрации документов и кнопка для выхода из системы.
Откроем частичный шаблон _variables
, добавим туда сведения о цвете, а так же установим прозрачность background-color
в значение 0.7
:
// Colors
$background-tint: #1734C1;
$background-color: rgba(0, 0, 0, .7);
$border-color-input: rgba(255, 255, 255, 0.42);
Теперь определим компоненты в маршрутизаторе. Для этого откроем файл index.js
в папке router
и приведём его к такому виду:
// Pages
import Home from '@/components/pages/Home'
import Authentication from '@/components/pages/Authentication/Authentication'
// Global components
import Header from '@/components/Header'
// Register components
Vue.component('app-header', Header)
Vue.use(Router)
Тут мы сначала импортируем компонент Home
, затем — Header
, после чего регистрируем его, помня о том, что знак @
при использовании webpack
является псевдонимом для папки src
. App-header
— это имя тега, который мы будем использовать для вывода компонента Header
.
В том, что касается имён тегов, хотелось бы привести выдержку из документации по Vue.js:
Обратите внимание, что Vue не требует соблюдения правил W3C для пользовательских имён тегов (таких как требования использования только нижнего регистра и применения дефисов), хотя следование этим соглашениям считается хорошей практикой.
Теперь настал черёд маршрутизатора:
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header
},
meta: {
requiredAuth: true
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
Здесь мы указываем на то, что компонентом по умолчанию для домашней страницы является Home
, а также включаем в эту страницу компонент Header
. Обратите внимание на то, что тут мы не вносим никаких изменений в маршрут входа в систему. Компонент Header
, представляющий шапку страницы, нам там не нужен.
Мы займёмся компонентом Header
позже, но на данном этапе работы нас устроит его нынешнее состояние.
Разработка компонента Home
Как обычно — откроем файл компонента, которым собираемся заниматься. Для этого надо перейти в папку pages
и открыть файл Home.vue
:
Focus Budget Manager
Тут мы выводим заголовок, представленный тегом h4
, содержащий название приложения. Ему назначены следующие классы:
white--text
: используется для окрашивания текста в белый цвет.text-xs-center
: используется для центровки текста по осиx
.my-0
: используется для установки полей по осиy
в0
.
Тут применяется компонент budget-list
, который мы создадим ниже. Он включает в себя компоненты budget-list-header
и budget-list-body
, которые играют роль слотов для размещения данных.
Кроме того, мы, в качестве свойств, передаём в budget-list-body
массив финансовых документов budgets
, данные из которого извлекаются при монтировании компонента. Мы передаём заголовок Authorization
, что даёт нам возможность работать с API. Так же тут передаётся, как параметр, user_id
, что даёт возможность указать то, какой именно пользователь запрашивает данные.
Разработка компонентов для работы со списком документов
Перейдём в папку components
и создадим в ней новую папку Budget
. Внутри этой папки создадим файл компонента BudgetListHeader.vue
:
Client
Title
Status
Actions
Это — просто шапка для страницы списка документов.
Теперь, в той же папке, создадим ещё один файл компонента и дадим ему имя BudgetListBody.vue
:
{{ budget.client }}
{{ budget.title }}
{{ budget.state }}
visibility
mode_edit
delete_forever
Здесь мы описываем тело страницы, и то, как оно будет выглядеть в различных средах, причём, ориентируемся мы на мобильные устройства.
Теперь, наконец, создадим в той же папке файл BudgetList.vue
и добавим в него код соответствующего компонента:
Обратите внимание на теги slot
. В них мы выводим компоненты. Эти теги называются именованными слотами.
Теперь нужно добавить компонент BudgetList
в маршрутизатор:
// ...
// Global components
import Header from '@/components/Header'
import BudgetList from '@/components/Budget/BudgetList'
// Register components
Vue.component('app-header', Header)
Vue.component('budget-list', BudgetList)
// ...
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
components: {
default: Home,
header: Header,
budgetList: BudgetList
},
meta: {
requiredAuth: true
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
// ...
export default router
Как и прежде, тут мы импортируем компоненты, регистрируем их и даём возможность компоненту Home
их использовать.
Доработка RESTful API
Вернёмся к серверной части проекта, поработаем над API. Для начала — немного его почистим. Для этого откроем файл user.js
из папки services/BudgetManagerAPI/app/api
и приведём его к такому виду:
const mongoose = require('mongoose');
const api = {};
api.signup = (User) => (req, res) => {
if (!req.body.username || !req.body.password) res.json({ success: false, message: 'Please, pass an username and password.' });
else {
const user = new User({
username: req.body.username,
password: req.body.password
});
user.save(error => {
if (error) return res.status(400).json({ success: false, message: 'Username already exists.' });
res.json({ success: true, message: 'Account created successfully' });
});
}
}
module.exports = api;
Тут мы удалили методы setup
и index
. Метод setup
нам больше не нужен, так как у нас уже есть средства для создания учётных записей. Метод index
не требуется из-за того, что мы не собираемся выводить список всех зарегистрированных пользователей. Кроме того, мы избавились от console.log
в методе signup
, и от пустого массив клиентов в методе создания нового пользователя.
Теперь поработаем над файлом user.js
, который хранится в папке services/BudgetManagerAPI/app/routes
:
const models = require('@BudgetManager/app/setup');
module.exports = (app) => {
const api = app.BudgetManagerAPI.app.api.user;
app.route('/api/v1/signup')
.post(api.signup(models.User));
}
Тут мы убрали маршруты, которые были нужны для старых методов.
Улучшение моделей
Перейдём к папке models
, которая находится по адресу BudgetManagerAPI/app/
и внесём некоторые улучшения в модели. Откроем файл user.js
. Тут мы собираемся модифицировать схему данных пользователя:
const Schema = mongoose.Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
}
});
Кроме того, создадим ещё несколько моделей. Начнём с модели, которая будет находиться в файле client.js
:
const mongoose = require('mongoose');
const Schema = mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
phone: {
type: String,
required: true
},
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
});
mongoose.model('Client', Schema);
Теперь поработаем над моделью, которая будет находиться в файле budget.js
:
const mongoose = require('mongoose');
const Schema = mongoose.Schema({
client: {
type: String,
required: true
},
state: {
type: String,
required: true
},
title: {
type: String,
required: true
},
total_price: {
type: Number,
required: true
},
client_id: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Client'
},
items: [{}]
});
mongoose.model('Budget', Schema);
Теперь нам не нужно использовать изменяемые массивы, увеличивающиеся по мере работы с ними. Вместо этого мы применяем ссылки для указания того, какие именно пользователи и клиенты нам нужны, используя ref
и ObjectID
.
Откроем файл index.js
из папки setup
и приведём его к такому виду:
const mongoose = require('mongoose'),
UserModel = require('@BudgetManagerModels/user'),
BudgetModel = require('@BudgetManagerModels/budget'),
ClientModel = require('@BudgetManagerModels/client');
const models = {
User: mongoose.model('User'),
Budget: mongoose.model('Budget'),
Client: mongoose.model('Client')
}
module.exports = models;
Расширение API
Теперь надо добавить в API методы, предназначенные для новых моделей, поэтому перейдём в папку api
и создадим там новый файл client.js
:
const mongoose = require('mongoose');
const api = {};
api.store = (User, Client, Token) => (req, res) => {
if (Token) {
const client = new Client({
user_id: req.body.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 successfull" });
})
} 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' });
}
module.exports = api;
Тут имеется метод для создания новых клиентов и для получения их полного списка. Эти методы защищены благодаря использования JWT-аутентификации.
Теперь создадим ещё один файл, назовём его budget.js
:
const mongoose = require('mongoose');
const api = {};
api.store = (User, Budget, Client, Token) => (req, res) => {
if (Token) {
Client.findOne({ _id: req.body.client_id }, (error, client) => {
if (error) res.status(400).json(error);
if (client) {
const budget = new Budget({
client_id: req.body.client_id,
user_id: req.body.user_id,
client: client.name,
state: req.body.state,
title: req.body.title,
total_price: req.body.total_price,
items: req.body.items
});
budget.save(error => {
if (error) 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(403).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(403).send({ success: false, message: 'Unauthorized' });
}
module.exports = api;
Его методы, как и в предыдущем случае, защищены JWT-аутентификацией. Один из этих трёх методов используется для создания новых документов, второй — для получения списка всех документов, связанных с учётной записью пользователя, и ещё один — для получения всех документов по конкретному клиенту.
Создание и защита маршрутов для документов и клиентов
Перейдём в папку routes
и создадим там файл budget.js
:
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')))
}
Затем создадим файл 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')));
}
Оба эти файла похожи друг на друга. В них мы сначала вызываем метод passport.authenticate
, а затем — методы API с передачей им моделей и секретного ключа.
Результаты
Теперь, если мы воспользуемся Postman для регистрации клиентов и документов, связанных с ними, вот что получится:
Итоги и домашнее задание
В этом материале мы исправили некоторые недочёты, поработали над клиентской и серверной частями приложения, начав реализацию его основной логики. В следующий раз мы продолжим развивать проект, в частности, разработаем механизмы для регистрации новых клиентов и создания связанных с ними финансовых документов.
Сейчас же, пока следующая статья из этой серии ещё не вышла, предлагаем всем желающим, в качестве упражнения, сделать форк репозитория автора этого материала и попытаться самостоятельно реализовать средства для регистрации клиентов и документов.
Уважаемые читатели! Если вы решили выполнить домашнюю работу — просим рассказать о том, что получилось.