[Перевод] Веб-приложение на Node и Vue, часть 5: завершение работы над проектом

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

zy3wpd4cfzhdyq_emhlapl-3ccs.jpeg

Сегодня работа над этим учебным проектом завершится. А именно, в данном материале пойдёт речь о разработке страниц по добавлению в систему записей о новых клиентах и финансовых документах, а также о создании механизмов для редактирования этих данных. Здесь же мы рассмотрим некоторые улучшения 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

Вот код шаблона компонента


Тут мы сначала добавляем в шаблон элемент v-select для установки состояния документа, затем — v-select для выбора клиента, который нам нужен. Далее, у нас имеется поле v-text-field для ввода заголовка документа и v-text-field для вывода описания.

Затем мы перебираем элементы budget.items, что даёт нам возможность добавлять элементы в документ и удалять их из него. Здесь же имеется красная кнопка, которая позволяет вызывать функцию removeItem, передавая ей элемент, который нужно удалить.

Далее, здесь есть три поля v-text-fields, предназначенные, соответственно, для названия товара, цены за единицу и количества.

В конце ряда имеется простой элемент span, в котором выводится промежуточный итог по строке, subtotal, представляющий собой произведение количества и цены товара.

Ниже списка товаров имеется ещё три элемента. Это — синяя кнопка, которая используется для добавления новых элементов путём вызова функции addItem, элемент span, который показывает общую стоимость всех товаров, которые имеются в документе (сумма показателей subtotal всех элементов), и зелёная кнопка, которая используется для сохранения документа в базу данных путём вызова функции saveBudget с передачей ей, в качестве параметра, документа, который мы хотим сохранить.

Скрипт компонента BudgetCreation

Вот код, который приводит компонент 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.

Шаблон компонента ClientCreation


Скрипт компонента ClientCreation


Стили компонента ClientCreation


Теперь пришла очередь компонента BudgetEdit.

▍Компонент BudgetEdit


Этот компонент, по сути, является модифицированной версией уже рассмотренного компонента BudgetCreation. Рассмотрим его составные части.

Шаблон компонента BudgetEdit


Единственное различие шаблонов компонентов BudgetEdit и BudgetCreation заключается в кнопке сохранения изменений и в связанной с ней логике. А именно, в BudgetCreation на ней написано Save, она вызывает метод saveBudget. В BudgetEdit эта кнопка несёт на себе надпись Update и вызывает метод fixClientNameAndUpdate.

Скрипт компонента BudgetCreation


Здесь всё начинается с получения трёх свойств. Это — clients, fixClientNameAndUpdate и selectedBudget. Данные тут те же самые, что и в компоненте BudgetCreation. А именно, тут имеется объект Budget и массив states.

Далее, здесь можно видеть обработчик события жизненного цикла компонента mounted, в котором мы вызываем метод parseBudget, о котором поговорим ниже. И, наконец, здесь есть объект methods, в котором присутствуют уже знакомые вам по компоненту BudgetCreation методы addItem и removeItem, а также новый метод parseBudget. Этот метод используется для того, чтобы установить значение объекта budget в то, которое передано в свойстве selectedBudget, но мы, кроме того, используем его для подсчёта промежуточных итогов по товарам документа и общей суммы по документу.

Стиль компонента BudgetCreation


▍Компонент ClientEdit


Этот компонент, по аналогии с только что рассмотренным, похож на соответствующий компонент, используемый для создания клиентов — ClientCreation. Главное отличие заключается в том, что тут вместо метода saveClient используется метод updateClient. Рассмотрим устройство компонента ClientEdit.

Шаблон компонента ClientEdit


Скрипт компонента ClientEdit


Стиль компонента ClientEdit


На этом мы завершаем создание новых компонентов и переходим к работе с компонентами, которые уже были в системе.

Доработка существующих компонентов


Теперь осталось лишь внести некоторые изменения в существующие компоненты и приложение будет готово к работе.

Начнём с компонента ListBody.

▍Компонент ListBody


Шаблон компонента ListBody

Напомним, что код этого компонента хранится в файле ListBody.vue

Исходный код


В этом компоненте надо выполнить буквально пару изменений и дополнений. Так, сначала добавим новое условие в конструкцию v-if блока md-list-item:

parsedBudgets === null


Кроме того, мы уберём первую кнопку, которую использовали для вывода документа, так как она нам больше не нужна из-за того, что увидеть документ можно, нажав на кнопку редактирования.

Тут мы добавили метод getItemAndEdit к новой первой кнопке и метод deleteItem к последней кнопке, передавая этому методу элемент, данные и переменную budgetsVisible в качестве параметров.

Ниже всего этого имеется блок md-item-list, который мы используем для вывода отфильтрованного после поиска списка документов.

Скрипт компонента ListBody


В этом компоненте мы получаем множество свойств. Опишем их:

  • data: это либо список документов, либо список клиентов, но никогда и то и другое.
  • budgetsVisible: используется для проверки того, просматриваем ли мы список документов или клиентов, может принимать значения true или false.
  • deleteItem: функция для удаления элемента, которая принимает, в качестве параметра, некий элемент.
  • getBudget: функция, которую мы используем для загрузки отдельного документа, который планируется редактировать.
  • getClient: функция, используемая для загрузки карточки отдельного клиента для последующего редактирования.
  • parsedBudgets: документы, отфильтрованные после выполнения поиска.


В компоненте есть всего один метод, getItemAndEdit. Он принимает, в качестве параметра, элемент, при этом, на основе анализа наличия у элемента свойства, содержащего телефонный номер, принимается решение о том, является ли элемент карточкой клиента или финансовым документом.

Стиль компонента ListBody


Правку кода компонента ListBody мы завершили, займёмся теперь компонентом Header.

▍Компонент Header


Шаблон компонента Header


Здесь, в первую очередь, мы меняем свойство v-model поля поиска на searchValue.

Кроме того, мы модифицируем элемент v-select, привязывая к его событию change метод selectState.

Скрипт компонента Header


Тут добавлены два новых свойства — selectState, представляющее собой функцию, и search, которое является строкой. В данных search теперь используется searchValue и приведённый к нижнему регистру массив элементов statusItems.

Стиль компонента Header


С компонентом Header мы разобрались, теперь поработаем с компонентом Home.

▍Компонент Home


Шаблон компонента Home