Электронная коммерция Производительность Astro

Создание быстрых сайтов электронной коммерции с Astro

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

Дэвид Уилсон
Дэвид Уилсон
15 мин чтения

Создание быстрых сайтов электронной коммерции с Astro

Сайты электронной коммерции сталкиваются с уникальными вызовами производительности: они должны быть достаточно быстрыми для конверсии посетителей, обеспечивая при этом богатый интерактивный опыт покупок. Архитектура островов Astro делает возможным создание сайтов электронной коммерции, которые превосходят в обеих областях. Это всеобъемлющее руководство показывает вам как.

Почему производительность важна для электронной коммерции

Влияние на бизнес

Производительность напрямую влияет на вашу прибыль:

  • 1-секундная задержка = 7% снижение конверсий
  • 100мс улучшение = 1% увеличение дохода
  • 53% мобильных пользователей покидают сайты, которые загружаются дольше 3 секунд

Core Web Vitals для электронной коммерции

Core Web Vitals Google особенно критичны для электронной коммерции:

  • Largest Contentful Paint (LCP): < 2.5с
  • First Input Delay (FID): < 100мс
  • Cumulative Layout Shift (CLS): < 0.1

Преимущества Astro для электронной коммерции

Статические страницы товаров

Страницы товаров могут быть предварительно отрендерены для максимальной скорости:

---
// src/pages/products/[slug].astro
export async function getStaticPaths() {
  const products = await fetchAllProducts();
  return products.map(product => ({
    params: { slug: product.slug },
    props: { product }
  }));
}

const { product } = Astro.props;
---

<Layout title={product.name}>
  <ProductPage product={product} />
</Layout>

Селективная интерактивность

Добавляйте JavaScript только там, где нужно:

---
import ProductGallery from '../components/ProductGallery.jsx';
import AddToCart from '../components/AddToCart.jsx';
import Reviews from '../components/Reviews.jsx';
---

<Layout>
  <!-- Статическая информация о товаре -->
  <h1>{product.name}</h1>
  <p>{product.description}</p>
  <span class="price">${product.price}</span>
  
  <!-- Интерактивные острова -->
  <ProductGallery images={product.images} client:load />
  <AddToCart product={product} client:idle />
  <Reviews productId={product.id} client:visible />
</Layout>

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

Начальная настройка

# Создание нового проекта Astro
npm create astro@latest ecommerce-site

# Добавление необходимых интеграций
npx astro add react
npx astro add tailwind
npx astro add image

# Установка зависимостей для электронной коммерции
npm install stripe @stripe/stripe-js
npm install zustand  # Для управления состоянием
npm install @headlessui/react  # Для UI компонентов

Структура проекта

src/
├── components/
│   ├── cart/
│   │   ├── AddToCart.jsx
│   │   ├── CartSummary.jsx
│   │   └── CartDrawer.jsx
│   ├── product/
│   │   ├── ProductCard.astro
│   │   ├── ProductGallery.jsx
│   │   └── ProductFilters.jsx
│   └── checkout/
│       ├── CheckoutForm.jsx
│       └── PaymentForm.jsx
├── layouts/
│   └── Layout.astro
├── pages/
│   ├── products/
│   │   ├── index.astro
│   │   └── [slug].astro
│   ├── cart.astro
│   └── checkout.astro
├── stores/
│   └── cart.js
└── utils/
    ├── stripe.js
    └── api.js

Реализация каталога товаров

Структура данных товара

// types/product.ts
export interface Product {
  id: string;
  name: string;
  slug: string;
  description: string;
  price: number;
  images: string[];
  category: string;
  tags: string[];
  inventory: number;
  variants?: ProductVariant[];
}

export interface ProductVariant {
  id: string;
  name: string;
  price: number;
  inventory: number;
  attributes: Record<string, string>;
}

Страница списка товаров

---
// src/pages/products/index.astro
import Layout from '../../layouts/Layout.astro';
import ProductCard from '../../components/product/ProductCard.astro';
import ProductFilters from '../../components/product/ProductFilters.jsx';

