发布于 2026-01-06 9 阅读
0

Remix 与 Shopify:绕过 Shopify 的 API 并走向开源

Remix 与 Shopify:绕过 Shopify 的 API 并走向开源

Shopify 似乎已经意识到开源正是开发者所需要的。他们先是推出了 Hydrogen,现在又更进一步,与 Remix 强强联手。这固然是好消息,但有些人可能仍然希望完全依赖开源和可定制性,构建一个涵盖所有技术栈的完全开源的系统。

本文将教你如何绕过 Shopify,仅使用开源工具,通过 Remix 搭建一个功能齐全的电子商务平台。三赢!

图片描述

工作原理

首先,我们将结合两个最流行的开源项目的功能来取代 Shopify 的功能:

  • Medusa是领先的开源电子商务平台,面向开发者,提供高度可定制性,并使用 Javascript 构建。
  • Strapi是领先的开源内容管理系统,专为开发者打造,同样具有高度可定制性,并且是用 Javascript 构建的。

在本教程中,您将学习如何使用 Remix 和 Medusa & Strapi 构建电子商务店面,从而避免使用完整的 Shopify 后端。

您可以在此代码库中找到本文的源代码。以下是最终结果的预览:

图片描述

先决条件

设置 Strapi

安装模板

npx create-strapi-app strapi-medusa --template shahednasser/strapi-medusa-template
Enter fullscreen mode Exit fullscreen mode

这会strapi-medusa在您的项目中创建一个名为 `<path_to_strapi_dashboard_name>` 的文件夹。安装完成后,Strapi 开发服务器将在 `<port_port_name>` 端口启动localhost:1337。同时,您的默认浏览器会打开一个新页面,供您创建新的管理员用户并登录。登录后,您即可访问 Strapi 控制面板。

图片描述

更改用户的授权设置

您的 Medusa 服务器需要 Strapi 用户的凭据才能向 Strapi 导入模拟数据。要创建新用户,请转到内容管理器,然后在“集合类型”下选择“用户”。

图片描述

点击右上角的“创建新条目”按钮。这将打开一个新表单,用于输入用户详细信息。

图片描述

请输入用户的用户名、邮箱地址和密码。完成后,点击右上角的“保存”按钮。

接下来,转到“设置”→“角色”→“已验证”,选择所有权限,然后点击“保存”。

图片描述

设置美杜莎

要启动 Medusa 服务器,请运行以下命令:

medusa new medusa-server --seed
Enter fullscreen mode Exit fullscreen mode

--seed标志会创建一个 SQLite 数据库,并用一些演示数据填充它。

切换到该medusa-server目录并转到medusa.config.js. 修改导出的对象末尾以启用 Redis:

module.exports = {
  projectConfig: {
    redis_url: REDIS_URL,
    //...
  }
  //...
};
Enter fullscreen mode Exit fullscreen mode

默认的 Redis 连接字符串是,redis://localhost:6379但如果您对其进行了更改,请转到该.env文件并添加以下内容:

REDIS_URL=<YOUR_REDIS_URL>
Enter fullscreen mode Exit fullscreen mode

<YOUR_REDIS_URL>你的连接字符串在哪里?

此外,由于 Remix 店面运行在 上localhost:3000,因此您必须添加一个环境变量STORE_CORS来设置店面的 URL。

添加以下内容.env

STORE_CORS=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

安装 Strapi 插件

要安装 Strapi 插件,请在 Medusa 服务器目录中运行以下命令:

yarn add medusa-plugin-strapi
Enter fullscreen mode Exit fullscreen mode

然后,添加以下环境变量:

STRAPI_USER=<STRAPI_IDENTIFIER>
STRAPI_PASSWORD=<STRAPI_PASSWORD>
STRAPI_PROTOCOL=http
STRAPI_URL=<STRAPI_URL> # Optional
STRAPI_PORT=<STRAPI_PORT> # Optional
Enter fullscreen mode Exit fullscreen mode

在哪里:

  • <STRAPI_IDENTIFIER>是您在上一步中创建的用户的电子邮件地址或用户名。
  • <STRAPI_PASSWORD>是您在上一步中创建的用户的密码。
  • <STRAPI_PROTOCOL>是您的 Strapi 服务器的协议。由于您使用的是本地 Strapi 服务器,请将其设置为http。默认值为https
  • <STRAPI_URL>是您的 Strapi 服务器的 URL。默认情况下,URL 为localhost
  • <STRAPI_PORT>是 Strapi 服务器运行的端口。默认情况下,端口为1337

最后,打开数组medusa-config.js并向其中添加以下新项plugins

const plugins = [
  //...
  {
    resolve: `medusa-plugin-strapi`,
    options: {
      strapi_medusa_user: process.env.STRAPI_USER,
      strapi_medusa_password: process.env.STRAPI_PASSWORD,
      strapi_url: process.env.STRAPI_URL, //optional
      strapi_port: process.env.STRAPI_PORT, //optional
      strapi_protocol: process.env.STRAPI_PROTOCOL //optional
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

测试集成

请确保 Strapi 服务器仍在运行。如果未运行,您可以在 Strapi 项目目录中运行以下命令来启动 Strapi 服务器:

yarn develop
Enter fullscreen mode Exit fullscreen mode

请确保您的 Redis 服务器也已启动并运行。

然后,在 Medusa 服务器的目录中,运行以下命令启动 Medusa 服务器:

yarn start
Enter fullscreen mode Exit fullscreen mode

这将启动您的 Medusa 服务器localhost:9000。您会看到一些product.created事件以及类似事件已被触发。

图片描述

这将使用您提供的演示产品更新 Strapi。

图片描述

在 Strapi 中添加 CMS 页面

现在您将使用 Strapi 来管理您店铺首页的内容。完成此操作后,您将能够通过 Strapi 控制三项内容:店铺顶部显示的标题文字;标题文字下方的副标题;以及首页上显示的产品列表。

在 Strapi 控制面板中,转到“插件”下的“内容类型构建器”。您可以在这里定义内容的模型/模式。

在“单一类型”下,点击“创建新的单一类型”。

图片描述

输入显示名称“首页”(如果您使用了其他名称,稍后将需要使用相应的 API ID),然后点击继续。

图片描述

接下来,选择组件字段,并将其显示名称设置为“Hero Text”,同时指定一个类别首页(点击类别下的“创建首页”)。然后,点击配置组件。

图片描述

hero_text然后在下一步中给它命名,然后单击“完成”。

图片描述

转到“组件”中的“首页”下的“英雄文本”组件,并创建三个名为“文本字段” start_text、“文本字段名称mid_text”和“文本字段名称”的文本字段end_text

图片描述

这里添加了三个文本字段,因为稍后在文章中,您将给它们添加特殊的下划线以mid_text突出显示。

返回“单一类型”下的“首页”类型,并为“产品”添加一个关联字段。关联关系应为“首页包含多个产品”。为其指定一个字段名称products_list

图片描述

最后,添加一个文本字段heading_2。保存首页内容类型的更改。

这是您的首页内容类型应该呈现的样子:

图片描述

接下来,转到“设置”→“用户和权限插件”→“角色”→“公开”,并为首页和产品类型启用“查找”权限。点击保存。

图片描述

现在,前往内容管理器,在“首页”下方添加您的主标题文字,并在右侧的“关联”部分添加您希望显示的产品。点击保存,然后发布。

图片描述

设置 Remix 商店页面

在本节中,您将使用 Remix 设置电子商务商店。

Remix 提供了三个官方预置模板供您根据需求使用,但您也可以从基本模板开始,或者创建自己的模板。

设置混音

要设置 Remix 应用(请在与medusa-server和不同的目录中执行此操作strapi-medusa),请运行以下命令:

npx create-remix@latest my-storefront
Enter fullscreen mode Exit fullscreen mode

它会问你几个问题。选择Just the basics,然后选择你偏好的托管平台(如果你不确定,可以选择 Remix App Server),选择 typescript,npm install如果你想使用,则选择 no yarn

然后,切换到该my-storefront目录并使用 yarn 安装依赖项:

cd my-storefront
yarn install
Enter fullscreen mode Exit fullscreen mode

配置 Tailwind CSS

安装 Tailwind CSS 来设计 UI 元素:

yarn add -D tailwindcss postcss autoprefixer concurrently
Enter fullscreen mode Exit fullscreen mode

运行程序npx tailwindcss init创建tailwind.config.js文件。然后,将其内容设置为以下内容:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

另外,请修改 package.json 文件中的 scripts:

{
  "scripts": {
    "build": "npm run build:css && remix build",
    "build:css": "tailwindcss -m -i ./styles/app.css -o app/styles/app.css",
    "dev": "concurrently \"npm run dev:css\" \"remix dev\"",
    "dev:css": "tailwindcss -w -i ./styles/app.css -o app/styles/app.css"
  }
}
Enter fullscreen mode Exit fullscreen mode

styles/app.css然后,创建包含以下内容的文件:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

最后,将以下内容添加到app/root.tsx导入列表之后:

import styles from "./styles/app.css"

export function links() {
  return [{ rel: "stylesheet", href: styles }]
}
Enter fullscreen mode Exit fullscreen mode

现在您可以在应用中使用 Tailwind CSS 了。

将 Storefront 连接到 Medusa 服务器

完成这些步骤后,我们就可以将您的店铺连接到您的 Medusa 服务器了。

首先,您需要使用以下命令安装一些软件包:

yarn add medusa-react react-query @medusajs/medusa
Enter fullscreen mode Exit fullscreen mode

medusa-react库使用react-query作为服务器端状态管理解决方案,并将该库列为对等依赖项。

要使用 `<Medusa>` 提供的钩子medusa-react,您需要MedusaProvider在组件树的某个位置引入 `<Medusa>`。`<Medusa>`MedusaProvider接受一个baseUrl指向您的 Medusa 服务器的 prop。其底层medusa-react使用medusa-js客户端库(基于 `<Medusa>` 构建axios)与您的服务器交互。

此外,由于 medusa-react 是基于 react-query 构建的,因此您可以传递一个表示 react-query 的QueryClientProvider属性的对象,该对象MedusaProvider将被传递。

您还需要将您的应用程序包装在一个 <app> 中,CartProvider因为这样您就可以使用 Medusa 提供的购物车功能,您稍后会进行此操作。

创建一个文件app/lib/config.ts。该文件将包含您的medusaClient配置信息,以便您可以在应用程序中使用 Medusa 的 Javascript 客户端。

import Medusa from '@medusajs/medusa-js';
import { QueryClient } from 'react-query';

const MEDUSA_BACKEND_URL = 'http://localhost:9000';

const STRAPI_API_URL = 'http://127.0.0.1:1337/api';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
      staleTime: 1000 * 60 * 60 * 24,
      retry: 1,
    },
  },
});

const medusaClient = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 });

export { MEDUSA_BACKEND_URL, STRAPI_API_URL, queryClient, medusaClient };
Enter fullscreen mode Exit fullscreen mode

现在转到您的目录app/root.tsx并导入所需的软件包:

import { MedusaProvider, CartProvider } from 'medusa-react';
import { MEDUSA_BACKEND_URL, queryClient } from './lib/config';
Enter fullscreen mode Exit fullscreen mode

您也可以在此处编辑meta以更改您的元数据

export const meta: MetaFunction = () => ({
  charset: 'utf-8',
  title: 'New Remix App',
  viewport: 'width=device-width,initial-scale=1',
});
Enter fullscreen mode Exit fullscreen mode

下面您将看到该App组件。在返回的 JSX 中,添加MedusaProvider一些CartProvider基本样式body

return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body className="bg-black text-slate-400 overflow-x-hidden justify-center flex">
        <MedusaProvider
          queryClientProviderProps={{ client: queryClient }}
          baseUrl={MEDUSA_BACKEND_URL}
        >
                    <CartProvider>
              <Outlet />
              <ScrollRestoration />
              <Scripts />
              <LiveReload />
                    </CartProvider>
        </MedusaProvider>
      </body>
    </html>
  );
Enter fullscreen mode Exit fullscreen mode

Strapi 的主页显示

