Создание приложения для торговых точек с помощью Serialized

Этот учебник был изначально написан для платформы Serialized. Вы можете просмотреть оригинальную статью в блоге Serialized здесь.

Когда мы думаем о технологиях, мы часто не думаем о повседневных предприятиях, таких как рестораны, киоски и магазины. Однако технологии используются в розничной торговле и общественном питании каждый день! Основное технологическое пересечение между этими видами бизнеса — это POS-система (что означает «точка продаж»). Именно эта программа обеспечивает вас тако из вашего любимого ресторана, свитером, на который вы положили глаз на Poshmark, и новым iPhone на сайте Apple. Они также позволяют сотрудникам звонить и детализировать заказы, обеспечивая основное средство коммуникации для заказов в рамках всего бизнеса.

Поскольку POS-системы являются основой многих розничных и продовольственных предприятий, меня заинтриговала идея создания такой системы. В этой статье мы погрузимся в создание веб-приложения POS, использующего React, Express и Serialized.

Что мы создаем

Наша POS-система будет использовать React для фронтенда, Express для бэкенда и Serialized для создания и хранения заказов, а также постоянного добавления товаров в заказы.

Serialized — это облачный API-движок для создания систем, управляемых событиями, — он помогает нам легко собирать полную хронологию и историю событий и объединять их в связанные группы. Применительно к нашей POS-системе мы будем использовать Serialized для отслеживания событий (клиенты, заказывающие товары) и объединения их в связанные группы (заказы клиентов).

Ниже приведена диаграмма того, как будет выглядеть поток пользователей приложения:

Три основные функции, на которых мы сосредоточимся в этом учебнике, это:

  1. создание новых заказов,
  2. добавление товаров в существующие заказы, и
  3. пометка заказов как выполненных.

Эти три сценария будут отражать сценарии использования нашей очень простой POS-системы. Конечный продукт будет выглядеть следующим образом:

Начало работы

Прежде чем приступить к созданию, убедитесь, что вы настроили следующее:

  • Node: Чтобы проверить, установлен ли у вас Node, вы можете запустить node -v в командной строке. Если версия не появится, вам нужно будет установить ее — вы можете найти инструкции по установке для вашей машины здесь.
  • npx: npx — это запуск пакетов для пакетов Node, который позволяет вам выполнять пакеты из реестра npm без необходимости их установки. Чтобы проверить, установлен ли он у вас (обычно он поставляется с npm, который поставляется с Node), вы можете запустить npx -v. Если версия не появится, вы можете установить npx, используя инструкции здесь.
  • Serialized: Чтобы использовать Serialized API, вам необходимо создать учетную запись. После создания учетной записи вам будет предложено создать проект, который также необходим для начала сборки с помощью API. Вы можете назвать свой проект как угодно — я выбрал POS App. Вы можете узнать больше о проектах в Serialized здесь.

Если вы предпочитаете не собирать, а просматривать код, я помогу вам! Вы можете посмотреть репозиторий GitHub для этого проекта здесь. Все инструкции по запуску проекта доступны в README.md в корневом каталоге репозитория. (Совет: репозиторий GitHub также является отличным источником подсказок, если вы застряли во время сборки проекта параллельно с учебником).

Настройка проекта