const products = await fetchProducts();
const categories = await fetchCategories();
---

<Layout title="Товары">
  <div class="container mx-auto px-4 py-8">
    <div class="flex gap-8">
      <!-- Боковая панель фильтров -->
      <aside class="w-64">
        <ProductFilters 
          categories={categories} 
          client:load 
        />
      </aside>
      
      <!-- Сетка товаров -->
      <main class="flex-1">
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="product-grid">
          {products.map(product => (
            <ProductCard product={product} />
          ))}
        </div>
      </main>
    </div>
  </div>
</Layout>

Статические карточки товаров

---
// src/components/product/ProductCard.astro
import { Image } from 'astro:assets';

export interface Props {
  product: Product;
}

const { product } = Astro.props;
---

<article class="product-card bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow">
  <a href={`/products/${product.slug}`} class="block">
    <div class="aspect-square overflow-hidden rounded-t-lg">
      <Image
        src={product.images[0]}
        alt={product.name}
        width={300}
        height={300}
        class="w-full h-full object-cover hover:scale-105 transition-transform"
        loading="lazy"
      />
    </div>
    
    <div class="p-4">
      <h3 class="font-semibold text-gray-900 mb-2">{product.name}</h3>
      <p class="text-gray-600 text-sm mb-3 line-clamp-2">{product.description}</p>
      
      <div class="flex items-center justify-between">
        <span class="text-lg font-bold text-gray-900">
          ${product.price.toFixed(2)}
        </span>
        
        {product.inventory > 0 ? (
          <span class="text-sm text-green-600">В наличии</span>
        ) : (
          <span class="text-sm text-red-600">Нет в наличии</span>
        )}
      </div>
    </div>
  </a>
</article>

Интерактивные функции товаров

Галерея изображений товара

// src/components/product/ProductGallery.jsx
import { useState } from 'react';

export default function ProductGallery({ images, productName }) {
  const [currentImage, setCurrentImage] = useState(0);

  return (
    <div class="product-gallery">
      {/* Основное изображение */}
      <div class="main-image mb-4">
        <img
          src={images[currentImage]}
          alt={`${productName} - Изображение ${currentImage + 1}`}
          class="w-full h-96 object-cover rounded-lg"
        />
      </div>
      
      {/* Навигация по миниатюрам */}
      <div class="thumbnails flex gap-2 overflow-x-auto">
        {images.map((image, index) => (
          <button
            key={index}
            onClick={() => setCurrentImage(index)}
            class={`flex-shrink-0 w-16 h-16 rounded border-2 ${
              index === currentImage ? 'border-blue-500' : 'border-gray-200'
            }`}
          >
            <img
              src={image}
              alt={`${productName} миниатюра ${index + 1}`}
              class="w-full h-full object-cover rounded"
            />
          </button>
        ))}
      </div>
    </div>
  );
}

Функциональность добавления в корзину

// src/components/cart/AddToCart.jsx
import { useState } from 'react';
import { useCartStore } from '../../stores/cart.js';