您的首页数据可在 Strapi 端点获取:(localhost:1337/api/home-page添加?populate=*参数可同时显示嵌套产品)。它返回一个包含 `<include>` 和data`<include>`meta数组的对象。您无需关心 `<include>` meta,真正需要关注的是 `<include>` data,它包含了您在 Strapi 控制面板中输入的所有内容。

app/types/StrapiResponse.ts首先,创建包含以下内容的文件:

// StrapiResponse.ts

export type StrapiResponseType = {
  data: {
    id: number;
    attributes: {
      createdAt: Date;
      updatedAt: Date;
      publishedAt: Date;
      hero_text: {
        id: number;
        start_text: string;
        mid_text: string;
        end_text: string;
      };
      products_list: {
        data: Array<StrapiProductData>;
      };
      heading_2: string;
    };
  };
  meta: {};
};

export type StrapiProductData = {
  id: number;
  attributes: {
    medusa_id: string;
    title: string;
    subtitle: string | null;
    description: string;
    handle: string;
    is_giftcard: boolean;
    status: 'draft' | 'proposed' | 'published' | 'rejected';
    thumbnail: string;
    discountable: boolean;
    weight: number;
    product_length: null;
    width: null;
    height: null;
    hs_code: null;
    origin_country: null;
    mid_code: null;
    material: string | null;
    createdAt: Date;
    updatedAt: Date;
  };
};
Enter fullscreen mode Exit fullscreen mode

这是 Strapi API 返回数据的格式。

接下来,创建一个实用函数,用于从 Strapi API 获取内容。创建一个app/models/home.server.ts包含以下内容的文件:

// home.server.ts

import { STRAPI_API_URL } from "~/lib/config";
import type {
  StrapiProductData,
  StrapiResponseType,
} from "~/types/StrapiResponse";

export const getHomePageData = async () => {
  const homePage: StrapiResponseType = await (
    await fetch(`${STRAPI_API_URL}/home-page?populate=*`)
  ).json();

  const { data } = homePage;

  const { attributes } = data;

  const heroText = attributes.hero_text;
  const products = attributes.products_list.data;
  const smallHeading = attributes.heading_2;

  const homePageData = { heroText, products, smallHeading };
  return homePageData;
};

export type homePageDataType = {
  heroText: {
    id: number;
    start_text: string;
    mid_text: string;
    end_text: string;
  };
  products: StrapiProductData[];
  smallHeading: string;
};
Enter fullscreen mode Exit fullscreen mode

在这个getHomePageData函数中,你应该只返回首页需要的数据。

在上面的代码示例中,你会注意到使用了 import 语句,这是因为它是 Remix 中默认设置的目录~的别名,如果你愿意,可以随时更改它。apptsconfig

目录下的所有文件app/routes都将构成一条路由。例如,目录app/routes/store.tsx将包含/store路由。

接下来,app/routes/index.tsx创建一个加载器函数:

import { getHomePageData, homePageDataType } from '~/models/home.server';

export const loader = async () => {
  const homePageData = await getHomePageData();

  return homePageData;
};
Enter fullscreen mode Exit fullscreen mode

要使用从加载器函数收到的响应,您需要在组件useLoaderData内部使用 Remix 的 hook Index

import { useLoaderData } from '@remix-run/react';

export default function Index() {
  const { heroText, products, smallHeading } =
    useLoaderData<homePageDataType>();

    ...
}
Enter fullscreen mode Exit fullscreen mode

这里,homePageData它被解构并引入useLoaderData,现在你可以在你的页面上使用它了。

然后,将返回的 JSX 更改为以下内容:

