Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
14.01.2022
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Как-то совсем упустил работу с заказами. Нам нужно на сервере создать две таблицы базы данных, создать маршруты и классы контроллера и модели. Потом протестировать все http-запросы — на создание заказа администратором и обычным пользователем, на получение списка всех заказов и одного заказа администратором и пользователем. Потом можно будет переходить на сторону клиента и создать форму для оформления заказа.
Создание таблиц базы данных
Файл server/models/mapping.js
:
/* * Описание моделей */ // модель «Заказ», таблица БД «orders» const Order = sequelize.define('order', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, name: {type: DataTypes.STRING, allowNull: false}, email: {type: DataTypes.STRING, allowNull: false}, phone: {type: DataTypes.STRING, allowNull: false}, address: {type: DataTypes.STRING, allowNull: false}, amount: {type: DataTypes.INTEGER, allowNull: false}, status: {type: DataTypes.INTEGER, allowNull: false, defaultValue: 0}, comment: {type: DataTypes.STRING}, }) // позиции заказа, в одном заказе может быть несколько позиций (товаров) const OrderItem = sequelize.define('order_item', { id: {type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true}, name: {type: DataTypes.STRING, allowNull: false}, price: {type: DataTypes.INTEGER, allowNull: false}, quantity: {type: DataTypes.INTEGER, allowNull: false}, }) /* * Описание связей */ // связь заказа с позициями: в заказе может быть несколько позиций, но // каждая позиция связана только с одним заказом Order.hasMany(OrderItem, {as: 'items', onDelete: 'CASCADE'}) OrderItem.belongsTo(Order) // связь заказа с пользователями: у пользователя может быть несколько заказов, // но заказ может принадлежать только одному пользователю User.hasMany(Order, {as: 'orders', onDelete: 'SET NULL'}) Order.belongsTo(User)
Маршруты, контроллер, модель
Файл server/routes/order.js
:
import express from 'express' import OrderController from '../controllers/Order.js' import authMiddleware from '../middleware/authMiddleware.js' import adminMiddleware from '../middleware/adminMiddleware.js' const router = new express.Router() /* * только для администратора магазина */ // получить список всех заказов магазина router.get( '/admin/getall', authMiddleware, adminMiddleware, OrderController.adminGetAll ) // получить список заказов пользователя router.get( '/admin/getall/user/:id([0-9]+)', authMiddleware, adminMiddleware, OrderController.adminGetUser ) // получить заказ по id router.get( '/admin/getone/:id([0-9]+)', authMiddleware, adminMiddleware, OrderController.adminGetOne ) // создать новый заказ router.post( '/admin/create', authMiddleware, adminMiddleware, OrderController.adminCreate ) // удалить заказ по id router.delete( '/admin/delete/:id([0-9]+)', authMiddleware, adminMiddleware, OrderController.adminDelete ) /* * для авторизованного пользователя */ // получить все заказы пользователя router.get( '/user/getall', authMiddleware, OrderController.userGetAll ) // получить один заказ пользователя router.get( '/user/getone/:id([0-9]+)', authMiddleware, OrderController.userGetOne ) // создать новый заказ router.post( '/user/create', authMiddleware, OrderController.userCreate ) /* * для неавторизованного пользователя */ // создать новый заказ router.post( '/guest/create', OrderController.guestCreate ) export default router
С заказами может работать как обычный пользователь, так и администратор. У администратора должна быть возможность запросить список всех заказов, список заказов пользователя, отдельный заказ по идентификатору. Кроме того, администратор может создать заказ от имени любого пользователя или даже от незарегистрированного пользователя. Например, заказ поступил по телефону, но нужно занести его в базу данных, чтобы в дальнейшем работать с ним, как и со всеми прочими заказами.
С другой стороны, обычный пользователь должен иметь возможность создать заказ из корзины. А если это зарегистрированный пользователь, то у него должна быть возможность просматривать историю своих заказов в личном кабинете. И возможность посмотреть состав каждого заказа из истории и текущий статус (новый, оплачен, доставлен, завершен). Таким образом, когда заказ создает администратор и обычный пользователь, на сервер будем отправлять разные запросы. Например, для администратора запрос будет таким:
POST /api/order/admin/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...kwNX0.vbDBq...tpMnQ Content-type: application/json; charset=utf-8
{ "name": "Сергей Иванов", "email": "ivanov@mail.ru", "phone": "(999) 123-45-67", "address": "Москва, улица Строителей, дом 123, кв.456", "comment": "Комментарий к заказу", "userId": 3, "items": [ {"name": "Товар раз", "price": 123, "quantity": 2}, {"name": "Товар два", "price": 456, "quantity": 1} ] }
А для обычного пользователя запрос будет таким:
POST /api/order/user/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...kwNX0.vbDBq...tpMnQ Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o Content-type: application/json; charset=utf-8
{ "name": "Сергей Иванов", "email": "ivanov@mail.ru", "phone": "(999) 123-45-67", "address": "Москва, улица Строителей, дом 123, кв.456", "comment": "Комментарий к заказу" }
Из заголовка Authorization
мы извлечем jwt-токен и после декодирования получим id
пользователя. А из заголовка Cookie
извечем id
корзины и выполнив запрос к БД — получим состав заказа.
Файл server/controllers/Order.js
:
import OrderModel from '../models/Order.js' import BasketModel from '../models/Basket.js' import UserModel from '../models/User.js' import AppError from '../errors/AppError.js' class Order { adminCreate = async (req, res, next) => { await this.create(req, res, next, 'admin') } userCreate = async (req, res, next) => { await this.create(req, res, next, 'user') } guestCreate = async (req, res, next) => { await this.create(req, res, next, 'guest') } async create(req, res, next, type) { try { const {name, email, phone, address, comment = null} = req.body // данные для создания заказа if (!name) throw new Error('Не указано имя покупателя') if (!email) throw new Error('Не указан email покупателя') if (!phone) throw new Error('Не указан телефон покупателя') if (!address) throw new Error('Не указан адрес доставки') let items, userId = null if (type === 'admin') { // когда заказ делает админ, id пользователя и состав заказа в теле запроса if (!req.body.items) throw new Error('Не указан состав заказа') if (req.body.items.length === 0) throw new Error('Не указан состав заказа') items = req.body.items // проверяем существование пользователя userId = req.body.userId ?? null if (userId) { await UserModel.getOne(userId) // будет исключение, если не найден } } else { // когда заказ делает обычный пользователь (авторизованный или нет), состав // заказа получаем из корзины, а id пользователя из req.auth.id (если есть) if (!req.signedCookies.basketId) throw new Error('Ваша корзина пуста') const basket = await BasketModel.getOne(parseInt(req.signedCookies.basketId)) if (basket.products.length === 0) throw new Error('Ваша корзина пуста') items = basket.products userId = req.auth?.id ?? null } // все готово, можно создавать const order = await OrderModel.create({ name, email, phone, address, comment, items, userId }) // корзину теперь нужно очистить await BasketModel.clear(parseInt(req.signedCookies.basketId)) res.json(order) } catch(e) { next(AppError.badRequest(e.message)) } } async adminGetAll(req, res, next) { try { const orders = await OrderModel.getAll() res.json(orders) } catch(e) { next(AppError.badRequest(e.message)) } } async adminGetUser(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id пользователя') } const order = await OrderModel.getAll(req.params.id) res.json(order) } catch(e) { next(AppError.badRequest(e.message)) } } async adminGetOne(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id заказа') } const order = await OrderModel.getOne(req.params.id) res.json(order) } catch(e) { next(AppError.badRequest(e.message)) } } async adminDelete(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id заказа') } const order = await OrderModel.delete(req.params.id) res.json(order) } catch(e) { next(AppError.badRequest(e.message)) } } async userGetAll(req, res, next) { try { const orders = await OrderModel.getAll(req.auth.id) res.json(orders) } catch(e) { next(AppError.badRequest(e.message)) } } async userGetOne(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id заказа') } const order = await OrderModel.getOne(req.params.id, req.auth.id) res.json(order) } catch(e) { next(AppError.badRequest(e.message)) } } } export default new Order()
Файл server/models/Order.js
:
import { Order as OrderMapping } from './mapping.js' import { OrderItem as OrderItemMapping } from './mapping.js' import AppError from '../errors/AppError.js' class Order { async getAll(userId = null) { let orders if (userId) { orders = await OrderMapping.findAll({where: {userId}}) } else { orders = await OrderMapping.findAll() } return orders } async getOne(id, userId = null) { let order if (userId) { order = await OrderMapping.findOne({ where: {id, userId}, include: [ {model: OrderItemMapping, as: 'items', attributes: ['name', 'price', 'quantity']}, ], }) } else { order = await OrderMapping.findByPk(id, { include: [ {model: OrderItemMapping, as: 'items', attributes: ['name', 'price', 'quantity']}, ], }) } if (!order) { throw new Error('Заказ не найден в БД') } return order } async create(data) { // общая стоимость заказа const items = data.items const amount = items.reduce((sum, item) => sum + item.price * item.quantity, 0) // данные для создания заказа const {name, email, phone, address, comment = null, userId = null} = data const order = await OrderMapping.create({ name, email, phone, address, comment, amount, userId }) // товары, входящие в заказ for (let item of items) { await OrderItemMapping.create({ name: item.name, price: item.price, quantity: item.quantity, orderId: order.id }) } // возвращать будем заказ с составом const created = await OrderMapping.findByPk(order.id, { include: [ {model: OrderItemMapping, as: 'items', attributes: ['name', 'price', 'quantity']}, ], }) return created } async delete(id) { let order = await OrderMapping.findByPk(id, { include: [ {model: OrderItemMapping, attributes: ['name', 'price', 'quantity']}, ], }) if (!order) { throw new Error('Заказ не найден в БД') } await order.destroy() return order } } export default new Order()
Тестирование http-запросов
Как обычно, будем использовать расширение Rest Client for VS Code, создаем файл test/order.http
:
### Список всех заказов (для администратора) GET /api/order/admin/getall HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA ### Получить один заказ (для администратора) GET /api/order/admin/getone/1 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA ### Получить заказы пользователя (для администратора) GET /api/order/admin/getall/user/5 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA ### Создать новый заказ (для администратора) POST /api/order/admin/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...EwMzJ9.cMZ6c...1CgMA Content-type: application/json; charset=utf-8 { "name": "Сергей Иванов", "email": "ivanov@mail.ru", "phone": "(999) 123-45-67", "address": "Москва, улица Строителей, дом 123, кв.456", "comment": "Комментарий к заказу", "userId": 3, "items": [ {"name": "Товар раз", "price": 123, "quantity": 2}, {"name": "Товар два", "price": 456, "quantity": 1} ] }
Чтобы получить значение заголовка Authorization
делаем следующее. Используя клиент для работы с базой данных — назначаем какому-нибудь пользователю роль ADMIN
. Потом на клиенте авторизуемся от имени этого пользователя, открываем в браузере консоль разработчика F12, переходим на вкладку «Приложение» и копируем значение token
из локального хранилища.
### Список всех заказов пользователя GET /api/order/user/getall HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb.....XVCJ9.eyJpZ...kwNX0.vbDBqh...tpMnQ Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o ### Получить один заказ пользователя GET /api/order/user/getone/5 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb.....XVCJ9.eyJpZ...kwNX0.vbDBqh...tpMnQ Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o ### Создать новый заказ пользователя POST /api/order/user/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb.....XVCJ9.eyJpZ...kwNX0.vbDBqh...tpMnQ Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o Content-type: application/json; charset=utf-8 { "name": "Сергей Иванов", "email": "ivanov@mail.ru", "phone": "(999) 123-45-67", "address": "Москва, улица Строителей, дом 123, кв.456", "comment": "Комментарий к заказу" }
Значение заголовка Authorization
получаем аналогично — только авторизуемся от имени обычного пользователя, с ролью USER
. Чтобы получить значение заголовка Cookie
— добавляем в корзину несколько товаров, открываем в браузере консоль разработчика F12, копируем значение basketId
либо из заголовков (вкладка «Сеть»), либо из cookie
(вкладка «Приложение»).
### Создать новый заказ посетителя
POST /api/order/guest/create HTTP/1.1
Host: localhost:7000
Cookie: basketId=s%3A38.mDPMc%2FsU2MOOiCtZZFPZ%2F9KWza4peanqnQoquOqX26o
Content-type: application/json; charset=utf-8
{ "name": "Сергей Иванов", "email": "ivanov@mail.ru", "phone": "(999) 123-45-67", "address": "Москва, улица Строителей, дом 123, кв.789", "comment": "Комментарий к заказу" }
Оформление заказ на клиенте
Для начала создаем файл http/orderAPI.js
, который будет содержать функции по отправке http-запросов на сервер:
import { guestInstance, authInstance } from './index.js' /* * только для администратора магазина */ // создать новый заказ export const adminCreate = async (body) => { const { data } = await authInstance.post('order/admin/create', body) return data } // получить список всех заказов магазина export const adminGetAll = async () => { const { data } = await authInstance.get('order/admin/getall') return data } // получить список заказов пользователя export const adminGetUser = async (id) => { const { data } = await authInstance.get(`order/admin/getall/user/${id}`) return data } // получить заказ по id export const adminGetOne = async (id) => { const { data } = await authInstance.get(`order/admin/getone/${id}`) return data } // удалить заказ по id export const adminDelete = async (id) => { const { data } = await authInstance.delete(`order/admin/delete/${id}`) return data } /* * для авторизованного пользователя */ // создать новый заказ export const userCreate = async (body) => { const { data } = await authInstance.post('order/user/create', body) return data } // получить список всех заказов пользователя export const userGetAll = async () => { const { data } = await authInstance.get('order/user/getall') return data } // получить один заказ пользователя export const userGetOne = async (id) => { const { data } = await authInstance.get(`order/user/getone/${id}`) return data } /* * для неавторизованного пользователя */ // создать новый заказ export const guestCreate = async (body) => { const { data } = await guestInstance.post('order/guest/create', body) return data }
Теперь создадим страницу с формой pages/Checkout.js
. Все элементы input
, которые обязательны для заполнения, будут управляемыми. Это значит, что введенное значение будем сохранять в состоянии компонента — чтобы было удобно валидировать.
import { Container, Form, Button } from 'react-bootstrap' import { useState } from 'react' const isValid = (input) => { let pattern switch (input.name) { case 'name': pattern = /^[-а-я]{2,}( [-а-я]{2,}){1,2}$/i return pattern.test(input.value.trim()) case 'email': pattern = /^[-_.a-z]+@([-a-z]+\.){1,2}[a-z]+$/i return pattern.test(input.value.trim()) case 'phone': pattern = /^\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{2}-[0-9]{2}$/i return pattern.test(input.value.trim()) case 'address': return input.value.trim() !== '' } } const Checkout = () => { const [value, setValue] = useState({name: '', email: '', phone: '', address: ''}) const [valid, setValid] = useState({name: null, email: null, phone: null, address: null}) const handleChange = (event) => { setValue({...value, [event.target.name]: event.target.value}) /* * Вообще говоря, проверять данные поля, пока пользователь не закончил ввод — неправильно, * проверять надо в момент потери фокуса. Но приходится проверять здесь, поскольку браузеры * автоматически заполняют поля. И отловить это событие — весьма проблематичная задача. */ setValid({...valid, [event.target.name]: isValid(event.target)}) } const handleSubmit = async (event) => { event.preventDefault() setValue({ name: event.target.name.value.trim(), email: event.target.email.value.trim(), phone: event.target.phone.value.trim(), address: event.target.address.value.trim(), }) setValid({ name: isValid(event.target.name), email: isValid(event.target.email), phone: isValid(event.target.phone), address: isValid(event.target.address), }) if (valid.name && valid.email && valid.phone && valid.address) { // форма заполнена правильно, можно отправлять данные } } return ( <Container> <h1 className="mb-4 mt-4">Оформление заказа</h1> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={value.name} onChange={e => handleChange(e)} isValid={valid.name === true} isInvalid={valid.name === false} placeholder="Введите имя и фамилию..." className="mb-3" /> <Form.Control name="email" value={value.email} onChange={e => handleChange(e)} isValid={valid.email === true} isInvalid={valid.email === false} placeholder="Введите адрес почты..." className="mb-3" /> <Form.Control name="phone" value={value.phone} onChange={e => handleChange(e)} isValid={valid.phone === true} isInvalid={valid.phone === false} placeholder="Введите номер телефона..." className="mb-3" /> <Form.Control name="address" value={value.address} onChange={e => handleChange(e)} isValid={valid.address === true} isInvalid={valid.address === false} placeholder="Введите адрес доставки..." className="mb-3" /> <Form.Control name="comment" className="mb-3" placeholder="Комментарий к заказу..." /> <Button type="submit">Отправить</Button> </Form> </Container> ) } export default Checkout
Компонент получился сложным, так что будем разрабатывать его постепенно — для начала нам нужно выяснить, авторизован ли пользователь и есть ли товары в корзине.
import { Container, Form, Button, Spinner } from 'react-bootstrap' import { useState, useContext, useEffect } from 'react' import { AppContext } from '../components/AppContext.js' import { fetchBasket } from '../http/basketAPI.js' import { check as checkAuth } from '../http/userAPI.js' import { Navigate } from 'react-router-dom' const isValid = (input) => { /* .......... */ } const Checkout = () => { const { user, basket } = useContext(AppContext) const [fetching, setFetching] = useState(true) // loader, пока получаем корзину const [value, setValue] = useState({name: '', email: '', phone: '', address: ''}) const [valid, setValid] = useState({name: null, email: null, phone: null, address: null}) useEffect(() => { // если корзина пуста, здесь делать нечего fetchBasket() .then( data => basket.products = data.products ) .finally( () => setFetching(false) ) // нужно знать, авторизован ли пользователь checkAuth() .then(data => { if (data) { user.login(data) } }) .catch( error => user.logout() ) }, []) if (fetching) { // loader, пока получаем корзину return <Spinner animation="border" /> } const handleChange = (event) => { /* .......... */ } const handleSubmit = async (event) => { /* .......... */ } return ( <Container> {basket.count === 0 && <Navigate to="/basket" replace={true} />} <h1 className="mb-4 mt-4">Оформление заказа</h1> <Form noValidate onSubmit={handleSubmit}> .......... </Form> </Container> ) } export default Checkout
Если корзина пуста — пользователь будет направлен на страницу корзины, где увидит сообщение «Ваша корзина пуста». После того, как заказ был создан, переменная order
изменяет свое значение — и пользователь увидит сообщение, что заказ успешно оформлен.
import { Container, Form, Button, Spinner } from 'react-bootstrap' import { useState, useContext, useEffect } from 'react' import { AppContext } from '../components/AppContext.js' import { userCreate, guestCreate } from '../http/orderAPI.js' import { fetchBasket } from '../http/basketAPI.js' import { check as checkAuth } from '../http/userAPI.js' import { Navigate } from 'react-router-dom' const isValid = (input) => { let pattern switch (input.name) { case 'name': pattern = /^[-а-я]{2,}( [-а-я]{2,}){1,2}$/i return pattern.test(input.value.trim()) case 'email': pattern = /^[-_.a-z]+@([-a-z]+\.){1,2}[a-z]+$/i return pattern.test(input.value.trim()) case 'phone': pattern = /^\+7 \([0-9]{3}\) [0-9]{3}-[0-9]{2}-[0-9]{2}$/i return pattern.test(input.value.trim()) case 'address': return input.value.trim() !== '' } } const Checkout = () => { const { user, basket } = useContext(AppContext) const [fetching, setFetching] = useState(true) // loader, пока получаем корзину const [order, setOrder] = useState(null) const [value, setValue] = useState({name: '', email: '', phone: '', address: ''}) const [valid, setValid] = useState({name: null, email: null, phone: null, address: null}) useEffect(() => { // если корзина пуста, здесь делать нечего fetchBasket() .then( data => basket.products = data.products ) .finally( () => setFetching(false) ) // нужно знать, авторизован ли пользователь checkAuth() .then(data => { if (data) { user.login(data) } }) .catch( error => user.logout() ) }, []) if (fetching) { // loader, пока получаем корзину return <Spinner animation="border" /> } if (order) { // заказ был успешно оформлен return ( <Container> <h1 className="mb-4 mt-4">Заказ оформлен</h1> <p>Наш менеджер скоро позвонит для уточнения деталей.</p> </Container> ) } const handleChange = (event) => { setValue({...value, [event.target.name]: event.target.value}) /* * Вообще говоря, проверять данные поля, пока пользователь не закончил ввод — неправильно, * проверять надо в момент потери фокуса. Но приходится проверять здесь, поскольку браузеры * автоматически заполняют поля. И отловить это событие — весьма проблематичная задача. */ setValid({...valid, [event.target.name]: isValid(event.target)}) } const handleSubmit = async (event) => { event.preventDefault() setValue({ name: event.target.name.value.trim(), email: event.target.email.value.trim(), phone: event.target.phone.value.trim(), address: event.target.address.value.trim(), }) setValid({ name: isValid(event.target.name), email: isValid(event.target.email), phone: isValid(event.target.phone), address: isValid(event.target.address), }) if (valid.name && valid.email && valid.phone && valid.address) { let comment = event.target.comment.value.trim() comment = comment ? comment : null // форма заполнена правильно, можно отправлять данные const body = {...value, comment} const create = user.isAuth ? userCreate : guestCreate create(body) .then( data => { setOrder(data) basket.products = [] } ) } } return ( <Container> {basket.count === 0 && <Navigate to="/basket" replace={true} />} <h1 className="mb-4 mt-4">Оформление заказа</h1> <Form noValidate onSubmit={handleSubmit}> <Form.Control name="name" value={value.name} onChange={e => handleChange(e)} isValid={valid.name === true} isInvalid={valid.name === false} placeholder="Введите имя и фамилию..." className="mb-3" /> <Form.Control name="email" value={value.email} onChange={e => handleChange(e)} isValid={valid.email === true} isInvalid={valid.email === false} placeholder="Введите адрес почты..." className="mb-3" /> <Form.Control name="phone" value={value.phone} onChange={e => handleChange(e)} isValid={valid.phone === true} isInvalid={valid.phone === false} placeholder="Введите номер телефона..." className="mb-3" /> <Form.Control name="address" value={value.address} onChange={e => handleChange(e)} isValid={valid.address === true} isInvalid={valid.address === false} placeholder="Введите адрес доставки..." className="mb-3" /> <Form.Control name="comment" className="mb-3" placeholder="Комментарий к заказу..." /> <Button type="submit">Отправить</Button> </Form> </Container> ) } export default Checkout
App
, потому что эти данные много где требуются — например, в компонентах NavBar
, AppRouter
, Checkout
. А сейчас получается, что надо их получать дважды — сначала в NavBar
, а потом еще в Checkout
. Пока не буду ничего изменять, подумаю над этим позже.
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
- Магазин на JavaScript, часть 11 из 19. Компоненты Signup и Login, компоненты Shop и Basket
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк