Веб-приложение на Node и Vue, часть 2: компоненты, формы, маршруты

https://medium.com/@gdomaradzki/building-a-budget-manager-with-vue-js-and-node-js-part-ii-f08c410c944d
  • Перевод
Перед вами — вторая часть серии материалов, которая посвящена созданию веб-приложения Budget Manager с использованием Node.js, Vue.js и MongoDB. В первой части мы занимались сервером, а именно — подготовили основные методы RESTful API и наладили JWT-аутентификацию. Сегодня приступим к работе над клиентской частью приложения, создадим каркас фронтенда, средства для регистрации в системе и входа в неё, поговорим о маршрутах и об их защите.

image

Установка 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, просим рассказать о том, для каких проектов вы его использовали, и довольны ли вы тем, что получилось.
RUVDS.com 477,72
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией
Комментарии 7
  • +2
    Меня пугают портянки из data.
    код
    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: ''
          }
        }



    Ух как много точек.
    @import "./../../../assets/styles";


    А meta везде есть? Смотрю и вижу только в одном роуте.
     
     if (to.meta.requiredAuth) {
    


    А это прям развеселило)))
    
    if (token) this.user.authenticated = true
    else this.user.authenticated = false
    


    Так и будете каждый раз заголовки добавлять?
    
    Axios.get(`${BudgetManagerAPI}/api/v1/users`, {
         headers: {
          'Authorization': Authentication.getAuthenticationHeader(this)
         }
    }).then(({data}) => (this.users = data))
    


    Валидация форм с автокомплитом в хроме нормально работает?
    Есть живой пример?
    • 0

      Отдельно веселит 'Authorization': Authentication.getAuthenticationHeader(this). так все-таки авторизация или аутентификация?

      • 0
        А чем собственно в условии не угодило?
        Можно было конечно написать
        this.user.authenticated = (token) ? true : false

        Но вариант
        
        if (token) {
        
            this.user.authenticated = true
        
        } else {
        
            this.user.authenticated = false
        
        }
        

        Тоже отличное читается, ну разве что да, для хипстеров такой код индусский
        • 0
          Хм… Как по мне можно и проще.
          this.user.authenticated = !!token;
          • –1
            Такой код индусский для любого кто даже очень поверхностно знаком с JavaScript.
            • 0
              Вы в notepad пишете? IDE даже подсвечивает такую явную особенность национального программирования.
              image
          • 0
            @import "./../../../assets/styles";

            Если быть чуть более конструктивным, вместо такого лучше использовать алиасы (в Webpack это будет блок resolve.alias).

            if (token) this.user.authenticated = true
            else this.user.authenticated = false

            Эпично, согласен, привет индусам.

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

            Самое читаемое