# Создание магазина электронной коммерции на 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. Вы должны увидеть страницу со следующим содержимым:

«01»

Осталось только установить плагин для добавления товаров. Вы можете использовать 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:

«02»

Для добавления нового товара нажмите New Product в правом верхнем углу и заполните его характеристики.

Подробную информацию о функциях администратора Medusa можно прочитать в документации (opens new window).

# Создание и настройка витрины магазина Nuxt.js

Для создания витрины зайдите в другой каталог и выполните команду:

npx create-nuxt-app nuxtjs-storefront

Вам нужно будет ответить на некоторые вопросы. Вот пример ответов:

«03»

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.

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

Если у вас есть какие-либо проблемы или вопросы по Medusa, обращайтесь к команде разработчиков в Discord (opens new window).