使用Nextjs14和shopify store front api创建自定义在线商城(二)

接上文使用 Nextjs14 和 shopify store front api 创建自定义在线商城,继续 Nextjs 搭建 shopify 商城网站内容。在上一篇文章中,我们已经配置好了 shopify 的 api,在这篇文章中,我们实现从 api 获取数据,渲染到页面并实现一个基本的购物流程。

一.服务器查询商品列表{#查询语句}

我们先到 Shopify GraphiQL App 后台来查询商品列表,查询商品的 id,名称,详情,价格,商品图片字段,handle,这些都是一些常用的字段, 其中 handle 是一个可读的字符串,用于构建用户友好的 URL,后期如果需要更多字段,我们再添加。查询前 6 个商品,查询语句是这样的。

shell 复制代码
query queryProduct {
  products(first: 6) {
    edges {
      node {
        id
        title
        description
        tags
        handle
        priceRange {
          minVariantPrice{
            amount
            currencyCode
          }
        }
        images(first: 2) {
          edges {
            node {
              url
              altText
            }
          }
        }
      }
    }
  }
}

查询结果如下:

如果不知道 GraphQL 查询语句的话,可以查看文档或者使用 chatgpt 提问,对于这种有明确要求,很具体的问题,chatgpt 是非常好用的。

二.创建一个工具函数

因为我们需要很多查询,为了方便后续多个查询,封装一个工具函数,在src/lib/路径创建一个 index.ts 文件,里面定义一个通用的封装函数。

ts 复制代码
export async function shopifyFetch<T>(
  query: string,
  variables?: Record<string, any>
): Promise<{ status: number; body?: T; error?: string }> {
  try {
    // 读取环境变量
    const apiUrl = process.env.NEXT_PUBLIC_API_URL;
    const accessToken = process.env.NEXT_PUBLIC_ACCESS_TOKEN;

    if (!apiUrl || !accessToken) {
      throw new Error('API URL or Access Token is not configured.');
    }
    //   发送请求
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Storefront-Access-Token': accessToken,
      },
      body: JSON.stringify({ query, variables }),
    });

    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }

    const responseBody: T = await response.json();
    // 返回结果
    return {
      status: response.status,
      body: responseBody,
    };
  } catch (error) {
    // 调试错误处理 部署时删除
    console.error('Error fetching data:', (error as Error).message);
    return {
      status: 500,
      error: (error as Error).message || 'Error receiving data',
    };
  }
}

请求参数包括 GraphQL 查询语句和可选的变量,返回请求状态,响应体或者错误。

三.创建请求商品列表函数

使用封装函数,创建具体的功能函数。

1.首先创建数据类型

然后在 src 目录下创建一个 types 文件夹,里面创建一个 index.ts 文件,添加一下数据类型, 可以把在 shopify 后台查询到的结果数据给 chatgpt,让他总结出数据类型。

ts 复制代码
// 商品列表shopify结构
export type IProductList = {
  data: {
    products: {
      edges: Array<{
        node: Product;
      }>;
    };
  };
};

// 商品列表响应对象
export type IProductListResponst = {
  status: number;
  body?: IProductList;
  error?: string;
};
type Price = {
  amount: number;
  currencyCode: string;
};
//  shopify商品数据结构
export type Product = {
  id: string;
  title: string;
  description: string;
  tags: string[];
  handle: string;
  priceRange: {
    minVariantPrice: Price;
  };
  images: {
    edges: Array<{
      node: Image;
    }>;
  };
};
// 商品图片
type Image = {
  url: string;
  altText?: string | null;
};

2.定义查询商品列表函数

src/lib/index.ts 创建一个查询商品列表的函数.

ts 复制代码
export async function fetchProductList(): Promise<IProductListResponst> {
  const productsListQuery = gql`
    query {
      products(first: 6) {
        edges {
          node {
            id
            title
            description
            tags
            handle
            priceRange {
              minVariantPrice {
                amount
                currencyCode
              }
            }
            images(first: 1) {
              edges {
                node {
                  url
                  altText
                }
              }
            }
          }
        }
      }
    }
  `;
  // 使用刚刚定义的函数发送请求
  const result = await shopifyFetch<IProductList>(productsListQuery);
  if (result.status === 200) {
    return { status: 200, body: result.body };
  } else {
    // 调试使用
    console.error('Failed to fetch product list:', result.error);
    return {
      status: 500,
      error: result.error || 'Failed to fetch product list.',
    };
  }
}

其中 productsListQuery 即为我们在后台(第一步)的查询语句

四.把数据渲染到前端

1.测试获取数据状态

app/pages.tsx 页面我们使用 fetchProductList 函数来获取数据,首先测试一下数据获取情况。

ts 复制代码
const result = await fetchProductList();

const productList = result.body?.data.products.edges;
//  先测试一下
console.log(productList);

因为这个是服务器端请求,所以查看你的终端而不是浏览器控制台,正常应该如下所示。

如果请求错误,除了检查代码的问题外,还要检查的你的网络代理

2.把数据渲染到页面中{#渲染商品列表}

成功获取到数据后,现在我们可以把数据渲染到页面中了。

tsx 复制代码
{
  productList?.map((product) => (
    <Link
      key={product.node.handle}
      href={`/products/${product.node.handle}`}
      className='group'
    >
      <div className='aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg bg-gray-200 xl:aspect-h-8 xl:aspect-w-7'>
        <Image
          width={200}
          height={200}
          alt={'product.node.images.edges[0].node.altText'}
          src={product.node.images.edges[0].node.url}
          className='h-full w-full object-cover object-center group-hover:opacity-75'
        />
      </div>
      <h3 className='mt-4 text-sm text-gray-700'>{product.node.title}</h3>
      <p className='mt-1 text-lg font-medium text-gray-900'>
        {product.node.priceRange.minVariantPrice.currencyCode}
      </p>
      <p className='mt-1 text-lg font-medium text-gray-900'>
        {product.node.priceRange.minVariantPrice.amount}
      </p>
    </Link>
  ));
}

都是一些简单的数据渲染,其中图片我们以获取到的第一张商品图片为展示图。shopify 的数据结构很深,好在我们使用的是 ts,数据结构有代码提示,还是很方便的。

3.配置 nextjs 安全地址

为了保护您的应用程序免受恶意用户的攻击,nextjs 需要进行配置才能使用外部图像参考文档。 因此需要添加域名配置,在 next.config.mjs 中添加类似这样的配置。

ts 复制代码
    images: {
        remotePatterns: [
          {
            protocol: 'https',
            hostname: 'cdn.shopify.com',
            port: '',
            pathname: '/s/files/**',
          },
        ],
        dangerouslyAllowSVG: true,
      },

现在查看你的页面应该类似这样

4.简单优化价格展示

在 util 文件夹创建一个 formatPrice.ts 文件,里面创建一个货币格式化的函数。

ts 复制代码
export const formatPrice = (price: number) =>
    Intl.NumberFormat("zh-CN", {
      style: "currency",
      currency: "CNY",
      minimumFractionDigits: 2
    }).format(price);

修改货币展示的代码

ts 复制代码
<p className='mt-1 text-lg font-medium text-gray-900'>
  {formatPrice(product.node.priceRange.minVariantPrice.amount)}
</p>

这样我们的获取商品列表就完成了。

五.查询单一商品详情

1.构建查询语句

回到 GraphiQL 后台,查询商品详情,不同于商品列表,我们需要提供一个变量$handle

ts 复制代码
query singleProduct($handle: String!) {
  product(handle: $handle) {
    title
    description
    updatedAt
    tags
    priceRange {
      minVariantPrice {
        amount
      }
    }
    images(first: 1) {
      edges {
        node {
          url
          altText
        }
      }
    }
    variants(first: 1) {
      edges {
        node {
          id
        }
      }
    }
  }
}

在后台的Variables部分添加一下变量,然后点击查询,会显示某一个商品的详情信息

2.添加数据类型

根据上一步的查询结果定义一个商品详情数据类型。

ts 复制代码
//  单一商品数据结构
export type ISingleProduct = {
  data: {
    product: {
      title: string;
      description: string;
      updatedAt: string;
      tags: string[];
      priceRange: {
        minVariantPrice: Price;
      };
      images: {
        edges: Array<{
          node: Image;
        }>;
      };
      variants: {
        edges: Array<{
          node: {
            id: string;
          };
        }>;
      };
    };
  };
};

3.定义查询函数

现在把查询语句整理到函数中, 在 lib/index.ts 中添加函数

ts 复制代码
// 查询某一商品详情
export async function fetchSingleProduct(
  handle: string
): Promise<ISingleProductResponst> {
  const singleProduct = gql`
    query singleProduct($handle: String!) {
      product(handle: $handle) {
        title
        description
        updatedAt
        tags
        priceRange {
          minVariantPrice {
            amount
          }
        }
        images(first: 1) {
          edges {
            node {
              url
              altText
            }
          }
        }
        variants(first: 1) {
          edges {
            node {
              id
            }
          }
        }
      }
    }
  `;

  const result = await shopifyFetch<ISingleProduct>(singleProduct);

  if (result.status === 200) {
    return { status: 200, body: result.body as ISingleProduct };
  } else {
    console.error('Failed to fetch product detail:', result.error);
    return {
      status: 500,
      error: result.error || 'Failed to fetch product detail.',
    };
  }
}

四.渲染商品详情页面

1.创建商品详情页面

创建一个目录 src/pages/products,在里面添加一个文件[handle].tsx,添加一下商品详情,样式参考这个网站,复制这个样式,添加到 handle.tsx 页面。

2. 获取路由参数

在之前的商品列表页面中,我们使用这样的跳转链接。

tsx 复制代码
<Link
  key={product.node.handle}
  href={`/products/${product.node.handle}`}
  className='group'
></Link>

现在首先要获取传递过来的参数

ts 复制代码
const params = useParams<{ handle: string }>();

定义一个 singleProduct 变量来储存数据,使用 useState 管理状态。

tsx 复制代码
const [singleProduct, setSingleProduct] = useState<ISingleProduct | undefined>(
  undefined
);

然后在 useeffect 中发送请求

tsx 复制代码
useEffect(() => {
  const fetchData = async () => {
    if (params?.handle) {
      try {
        const response = await fetchSingleProduct(params.handle);
        const singleProduct = response.body;
        setSingleProduct(singleProduct);
      } catch (error) {
        console.error('Error fetching product:', error);
      }
    }
  };

  fetchData();
}, [params?.handle]);

console.log('singleProduct', singleProduct);

在浏览器控制台查看有这样的输出

你也可以使用react-query 来发送请求。

这样我们成功获取到了商品详情数据,然后继续把数据渲染到页面上

tsx 复制代码
<section className='text-gray-600 body-font overflow-hidden'>
  <div className='container px-5 py-24 mx-auto'>
    <div className='lg:w-4/5 mx-auto flex flex-wrap'>
      <div className='lg:w-1/2 w-full lg:pr-10 lg:py-6 mb-6 lg:mb-0'>
        <h2 className='text-sm title-font text-gray-500 tracking-widest'>
          BRAND NAME
        </h2>
        <h1 className='text-gray-900 text-3xl title-font font-medium mb-4'>
          {singleProduct?.data.product.title}
        </h1>
        <div className='flex mb-4'>
          <a className='flex-grow text-indigo-500 border-b-2 border-indigo-500 py-2 text-lg px-1'>
            Details
          </a>
        </div>
        <p className='leading-relaxed mb-4'>
          {singleProduct?.data.product.description}{' '}
        </p>
        <div className='flex border-t border-gray-200 py-2'>
          <span className='text-gray-500'>Color</span>
          <span className='ml-auto text-gray-900'>
            {singleProduct?.data.product.tags[0]}
          </span>
        </div>

        <div className='flex border-t border-b mb-6 border-gray-200 py-2'>
          <span className='text-gray-500'>Quantity</span>
          <span className='ml-auto text-gray-900'>1</span>
        </div>
        <div className='flex'>
          <span className='title-font font-medium text-2xl text-gray-900'>
            {singleProduct?.data.product.priceRange.minVariantPrice.amount}
          </span>
          <button
            onClick='{checkout}'
            className='flex ml-auto text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded'
          >
            马上购买
          </button>
        </div>
      </div>
      {imageUrl && (
        <image
          width='{400}'
          height='{400}'
          alt='ecommerce'
          className='lg:w-1/2 w-full lg:h-auto h-64 object-cover object-center rounded'
          src='{imageUrl}'
        />
      )}
    </div>
  </div>
</section>

完成后的商品详情页面

五、创建订单

重复上面的步骤,在后台创建创建订单的 GraphQL 查询语句.

1.创建查询语句

shell 复制代码
mutation createCart($lines: [CartLineInput!]!) {
  cartCreate(input: { lines: $lines }) {
    cart {
      id
      checkoutUrl
      lines(first: 5) {
        edges {
          node {
            merchandise {
              ... on ProductVariant {
                id
              }
            }
            quantity
          }
        }
      }
    }

  }
}

2.创建查询函数

lib/index.ts 中创建一个 createCart 函数

ts 复制代码
export async function createCart(
  lines: ILineCollection
): Promise<ICartCreateResponse> {
  const checkoutQuery = gql`
    mutation createCart($lines: [CartLineInput!]!) {
      cartCreate(input: { lines: $lines }) {
        cart {
          id
          checkoutUrl
          lines(first: 5) {
            edges {
              node {
                merchandise {
                  ... on ProductVariant {
                    id
                  }
                }
                quantity
              }
            }
          }
        }
      }
    }
  `;

  try {
    const result = await shopifyFetch(checkoutQuery, { lines: lines });

    if (result.body) {
      return { status: 200, body: result.body as ICartCreate };
    } else {
      throw new Error('Failed to fetch create cart.');
    }
  } catch (error) {
    // 调试用部署时删除
    console.error('Database Error:', error);
    throw new Error('Failed to fetch create cart.');
  }
}

3 使用查询函数

定义一个 lineCollections 数组,为了方便,简化 quantity 为 1。

tsx 复制代码
const lineCollections = [
  {
    merchandiseId: singleProduct?.data.product.variants.edges[0].node.id,
    quantity: 1,
  },
];

在商品详情页面中定义一个创建订单的 checkout 函数,这个函数会返回一个checkoutUrl,这个地址是shooify的真实支付页面。所以当请求成功后,跳转到这个页面进行支付。

ts 复制代码
// 创建订单
async function checkout() {
  const data = createCart(lineCollections);
  const checkoutUrl = (await data).body?.data.cartCreate.cart.checkoutUrl;
  if (checkoutUrl) {
    window.location.href = checkoutUrl;
  } else {
    console.error('Checkout URL not found');
  }
}

给 button 绑定创建订单的函数。

ts 复制代码
<button
  onClick={checkout}
  className='flex ml-auto text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded'
>
  马上购买
</button>

4.测试流程

在商品列表选择某一个商品,点击马上购买,如果你已经登陆 shopify 账户会跳转到 shopify 的支付页面,否则会跳转到登陆页面。

根据 Shopify 的政策,使用 Storefront API 构建的应用程序不能完全复制 Shopify 的所有功能。特别是支付环节,必须借助 Shopify 的支付系统来完成,这也确保了交易的安全性和合规性。 因此,我们完成了一个使用 Next.js 14 和 Shopify Storefront API 构建一个基本的在线商店. 你可以把他部署到vercel上面.部署到vercel上面:基本上是导入,修改环境变量,自动部署,鉴于这篇文章已经够长了,我这里就不赘述了。完成后类似这样的页面。

总结

本文通过使用 Next.js 14 和 Shopify Storefront API 构建了一个简单的电子商务应用程序。实际开发中,一些和商品属性密切关联的功能,需要根据具体商品属性和营销需求来综合考虑。同时,还可以针对样式美化和更详细的错误处理进行优化,以提升用户体验。 总的来说,本文为您提供了一个基本的 Next.js 14 和 Shopify 电子商务应用程序的构建过程。如果 什么疑问和错误,欢迎留言讨论,如果这篇文章对您有所帮助,欢迎分享给您的朋友和同事。

原文地址

相关推荐
Whbbit1999几秒前
hono 集成 Better Auth
javascript·后端
冰镇生鲜4 分钟前
Cursor 前端AI编程 最佳实践指南
前端·mcp·trae
三思而后行,慎承诺7 分钟前
babel核心知识点
前端
胖方Hale9 分钟前
06. Typescript 联合类型、交叉类型、类型断言
前端·typescript
袁煦丞11 分钟前
3个月甩掉10万张纸质文件!这个开源神器让我从「文件坟墓」里爬出来了
前端·程序员·远程工作
Mike_jia24 分钟前
Keep深度解析:开源AIOps告警中枢的实践革命
前端
Mintopia29 分钟前
计算机图形学进阶探索与实践
前端·javascript·计算机图形学
代码小学僧31 分钟前
团队协作必备!pnpm 版本管理与 corepack 使用指南
前端·node.js·团队管理