# Создание магазина электронной коммерции на Vue.js с помощью интерфейса Medusa и Vue Storefront
Фреймворк Vue.js — гибкий инструмент веб-разработки, который отличается высокой производительностью. Он не нагружает ПО и легко интегрируется с другими фреймворками. В этой статье мы рассмотрим, как создать магазин на Vue.js с помощью Medusa, платформы для электронной коммерции. Интерфейс Medusa и серверная часть отделены друг от друга, поэтому разработчики могут выбирать любой инструмент для создания своей витрины, например, Vue Storefront (opens new window) с набором готовых компонентов. Полный код проекта вы найдёте в репозитории GitHub (opens new window).
Примечание
На вашем устройстве должен быть установлен Node.js. Версия — не ниже 14.
# Установка Medusa
# Создание и настройка сервера Medusa
Установите инструмент Medusa CLI — введите в терминале команду:
npm install @medusajs/medusa-cli -g
Затем создайте сам сервер Medusa:
medusa new local-medusa-server --seed
Ваш сервер будет создан, и в базу данных SQLite добавятся некоторые тестовые данные.
Далее перейдите в созданный каталог и запустите сервер:
cd local-medusa-server && medusa develop
Проверьте, работает ли сервер Medusa. Введите URL в адресной строке браузера — http://localhost:9000/store/products
. Вы должны увидеть страницу со следующим содержимым:
Осталось только установить плагин для добавления товаров. Вы можете использовать MinIO (opens new window), S3 (opens new window) или Spaces (opens new window).
# Установка и запуск администратора Medusa
Для установки панели администратора откройте новый каталог и выполните команду:
git clone https://github.com/medusajs/admin medusa-admin
Когда панель будет установлена, перейдите в созданный каталог и установите зависимости с помощью NPM:
cd medusa-admin && npm install
Ещё раз проверьте работу сервера, после этого запустите администратора Medusa с помощью команды:
npm start
Административная панель будет открываться по адресу http://localhost:7000/
. Если вы заполнили свою базу данных демонстрационными данными с помощью --seed опции при создании администратора Medusa, вы можете войти в систему, используя адрес электронной почты admin@medusa-test.com и пароль supersecret. Если вы не заполнили БД, используйте команду user (opens new window).
После авторизации откройте в левом меню пункт Products:
Для добавления нового товара нажмите New Product в правом верхнем углу и заполните его характеристики.
Подробную информацию о функциях администратора Medusa можно прочитать в документации (opens new window).
# Создание и настройка витрины магазина Nuxt.js
Для создания витрины зайдите в другой каталог и выполните команду:
npx create-nuxt-app nuxtjs-storefront
Вам нужно будет ответить на некоторые вопросы. Вот пример ответов:
Package manager можно изменить, как вам удобно.
После установки витрины перейдите в созданную папку:
cd nuxtjs-storefront
# Подключение Vue Storefront к серверу Medusa
Первая задача — установка Axios (opens new window), HTTP-пакета для выполнения запросов между сервером Medusa и вашей витриной. Установим его в каталог nuxtjs-storefront:
npm i -s axios
Вторая задача — настройка витрины для использования порта 8000.
Откройте файл nuxt.config.js и добавьте в него строку с номером порта:
export default {
ssr: false,
server: {
port: 8000
},
//...
}
Теперь добавим URL-адрес сервера Medusa в качестве переменной среды.
Установим dotenv модуль:
npm install @nuxtjs/dotenv
Далее зарегистрируем установленный модуль в файле nuxt.config.js:
buildModules: [
'@nuxtjs/dotenv'
],
Нам осталось создать .env файл в корне каталога проекта и добавить URL-адрес сервера в качестве переменной среды:
baseUrl=http://localhost:9000
# Установка и настройка пользовательского интерфейса Vue Storefront
# Установка Vue Storefront
Можно использовать готовые компоненты пользовательского интерфейса Vue Storefront.
Для его установки выполните в каталоге nuxtjs-storefront команду:
npm install --save @storefront-ui/vue
# Настройка компонентов Vue Storefront
Приложение Vue будет иметь следующие компоненты:
- Navbar для отображения логотипа и ссылок в шапке сайта;
- ProductCard для отображения краткой информации о продукте.
- Footer для отображения полезных ссылок для клиентов.
Для настройки Navbar создайте файл components/App/Navbar.vueсо следующим содержимым:
<template>
<SfHeader :logo="shopLogo" :title="shopName" active-icon="account">
<template #navigation>
<SfHeaderNavigationItemv v-for="(category, key) in navbarLinks" :key="`sf-header-navigation-item-${key}`"
:link="`${category.link}`" :label="category.title" />
</template>
</SfHeader>
</template>
<script>
import {
SfHeader
} from "@storefront-ui/vue";
export default {
name: "Default",
components: {
SfHeader,
},
data() {
return {
shopName: "My Storefront App",
shopLogo: "/logo.svg",
navbarLinks: [{
title: "Products",
link: "/",
}, ],
};
},
};
</script>
Это базовый компонент, определяющий навигацию по вашей странице. Пользовательский интерфейс Vue Storefront предоставляет компонент SfHeader (opens new window). Внутри него можно размещать другие компоненты внутренней навигации — SfHeaderNavigation, SfHeaderNavigationItem.
Чтобы на панели навигации отображался ваш логотип, добавьте файл logo.svg в папку static. Если имя вашего лого другое, проверьте, верно ли оно указано в свойстве shopLogo функции data.
Для настройки компонента Footer создайте файл components/App/Footer.vue со следующим содержимым:
<template>
<SfFooter>
<SfFooterColumn v-for="(column, key) in footerColumns" :key="key" :title="column.title">
<SfList>
<SfListItem v-for="(menuItem, index) in column.items" :key="index">
<SfMenuItem :label="menuItem" />
</SfListItem>
</SfList>
</SfFooterColumn>
</SfFooter>
</template>
<script>
import {
SfFooter,
SfList,
SfMenuItem
} from "@storefront-ui/vue";
export default {
name: "Default",
components: {
SfFooter,
SfList,
SfMenuItem,
},
data() {
return {
footerColumns: [{
title: "About us",
items: ["Who we are", "Quality in the details", "Customer Reviews"],
},
{
title: "Departments",
items: ["Women fashion", "Men fashion", "Kidswear", "Home"],
},
{
title: "Help",
items: ["Customer service", "Size guide", "Contact us"],
},
{
title: "Payment & delivery",
items: ["Purchase terms", "Guarantee"],
},
],
};
},
};
</script>
Здесь будут полезные ссылки для ваших клиентов. Библиотека пользовательского интерфейса Vue Storefront предоставляет предопределённый пользовательский интерфейс для нижних колонтитулов с использованием компонента SfFooter (opens new window).
Для настройки компонента ProductCard создайте файл components/ProductCard.vue со следующим содержимым:
<template>
<SfProductCard :image="item.thumbnail" :imageWidth="216" :imageHeight="326" badgeLabel="" badgeColor=""
:title="item.title" :link="this.url" :linkTag="item.id" :scoreRating="4" :reviewsCount="7" :maxRating="5"
:regularPrice="this.highestPrice.amount" :specialPrice="this.lowestPrice.amount" wishlistIcon="heart"
isInWishlistIcon="heart_fill" :isInWishlist="false" showAddToCartButton :isAddedToCart="false"
:addToCartDisabled="false" />
</template>
Карточка продукта использует компонент SfProductCard (opens new window), предоставляемый пользовательским интерфейсом Vue Storefront.
В конец этого же файла добавьте следующее:
<script>
import { SfProductCard } from "@storefront-ui/vue"; // Import the components
export default {
name:"ProductCard",
components:{
SfProductCard
},
props: {
item: {
type: Object,
}
},
computed:{
url(){
return `/products/${this.item.id}`; // Product page
},
lowestPrice() {
// Get the lowest price from the list of prices.
const lowestPrice = this.item.variants.reduce(
(acc, curr) => {
return curr.prices.reduce((lowest, current) => {
if (lowest.amount > current.amount) {
return current;
}
return lowest;
});
},
{ amount: 0 }
);
// Format the amount and also add currency
return {
amount:
lowestPrice.amount > 0
? (lowestPrice.amount / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})
: 0,
currency_code: "USD",
};
},
highestPrice() {
// Get the highest price from the list of prices
const highestPrice = this.item.variants.reduce(
(acc, curr) => {
return curr.prices.reduce((highest, current) => {
if (highest.amount < current.amount) {
return current;
}
return highest;
});
},
{ amount: 0 }
);
// Format the amount and also add currency
return {
amount:
highestPrice.amount > 0
? (highestPrice.amount / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
})
: 0,
currency_code: "USD",
};
},
}
}
</script>
Этот скрипт подготавливает данные для отображения.
# Создание макета витрины
Для настройки макета создайте файл layouts/default.vue следующего содержания:
<template>
<div>
<app-navbar />
<main>
<div class="container">
<Nuxt />
</div>
</main>
<app-footer />
</div>
</template>
<script>
import "@storefront-ui/vue/styles.scss"; // vuestorefront UI styles.
export default {
name: 'DefaultLayout'
}
</script>
Скрипт отобразит страницу с макетом пользовательского интерфейса Vue Storefront по умолчанию.
# Создание домашней страницы
В pages каталоге отредактируйте файл index.vue следующим образом:
<template>
<div>
<div class="row">
<div class="col-md-12">
<SfHero class="hero" :slider-options="{ autoplay: false }">
<SfHeroItem
v-for="(img, index) in heroes"
:key="index"
:title="img.title"
:subtitle="img.subtitle"
:button-text="img.buttonText"
:background="img.background"
:class="img.className"
/>
</SfHero>
</div>
<div class="col-md-12">
<h4 class="text-center mt-5 mb-5">All Products</h4>
</div>
</div>
<div v-if="products.length">
<div class="row">
<ProductCard v-for="product in products" :key="product.id" :item="product" />
</div>
</div>
</div>
</template>
<script>
import Axios from 'axios';
import { SfHero } from "@storefront-ui/vue";
export default {
name: 'ProductsIndex',
components: {
SfHero,
},
data () {
return {
products: [],
heroes: [
{
title: "Colorful T-shirts already in store",
subtitle: "JUNE COLLECTION 2022",
buttonText: "Learn more",
background: "rgb(236, 239, 241)",
},
{
title: "Colorful Sweatshirts already in store",
subtitle: "JUNE COLLECTION 2022",
buttonText: "Learn more",
background: "rgb(239, 235, 233)",
},
{
title: "Colorful Sweatpants already in store",
subtitle: "JUNE COLLECTION 2022",
buttonText: "Learn more",
background: "rgb(236, 239, 241)",
},
],
}
},
async fetch(){ // Fetching the products from Medusa server
try{
const {data:{products}} = await Axios.get(`${process.env.baseUrl}/store/products`);
this.products = products
}catch(e){
console.log('An error occured', e)
}
}
}
</script>
Этот скрипт извлекает товары с сервера Medusa с помощью Axios и отображает их с помощью ProductCard компонента, который вы создали ранее.
Вы также добавляете карусель с помощью компонента SfHero (opens new window). Он показывает автоматический слайдер с кнопками и текстом. Каждый слайд определяется как SfHeroItem компонент.
# Тестирование домашней страницы
Чтобы протестировать домашнюю страницу, убедитесь, что сервер Medusa запущен, и запустите сервер разработки Nuxt.js:
npm run dev
Затем откройте в браузере страницу по адресу http://localhost:8000
. Вы должны увидеть домашнюю страницу с каруселью, товарами и футером.
# Создание единой страницы товара
Создадим одну страницу товара, чтобы отобразить более подробную информацию о нём.
Создадим файл _pages/products/id.vue со следующим содержимым:
<template>
<div id="product">
<SfBreadcrumbs class="breadcrumbs desktop-only" :breadcrumbs="breadcrumbs" />
<p v-if="$fetchState.pending">Fetching Data...</p>
<p v-else-if="$fetchState.error">An error occurred :(</p>
<div v-else class="product">
<SfGallery :images="this.getImages" class="product__gallery" :image-width="422" :image-height="664"
:thumb-width="160" :thumb-height="160" :enableZoom="true"/>
<div class="product__info">
<div class="product__header">
<SfHeading :title="product.title" :level="1" class="sf-heading--no-underline sf-heading--left" />
<SfIcon icon="drag" size="42px" color="#E0E0E1" class="product__drag-icon smartphone-only" />
</div>
<div class="product__price-and-rating">
<SfPrice :regular="this.lowestPrice.amount" />
</div>
<div>
<p class="product__description desktop-only">{{ this.product.description }}
</p>
<SfAddToCart v-model="qty" class="product__add-to-cart" @click="addToCart" />
</div>
</div>
</div>
<transition name="slide">
<SfNotification class="notification desktop-only" type="success" :visible="isOpenNotification"
:message="`${qty} ${product.title} has been added to CART`" @click:close="isOpenNotification = true">
<template #icon>
<span></span></template></SfNotification>
</transition>
</div>
</template>
На этой странице отображаются следующие компоненты из библиотеки пользовательского интерфейса Vue Storefront:
- SfBreadcrumbs (opens new window) — отображает путь к текущему товару;
- SfGallery (opens new window) — упорядочивает изображения товаров, которые пользователи могут просматривать с помощью функции увеличения;
- SfHeading (opens new window) — отображает названия продуктов и может иметь описание;
- SfPrice (opens new window) — отображает цену товара;
- SfAddToCart (opens new window) — отображает кнопку «Добавить в корзину»;
- SfNotification (opens new window) — отображает уведомление внизу страницы, указывающее, что товары добавлены в корзину. Этот компонент используется только для имитации добавления товара в корзину.
Затем добавьте следующее в конец того же файла:
<script>
import { // UI components
SfGallery,
SfHeading,
SfPrice,
SfIcon,
SfAddToCart,
SfBreadcrumbs,
SfNotification,
} from "@storefront-ui/vue";
import Axios from "axios";
export default {
name: "Product",
components: {
SfGallery,
SfHeading,
SfPrice,
SfIcon,
SfAddToCart,
SfBreadcrumbs,
SfNotification,
},
data() { // default data
return {
current: 1,
qty: 1,
selected: false,
product: {
name: "",
title: "",
description: "",
images: [],
price: {
regular: 0,
},
},
breadcrumbs: [{
text: "Home",
link: "/",
}, ],
isOpenNotification: false,
};
},
computed: {
getImages() { // Format Product images in a way that the component understands.
return this.product ?
this.product.images.map((image) => {
return {
mobile: {
url: image.url,
},
desktop: {
url: image.url,
},
big: {
url: image.url,
},
alt: this.product?.title,
name: this.product?.title,
};
}) :
[];
},
lowestPrice() {
// Get the least price
const lowestPrice = this.product.variants ?
this.product.variants.reduce(
(acc, curr) => {
return curr.prices.reduce((lowest, current) => {
if (lowest.amount > current.amount) {
return current;
}
return lowest;
});
}, {
amount: 0
}
) :
{
amount: 0
};
// Format the amount and append the currency.
return {
amount: lowestPrice.amount > 0 ?
(lowestPrice.amount / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
}) :
0,
currency: "USD",
};
},
},
methods: {
addToCart() {
this.isOpenNotification = true; // show notification
setTimeout(() => {
this.isOpenNotification = false; // hide notification
}, 3000);
},
},
async fetch() {
// Fetch the product based on the id.
try {
const {
data: {
product
},
} = await Axios.get(
`${process.env.baseUrl}/store/products/${this.$route.params.id}`);
this.product = product;
} catch (e) {
// eslint-disable-next-line no-console
console.log("The server is not responding");
}
},
};
</script>
Этот скрипт импортирует компоненты, используемые в шаблоне, извлекает информацию о продукте с сервера Medusa и форматирует информацию о продукте перед их отображением в шаблоне.
Осталось добавить следующее в конец того же файла:
<style lang="scss" scoped>
@import "~@storefront-ui/vue/styles";
#product {
box-sizing: border-box;
@include for-desktop {
max-width: 1272px;
padding: 0 var(--spacer-sm);
margin: 0 auto;
}
}
.product {
margin-bottom:20px;
@include for-desktop {
display: flex;
}
&__info {
margin: var(--spacer-xs) auto;
@include for-desktop {
max-width: 32.625rem;
margin: 0 0 0 7.5rem;
}
}
&__header {
--heading-title-color: var(--c-link);
--heading-title-font-weight: var(--font-weight--bold);
--heading-title-font-size: var(--h3-font-size);
--heading-padding: 0;
margin: 0 var(--spacer-sm);
display: flex;
justify-content: space-between;
@include for-desktop {
--heading-title-font-weight: var(--font-weight--semibold);
margin: 0 auto;
}
}
&__drag-icon {
animation: moveicon 1s ease-in-out infinite;
}
&__price-and-rating {
margin: 0 var(--spacer-sm) var(--spacer-base);
align-items: center;
@include for-desktop {
display: flex;
justify-content: space-between;
margin: var(--spacer-sm) 0 var(--spacer-lg) 0;
}
}
&__count {
@include font(
--count-font,
var(--font-weight--normal),
var(--font-size--sm),
1.4,
var(--font-family--secondary)
);
color: var(--c-text);
text-decoration: none;
margin: 0 0 0 var(--spacer-xs);
}
&__description {
color: var(--c-link);
@include font(
--product-description-font,
var(--font-weight--light),
var(--font-size--base),
1.6,
var(--font-family--primary)
);
}
&__add-to-cart {
margin: var(--spacer-base) var(--spacer-sm) 0;
@include for-desktop {
margin-top: var(--spacer-2xl);
}
}
&__guide,
&__compare,
&__save {
display: block;
margin: var(--spacer-xl) 0 var(--spacer-base) auto;
}
&__compare {
margin-top: 0;
}
&__property {
margin: var(--spacer-base) 0;
&__button {
--button-font-size: var(--font-size--base);
}
}
&__additional-info {
color: var(--c-link);
@include font(
--additional-info-font,
var(--font-weight--light),
var(--font-size--sm),
1.6,
var(--font-family--primary)
);
&__title {
font-weight: var(--font-weight--normal);
font-size: var(--font-size--base);
margin: 0 0 var(--spacer-sm);
&:not(:first-child) {
margin-top: 3.5rem;
}
}
&__paragraph {
margin: 0;
}
}
&__gallery {
flex: 1;
}
}
.breadcrumbs {
margin: var(--spacer-base) auto var(--spacer-lg);
}
.notification {
position: fixed;
bottom: 0;
left: 0;
right: 0;
--notification-border-radius: 0;
--notification-max-width: 100%;
--notification-font-size: var(--font-size--lg);
--notification-font-family: var(--font-family--primary);
--notification-font-weight: var(--font-weight--normal);
--notification-padding: var(--spacer-base) var(--spacer-lg);
}
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s;
}
.slide-enter {
transform: translateY(40px);
}
.slide-leave-to {
transform: translateY(-80px);
}
@keyframes moveicon {
0% {
transform: translate3d(0, 0, 0);
}
50% {
transform: translate3d(0, 30%, 0);
}
100% {
transform: translate3d(0, 0, 0);
}
}
</style>
# Тестовая страница отдельного товара
Убедитесь, что ваш сервер Medusa и серверы разработки Nuxt.js запущены. Затем перейдите в браузере по адресу localhost:8000 и нажмите на товар на главной странице. Вы будете перенаправлены на страницу товара с дополнительной информацией о нём.
Если вы нажмёте кнопку «Добавить в корзину», внизу страницы появится уведомление о том, что вы добавили товар в корзину. Как мы упоминали ранее, это только имитация, фактически товар не будет добавлен в корзину.
# Заключение
В этом руководстве представлены лишь основы работы с Medusa и пользовательским интерфейсом Vue Storefront.
Но вы также сможете добавлять другие функции, например:
- Корзину (opens new window), используя компонент Vue Storefront (opens new window).
- Плагин Stripe (opens new window) для оплаты.
- Компоненты для оформления заказа (opens new window), например, Checkout (opens new window) и Order Summary (opens new window).
Если у вас есть какие-либо проблемы или вопросы по Medusa, обращайтесь к команде разработчиков в Discord (opens new window).