export default function AddToCart({ product }) {
  const [quantity, setQuantity] = useState(1);
  const [selectedVariant, setSelectedVariant] = useState(null);
  const [isAdding, setIsAdding] = useState(false);
  
  const addToCart = useCartStore(state => state.addItem);

  const handleAddToCart = async () => {
    setIsAdding(true);
    
    try {
      await addToCart({
        productId: product.id,
        variantId: selectedVariant?.id,
        quantity,
        price: selectedVariant?.price || product.price
      });
      
      // Показать обратную связь об успехе
      setIsAdding(false);
    } catch (error) {
      console.error('Не удалось добавить в корзину:', error);
      setIsAdding(false);
    }
  };

  return (
    <div class="add-to-cart space-y-4">
      {/* Выбор варианта */}
      {product.variants && (
        <div>
          <label class="block text-sm font-medium mb-2">
            Выберите вариант
          </label>
          <select
            value={selectedVariant?.id || ''}
            onChange={(e) => {
              const variant = product.variants.find(v => v.id === e.target.value);
              setSelectedVariant(variant);
            }}
            class="w-full border rounded-md px-3 py-2"
          >
            <option value="">Выберите вариант</option>
            {product.variants.map(variant => (
              <option key={variant.id} value={variant.id}>
                {variant.name} - ${variant.price.toFixed(2)}
              </option>
            ))}
          </select>
        </div>
      )}
      
      {/* Селектор количества */}
      <div>
        <label class="block text-sm font-medium mb-2">
          Количество
        </label>
        <div class="flex items-center space-x-2">
          <button
            onClick={() => setQuantity(Math.max(1, quantity - 1))}
            class="w-8 h-8 border rounded flex items-center justify-center"
          >
            -
          </button>
          <span class="w-12 text-center">{quantity}</span>
          <button
            onClick={() => setQuantity(quantity + 1)}
            class="w-8 h-8 border rounded flex items-center justify-center"
          >
            +
          </button>
        </div>
      </div>
      
      {/* Кнопка добавления в корзину */}
      <button
        onClick={handleAddToCart}
        disabled={isAdding || product.inventory === 0}
        class={`w-full py-3 px-6 rounded-lg font-medium ${
          product.inventory === 0
            ? 'bg-gray-300 text-gray-500 cursor-not-allowed'
            : 'bg-blue-600 text-white hover:bg-blue-700'
        }`}
      >
        {isAdding ? 'Добавление...' : 'Добавить в корзину'}
      </button>
    </div>
  );
}

Реализация корзины покупок

Управление состоянием корзины

// src/stores/cart.js
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useCartStore = create(
  persist(
    (set, get) => ({
      items: [],
      isOpen: false,
      
      addItem: async (item) => {
        const existingItem = get().items.find(
          i => i.productId === item.productId && i.variantId === item.variantId
        );
        
        if (existingItem) {
          set(state => ({
            items: state.items.map(i =>
              i.productId === item.productId && i.variantId === item.variantId
                ? { ...i, quantity: i.quantity + item.quantity }
                : i
            )
          }));
        } else {
          set(state => ({
            items: [...state.items, { ...item, id: Date.now() }]
          }));
        }
        
        // Открыть выдвижную панель корзины
        set({ isOpen: true });
      },
      
      removeItem: (itemId) => {
        set(state => ({
          items: state.items.filter(item => item.id !== itemId)
        }));
      },
      
      updateQuantity: (itemId, quantity) => {
        if (quantity <= 0) {
          get().removeItem(itemId);
          return;
        }
        
        set(state => ({
          items: state.items.map(item =>
            item.id === itemId ? { ...item, quantity } : item
          )
        }));
      },
      
      clearCart: () => set({ items: [] }),
      
      toggleCart: () => set(state => ({ isOpen: !state.isOpen })),
      
      getTotal: () => {
        return get().items.reduce(
          (total, item) => total + (item.price * item.quantity),
          0
        );
      },
      
      getItemCount: () => {
        return get().items.reduce(
          (count, item) => count + item.quantity,
          0
        );
      }
    }),
    {
      name: 'cart-storage',
      partialize: (state) => ({ items: state.items })
    }
  )
);

Компонент выдвижной панели корзины