Настройка проекта основана на этом руководстве от freeCodeCamp.

  1. Для начала инициализируйте каталог проекта на вашей машине в выбранном вами месте, выполнив команду mkdir pos-app или создав папку pos-app вручную. Вставьте cd в нее в Терминале и выполните команду

     npx create-react-app client
    

    Это создаст папку client, в которой будет находиться фронтенд вашего приложения.

  2. После создания папки client выполните следующие команды, чтобы войти в только что созданную папку client, а затем запустите сервер фронтенда:

    cd client
    npm start
    

    Если ваш проект был настроен правильно, вы должны увидеть приложение React по умолчанию в браузере по адресу [localhost:3000](http://localhost:3000):

  3. Если ваш фронтенд успешно запустился, пришло время настроить бэкенд! Завершите работу сервера фронтенда, выполнив CTRL + C. Затем используйте команду cd ../ из папки client, чтобы вернуться в корневой каталог вашего проекта. Затем выполните следующие команды для создания приложения Express в папке с названием api и запустите бэкенд:

    npx express-generator api
    cd api
    npm install
    npm start
    

    Если ваш бэкенд настроен правильно, вы должны увидеть это представление после выполнения команды npm start:

    Вы можете узнать больше о пакете express-generator, используемом для настройки бэкенда здесь.

  4. На данный момент фронтенд и бэкенд подключены к localhost:3000. Поскольку в процессе разработки приложения вам придется запускать оба сервера одновременно, вам нужно будет изменить порт, на котором работает бэкенд, чтобы избежать столкновения портов. Для этого перейдите в файл bin/www в каталоге api. Обновите строку 15 так, чтобы ее значение по умолчанию теперь указывало на порт 9000. После обновления строка будет выглядеть следующим образом:

    var port = normalizePort(process.env.PORT || '9000');
    

    Теперь при запуске npm start в папке api для запуска бэкенда, вы сможете увидеть запущенный Express-сервер на localhost:9000.

Настройка Serialized

  1. Для того чтобы использовать Serialized с приложением, которое было настроено в шагах выше, вы можете установить клиент Serialized для Javascript и Typescript. Поскольку API Serialized будет вызываться в бэкенде Express, выполните следующую команду для установки клиента в каталог api:

    npm install @serialized/serialized-client
    
  2. После установки клиента создайте файл .env в директории api для настройки переменных окружения для ключей Serialized API, которые будут передаваться в клиент для доступа к информации о вашем аккаунте. Ваш файл .env будет содержать эти две переменные окружения:

    SERIALIZED_ACCESS_KEY=
    SERIALIZED_SECRET_ACCESS_KEY=
    

    Чтобы найти значения SERIALIZED_ACCESS_KEY и SERIALIZED_SECRET_ACCESS_KEY, перейдите в раздел Settings > API Keys в вашей панели Serialized для созданного вами проекта и установите переменные окружения в соответствующие значения.

Создание новых заказов

Теперь, когда API Serialized и авторизация настроены, вы можете сделать первый вызов из вашего приложения к API! В этом разделе мы рассмотрим наш первый случай использования Serialized Aggregates API для создания нового заказа в нашей POS-системе.

  1. Чтобы начать, создайте файл order.js в каталоге api. Этот файл будет служить основой для определения понятия «заказ» в Serialized. Здесь же вы будете создавать или добавлять товары в заказы, а также другую логику и обработчики событий для запуска функциональности нашего приложения.

    Вставьте следующий код в файл order.js:

    const { DomainEvent } = require("@serialized/serialized-client");
    
    class Order {
        get aggregateType() {
          return "order";
        }
    
        constructor(state) {
          this.orderId = state.orderId;
          this.items = state.items;
          this.total = state.total;
          this.completed = state.completed;
        }
    
        createOrder(orderId) {
          if (!orderId || orderId.length !== 36) throw "Invalid orderId";
          return [DomainEvent.create(new OrderCreated(orderId))];
        }
    
        get eventHandlers() {
          return {
            OrderCreated(state, event) {
              console.log("Handling OrderCreated", event);
              return OrderState.newState(event.orderId).withOrderId(event.orderId);
            },
          };
        }
    }
    
    class OrderCreated {
      constructor(orderId) {
        this.orderId = orderId;
      }
    }
    
    class OrderState {
        constructor({ orderId, items = [], total = 0.0, completed = false }) {
          this.orderId = orderId;
          this.items = items;
          this.total = total;
          this.completed = completed;
        }
    
        static newState(orderId) {
          return new OrderState({ orderId });
        }
    
        withOrderId(orderId) {
          return Object.assign({}, this, { orderId });
        }
    }
    
    module.exports = { Order };
    

    Чтобы разобраться с этим файлом, давайте разберем его по классам:

  • Заказ: Этот класс является представлением фактического объекта заказа. Объект Order определяется как Aggregate в Serialized, что означает, что это процесс, состоящий из событий, которые будут действиями, происходящими с конкретным объектом Order. В данном учебнике этими событиями будут создание новых заказов, добавление товара в заказ и завершение заказа.

    • Как указано в конструкторе класса Order, для объявления нового экземпляра Order потребуется передать объект state, представляющий заказ и его текущие статистики. Это связано с тем, что каждый агрегат состоит из событий, и они отвечают за обновление состояния всего заказа по мере их срабатывания.
    • Далее инициализируется функция createOrder() — она проверяет, существует ли данный orderId и соответствует ли он 36-символьному формату UUID, указанному для идентификаторов заказов. Затем инициализируется наше новое событие создания заказа вызовом DomainEvent.create().
    • Наконец, объявляется функция eventHandlers(), которая принимает текущее состояние заказа и событие, произошедшее с заказом.
      • На данном этапе обучения пока возвращен только обработчик события OrderCreated, но будут добавлены дополнительные обработчики для других типов событий. Обработчики событий будут записывать событие в консоль и использовать объект OrderState для отслеживания состояния заказа.
  • OrderCreated: Этот класс представляет тип события — в данном сценарии это то, что был создан новый заказ. Для каждого нового добавленного события потребуется новый класс, определяющий, какую информацию событие передает API. Имя класса должно соответствовать обработчику события, которому он соответствует (в данном случае OrderCreated. Для создания нового заказа требуется только одно свойство — orderId, поэтому это единственное свойство, объявленное в данном классе.

  • OrderState: Этот класс определяет текущее состояние заказа и отслеживает его изменения, чтобы их можно было передавать в виде событий объекту Order, который будет посылать события в Serialize по мере их возникновения. Помните, что изменение состояния может быть любым: от добавления новых элементов в заказ до пометки его как завершенного — последнее обозначается свойством OrderState completed, установленным в true.

  1. Когда файл order.js будет создан, добавьте файл order-client.js в ту же директорию. Этот файл будет действовать как клиент, который соединяет аутентификацию для Serialized Aggregates API с функциональностью, написанной в order.js. Вставьте следующий код в файл order-client.js:

    const { Order } = require("./order");
    
    const handleError = async function (handler) {
      try {
        await handler();
      } catch (error) {
        throw new Error("Failed to process command: " + error);
      }
    };
    
    class OrderClient {
      constructor(serializedClient) {
        this.client = serializedClient.aggregateClient(Order);
      }
    
      async createOrder(orderId) {
        await handleError(
          async () =>
            await this.client.create(orderId, (order) => {
              return order.createOrder(orderId);
            })
        );
      }
    }
    
    module.exports = OrderClient;
    

    Файл импортирует класс Order из предыдущего файла order.js. Затем инициализируется обработчик ошибок для обработки общей логики API-запроса вызова определенной функции, а также для отлова и вывода на экран любых потенциальных ошибок. Кроме того, объявляется класс OrderClient. Этот класс предполагает, что передается аутентифицированный экземпляр клиента API Serialized с общей аутентификацией (serializedClient), и использует его для инициализации экземпляра клиента API Aggregates с помощью функции aggregateClient().

  2. После настройки order.js и order-client.js можно создать маршрут, который будет инициализировать аутентифицированного клиента Serialized API и сделает необходимые API-запросы доступными для вызова из фронтенда. Перейдите в каталог api/routes и создайте файл orders.js со следующим кодом внутри:

    var express = require("express");
    require("dotenv").config();
    var router = express.Router();
    const { Serialized } = require("@serialized/serialized-client");
    const OrderClient = require("../order-client");
    
    const serializedClient = Serialized.create({
      accessKey: process.env.SERIALIZED_ACCESS_KEY,
      secretAccessKey: process.env.SERIALIZED_SECRET_ACCESS_KEY,
    });
    const orderClient = new OrderClient(serializedClient);
    
    router.post("/create", async function (req, res, next) {
      const { orderId } = req.body;
      console.dir(req.body);
      try {
        var response = await orderClient.createOrder(orderId);
        res.send(response);
      } catch (error) {
        console.log(error);
        res.status(400).json({ error: error });
      }
    });
    
    module.exports = router;
    

    Приведенный выше код инициализирует аутентифицированный экземпляр Serialized клиента, используя ключи доступа вашей учетной записи, создает новый экземпляр OrderClient, определенный в order-client.js, используя этот Serialized клиент, а затем вызывает функцию на этом экземпляре OrderClient для создания нового заказа на основе переданной информации. Затем объявляется POST-маршрут /create. Этот маршрут принимает orderId в теле запроса. Используя экземпляр OrderClient, объявленный в верхней части файла, он вызывает функцию createOrder() из файла order-client.js и передает orderId.

  3. Теперь, когда маршрут orders.js создан, его нужно добавить в app.js в директории api, чтобы его можно было вызывать в приложении. Добавьте инициализацию для переменной ordersRouter в строке 9 в api/app.js:

    var ordersRouter = require("./routes/orders");
    

    Затем, в строке 24 файла api/app.js, добавьте объявление app.use() для ordersRouter, чтобы указать маршрут /orders на конечные точки в этом файле:

    app.use("/orders", ordersRouter);
    

    Теперь, когда этот маршрут добавлен, мы можем POST на конечную точку /orders/create на localhost:9000, чтобы создать новый заказ!

Подключение нашего React Frontend

Теперь, когда маршруты API настроены на стороне Express, давайте вызовем его из фронтенда React! Мы можем настроить фронтенд-приложение на вызов API к только что созданному маршруту /orders/create, чтобы мы могли сделать заказ с фронтенда.

  1. Браузеры часто применяют политику одинакового происхождения для запросов, что приводит к ошибкам CORS (Cross-Origin Resource Policy) в случае, если запросы на определенном домене выполняются из домена другого происхождения. В данном примере используется [localhost:3000](http://localhost:3000) для фронтенда, в то время как мы получаем информацию из конечной точки [localhost:9000](http://localhost:9000) из бэкенда Express — эта разница в URL-адресах может привести к CORS-ошибке, поскольку браузер может сказать, что это нарушает политику одинакового происхождения. Чтобы предотвратить ошибки CORS в вашем приложении после подключения фронтенда и бэкенда, установите пакет CORS в api с помощью следующей команды:

    npm install --save cors
    
  2. В api/app.js добавьте следующее в строке 6, чтобы добавить пакет CORS, который только что был установлен на бэкенд:

    var cors = require("cors");
    

    Затем в строке 23 добавьте следующую строку, чтобы указать вашему приложению Express использовать пакет CORS:

    app.use(cors());
    

    Возможно, на этом этапе стоит проверить api/app.js в репозитории GitHub, чтобы убедиться, что все настроено правильно.

  3. В директории client создайте новую папку внутри src под названием components и инициализируйте файл POSHome.js:

    import React from "react";
    
    export default function POSHome() {
    
      async function createOrder() {
        var generatedOrderId = crypto.randomUUID();
        var data = { orderId: generatedOrderId };
        var order = await fetch("http://localhost:9000/orders/create", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(data),
        });
      }
    
      return (
        <div>
          <h1>POS System ☕️</h1>
          <div>
            <button onClick={createOrder}>Create Order</button>
          </div>
        </div>
      );
    }
    

    Этот файл объявляет функциональный компонент под названием POSHome (это место, где будет находиться домашняя страница POS-системы).

    На этой странице будет находиться кнопка, которая при нажатии вызывает createOrder(). Эта функция использует crypto.randomUUID() для генерации UUID, который будет соответствовать стандартам, ожидаемым бэкендом, запихивает все это в объект data и отправляет его в нашу новую конечную точку /orders/create.

  4. Замените client/src/App.js следующим кодом, чтобы компонент POSHome передавался в основное приложение и был виден с главной страницы:

    import "./App.css";
    import POSHome from "./components/POSHome";
    
    function App() {
      return (
        <div className="App">
            <POSHome />
        </div>
      );
    }
    
    export default App;
    
  5. Откройте новое окно или вкладку в Терминале так, чтобы у вас были открыты две вкладки или окна. В одной вкладке запустите npm start в папке api. В другой вкладке запустите npm start в папке client. Когда [localhost:3000](http://localhost:3000) запустит фронтенд, вы увидите следующий экран:

    Нажмите кнопку Create Order, а затем перейдите на панель Serialized для вашего проекта и перейдите на страницу Data Explorer. Вы должны увидеть запись для нового заказа — того, который мы только что создали при загрузке страницы из компонента POSHome frontend, вызывающего конечную точку /orders/create:

    Если вы посмотрите вкладку или окно терминала, в котором запущен сервер api, вы также увидите что-то вроде следующего:

    OPTIONS /orders/create 204 0.236 ms - 0
    { orderId: 'd3ce8600-9e71-4417-9726-ab3b9056df48' }
    POST /orders/create 200 719.752 ms - -
    

    Это журнал событий с конечной точки бэкенда, в котором регистрируется случай создания нового заказа. Любые console.log заявления, сделанные с бэкенда, также будут отображаться здесь.

Интеграция нашей функциональности в приложение

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

  1. Начнем с инициализации набора данных, который будет представлять товары, которые вы будете продавать в POS. В папке client/src создайте папку data и добавьте в нее файл items.json. Внутри файла настройте что-то вроде этого:

    {
        "items": [
            {
                "name": "Tea",
                "price": 3.99
            },
            {
                "name": "Coffee",
                "price": 4.99
            },
            {
                "name": "Bagel",
                "price": 2.50
            }
        ]
    }
    

    Здесь мы добавили несколько предметов инвентаря в массив свойств items, каждый из которых имеет свойство name и price.

  2. Теперь, когда данные о том, какие товары продаются в POS-системе, добавлены, их нужно отобразить в представлении. Для этого потребуется новый компонент, который будет отображаться только при нажатии на кнопку Create Order, добавленную в предыдущем шаге. В client/src/components добавьте файл ItemDisplay.js для нового компонента потока оформления заказа. Вот как это может выглядеть:

    import React from "react";
    
    export default function ItemDisplay (props) {
      var data = require("../data/items.json");
    
      return (
        <div>
          <div>
            {data.items.map((item, index) => {
              return (
                <button key={index}>
                  {item.name}
                </button>
              );
            })}
          </div>
        </div>
      );
    }
    

    В компоненте ItemDisplay данные из items.json импортируются в переменную data. Затем, в return компонента, каждый элемент в data перебирается и заменяется кнопкой с названием этого элемента в качестве метки.

  3. Теперь давайте обновим client/src/components/POSHome.js так, чтобы при создании заказа отображался компонент ItemDisplay. Для этого мы будем использовать переменные состояния — они отлично подходят для условного отображения компонентов. Для начала обновите строку import в верхней части POSHome.js, чтобы она импортировала и хук useState. Пока мы там, импортируйте компонент ItemDisplay из предыдущей статьи.

    import React, { useState } from "react";
    import ItemDisplay from "./ItemDisplay";
    
  4. Хук useState инициализирует переменную состояния и даст нам возможность обновлять ее в будущем. Начнем с startedOrder — она будет отслеживать, был ли начат заказ, и если да, то будет отображать компонент ItemDisplay. Переменная будет инициализирована в строке 5 с начальным значением false следующим образом:

    const [startedOrder, setStartedOrder] = useState(false);
    
  5. Далее обновите функцию return() в компоненте POSHome так, чтобы она выглядела следующим образом:

    return (
      <div>
        <h1>POS System ☕️</h1>
        {!startedOrder && (
          <div>
            <button onClick={createOrder}>Create Order</button>
          </div>
        )}
        {startedOrder && (
          <ItemDisplay />
        )}
      </div>
    );
    

    В приведенном выше примере JSX используется для условного отображения определенных элементов в зависимости от значения переменной состояния startedOrder. Логика, реализованная здесь, гласит: «Если значение равно false, отобразите кнопку «Создать заказ». Если значение истинно, отобразите компонент ItemDisplay«.

  6. Последней частью этого является установка startedOrder в true, когда заказ создан. Это можно сделать в функции createOrder(), описанной выше. Добавьте следующий блок внутри функции в строке 15:

    // if order was successful
    if (order.status === 200) {
      setStartedOrder(true);
      setOrderId(generatedOrderId);
    }
    
  7. Теперь пришло время протестировать поток! Загрузите frontend и backend вашего приложения, запустив npm start в каталогах api и client в двух разных вкладках или окнах терминала. После загрузки client вы должны увидеть, как ваше приложение появится в localhost:3000. Нажмите кнопку Создать заказ, и вы увидите, что ваши товары появляются на странице в виде кнопок, как на скриншоте ниже. На этой странице, отображающей компонент ItemDisplay, вы сможете выбрать товары и добавить их в заказ, который будет добавлен в разделе ниже.

Добавление товаров в заказ

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

Чтобы начать, давайте сначала перейдем в бэкенд.

  1. В /client/api/order.js добавьте класс события ItemAdded под тем местом, где объявлен класс OrderCreated:

    class ItemAdded {
      constructor(orderId, itemName, itemPrice) {
        this.orderId = orderId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
      }
    }
    

    Это объявляет класс для нового события ItemAdded, которое будет принимать orderId, itemName, и itemPrice.

  2. Добавьте функцию itemAdded() в класс Order, добавив следующий код в строке 19:

    addItem(itemName, itemPrice) {
      if (this.completed)
        throw "List cannot be changed since it has been completed";
      return [DomainEvent.create(new ItemAdded(this.orderId, itemName, itemPrice))];
    }
    

    Эта функция сначала проверит, завершен ли заказ — если да, то выдаст ошибку, так как новые элементы не могут быть добавлены. Если нет, то она возьмет orderId непосредственно из экземпляра объекта Order и примет itemName и itemPrice для регистрации события о том, какой элемент был добавлен в заказ.

  3. В классе Order добавьте новый обработчик события для добавляемого элемента:

    ItemAdded(state, event) {
      console.log("Handling ItemAdded", event);
      return new Order(state).addItem({
        orderId: event.orderId,
        itemName: event.itemName,
        itemPrice: event.itemPrice
      });
    },
    
  4. Добавьте следующее внутри класса OrderState в строке 64:

    addItem(itemName, itemPrice) {
      return Object.assign({}, this, { items: this.items.unshift({itemName: itemName, itemPrice: itemPrice}) });
    }
    

    Приведенный выше код обновит свойство массива items объекта OrderState так, что новый элемент будет помещен в массив.

    На данном этапе, вероятно, будет хорошей идеей сверить ваш order.js с репозиторием GitHub, чтобы убедиться, что он соответствует.

  5. После обновления api/order.js перейдите в файл order-client.js, чтобы добавить функцию addItem(), которая будет запрашивать логику addItem(), которая была только что добавлена. Вставьте следующее в класс OrderClient в строке 24:

    async addItem(orderId, itemName) {
      await handleError(
        async () =>
          await this.client.update(orderId, (order) => {
            return order.addItem(itemName);
          })
      );
    }
    
  6. Наконец, добавьте маршрут в api/routes/orders.js, чтобы функциональность добавления товара в заказ можно было вызвать из фронтенда. Добавьте этот код в строку 24:

    router.post("/add-item", async function (req, res, next) {
      const { orderId, itemName, itemPrice } = req.body;
      console.dir(req.body);
      try {
        var response = await orderClient.addItem(orderId, itemName, itemPrice);
        res.send(response);
      } catch (error) {
        console.log(error);
        res.status(400).json({ error: error });
      }
    });
    

    Приведенный выше запрос создаст конечную точку по адресу /orders/add-item, которая примет orderId, itemName и itemPrice в теле запроса для добавления товара и учета его свойств, когда он будет добавлен в заказ с определенным orderId.

Потребление конечной точки, которую мы только что создали

Теперь, когда бэкенд завершен, давайте вызовем эту конечную точку во фронтенде! Когда в компоненте ItemDisplay выбирается кнопка товара, она должна вызвать конечную точку /orders/add-item, а также отобразить товарный чек и общую сумму заказа товаров, добавленных на данный момент в заказ.

  1. Чтобы начать, перейдите в /client/src/POSHome.js. Поскольку запрос /add-item принимает orderId, нам нужно передать его компоненту ItemDisplay, чтобы сделать вызов API. Для этого вам понадобится переменная state, которая будет отслеживать идентификаторы заказов. Добавьте следующее объявление переменной состояния:

    const [orderId, setOrderId] = useState("");
    
  2. Затем в createOrder() добавьте следующую строку под setStartedOrder(true);, чтобы установить переменную состояния orderId в ID успешно созданного (и, следовательно, текущего) заказа:

    setOrderId(generatedOrderId);
    
  3. Наконец, обновите строку <ItemDisplay /> в вашей return() до следующей, чтобы передать переменную состояния orderId в качестве параметра:

    <ItemDisplay orderId={orderId} />
    
  4. Отлично! Чтобы отслеживать выбранные элементы, давайте сделаем нечто подобное в /client/src/ItemDisplay.js. Там импортируйте хук useState в верхней части, как мы это сделали с POSHome, и инициализируйте переменные состояния itemsInOrder и orderTotal следующим образом:

    const [itemsInOrder, setItemsInOrder] = useState([]);
    const [orderTotal, setOrderTotal] = useState(0);
    
  5. После добавления переменных состояния, давайте добавим функцию addItemToOrder(), которая будет вызывать конечную точку /orders/add-item, которую мы создали ранее. Добавьте следующую функцию в компонент ItemDisplay над return():

    async function addItemToOrder (name, price) {
      // add in item to order
      var data = { orderId: props.orderId, itemName: name, itemPrice: roundedPrice };
      var order = await fetch("http://localhost:9000/orders/add-item", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
    
      // if order was successful
      if (order.status === 200) {
        var roundedPrice = price.toFixed(2);
        // push item name to setItemsInOrder
        // add total to orderTotal
        setItemsInOrder([...itemsInOrder, { name: name, price: roundedPrice }]);
        setOrderTotal(orderTotal + price);
      }
    }
    

    Функция будет принимать name и price. Затем объявляется объект data, который принимает orderId, itemName и itemPrice — требования к телу запроса. Наконец, запрос выполняется с передачей всех необходимых данных. Если заказ оказывается успешным, то для отображения цены с двумя десятичными знаками price преобразуется с помощью price.toFixed(2). Затем name и price добавляются в массив itemsInOrder, а price добавляется к сумме заказа.

  6. Добавьте событие onClick к тегу <button> в return(). Внутри события вызовите функцию addItemToOrder(). Тег должен выглядеть следующим образом:

    <button
      key={index}
      onClick={() => {
        addItemToOrder(item.name, item.price);
      }}
    >
    

    Это будет вызывать функцию addItemToOrder() каждый раз, когда нажимается кнопка элемента.

  7. Внутри основного <div> в функции return(), после первого вложенного <div>, добавьте секцию для отображения названия и цены товара, а также общей суммы заказа. Он будет динамически обновляться по мере обновления переменных состояния ordreTotal и itemsInOrder.

    <div>
      <h2>Items Ordered</h2>
      <ul className="receipt">
        {itemsInOrder.map((item, index) => {
          return (
            <li key={index}>
              <div className="receiptEntry">
                <div className="itemName">{item.name}</div>
                <div className="itemPrice">{"$" + item.price}</div>
              </div>
            </li>
          );
        })}
      </ul>
      <p>
        <b>Order Total:</b> ${(Math.round(orderTotal * 100) / 100).toFixed(2)}
      </p>
    </div>
    
  8. Наконец, пришло время протестировать функциональность! Запустите фронтенд и бэкенд вашего приложения. Когда приложение загрузится, нажмите кнопку Создать заказ. Вы должны увидеть следующую страницу:

    По мере нажатия на кнопки название товара и цена должны появляться в разделе «Заказанные товары», а общая сумма заказа также должна увеличиваться. Вот пример того, как это должно выглядеть, если вы нажмете кнопки «Чай», «Кофе» и «Бублик»:

    Чтобы подтвердить, что товары были добавлены в заказ, перейдите в Serialized Dashboard > Data explorer > Aggregates > order (в колонке Aggregate type) > Aggregates > щелкните на Aggregate ID верхней (и самой последней) записи. После этого у вас должно появиться следующее представление:

    Если вы нажмете на любой из идентификаторов события ItemAdded, вы увидите объект, содержащий данные, отправленные из события ItemAdded в вашем приложении:

    Приведенное выше событие ItemAdded относится к рогалику за $2,50, который был добавлен к заказу.

Завершение заказов

Последним примером использования будет завершение заказов. Как только заказ будет завершен с помощью компонента ItemDisplay, компонент исчезнет, а кнопка Create Order появится снова, чтобы начать новый заказ.

Давайте начнем с бэкенда!

  1. Во-первых, в /client/api/order.js добавьте класс события OrderCompleted:

    class OrderCompleted {
      constructor(orderId, total) {
        this.orderId = orderId;
        this.total = total;
      }
    }
    

    Этот класс событий требует orderId и окончательную сумму заказа total для завершения заказа.

  2. Аналогично потоку addOrder, нам нужно добавить новую функцию completeOrder() в класс Order:

    completeOrder(total) {
      if (!this.completed) {
        return [DomainEvent.create(new OrderCompleted(this.orderId, total))];
      } else {
        // Don't emit event if already completed
        return [];
      }
    }
    

    Приведенная выше функция сначала проверит, завершен заказ или нет. Если он не завершен, то будет создано новое событие типа OrderCompleted класса, который был добавлен выше. Оно также передает необходимые свойства, принимая orderId от экземпляра объекта Order и передавая total.

  3. Далее добавьте обработчик события OrderCompleted:

    OrderCompleted(state, event) {
      console.log("Handling OrderCompleted", event);
      return new Order(state).completeOrder({
        orderId: event.orderId,
        total: event.total,
      });
    },
    
  4. Затем в OrderState добавьте функцию completeOrder:

    completeOrder(total) {
      return Object.assign({}, this, { completed: true, total: total });
    }
    
  5. Далее, в api/order-client.js, добавьте функцию completeOrder(), чтобы вызвать completeOrder() из order.js:

    async completeOrder(orderId, total) {
      await handleError(
        async () =>
          await this.client.update(orderId, (order) => {
            return order.completeOrder(total);
          })
      );
    }
    
  6. Наконец, добавьте маршрут /orders/complete в api/routes/orders.js:

    router.post("/complete", async function (req, res, next) {
      const { orderId, total } = req.body;
      console.dir(req.body);
      try {
        var response = await orderClient.completeOrder(orderId, total);
        res.send(response);
      } catch (error) {
        console.log(error);
        res.status(400).json({ error: error });
      }
    });
    

Давайте ненадолго вернемся к фронтенду.

  1. Чтобы эта логика работала из ItemDisplay, вам нужно обновить переменную состояния startedOrder из компонента ItemDisplay. Для этого функция setStartedOrder может быть передана в качестве свойства из POSHome. В client/src/components/POSHome.js передайте setStartedOrder компоненту <ItemDisplay> так, чтобы он выглядел следующим образом:

    <ItemDisplay orderId={orderId} setStartedOrder={setStartedOrder} />
    
  2. Теперь в /client/src/components/ItemDisplay.js добавьте новую функцию completeOrder(). Она выполнит вызов конечной точки /orders/complete и передаст переменную orderId из props, а также переменную состояния orderTotal.

    async function completeOrder() {
      // add in item to order
      var data = { orderId: props.orderId, total: orderTotal };
      var order = await fetch("http://localhost:9000/orders/complete", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });
    
      // if order was successful
      if (order.status === 200) {
        props.setStartedOrder(false);
      }
    }
    
    function exitOrder() {
      props.setStartedOrder(false);
    }
    

    Эти две функции являются выбором, который может сделать пользователь, находясь на этом экране. Они могут завершить заказ — в этом случае будет вызвана функция setStartedOrder() и переменная состояния будет установлена в false, запуская условный оператор, который мы сделали ранее — или они могут просто выйти из системы. Свяжите их с кнопками в нашей функции render, чтобы пользователь мог вызвать этот код. Все сходится!

  3. Теперь пришло время протестировать ваше приложение! Запустите фронтенд и бэкенд в двух разных окнах терминала и проверьте сквозной поток. Он должен выглядеть следующим образом:

  4. Чтобы подтвердить, что заказы были отмечены как выполненные, перейдите в Serialized Dashboard и перейдите в Data explorer → Aggregates → order (в колонке Aggregate type) → Aggregates. Щелкните на идентификаторе агрегата верхней (и самой последней) записи. У вас должно появиться следующее представление:

    Если вы нажмете на идентификатор события OrderCompleted, появятся данные, отправленные из приложения (общая сумма заказа):

Оглядываясь назад

На данный момент единственное, чего не хватает, это немного CSS. Этот учебник уже немного затянулся, поэтому я оставлю это упражнение для читателя, но если вы хотите, вы всегда можете посмотреть, что я написал в репозитории GitHub. Вот как это выглядело в итоге:

Я очень доволен тем, что мы создали! Нам удалось использовать Serialized’s Aggregates API для создания очень простого приложения POS (point-of-sale), чтобы пользователи могли создавать заказы, добавлять товары в заказ и либо завершать заказ, либо выходить из него. Все события, происходящие в рамках этого заказа, отправляются в Serialized, где они хранятся в группах событий, или Aggregates, причем каждый экземпляр Aggregate представляет заказ.

Мы можем вернуться к этому в будущем, чтобы показать другую половину функциональности Serialized, которую мы даже не успели затронуть, но если вы хотите самостоятельно построить что-то еще на основе этого приложения, возможно, попробуйте это сделать:

  • Поэкспериментируйте с усложнением пользовательского интерфейса — добавьте изображения для товаров, добавьте больше товаров, даже добавьте описания товаров и отправьте их в Serialized!
  • Добавьте фронтенд и бэкенд тестирование для компонентов, функциональности, запросов и маршрутов.

Большое спасибо за то, что следили за нами! Вы можете связаться со мной в Twitter и не стесняйтесь обращаться ко мне, если есть вопросы или обратная связь. ⭐️

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *