Этот учебник был изначально написан для платформы Serialized. Вы можете просмотреть оригинальную статью в блоге Serialized здесь.
Когда мы думаем о технологиях, мы часто не думаем о повседневных предприятиях, таких как рестораны, киоски и магазины. Однако технологии используются в розничной торговле и общественном питании каждый день! Основное технологическое пересечение между этими видами бизнеса — это POS-система (что означает «точка продаж»). Именно эта программа обеспечивает вас тако из вашего любимого ресторана, свитером, на который вы положили глаз на Poshmark, и новым iPhone на сайте Apple. Они также позволяют сотрудникам звонить и детализировать заказы, обеспечивая основное средство коммуникации для заказов в рамках всего бизнеса.
Поскольку POS-системы являются основой многих розничных и продовольственных предприятий, меня заинтриговала идея создания такой системы. В этой статье мы погрузимся в создание веб-приложения POS, использующего React, Express и Serialized.
Что мы создаем
Наша POS-система будет использовать React для фронтенда, Express для бэкенда и Serialized для создания и хранения заказов, а также постоянного добавления товаров в заказы.
Serialized — это облачный API-движок для создания систем, управляемых событиями, — он помогает нам легко собирать полную хронологию и историю событий и объединять их в связанные группы. Применительно к нашей POS-системе мы будем использовать Serialized для отслеживания событий (клиенты, заказывающие товары) и объединения их в связанные группы (заказы клиентов).
Ниже приведена диаграмма того, как будет выглядеть поток пользователей приложения:
Три основные функции, на которых мы сосредоточимся в этом учебнике, это:
- создание новых заказов,
- добавление товаров в существующие заказы, и
- пометка заказов как выполненных.
Эти три сценария будут отражать сценарии использования нашей очень простой 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.
-
Для начала инициализируйте каталог проекта на вашей машине в выбранном вами месте, выполнив команду
mkdir pos-app
или создав папкуpos-app
вручную. Вставьтеcd
в нее в Терминале и выполните командуnpx create-react-app client
Это создаст папку
client
, в которой будет находиться фронтенд вашего приложения. -
После создания папки
client
выполните следующие команды, чтобы войти в только что созданную папкуclient
, а затем запустите сервер фронтенда:cd client npm start
Если ваш проект был настроен правильно, вы должны увидеть приложение React по умолчанию в браузере по адресу
[localhost:3000](http://localhost:3000)
: -
Если ваш фронтенд успешно запустился, пришло время настроить бэкенд! Завершите работу сервера фронтенда, выполнив CTRL + C. Затем используйте команду
cd ../
из папкиclient
, чтобы вернуться в корневой каталог вашего проекта. Затем выполните следующие команды для создания приложения Express в папке с названиемapi
и запустите бэкенд:npx express-generator api cd api npm install npm start
Если ваш бэкенд настроен правильно, вы должны увидеть это представление после выполнения команды
npm start
:Вы можете узнать больше о пакете
express-generator
, используемом для настройки бэкенда здесь. -
На данный момент фронтенд и бэкенд подключены к
localhost:3000
. Поскольку в процессе разработки приложения вам придется запускать оба сервера одновременно, вам нужно будет изменить порт, на котором работает бэкенд, чтобы избежать столкновения портов. Для этого перейдите в файлbin/www
в каталогеapi
. Обновите строку 15 так, чтобы ее значение по умолчанию теперь указывало на порт 9000. После обновления строка будет выглядеть следующим образом:var port = normalizePort(process.env.PORT || '9000');
Теперь при запуске
npm start
в папкеapi
для запуска бэкенда, вы сможете увидеть запущенный Express-сервер наlocalhost:9000
.
Настройка Serialized
-
Для того чтобы использовать Serialized с приложением, которое было настроено в шагах выше, вы можете установить клиент Serialized для Javascript и Typescript. Поскольку API Serialized будет вызываться в бэкенде Express, выполните следующую команду для установки клиента в каталог
api
:npm install @serialized/serialized-client
-
После установки клиента создайте файл
.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-системе.
-
Чтобы начать, создайте файл
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
для отслеживания состояния заказа.
- На данном этапе обучения пока возвращен только обработчик события
- Как указано в конструкторе класса Order, для объявления нового экземпляра Order потребуется передать объект
-
OrderCreated: Этот класс представляет тип события — в данном сценарии это то, что был создан новый заказ. Для каждого нового добавленного события потребуется новый класс, определяющий, какую информацию событие передает API. Имя класса должно соответствовать обработчику события, которому он соответствует (в данном случае
OrderCreated
. Для создания нового заказа требуется только одно свойство —orderId
, поэтому это единственное свойство, объявленное в данном классе. -
OrderState: Этот класс определяет текущее состояние заказа и отслеживает его изменения, чтобы их можно было передавать в виде событий объекту Order, который будет посылать события в Serialize по мере их возникновения. Помните, что изменение состояния может быть любым: от добавления новых элементов в заказ до пометки его как завершенного — последнее обозначается свойством
OrderState
completed
, установленным вtrue
.
-
Когда файл
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()
. -
После настройки
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
. -
Теперь, когда маршрут
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
, чтобы мы могли сделать заказ с фронтенда.
-
Браузеры часто применяют политику одинакового происхождения для запросов, что приводит к ошибкам 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
-
В
api/app.js
добавьте следующее в строке 6, чтобы добавить пакет CORS, который только что был установлен на бэкенд:var cors = require("cors");
Затем в строке 23 добавьте следующую строку, чтобы указать вашему приложению Express использовать пакет CORS:
app.use(cors());
Возможно, на этом этапе стоит проверить
api/app.js
в репозитории GitHub, чтобы убедиться, что все настроено правильно. -
В директории
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
. -
Замените
client/src/App.js
следующим кодом, чтобы компонентPOSHome
передавался в основное приложение и был виден с главной страницы:import "./App.css"; import POSHome from "./components/POSHome"; function App() { return ( <div className="App"> <POSHome /> </div> ); } export default App;
-
Откройте новое окно или вкладку в Терминале так, чтобы у вас были открыты две вкладки или окна. В одной вкладке запустите
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
заявления, сделанные с бэкенда, также будут отображаться здесь.
Интеграция нашей функциональности в приложение
Теперь, когда вы погрузились в код фронтенда, давайте разберем оставшийся поток для создания, добавления товаров, а затем завершения заказа.
-
Начнем с инициализации набора данных, который будет представлять товары, которые вы будете продавать в 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
. -
Теперь, когда данные о том, какие товары продаются в 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
перебирается и заменяется кнопкой с названием этого элемента в качестве метки. -
Теперь давайте обновим
client/src/components/POSHome.js
так, чтобы при создании заказа отображался компонентItemDisplay
. Для этого мы будем использовать переменные состояния — они отлично подходят для условного отображения компонентов. Для начала обновите строкуimport
в верхней частиPOSHome.js
, чтобы она импортировала и хукuseState
. Пока мы там, импортируйте компонентItemDisplay
из предыдущей статьи.import React, { useState } from "react"; import ItemDisplay from "./ItemDisplay";
-
Хук
useState
инициализирует переменную состояния и даст нам возможность обновлять ее в будущем. Начнем сstartedOrder
— она будет отслеживать, был ли начат заказ, и если да, то будет отображать компонентItemDisplay
. Переменная будет инициализирована в строке 5 с начальным значениемfalse
следующим образом:const [startedOrder, setStartedOrder] = useState(false);
-
Далее обновите функцию
return()
в компонентеPOSHome
так, чтобы она выглядела следующим образом:return ( <div> <h1>POS System ☕️</h1> {!startedOrder && ( <div> <button onClick={createOrder}>Create Order</button> </div> )} {startedOrder && ( <ItemDisplay /> )} </div> );
В приведенном выше примере JSX используется для условного отображения определенных элементов в зависимости от значения переменной состояния
startedOrder
. Логика, реализованная здесь, гласит: «Если значение равно false, отобразите кнопку «Создать заказ». Если значение истинно, отобразите компонентItemDisplay
«. -
Последней частью этого является установка
startedOrder
вtrue
, когда заказ создан. Это можно сделать в функцииcreateOrder()
, описанной выше. Добавьте следующий блок внутри функции в строке 15:// if order was successful if (order.status === 200) { setStartedOrder(true); setOrderId(generatedOrderId); }
-
Теперь пришло время протестировать поток! Загрузите frontend и backend вашего приложения, запустив
npm start
в каталогахapi
иclient
в двух разных вкладках или окнах терминала. После загрузкиclient
вы должны увидеть, как ваше приложение появится вlocalhost:3000
. Нажмите кнопку Создать заказ, и вы увидите, что ваши товары появляются на странице в виде кнопок, как на скриншоте ниже. На этой странице, отображающей компонентItemDisplay
, вы сможете выбрать товары и добавить их в заказ, который будет добавлен в разделе ниже.
Добавление товаров в заказ
Теперь, когда мы отображаем доступные товары, нам нужно иметь возможность добавить эти товары в выполняемый заказ.
Чтобы начать, давайте сначала перейдем в бэкенд.
-
В
/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
. -
Добавьте функцию
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
для регистрации события о том, какой элемент был добавлен в заказ. -
В классе
Order
добавьте новый обработчик события для добавляемого элемента:ItemAdded(state, event) { console.log("Handling ItemAdded", event); return new Order(state).addItem({ orderId: event.orderId, itemName: event.itemName, itemPrice: event.itemPrice }); },
-
Добавьте следующее внутри класса
OrderState
в строке 64:addItem(itemName, itemPrice) { return Object.assign({}, this, { items: this.items.unshift({itemName: itemName, itemPrice: itemPrice}) }); }
Приведенный выше код обновит свойство массива
items
объектаOrderState
так, что новый элемент будет помещен в массив.На данном этапе, вероятно, будет хорошей идеей сверить ваш
order.js
с репозиторием GitHub, чтобы убедиться, что он соответствует. -
После обновления
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); }) ); }
-
Наконец, добавьте маршрут в
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
, а также отобразить товарный чек и общую сумму заказа товаров, добавленных на данный момент в заказ.
-
Чтобы начать, перейдите в
/client/src/POSHome.js
. Поскольку запрос/add-item
принимаетorderId
, нам нужно передать его компонентуItemDisplay
, чтобы сделать вызов API. Для этого вам понадобится переменная state, которая будет отслеживать идентификаторы заказов. Добавьте следующее объявление переменной состояния:const [orderId, setOrderId] = useState("");
-
Затем в
createOrder()
добавьте следующую строку подsetStartedOrder(true);
, чтобы установить переменную состоянияorderId
в ID успешно созданного (и, следовательно, текущего) заказа:setOrderId(generatedOrderId);
-
Наконец, обновите строку
<ItemDisplay />
в вашейreturn()
до следующей, чтобы передать переменную состоянияorderId
в качестве параметра:<ItemDisplay orderId={orderId} />
-
Отлично! Чтобы отслеживать выбранные элементы, давайте сделаем нечто подобное в
/client/src/ItemDisplay.js
. Там импортируйте хукuseState
в верхней части, как мы это сделали сPOSHome
, и инициализируйте переменные состоянияitemsInOrder
иorderTotal
следующим образом:const [itemsInOrder, setItemsInOrder] = useState([]); const [orderTotal, setOrderTotal] = useState(0);
-
После добавления переменных состояния, давайте добавим функцию
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
добавляется к сумме заказа. -
Добавьте событие
onClick
к тегу<button>
вreturn()
. Внутри события вызовите функциюaddItemToOrder()
. Тег должен выглядеть следующим образом:<button key={index} onClick={() => { addItemToOrder(item.name, item.price); }} >
Это будет вызывать функцию
addItemToOrder()
каждый раз, когда нажимается кнопка элемента. -
Внутри основного
<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>
-
Наконец, пришло время протестировать функциональность! Запустите фронтенд и бэкенд вашего приложения. Когда приложение загрузится, нажмите кнопку Создать заказ. Вы должны увидеть следующую страницу:
По мере нажатия на кнопки название товара и цена должны появляться в разделе «Заказанные товары», а общая сумма заказа также должна увеличиваться. Вот пример того, как это должно выглядеть, если вы нажмете кнопки «Чай», «Кофе» и «Бублик»:
Чтобы подтвердить, что товары были добавлены в заказ, перейдите в Serialized Dashboard > Data explorer > Aggregates > order (в колонке Aggregate type) > Aggregates > щелкните на Aggregate ID верхней (и самой последней) записи. После этого у вас должно появиться следующее представление:
Если вы нажмете на любой из идентификаторов события
ItemAdded
, вы увидите объект, содержащий данные, отправленные из событияItemAdded
в вашем приложении:Приведенное выше событие
ItemAdded
относится к рогалику за $2,50, который был добавлен к заказу.
Завершение заказов
Последним примером использования будет завершение заказов. Как только заказ будет завершен с помощью компонента ItemDisplay
, компонент исчезнет, а кнопка Create Order появится снова, чтобы начать новый заказ.
Давайте начнем с бэкенда!
-
Во-первых, в
/client/api/order.js
добавьте класс событияOrderCompleted
:class OrderCompleted { constructor(orderId, total) { this.orderId = orderId; this.total = total; } }
Этот класс событий требует
orderId
и окончательную сумму заказаtotal
для завершения заказа. -
Аналогично потоку
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
. -
Далее добавьте обработчик события
OrderCompleted
:OrderCompleted(state, event) { console.log("Handling OrderCompleted", event); return new Order(state).completeOrder({ orderId: event.orderId, total: event.total, }); },
-
Затем в
OrderState
добавьте функциюcompleteOrder
:completeOrder(total) { return Object.assign({}, this, { completed: true, total: total }); }
-
Далее, в
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); }) ); }
-
Наконец, добавьте маршрут
/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 }); } });
Давайте ненадолго вернемся к фронтенду.
-
Чтобы эта логика работала из
ItemDisplay
, вам нужно обновить переменную состоянияstartedOrder
из компонентаItemDisplay
. Для этого функцияsetStartedOrder
может быть передана в качестве свойства изPOSHome
. Вclient/src/components/POSHome.js
передайтеsetStartedOrder
компоненту<ItemDisplay>
так, чтобы он выглядел следующим образом:<ItemDisplay orderId={orderId} setStartedOrder={setStartedOrder} />
-
Теперь в
/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
, чтобы пользователь мог вызвать этот код. Все сходится! -
Теперь пришло время протестировать ваше приложение! Запустите фронтенд и бэкенд в двух разных окнах терминала и проверьте сквозной поток. Он должен выглядеть следующим образом:
-
Чтобы подтвердить, что заказы были отмечены как выполненные, перейдите в 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 и не стесняйтесь обращаться ко мне, если есть вопросы или обратная связь. ⭐️