// src/components/cart/CartDrawer.jsx
import { Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { useCartStore } from '../../stores/cart.js';

export default function CartDrawer() {
  const { items, isOpen, toggleCart, removeItem, updateQuantity, getTotal } = useCartStore();

  return (
    <Transition.Root show={isOpen} as={Fragment}>
      <Dialog as="div" className="relative z-50" onClose={toggleCart}>
        <Transition.Child
          as={Fragment}
          enter="ease-in-out duration-500"
          enterFrom="opacity-0"
          enterTo="opacity-100"
          leave="ease-in-out duration-500"
          leaveFrom="opacity-100"
          leaveTo="opacity-0"
        >
          <div className="fixed inset-0 bg-gray-500 bg-opacity-75" />
        </Transition.Child>

        <div className="fixed inset-0 overflow-hidden">
          <div className="absolute inset-0 overflow-hidden">
            <div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10">
              <Transition.Child
                as={Fragment}
                enter="transform transition ease-in-out duration-500"
                enterFrom="translate-x-full"
                enterTo="translate-x-0"
                leave="transform transition ease-in-out duration-500"
                leaveFrom="translate-x-0"
                leaveTo="translate-x-full"
              >
                <Dialog.Panel className="pointer-events-auto w-screen max-w-md">
                  <div className="flex h-full flex-col bg-white shadow-xl">
                    {/* Заголовок */}
                    <div className="flex items-center justify-between px-4 py-6 border-b">
                      <Dialog.Title className="text-lg font-medium">
                        Корзина покупок ({items.length})
                      </Dialog.Title>
                      <button
                        onClick={toggleCart}
                        className="text-gray-400 hover:text-gray-500"
                      >
                        ×
                      </button>
                    </div>

                    {/* Товары в корзине */}
                    <div className="flex-1 overflow-y-auto px-4 py-6">
                      {items.length === 0 ? (
                        <p className="text-center text-gray-500">Ваша корзина пуста</p>
                      ) : (
                        <div className="space-y-4">
                          {items.map(item => (
                            <CartItem
                              key={item.id}
                              item={item}
                              onUpdateQuantity={updateQuantity}
                              onRemove={removeItem}
                            />
                          ))}
                        </div>
                      )}
                    </div>

                    {/* Футер */}
                    {items.length > 0 && (
                      <div className="border-t px-4 py-6">
                        <div className="flex justify-between text-lg font-medium mb-4">
                          <span>Итого</span>
                          <span>${getTotal().toFixed(2)}</span>
                        </div>
                        <a
                          href="/checkout"
                          className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg text-center block hover:bg-blue-700"
                        >
                          Оформить заказ
                        </a>
                      </div>
                    )}
                  </div>
                </Dialog.Panel>
              </Transition.Child>
            </div>
          </div>
        </div>
      </Dialog>
    </Transition.Root>
  );
}

function CartItem({ item, onUpdateQuantity, onRemove }) {
  return (
    <div className="flex items-center space-x-4">
      <img
        src={item.image}
        alt={item.name}
        className="w-16 h-16 object-cover rounded"
      />
      <div className="flex-1">
        <h4 className="font-medium">{item.name}</h4>
        <p className="text-sm text-gray-500">${item.price.toFixed(2)}</p>
      </div>
      <div className="flex items-center space-x-2">
        <button
          onClick={() => onUpdateQuantity(item.id, item.quantity - 1)}
          className="w-6 h-6 border rounded text-sm"
        >
          -
        </button>
        <span className="w-8 text-center">{item.quantity}</span>
        <button
          onClick={() => onUpdateQuantity(item.id, item.quantity + 1)}
          className="w-6 h-6 border rounded text-sm"
        >
          +
        </button>
      </div>
      <button
        onClick={() => onRemove(item.id)}
        className="text-red-500 hover:text-red-700"
      >
        Удалить
      </button>
    </div>
  );
}

Оформление заказа и интеграция платежей

Настройка Stripe

// src/utils/stripe.js
import { loadStripe } from '@stripe/stripe-js';

export const stripePromise = loadStripe(import.meta.env.PUBLIC_STRIPE_PUBLISHABLE_KEY);

export async function createPaymentIntent(amount, currency = 'usd') {
  const response = await fetch('/api/create-payment-intent', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ amount, currency }),
  });

  return response.json();
}

API маршрут Payment Intent

// src/pages/api/create-payment-intent.ts
import type { APIRoute } from 'astro';
import Stripe from 'stripe';

const stripe = new Stripe(import.meta.env.STRIPE_SECRET_KEY, {
  apiVersion: '2023-10-16',
});

