Tego nie wiesz o Medusa.js – Część 1

Autor Viktor Holik

Featured image

Medusa.js to coś więcej niż tylko kolejna opcja, jak Shopify czy WooCommerce. To potężny zbiór narzędzi eCommerce, bez tradycyjnego frontendu, dzięki któremu Twój rozwój będzie szybszy, bardziej niezawodny i innowacyjny.

W tym artykule podzielę się z Tobą kilkoma fajnymi wskazówkami i trikami dotyczącymi Medusa.js, których wielu programistów jeszcze nie odkryło. Te pomysły mogą naprawdę pomóc w usprawnieniu działania sklepów Medusa.js.

Zaczynajmy!

Transakcje

Czy kiedykolwiek spotkałeś się z sytuacją, w której wykonujesz wiele operacji, a jedna z nich kończy się niepowodzeniem, sprawiając niespójności? Rozważ następujący scenariusz we fragmencie kodu związanym z obsługą webhooków dla zamówień:

// api/webhook/[order_id].ts
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  // Kod inicjujący usługi...
  const { order_id: orderId } = req.params;
  // Odbierz zamówienie...
  
  // Spróbuj zaktualizować status każdego produktu w zamówieniu
  for (const lineItem of order.items) {
    // A co, jeśli aktualizacja się nie powiedzie?
    await productService.update(lineItem.product_id, {status: ProductStatus.PROPOSED})
  }

  return res.sendStatus(200);
};

Takie podejście może powodować niespójność danych, w przypadku której niektóre produkty są oznaczone jako „proponowane”, a inne nie. Utrudnia to śledzenie i poprawianie błędów w przypadku aktualizacji tylko części danych.

Aby rozwiązać ten problem, rozważ użycie transakcji do zakończenia operacji:

// api/webhook/[order_id].ts
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  // Kod inicjujący usługi i menedżera transakcji...
  const { order_id: orderId } = req.params;

  try {
    // Rozpocznij transakcję
    await manager.transaction(async transactionManager => {
      // Pobierz zamówienie w ramach transakcji...
      
      for (const lineItem of order.items) {
        // Skorzystaj z menedżera transakcji, aby upewnić się, że wszystkie aktualizacje stanowią część transakcji
        await productService.withTransaction(transactionManager).update(lineItem.product_id, {status: ProductStatus.PROPOSED})
      }
    });
  } catch(e) {
    // Obsługa błędów, na przykład rejestrowanie i wysyłanie odpowiedzi na niepowodzenie
    return res.sendStatus(500)
  }

  return res.sendStatus(200);
};

Korzystanie z transakcji gwarantuje, że wszystkie operacje na bazie danych albo zakończą się pomyślnie, albo zawiodą razem. Oznacza to, że jeśli aktualizacja się nie powiedzie, wszystkie zmiany dokonane w ramach poprzednich operacji w transakcji zostaną cofnięte, co zapobiega częściowym aktualizacjom i pozwala zachować integralność danych.

Możesz zadeklarować transakcje w usługach niestandardowych, rozszerzając je z TransactionBaseService.

Pamięć podręczna

W Medusie buforowanie jest potężnym narzędziem używanym do przechowywania wyników różnych obliczeń, takich jak wybór ceny lub obliczenia podatkowe. Jednak mniej znanym przypadkiem użycia jest wykorzystanie usługi pamięci podręcznej do przechowywania własnych danych, co znacznie zwiększa wydajność.

Rozważmy scenariusz, w którym Twój sklep ma ogromny asortyment produktów. Pobieranie tych produktów z bazy danych przy każdym wywołaniu API może być nieefektywne i spowalniać aplikację. Tutaj w grę wchodzi usługa pamięci podręcznej, oferująca sposób tymczasowego przechowywania danych i znacznie szybszego dostępu do nich.

Oto podstawowy przykład pobierania produktów bez korzystania z pamięci podręcznej:

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const productService: ProductService = req.scope.resolve('productService');
  // Produkty są pobierane z bazy danych przy każdym dostępie do route GET
  const [products, count] = await productService.listAndCount({take: 100});

  res.status(200).json({ products, count });
};

Aby poprawić wydajność, możesz skorzystać z usługi pamięci podręcznej w następujący sposób:

const CACHE_KEY = 'products';

export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
  const cacheService: ICacheService = req.scope.resolve('cacheService');
  const productService: ProductService = req.scope.resolve('productService');

  // Spróbuj odzyskać zapisane w pamięci podręcznej dane produktów
  const cached = (await cacheService.get(CACHE_KEY)) as
    | Record<string, unknown>
    | undefined;

  // Jeśli istnieją dane zapisane w pamięci podręcznej, zwróć je zamiast wysyłać zapytania do bazy danych
  if (cached) {
    return res.json(cached.data);
  }

  // Pobierz produkty z bazy danych, jeśli nie znaleziono pamięci podręcznej
  const [products, count] = await productService.listAndCount({});

  // Buforuj nowo pobrane dane produktów przez 1 godzinę
  await cacheService.set(
    CACHE_KEY,
    { data: { products, count } },
    60 * 60, // Cache duration of 1 hour
  );

  res.status(200).json({ products, count });
};

Domyślna strategia buforowania Medusy wykorzystuje @medusajs/cache-inmemory, która jest odpowiednia do zastosowań programistycznych lub na małą skalę. Jednak w środowiskach produkcyjnych, szczególnie tych o dużym natężeniu ruchu lub dużych zbiorach danych, zaleca się przejście na @medusajs/cache-redis.

Moduły

