Перед вами — вторая часть серии материалов, которая посвящена созданию веб-приложения Budget Manager с использованием Node.js, Vue.js и MongoDB. В первой части мы занимались сервером, а именно — подготовили основные методы RESTful API и наладили JWT-аутентификацию. Сегодня приступим к работе над клиентской частью приложения, создадим каркас фронтенда, средства для регистрации в системе и входа в неё, поговорим о маршрутах и об их защите.
Установка Vue.js и использование vue-cli
Установить Vue.js довольно просто. Мы планируем использовать vue-cli
с шаблоном webpack
. Обратившись к руководству по Vue, можно выяснить, что для установки vue-cli
и подготовки рабочей среды используются такие команды:
# установка vue-cli
$ npm install --global vue-cli
# создание нового проекта с использованием шаблона "webpack"
$ vue init webpack my-project
# установка зависимостей
$ cd my-project
$ npm install
$ npm run dev
Продолжим работу над проектом, создав папку application
в его корневой директории. Этот шаг можно и пропустить, создав папку в процессе работы с vue-cli
. Если вы решите не создавать папку, тогда вам нужно дать проекту имя, выполнив команду такого вида:
vue init webpack name-of-your-project
Вот как выглядит проект после создания папки application
:
Теперь перейдём в только что созданную папку с помощью интерпретатора командной строки, и, если vue-cli
ещё не установлен, выполним следующую команду:
npm i --g vue-cli
Эта команда позволяет установить vue-cli
глобально, поэтому неважно, в какой именно папке мы будем находиться, выполнив её.
Теперь вызовем следующую команду:
vue init webpack
Обратите внимание на то, что тут не указано имя проекта, так как подразумевается, что команда выполняется в папке application
, уже созданной для размещения в ней приложения.
После выполнения вышеприведённой команды и загрузки шаблона вам будет задан ряд вопросов:
Если хотите, на данном этапе может поменять название проекта, описание, сведения об авторе. Для того, чтобы не отклоняться от этого материала, оставьте всё остальное таким, как показано на рисунке.
Далее, всё ещё оставаясь в папке application
, устанавливаем зависимости и запускаем проект.
npm i
npm run dev
Теперь можно полюбоваться на стандартную страницу Vue.
Очистка приложения Vue
Уберём из приложения некоторые ненужные нам стандартные элементы. Для этого надо перейти в папку application/src/assets
и удалить logo.png
, так как этим файлом мы пользоваться не будем. Далее, откроем файл корневого компонента App.vue
из папки application/src
и приведём его к виду, представленному следующим фрагментом кода:
<template>
<div id="app">
<v-container>
<router-view/>
</v-container>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
Теперь надо очистить маршруты. Для этого откроем файл index.js
в папке router
и приведём его к такому виду:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{}
]
})
Обратите внимание на то, что существует множество способов структурирования маршрутов, но, для того, чтобы не усложнять проект, мы просто импортируем компоненты и зададим маршруты в этом файле.
Теперь, на последнем этапе очистки стандартного приложения, удалим файл HelloWorld.vue
из папки components
.
Установка зависимостей
Прежде чем заняться разработкой фронтенда приложения, нужно установить ещё несколько зависимостей. Перед выполнением следующей команды проверьте, находитесь ли вы в папке application
:
npm i --save axios vuetify vue-cookie
Теперь установим зависимости разработки:
npm i --save-dev sass-loader node-sass
Мы будем использовать axios
для обработки HTTP-запросов. В vuetify
нас интересуют визуальные компоненты и возможность пользоваться компоновкой элементов на основе сетки. Библиотеку vue-cookie
будем применять для работы с куки-файлами. Пакеты sass-loader
и node-sass
позволят нам пользоваться SCSS.
Начало работы над фронтендом приложения
Теперь, когда все подготовительные мероприятия завершены, займёмся разработкой. Перейдём к папке components
и создадим в ней папку pages
, в которой создадим папку Authentication
. В этой папке надо создать файл Authentication.vue
, представляющий компонент, которым будем пользоваться для аутентификации. Вот что должно в итоге получиться:
В файле Authentication.vue
разместим следующий код:
<template>
<h1>Auth!</h1>
</template>
<script>
export default {}
</script>
Расширением этого компонента займёмся позже, а пока перейдём в папку router
и поработаем с маршрутами.
Для начала импортируем компонент Authentication
и настроим маршрут для его использования:
import Vue from 'vue'
import Router from 'vue-router'
// Pages
import Authentication from '@/components/pages/Authentication/Authentication'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
После этого, если перейти по адресу http://localhost:8080/#/login, можно увидеть пустую страницу с надписью «Auth!». Это говорит о том, что маршрут аутентификации работает.
Теперь откроем файл main.js
из папки src
и импортируем vuetify
и vue-cookie
:
import VueCookie from 'vue-cookie'
import Vuetify from 'vuetify'
import('../node_modules/vuetify/dist/vuetify.min.css')
Vue.use(VueCookie)
Vue.use(Vuetify)
Vue.config.productionTip = false
Перейдём к компоненту App.vue
из папки src
и займёмся стилями. Сначала нужно подготовить тег style
. Разместим его сразу после закрытия тега script
:
<style lang="scss">
</style>
Теперь переходим в папку src/assets
и создаём в ней файл styles.scss
и папку partials
. В этой папке создадим два частичных шаблона, представленных файлами _variables.scss
и _animations.scss
. В результате должна получиться такая структура:
В файле _variables.scss
зададим такие параметры:
// Colors
$background-tint: #1734C1;
$background-color: rgba(0, 0, 0, .5);
В файл _animations.css
добавим описания анимаций bounceIn
и slideInFromLeft
:
@keyframes bounceIn {
to {
animation-timing-function: cubic-bezier(.215, .61, .355, 1)
}
0% {
opacity: 0;
transform: scale3d(.3, .3, .3)
}
20% {
transform: scale3d(1.1, 1.1, 1.1)
}
40% {
transform: scale3d(.9, .9, .9)
}
60% {
opacity: 1;
transform: scale3d(1.03, 1.03, 1.03)
}
80% {
transform: scale3d(.97, .97, .97)
}
to {
opacity: 1;
transform: scaleX(1)
}
}
@keyframes slideInFromLeft {
from {
transform: translateX(-2500px);
opacity: 0
}
50% {
transform: translateX(0);
opacity: 1;
}
70% {
transform: translateX(-20px);
}
90% {
transform: translateX(10px);
}
to {
transform: translateX(0);
}
}
Импортируем частичные шаблоны в styles.scss
:
@import "./partials/variables";
@import "./partials/animations";
Теперь, в папке assets
, создадим папку images
. Сюда можно поместить любое изображение, которое будет использоваться в качестве фона. Здесь, в репозитории, можно найти изображение, которое применяется в этом материале.
Настроим внешний вид приложения, приведя к следующему виду блок стилизации в файле App.vue
:
<style lang="scss">
@import "./assets/styles";
body {
background: url('./assets/images/background.jpg') no-repeat center center fixed;
&:after {
content: '';
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: $background-tint;
opacity: .3;
z-index: -1;
}
}
</style>
Тут мы импортируем ранее подготовленные стили scss и задаём использование фиксированного фонового изображения для приложения. Мы стремимся к тому, чтобы на любом устройстве экран приложения выглядел примерно одинаково.
В псевдоэлементе body :after
мы задаём параметр background-color
, записывая в него значение переменной $background-tint
. Это позволит применить к фоновому изображению пурпурный цветной фильтр. Вот как выглядят разные варианты фонового изображения.
Обратите внимание на то, что всё, что касается стилизации и работы с фоном, на функционал приложения не влияет, поэтому вы вполне можете пропустить эти шаги или украсить приложение так, как вам захочется.
Масштабирование области просмотра и загрузка иконок
Благодаря этому шагу мы обеспечим правильное отображение приложения на мобильных устройствах. Кроме того, на данном этапе работы мы загрузим иконки в стиле Material Design. Для того, чтобы всё это сделать, перейдём к файлу index.html
, который расположен в папке application
и добавим следующее в тег head
:
<meta name="viewport" content="initial-scale=1">
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet">
Разработка компонента Authentication
Теперь, когда мы немного украсили приложение, избавившись, по крайней мере, от скучных белых страниц, продолжим работу над компонентом Authentication
. Создадим в папке Authentication
файл index.js
.
Импортируем в него то, что нам понадобится и объявим константу, в которую запишем путь к API:
import Axios from 'axios'
import router from '@/router'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
Теперь создадим объект Authentication
, который будет содержать нужные нам методы:
export default {
user: { authenticated: false }
}
Мы начинаем работу над этим компонентом, объявляя объект с именем user
, который хранит сведения о том, аутентифицирован ли пользователь.
Теперь напишем методы:
import Axios from 'axios'
import router from '@/router'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
export default {
user: { authenticated: false },
authenticate (context, credentials, redirect) {
Axios.post(`${BudgetManagerAPI}/api/v1/auth`, credentials)
.then(({data: {token}}) => {
context.$cookie.set('token', token, '1D')
context.validLogin = true
this.user.authenticated = true
if (redirect) router.push(redirect)
}).catch(({response: {data}}) => {
context.snackbar = true
context.message = data.message
})
},
signup (context, credentials, redirect) {
Axios.post(`${BudgetManagerAPI}/api/v1/signup`, credentials)
.then(({data: {token}}) => {
context.$cookie.set('token', token, '1D')
context.validSignUp = true
this.user.authenticated = true
if (redirect) router.push(redirect)
}).catch(({response: {data}}) => {
context.snackbar = true
context.message = data.message
})
},
checkAuthentication () {
const token = document.cookie
if (token) this.user.authenticated = true
else this.user.authenticated = false
},
getAuthenticationHeader (context) {
return `Bearer ${context.$cookie.get('token')}`
}
}
В первом методе используются три аргумента:
context
: это — компонент Vue.credentials
: тут будут имя пользователя (username
) и пароль (password
).redirect
: здесь будет путь, по которому мы собираемся перенаправить пользователя.
Axios используется здесь для выполнения POST-запроса к API с передачей аргумента credentials
. Затем мы деструктурируем ответ, data
, так как нас здесь интересует лишь значение token
, сохраняем это значение в куки-файле и задаём срок жизни этих данных, равный одному дню. Также мы устанавливаем в true
переменную validLogin
и значение authenticated
объекта user
, и, наконец, перенаправляем пользователя по пути из аргумента redirect
.
В противном случае мы устанавливаем поле snackbar
объекта context
в true
и записываем в message
сообщение об ошибке.
Второй метод очень похож на первый, его мы используем для создания новой учётной записи. Разница между ним и первым методом заключается в конечной точке, с которой мы работаем.
Третий метод используется для проверки того, аутентифицирован пользователь или нет.
Последний метод позволяет возвратить заголовок Authorization
.
Теперь продолжим работу над компонентом, открыв файл Authentication.vue
. Тут мы будем пользоваться средствами Vuetify:
<template>
<div class="l-auth-container">
<div class="l-auth">
<v-form v-model="validLogin">
<v-text-field label="Username"
v-model="credentials.username"
prepend-icon="account_box"
:rules="rules"
required
color="light-blue lighten-1">
</v-text-field>
<v-text-field label="Password"
v-model="credentials.password"
prepend-icon="lock"
:rules="rules"
:append-icon="loginPasswordVisible ? 'visibility' : 'visibility_off'"
:append-icon-cb="() => (loginPasswordVisible = !loginPasswordVisible)"
:type="loginPasswordVisible ? 'text' : 'password'"
color="light-blue lighten-1"
required>
</v-text-field>
<v-btn flat color="light-blue lighten-1" @click.native="signUpVisible = true">Create account</v-btn>
<v-btn color="light-blue lighten-1" @click.native="submitAuthentication()">Login</v-btn>
</v-form>
</div>
<div class="l-signup" v-if="signUpVisible">
<v-form v-model="validSignUp">
<v-text-field label="Username"
v-model="newUser.username"
prepend-icon="account_box"
:rules="rules"
required
color="light-blue lighten-1">
</v-text-field>
<v-text-field label="Password"
v-model="newUser.password"
prepend-icon="lock"
:rules="rules"
:append-icon="signUpPasswordVisible ? 'visibility' : 'visibility_off'"
:append-icon-cb="() => (signUpPasswordVisible = !signUpPasswordVisible)"
:type="signUpPasswordVisible ? 'text' : 'password'"
color="light-blue lighten-1"
required>
</v-text-field>
<v-btn block color="light-blue lighten-1" @click.native="submitSignUp()">Sign Up</v-btn>
</v-form>
</div>
<v-snackbar timeout="6000"
bottom="bottom"
color="red lighten-1"
v-model="snackbar">
{{ message }}
</v-snackbar>
</div>
</template>
Здесь имеется элемент div
с классом l-auth-container
, который выполняет роль контейнера. Следом идёт ещё один div
с классом l-auth
, который содержит структуру элементов для организации формы ввода, в частности, это элемент v-form
, привязанный к данным переменной validLogin
.
Внутри него находится пара подписанных полей ввода v-text-field
, которые привязаны к данным из credentials
(мы займёмся этими данными ниже). Поля снабжены иконками, взятыми из https://material.io/icons/, с ними также связаны правила по проверке ввода (и там и там — одни и те же правила, не будем усложнять проект), кроме того, оба эти поля являются обязательными.
Второе поле ввода предназначено для пароля, оно снабжено иконкой, которая указывает на то, может ли пользователь видеть вводимый пароль. У этой иконки есть коллбэк, являющийся стрелочной функцией, который позволяет переключать значение переменной loginPasswordVisible
с true
на false
и наоборот. Если эта переменная установлена в true
, то параметр type
поля ввода устанавливается в text
, иначе это password
.
И, наконец, тут присутствуют описания кнопок, которые мы используем для создания новой учётной записи или для отправки формы с целью входа в систему.
Следующий фрагмент кода описывает структуру формы регистрации в системе, которая видна лишь в том случае, если переменная signUpVisible
установлена в true
. Устройство этой формы похоже на устройство формы входа в систему, тут изменены лишь несколько строк. В частности, здесь используется переменная signUpPasswordVisible
вместо loginPasswordVisible
и другой метод обработки щелчка по кнопке.
Далее, тут имеется панель v-snackbar
, которая, в ходе аутентификации, используется для вывода сообщений.
Теперь, в том же файле Authentication.vue
, опишем скрипт компонента:
<script>
import Authentication from '@/components/pages/Authentication'
export default {
data () {
return {
snackbar: false,
validLogin: false,
validSignUp: false,
signUpVisible: false,
loginPasswordVisible: false,
signUpPasswordVisible: false,
rules: [ (value) => !!value || 'This field is required' ],
credentials: {
username: '',
password: ''
},
newUser: {
username: '',
password: ''
},
message: ''
}
},
methods: {
submitAuthentication () {
Authentication.authenticate(this, this.credentials, '/')
},
submitSignUp () {
Authentication.signup(this, this.newUser, '/')
}
}
}
</script>
Тут всё начинается с импорта файла index.js
из папки Authentication
, так как нам нужен метод authenticate
, определённый внутри этого файла.
Взглянем теперь на переменные, хранящие данные компонента:
snackbar
: используется для панели сообщений.validLogin
: используется для проверки формы входа в систему.validSignUp
: используется для проверки формы регистрации.signUpVisible
: используется для вывода формы регистрации (при установке вtrue
).loginPasswordVisible
: указывает на то, может ли пользователь видеть пароль, вводимый в форме входа в систему.signUpPasswordVisible
: указывает на то, можно ли видеть пароль, вводимый в форме регистрации.rules
: правила проверки данных, введённых в поля форм.credentials
: объект, привязанный к полям ввода формы входа в систему, используемый для аутентификации пользователя.newUser
: объект, привязанный к полям ввода формы регистрации в системе.message
: используется для вывода сообщений в ходе аутентификации.
В этом компоненте есть пара методов. В методе submitAuthentication
осуществляется вызов метода authenticate
из файла Authentication
, с передачей контекста, учётных данных и пути для перенаправления. Метод submitSignUp
используется для вызова метода signup
.
И, наконец, вот код стилизации компонента, который надо разместить в том же файле Authentication.vue
(тут вы можете дать волю фантазии и сделать всё таким, как вам хочется):
<style lang="scss">
@import "./../../../assets/styles";
.l-auth {
background-color: $background-color;
padding: 15px;
margin: 45px auto;
min-width: 272px;
max-width: 320px;
animation: bounceIn 1s forwards ease;
}
.l-signup {
background-color: $background-color;
padding: 15px;
margin: 45px auto;
min-width: 272px;
max-width: 320px;
animation: slideInFromLeft 1s forwards ease;
}
</style>
Вот как выглядят компоненты для входа в систему и регистрации нового пользователя.
Разработка компонента Home
Перейдём в папку pages
и создадим файл компонента Home.vue
:
На данный момент в шаблоне этого компонента, код которого представлен ниже, будут лишь несколько текстовых сообщений:
<template>
<div>
<h3>Hi! this is our App's Home</h3>
<ul>
<li v-if="users != null" v-for="user in users">
{{ user.username }}
</li>
</ul>
</div>
</template>
<script>
import Axios from 'axios'
import Authentication from '@/components/pages/Authentication'
const BudgetManagerAPI = `http://${window.location.hostname}:3001`
export default {
data () {
return {
users: []
}
},
mounted () {
this.getAllUsers()
},
methods: {
getAllUsers (context) {
Axios.get(`${BudgetManagerAPI}/api/v1/users`, {
headers: {
'Authorization': Authentication.getAuthenticationHeader(this)
}
}).then(({data}) => (this.users = data))
}
}
}
</script>
Этот компонент является основой для домашней страницы, которой мы займёмся в следующей части этой серии. А пока выполним GET-запрос к API для получения всех зарегистрированных пользователей, используя отладочный метод API и передавая токен в заголовке запроса. Вот как это будет выглядеть:
Защита системы навигации
Откроем файл index.js
из папки router
. Вот к какому виду его нужно привести:
import Vue from 'vue'
import Router from 'vue-router'
import * as Auth from '@/components/pages/Authentication'
// Pages
import Home from '@/components/pages/Home'
import Authentication from '@/components/pages/Authentication/Authentication'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home,
meta: {
requiredAuth: true
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
router.beforeEach((to, from, next) => {
if (to.meta.requiredAuth) {
if (Auth.default.user.authenticated) {
next()
} else {
router.push('/login')
}
} else {
next()
}
})
export default router
Рассмотрим этот код.
import * as Auth from '@/components/pages/Authentication'
В этой строке мы импортируем файл Authentication
, называя его Auth
, так как компонент Authentication
также был импортирован.
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home,
meta: {
requiredAuth: true
}
},
{
path: '/login',
name: 'Authentication',
component: Authentication
}
]
})
Тут мы даём имя объекту Router
для того, чтобы позже создать защиту системы навигации. Также мы добавляем путь к компоненту Home
. В параметре meta.requiredAuth
будет записано true
. Это означает, что если к этому компоненту попытается получить доступ неаутентифицированный пользователь, он будет перенаправлен на страницу входа в систему.
router.beforeEach((to, from, next) => {
if (to.meta.requiredAuth) {
if (Auth.default.user.authenticated) {
next()
} else {
router.push('/login')
}
} else {
next()
}
})
Здесь мы защищаем систему навигации. А именно, регистрируем глобальный сторожевой хук, пользуясь которым перед переходом по каждому маршруту проверяем, установлен ли его параметр meta.requiredAuth
в true
. Если это так, мы проверяем объект пользователя из Authentication
. Если пользователь не аутентифицирован, мы перенаправляем его на страницу входа в систему.
export default router
Этой командой экспортируем маршрутизатор.
Теперь откройте файл main.js
в папке application
. Тут мы собираемся импортировать файл Authentication
и вызвать метод checkAuthentication
:
import Vuetify from 'vuetify'
import Authentication from '@/components/pages/Authentication'
import('../node_modules/vuetify/dist/vuetify.min.css')
Vue.use(VueCookie)
Vue.use(Vuetify)
Vue.config.productionTip = false
Authentication.checkAuthentication()
Без этого, если пользователь перезагрузит страницу или закроет её, а потом снова откроет, он будет перенаправлен на страницу входа в систему.
Итоги
Сегодня мы рассказали о том, как создать приложение Vue.js, как разрабатывать компоненты Vue, рассмотрели выполнение HTTP-запросов и защиту маршрутов приложения. В следующей части займёмся доработкой компонента Home и продолжим развитие клиентской и серверной частей приложения.
Уважаемые читатели! Если вы хорошо разбираетесь в Vue.js, просим рассказать о том, для каких проектов вы его использовали, и довольны ли вы тем, что получилось.
Автор: ru_vds