export const POST: APIRoute = async ({ request }) => {
  try {
    const { amount, currency } = await request.json();

    const paymentIntent = await stripe.paymentIntents.create({
      amount: Math.round(amount * 100), // Конвертация в копейки
      currency,
      automatic_payment_methods: {
        enabled: true,
      },
    });

    return new Response(JSON.stringify({
      clientSecret: paymentIntent.client_secret,
    }), {
      status: 200,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  } catch (error) {
    return new Response(JSON.stringify({
      error: error.message,
    }), {
      status: 400,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }
};

Форма оформления заказа

// src/components/checkout/CheckoutForm.jsx
import { useState, useEffect } from 'react';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { stripePromise, createPaymentIntent } from '../../utils/stripe.js';
import { useCartStore } from '../../stores/cart.js';

function CheckoutFormInner() {
  const stripe = useStripe();
  const elements = useElements();
  const { items, getTotal, clearCart } = useCartStore();
  
  const [isProcessing, setIsProcessing] = useState(false);
  const [clientSecret, setClientSecret] = useState('');
  const [error, setError] = useState('');

  useEffect(() => {
    // Создание payment intent при монтировании компонента
    createPaymentIntent(getTotal())
      .then(({ clientSecret }) => setClientSecret(clientSecret))
      .catch(err => setError(err.message));
  }, []);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setIsProcessing(true);
    setError('');

    if (!stripe || !elements) {
      return;
    }

    const cardElement = elements.getElement(CardElement);

    const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
      payment_method: {
        card: cardElement,
        billing_details: {
          name: event.target.name.value,
          email: event.target.email.value,
        },
      },
    });

    if (error) {
      setError(error.message);
      setIsProcessing(false);
    } else if (paymentIntent.status === 'succeeded') {
      // Платеж успешен
      clearCart();
      window.location.href = '/order-confirmation';
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-6">
      {/* Информация о клиенте */}
      <div className="grid grid-cols-2 gap-4">
        <div>
          <label className="block text-sm font-medium mb-2">
            Имя
          </label>
          <input
            type="text"
            name="firstName"
            required
            className="w-full border rounded-md px-3 py-2"
          />
        </div>
        <div>
          <label className="block text-sm font-medium mb-2">
            Фамилия
          </label>
          <input
            type="text"
            name="lastName"
            required
            className="w-full border rounded-md px-3 py-2"
          />
        </div>
      </div>

      <div>
        <label className="block text-sm font-medium mb-2">
          Email
        </label>
        <input
          type="email"
          name="email"
          required
          className="w-full border rounded-md px-3 py-2"
        />
      </div>

      {/* Информация о платеже */}
      <div>
        <label className="block text-sm font-medium mb-2">
          Информация о карте
        </label>
        <div className="border rounded-md p-3">
          <CardElement
            options={{
              style: {
                base: {
                  fontSize: '16px',
                  color: '#424770',
                  '::placeholder': {
                    color: '#aab7c4',
                  },
                },
              },
            }}
          />
        </div>
      </div>

      {error && (
        <div className="text-red-600 text-sm">{error}</div>
      )}

      <button
        type="submit"
        disabled={!stripe || isProcessing}
        className={`w-full py-3 px-6 rounded-lg font-medium ${
          isProcessing
            ? 'bg-gray-300 text-gray-500'
            : 'bg-blue-600 text-white hover:bg-blue-700'
        }`}
      >
        {isProcessing ? 'Обработка...' : `Оплатить $${getTotal().toFixed(2)}`}
      </button>
    </form>
  );
}

export default function CheckoutForm() {
  return (
    <Elements stripe={stripePromise}>
      <CheckoutFormInner />
    </Elements>
  );
}

Оптимизация производительности

Оптимизация изображений

---
// Оптимизация изображений товаров
import { Image } from 'astro:assets';
---

<!-- Используйте компонент Image Astro для автоматической оптимизации -->
<Image
  src={product.image}
  alt={product.name}
  width={400}
  height={400}
  format="webp"
  loading="lazy"
  class="product-image"
/>

<!-- Генерация нескольких размеров для адаптивных изображений -->
<Image
  src={product.image}
  alt={product.name}
  widths={[300, 600, 900]}
  sizes="(max-width: 768px) 300px, (max-width: 1200px) 600px, 900px"
  loading="lazy"
/>

Ленивая загрузка компонентов

---
// Загружайте тяжелые компоненты только когда нужно
import ProductReviews from '../components/ProductReviews.jsx';
import RelatedProducts from '../components/RelatedProducts.jsx';
---

<Layout>
  <!-- Критический контент загружается немедленно -->
  <ProductInfo product={product} />
  <AddToCart product={product} client:idle />
  
  <!-- Некритический контент загружается когда виден -->
  <ProductReviews productId={product.id} client:visible />
  <RelatedProducts category={product.category} client:visible />
</Layout>

Стратегии кеширования

// src/utils/cache.ts
const cache = new Map();

export async function getCachedData(key: string, fetcher: () => Promise<any>, ttl = 300000) {
  const cached = cache.get(key);
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data;
  }
  
  const data = await fetcher();
  cache.set(key, { data, timestamp: Date.now() });
  
  return data;
}

// Использование на страницах
const products = await getCachedData('products', fetchProducts);

SEO для электронной коммерции

Разметка схемы товара

---
const productSchema = {
  "@context": "https://schema.org",
  "@type": "Product",
  "name": product.name,
  "description": product.description,
  "image": product.images,
  "sku": product.sku,
  "brand": {
    "@type": "Brand",
    "name": "Ваш бренд"
  },
  "offers": {
    "@type": "Offer",
    "price": product.price,
    "priceCurrency": "USD",
    "availability": product.inventory > 0 ? "InStock" : "OutOfStock",
    "seller": {
      "@type": "Organization",
      "name": "Ваш магазин"
    }
  }
};
---

<script type="application/ld+json">
  {JSON.stringify(productSchema)}
</script>

Динамические мета-теги

---
// Генерация SEO-дружественных мета-тегов для каждого товара
const title = `${product.name} | Ваш магазин`;
const description = `${product.description.substring(0, 160)}...`;
const image = product.images[0];
---

<Layout title={title} description={description} image={image}>
  <!-- Контент товара -->
</Layout>

Мониторинг и аналитика

Мониторинг производительности

// Мониторинг Core Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  // Отправка в ваш сервис аналитики
  gtag('event', metric.name, {
    value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
    event_category: 'Web Vitals',
    event_label: metric.id,
    non_interaction: true,
  });
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);