Moduły to pakiety z samodzielną logiką commerce, promujące oddzielenie problemów, łatwość konserwacji i możliwość ponownego użycia. Moduły zwiększają rozszerzalność Medusy, umożliwiając dostosowanie podstawowej logiki commerce i kompozycji za pomocą innych narzędzi. Ta elastyczność pozwala na większy wybór narzędzi technologicznych używanych w połączeniu z Medusa.js.

Moduły Medusa.js możesz uruchomić w funkcji Next.js lub kompatybilnym środowisku Node.js. W tej chwili moduły są nadal w fazie beta, ale tak czy inaczej możesz już spróbować z nich skorzystać. Oto przykład:

  1. Zainstaluj żądany moduł. Listę dostępnych produktów znajdziesz na stronie Medusa.js - dokumentacja.
npm install @medusajs/product

2.Dodaj URL bazy danych do zmiennych środowiskowych

POSTGRES_URL=<DATABASE_URL>

3.Zastosuj migrację baz danych

Jeśli korzystasz z istniejącej bazy danych Medusa.js, możesz pominąć ten krok. Ma on zastosowanie tylko wtedy, gdy moduł jest używany oddzielnie od pełnej konfiguracji Medusa.js.

Zanim będzie można uruchomić migracje, dodaj w pliku package.json następujące skrypty:

"scripts": {
    //...inne skrypty
    "product:migrations:run": "medusa-product-migrations-up",
    "product:seed": "medusa-product-seed ./seed-data.js"
},
  1. Zmień konfigurację Next.js
const nextConfig = {
  experimental: {
    serverComponentsExternalPackages: ["@medusajs/product"],
  },
}

module.exports = nextConfig

Moduły Medusa można uruchamiać w ramach funkcji Next.js lub dowolnego kompatybilnego środowiska Node.js. Chociaż moduły te są obecnie w fazie beta, są już dostępne do użytku. Oto przykład:

// /app/api/products/route.ts
import { NextResponse } from "next/server";
import { initialize as setupProductModule } from "@medusajs/product";

export async function GET(req: Request) {
  const productModule = await setupProductModule();
  
  // Wyodrębnij kod kraju z nagłówków żądań
  const country: string = req.headers.get("x-country") || "US";

  const continent = continentMap[country];

  // Pobierz spersonalizowane listy produktów
  const result = await productModule.list({
          tags: { value: [continent] },
  });

  return NextResponse.json({ products: result });
}

Oto pełny przykład użycia modułu produktu w Next.js autorstwa zespołu Medusa.js.

Testowanie

Medusa bezproblemowo integruje testy jednostkowe z Jest, zwiększając tę możliwość za pomocą pakietu medusa-test-utils, który znacznie upraszcza proces testowania.

Oto niektóre z najbardziej wartościowych narzędzi, jakie oferuje:

MockRepository: To próbne repozytorium, które możesz łatwo dostosować.

const userRepository = MockRepository({
 find: () => Promise.resolve([{ id: IdMap.getId('ironman'), role: 'admin' }]),
});

IdMap: narzędzie do zarządzania mapą unikalnych identyfikatorów powiązanych z określonymi kluczami, ułatwiające spójne odwoływanie się do identyfikatorów w testach.

import { IdMap } from "medusa-test-utils";

export const products = {
  product1: {
    id: IdMap.getId("product1"),
    title: "Product 1",
  },
  product2: {
    id: IdMap.getId("product2"),
    title: "Product 2",
  }
};

export const ProductServiceMock = {
  retrieveVariants: jest.fn().mockImplementation((productId) => {
    if (productId === IdMap.getId("product1")) {
      return Promise.resolve([
        { id: IdMap.getId("1"), product_id: IdMap.getId("product1") },
        { id: IdMap.getId("2"), product_id: IdMap.getId("product2") },
      ])
    }

    return [];
  }),
};

MockManager

const userService = new UserService({
  manager: MockManager,
  userRepository,
});

it("successfully retrieves a user", async () => {
  const result = await userService.retrieve(IdMap.getId("ironman"));

  expect(result.id).toEqual(IdMap.getId("ironman"));
});

Testowanie integracji

Chociaż Medusa nie oferuje gotowego rozwiązania do testowania integracji, Riqwan Thamir z głównego zespołu Medusy opracował projekt integracji usług Medusa.js z zestawami testów integracyjnych. Oto podstawowe podejście do konfigurowania testów integracyjnych w Medusie.

Mam nadzieję, że te informacje na temat możliwości testowania Medusy okazały się przydatne. Jeśli interesują Cię takie wskazówki, okazanie wsparcia poprzez zaangażowanie pomoże! Dziękuję za przeczytanie, czekaj na drugą część i więcej przydatnych treści.

Inne posty na blogu

Frontend Developer: Mistrz efektywności i UX — Kluczowe strategie dla optymalnego performance aplikacji

W dzisiejszym nieustannie rozwijającym się świecie cyfrowym, gdzie użytkownicy oczekują szybkości, wydajności i bezproblemowego korzystania z aplikacji internetowych, rola Frontend Developer’a staje się niezmiernie istotna...

Wspierają nas inwestorzy Medusa - Catch The Tornado

Do naszej firmy dołączyli Piotr i Tomasz Karwatkowie.

Opowiedz nam o swoim projekcie

Myślisz o nowym projekcie? Zrealizujmy go!

Naciskając „Wyślij wiadomość” udzielasz nam, tj. Rigby, zgody na email marketing naszych usług w ramach komunikacji dotyczącej Twojego projektu. Zgodę możesz wycofać, np. pisząc na adres hello@rigbyjs.com.
Więcej
placeholder

Grzegorz Tomaka

Co-CEO & Co-founder

LinkedIn icon
placeholder

Jakub Zbaski

Co-CEO & Co-founder

LinkedIn icon