export default function Index() {
    //...

  return (
    <div className="px-10 sm:px-20 md:px-44 pt-44 max-w-[100rem] flex-grow w-screen">
      {/* Hero Section */}
      <div>
        <h1 className="text-[2.5rem] sm:text-5xl lg:text-6xl xl:text-8xl relative font-medium lg:leading-[1.15] xl:leading-[1.2]">
          {heroText.start_text}{' '}
          {heroText.mid_text.split(' ').map((text) => (
            <span key={text} className="text-gray-50">
              <span className="relative">
                {text}
                <div className="h-1 bg-emerald-200 w-full absolute bottom-0 left-0 inline-block" />
              </span>{' '}
            </span>
          ))}
          {heroText.end_text}
        </h1>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

heroText.start_textstart_text它会从您在 Strapi 中创建的组件下的文本字段中提取数据hero_text。同样,` from`heroText.mid_text和`from` 分别heroText.end_text是 Strapi 中的 `from`mid_text和 ` end_textfrom` 字段。

然后,mid_text为了使每个单词都有统一的下划线(如果有多个单词),它被拆分了,稍后您将在主页 UI 中看到这种情况。

app/components/productCard.tsx要展示您的产品,请创建包含以下内容的文件:

import { Link } from '@remix-run/react';

interface ProductCardType {
  image: string;
  title: string;
  handle: string;
}

export default function ProductCard({ image, title, handle }: ProductCardType) {
  return (
    <Link to={`/products/${handle}`}>
      <div className="flex flex-col space-y-1 p-2 hover:bg-slate-400 hover:bg-opacity-25 cursor-pointer active:scale-95 transition ease-in-out duration-75">
        <img src={image} alt="" />
        <h3 className="pt-2 text-white text-xl">{title}</h3>
      </div>
    </Link>
  );
}
Enter fullscreen mode Exit fullscreen mode

此元素Link来自 Remix,可帮助您跳转到产品页面。Medusa 产品中提供的 handle 属性将用作 slug。

现在回到你的页面app/routes/index.tsx,你需要将 Strapi 的响应(产品)映射到该页面。

在你的英雄部分下方执行此操作:

import ProductCard from '~/components/productCard';

export default function Index() {
    ...

    return (
        <div className="px-10 sm:px-20 md:px-44 pt-44 max-w-[100rem] flex-grow w-screen">
            ...

            <div className="flex flex-col items-center pt-40 pb-44">
            <h2 className="text-2xl sm:text-3xl lg:text-4xl pb-10 text-white">
              {smallHeading}
          </h2>
          <div className="grid grid-cols-2 xl:grid-cols-4 gap-x-6">
              {products.map(({ attributes }) => (
                <ProductCard
                  key={attributes.medusa_id}
                image={attributes.thumbnail}
                handle={attributes.handle}
                title={attributes.title}
              />
            ))}
          </div>
          </div>
        </div>
)
Enter fullscreen mode Exit fullscreen mode

测试主页

要测试您的主页,请启动您的 Remix 开发服务器yarn dev(确保您的 Medusa 和 Strapi 服务器已经运行)。

您的应用已准备就绪localhost:3000,界面如下所示:

图片描述

使用 Medusa 实现添加到购物车功能

要将产品添加到购物车,首先需要将购物车与客户关联起来。为此,您可以为您的应用程序创建一个包装器,该包装器会检查购物车是否已初始化或需要创建,并执行必要的操作。

app/components/outletContainer.tsx创建包含以下内容的文件:

import { useCart } from 'medusa-react';
import { ReactNode, useEffect } from 'react';

import { medusaClient } from '~/lib/config';

interface OutletContainerType {
  children: ReactNode;
}

export default function OutletContainer({ children }: OutletContainerType) {
  const { setCart } = useCart();

  useEffect(() => {
    const localCartId = localStorage.getItem('cart_id');
    localCartId
      ? medusaClient.carts.retrieve(localCartId).then(({ cart }) => {
          setCart(cart);
        })
      : medusaClient.carts.create().then(({ cart }) => {
          localStorage.setItem('cart_id', cart.id);
          setCart(cart);
        });
  }, []);

  return <div>{children}</div>;
}
Enter fullscreen mode Exit fullscreen mode

你正在使用medusa-react`s`useCart钩子,setCart它会将你的购物车设置到全局位置。之后你可以在应用的任何位置使用它。它还outletContainer会将购物车保存到本地存储中,以便即使用户下次访问,添加的商品也会保留。

您还需要在商品添加到购物车时显示弹出式通知。请安装以下插件react-hot-toast来实现此功能:

yarn add react-hot-toast
Enter fullscreen mode Exit fullscreen mode

现在,回到你的代码app/root.tsx,用 `<script>` 标签包裹你的<Outlet />代码OutletContainer。此外,添加`<script>`<Toaster />标签react-hot-toast,这将允许你显示通知:

import OutletContainer from './components/outletContainer';
import { Toaster } from 'react-hot-toast';

export default function App() {
  return (
    ...
        <CartProvider>
          <OutletContainer>
            <Outlet />
        </OutletContainer>
        ...
                <Toaster />
      </CartProvider>
      ...
  );
}
Enter fullscreen mode Exit fullscreen mode

创建产品页面

在本节中,您将创建一个产品页面。部署到生产环境时,您无法为每个产品创建单独的页面,因此您需要创建一个动态页面,该页面将根据产品的版本号运行handle。在 Remix 中,您将动态页面命名为 `<div>` $slug.tsx

你需要handle从页面的 URL 中获取信息,你可以使用加载函数来实现,但使用useParams钩子函数会更简单。

app/routes/products/$slug.tsx创建包含以下内容的文件:

import { useParams } from '@remix-run/react';
import { useCart, useCreateLineItem, useProducts } from 'medusa-react';

export default function ProductSlug() {
  const { slug } = useParams();
}
Enter fullscreen mode Exit fullscreen mode

slug是从您的 URL 获取页面的 slug,例如,localhost:3000/products/sweatshirtslug 是(记住您在组件中sweatshirt传递了)。handleProductCard

接下来,使用useProducts钩子从 Medusa 获取您的产品并将其添加到 UI 中:

export default function ProductSlug() {
  ...

  const { products } = useProducts(
    {
      handle: slug,
    },
    {}
  );

if (!products) {
    return <div></div>; // you can use skeleton loader here instead.
  }

const product = products[0];

return (
  <div className="flex flex-col items-center lg:justify-between lg:flex-row px-10 sm:px-20 md:px-44 pt-44 max-w-[100rem] flex-grow w-screen">
    <img src={product.thumbnail!} className="h-96 w-auto" />
    <div>
      <h1 className="text-4xl pb-10 text-white">{product.title}</h1>
      <p className="w-72">{product.description}</p>
    </div>
  </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

这里useProducts使用了钩子函数并传递了别名。产品加载时,显示一个空的div(您也可以使用骨架加载器)。

最后,使用钩子返回的第一个项目,useProducts即页面 URL 中包含句柄的产品。

请注意,此处使用的标题和描述数据来自 Medusa 服务器,因为 Strapi 插件支持双向同步。因此,您在 Strapi 中对产品进行的任何更改都会同步到 Medusa 服务器。您也可以选择显示来自 Strapi 的 CMS 产品数据。

您还需要根据客户所在地区显示价格。为此,请创建以下文件app/lib/formatPrice.ts

import { formatVariantPrice } from 'medusa-react';
import type { Cart } from "medusa-react/dist/types";
import type { ProductVariant } from '@medusajs/medusa';

export const formatPrice = (variant: ProductVariant, cart: Cart) => {
  if (cart)
    return formatVariantPrice({
      variant: variant,
      region: cart.region,
    });
};
Enter fullscreen mode Exit fullscreen mode

formatVariantPrice您可以在此处使用该函数medusa-react。这将根据用户所在地区和所选产品变体对价格进行格式化。

然后,在以下情况下使用它app/routes/products/$slug.tsx

import { formatPrice } from '~/lib/formatPrice';

export default ProductSlug() {
    ...

    const { cart } = useCart();

    return (
        <div className="flex flex-col items-center lg:justify-between lg:flex-row px-10 pb-44 sm:px-20 md:px-44 pt-44 max-w-[100rem] flex-grow w-screen">
      <img src={product.thumbnail!} className="h-96 w-auto" />
      <div>
        <h1 className="text-4xl pt-5 lg:pt-0 pb-5 lg:pb-10 text-white">
          {product.title}
        </h1>
        <p className="w-72">{product.description}</p>
        <p className="text-xl text-white pt-5">
          {formatPrice(product.variants[0], cart)}
        </p>
      </div>
    </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

接下来,创建一个功能,用于将商品添加到购物车并推送通知。

import toast from 'react-hot-toast';

export default function ProductSlug() {
    ... 

  const { mutate } = useCreateLineItem(cart?.id!);

  const addItem = () => {
    mutate(
      {
        variant_id: products?.slice(0, 1)[0].variants[0].id!,
        quantity: 1,
      },
      {
        onSuccess: () => {
          toast('Added to Cart!');
        },
      }
    );
  };

    ...
}
Enter fullscreen mode Exit fullscreen mode

useCreateLineItem钩子函数允许您添加商品。它需要购物车 ID。该addItem函数会将商品添加到购物车,然后显示一条弹出通知。

在返回的 JSX 中添加一个按钮,点击该按钮即可运行此函数:

export default function ProductSlug() {
  ...

  return (
    <div className="flex flex-col items-center lg:justify-between lg:flex-row px-10 sm:px-20 md:px-44 pt-44 max-w-[100rem] flex-grow w-screen">
      <img src={product.thumbnail!} className="h-96 w-auto" />
      <div>
        <h1 className="text-4xl pb-10 text-white">{product.title}</h1>
        <p className="w-72">{product.description}</p>
                <p className="text-xl text-white pt-5">
          {formatPrice(product.variants[0])}
        </p>
        <button
          className="p-5 rounded-md w-full bg-slate-400 bg-opacity-25 mt-10 cursor-pointer active:scale-95 transition ease-in-out duration-75"
          onClick={() => addItem()}
        >
          Add item
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

最后一步是添加导航栏,以便轻松导航至购物车。

app/components/topNavigator.tsx创建包含以下内容的文件:

import { Link } from '@remix-run/react';

export default function TopNavigator() {
  return (
    <nav className="flex w-screen fixed top-0 right-0 left-0 items-center py-4 flex-row justify-between px-10 sm:px-20 md:px-44 z-10 bg-black">
      <Link to="/" className="text-xl">
        MRS
      </Link>
      <Link to="/cart">Cart</Link>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

将此组件添加TopNavigator到您的代码中,使其显示在所有页面上。将其添加到`:`root.tsx标签上方。Outlet

import TopNavigator from './components/topNavigator';

export default function App() {
  return (
    ...
        <CartProvider>
          <OutletContainer>
                    <TopNavigator />
            <Outlet />
        </OutletContainer>
        ...
      </CartProvider>
      ...
  );
}
Enter fullscreen mode Exit fullscreen mode

测试产品页面

要测试您的产品页面,请重新启动您的 Remix 服务器(确保 Strapi 和 Medusa 服务器已在运行)。

点击主页上的任何产品,即可查看详细信息。

图片描述

创建购物车页面

现在,您将创建最终的购物车页面。

app/routes/cart.tsx创建包含以下内容的文件:

import { useState, useEffect } from "react";
import { medusaClient } from "~/lib/config";

import type { Cart as CartType } from "medusa-react/dist/types";

export default function Cart() {
  const [cart, setCart] = useState<CartType>();

  useEffect(() => {
    medusaClient.carts
      .retrieve(localStorage.getItem("cart_id")!)
      .then(({ cart }) => {
        setCart(cart);
      });
  }, [cart]);

  return (
    <div className="px-10 sm:px-20 md:px-44 pt-44 max-w-[100rem] flex-grow w-screen">
      {cart?.items.map((variant) => (
        <div
          key={variant.id}
          className="flex flex-col xl:flex-row h-64 my-10 space-x-8 space-y-4 items-center"
        >
          <img className="h-full" src={variant.thumbnail!} />
          <div>
            <h3 className="pt-2 text-white text-xl">{variant.title}</h3>
            <p className="text-slate-400">{variant.quantity}</p>
          </div>
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

cart.items是一个包含顾客购物车中所有商品的数组。您需要显示每个商品的缩略图、标题和数量。

测试购物车页面

重启 Remix 服务器(确保 Strapi 和 Medusa 服务器已在运行)。将商品添加到购物车后,它将显示在购物车页面上。

图片描述

结论

通过本教程,您可以了解使用完全开源的工具构建电子商务商店是多么容易。

还有很多方法可以改进您的店铺页面,例如:

如果您对 Medusa 有任何疑问或问题,请随时通过Discord联系 Medusa 团队 

文章来源:https://dev.to/medusajs/remix-shopify-circumvent-shopifys-apis-and-go-open-source-36g2