Отслеживание электронной коммерции

// Отслеживание событий электронной коммерции
export function trackPurchase(transactionId, items, value) {
  gtag('event', 'purchase', {
    transaction_id: transactionId,
    value: value,
    currency: 'USD',
    items: items.map(item => ({
      item_id: item.productId,
      item_name: item.name,
      category: item.category,
      quantity: item.quantity,
      price: item.price
    }))
  });
}

export function trackAddToCart(item) {
  gtag('event', 'add_to_cart', {
    currency: 'USD',
    value: item.price * item.quantity,
    items: [{
      item_id: item.productId,
      item_name: item.name,
      category: item.category,
      quantity: item.quantity,
      price: item.price
    }]
  });
}

Заключение

Создание быстрых сайтов электронной коммерции с Astro требует стратегического подхода к оптимизации производительности. Используя статическую генерацию для страниц товаров, селективную гидратацию для интерактивных функций и тщательное внимание к Core Web Vitals, вы можете создать опыт электронной коммерции, который одновременно конвертирует посетителей и хорошо ранжируется в поисковых системах.

Ключевые выводы:

  • Предварительно рендерите страницы товаров для максимальной скорости
  • Используйте архитектуру островов для селективной интерактивности
  • Реализуйте эффективное управление состоянием для функциональности корзины
  • Оптимизируйте изображения и ленивую загрузку некритического контента
  • Отслеживайте метрики производительности и пользовательский опыт
  • Фокусируйтесь на Core Web Vitals для лучших рейтингов в поиске

Сочетание подхода Astro, ориентированного на производительность, с современными лучшими практиками электронной коммерции создает мощную основу для создания успешных интернет-магазинов.