Heads up: Dieses Tutorial deckt eine frühere WooGraphQL-Version ab und ist nicht mehr auf dem neuesten Stand. Eine neu geschriebene Version ist in Arbeit – wir werden von hier aus darauf verlinken, sobald sie veröffentlicht wurde.
Inhaltsverzeichnis
- Einrichten von WordPress für Ihre Headless eCommerce-Anwendung
- Erstellen und Einrichten Ihrer eCommerce-App für die Entwicklung
- Erstellen von Shop Listing Pages mit WooGraphQL
- Erstellen von Benutzer-Login und Navigation
- Erstellen der Produktseite und Warenkorboptionen
Willkommen zum zweiten Teil unserer Tutorial-Serie zum Erstellen einer Headless-E-Commerce-Anwendung! In diesem nächsten Kapitel wird die E-Commerce-App so eingerichtet, dass sie mit Ihrem Shop verbunden ist. Wir werden Next.js für das Frontend nutzen und mehrere andere Tools wie shadcn/ui für Bauteile und GraphQL Code Generator für die Handhabung von GraphQL-Operationen.
Erstellen und Einrichten unserer E-Commerce-App für die Entwicklung
Nachdem Sie Ihren WooCommerce-Shop mit WooGraphQL eingerichtet haben, sollten Sie einen GraphQL-Endpunkt bei https://site-name/graphqlWenn Sie das gebündelte Entwicklungssetup aus dem letzten Kapitel verwenden, ist dies http://localhost:8080/wp/graphql. Lassen Sie uns damit beginnen.
Next.js-Anwendung erstellen
Beginnen wir mit der Erstellung unserer Next.js-Anwendung und dem Einrichten einiger notwendiger Konfigurationen
1. Generieren Sie zunächst eine neue Next.js-Anwendung, indem Sie den folgenden Befehl in Ihrem Terminal ausführen: npx create-next-app@latest und folgen Sie den Anweisungen und navigieren Sie zum neu erstellten Projektverzeichnis.
2. Dann installieren Sie die dotenv-flow und deepmerge npm-Pakete, indem Sie den folgenden Befehl in Ihrem Terminal ausführen: npm i dotenv-flow deepmerge
3. Erstellen Sie eine neue .env.local Datei im Stammverzeichnis Ihres Projekts und füllen Sie es mit den folgenden Details:
GRAPHQL_ENDPOINT=[GRAPHQL_ENDPOINT]
FRONTEND_URL=[NEXT_APP_URL]
BACKEND_URL=[WP_URL]
SITE_NAME=[SITE_NAME]
SITE_DESCRIPTION=[SITE_DESCRIPTION]Vergessen Sie nicht, die Klammerwerte durch Ihre jeweiligen Äquivalente zu ersetzen.
4. Erstellen Sie eine neue next.config.js Datei im Stammverzeichnis Ihres Projekts und fügen Sie folgenden Code hinzu:
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
dangerouslyAllowSVG: true,
formats: ['image/avif', 'image/webp'],
domains: ['localhost'],
minimumCacheTTL: 60,
disableStaticImages: true,
},
env: {
GRAPHQL_ENDPOINT: process.env.GRAPHQL_ENDPOINT,
FRONTEND_URL: process.env.FRONTEND_URL,
BACKEND_URL: process.env.BACKEND_URL,
SITE_NAME: process.env.SITE_NAME,
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
},
}
module.exports = nextConfig;5. Aktualisierung der content Eigentum in Ihrem tailwind.config.js Datei an:
...
content: [
'./ui/**/*.{ts,tsx}',
'./server/**/*.{ts,tsx}',
'./client/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
]
...Installieren shadcn/ui Komponenten
Wir werden die shadcn/ui Bibliothek für die UI-Komponenten unserer Anwendung. Hier ist, wie man es installiert und konfiguriert.
1. Führen Sie den Befehl shadcn-ui init aus: npx shadcn-ui@latest init Folgen Sie dann den Aufforderungen.
2. Öffnen Sie die components.json Datei, die generiert wurde und deren Inhalt durch Folgendes ersetzt:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "/",
"utils": "@/utils/ui"
}
}3. Umbenennung lib/utils.ts Datei /utils/ui.ts und schaffen eine /ui Verzeichnis.
4. Fügen Sie die erforderlichen Komponenten zu Ihrem Projekt hinzu, indem Sie die folgenden Befehle in Ihrem Terminal ausführen:
npx shadcn-ui@latest add aspect-ratio
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add form
npx shadcn-ui@latest add input
npx shadcn-ui@latest add label
npx shadcn-ui@latest add radio-group
npx shadcn-ui@latest add select
npx shadcn-ui@latest add sheet
npx shadcn-ui@latest add slider
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add toastErstellen anderer UI-Komponenten
Wir werden einige zusätzliche Komponenten für unsere Anwendung erstellen.
1. Erstellen Sie eine neue ui/Image/index.ts Datei und fügen Sie folgenden Code hinzu:
'use client';
export * from './Image';
2. Erstellen Sie eine neue ui/Image/Image.tsx Datei und fügen Sie folgenden Code hinzu:
import { useState } from 'react';
import NextImage from 'next/image';
import { cn } from '@/utils/ui';
import { LoadingSpinner } from '@/ui/LoadingSpinner';
import { AspectRatio } from "@/ui/aspect-ratio"
export type ImageProps = {
className?: string;
src: string;
sizes?: string;
width?: number;
height?: number;
ratio?: number;
alt: string;
style?: JSX.IntrinsicElements['img']['style']
fill?: boolean;
priority?: boolean;
}
export function Image(props: ImageProps) {
const [isLoading, setLoading] = useState(true);
const {
className = '',
src,
alt,
sizes,
width,
height,
ratio,
style,
fill = true,
priority,
} = props;
return (
<div
className={cn(
'overflow-hidden group relative',
className && className,
)}
style={style}
>
<AspectRatio ratio={ratio}>
{isLoading && (
<LoadingSpinner className="position absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"/>
)}
<NextImage
src={src}
alt={alt}
width={width as number}
height={height as number}
sizes={sizes}
className={cn(
'group-hover:opacity-75 object-cover',
'duration-700 ease-in-out',
isLoading
? 'grayscale blur-2xl scale-110'
: 'grayscale-0 blur-0 scale-100',
)}
fill={fill}
onLoadingComplete={() => setLoading(false)}
priority={priority}
/>
</AspectRatio>
</div>
);
}
3. Erstellen Sie eine neue ui/LoadingSpinner/index.ts Datei und fügen Sie folgenden Code hinzu:
export * from './LoadingSpinner';
4. Erstellen Sie eine neue ui/LoadingSpinner/LoadingSpinner.tsx Datei und fügen Sie folgenden Code hinzu:
export interface LoadingSpinnerProps {
className?: string;
noText?: boolean;
color?: string;
}
export function LoadingSpinner({ className = '', noText = false, color = 'amber-400' }: LoadingSpinnerProps) {
return (
<div aria-label="Loading..." role="status" className={`relative flex items-center justify-center space-x-2 ${className}`}>
<svg className="h-6 w-6 animate-spin stroke-amber-600" viewBox="0 0 256 256">
<line x1="128" y1="32" x2="128" y2="64" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="195.9"
y1="60.1"
x2="173.3"
y2="82.7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
<line x1="224" y1="128" x2="192" y2="128" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="195.9"
y1="195.9"
x2="173.3"
y2="173.3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
<line x1="128" y1="224" x2="128" y2="192" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="60.1"
y1="195.9"
x2="82.7"
y2="173.3"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
<line x1="32" y1="128" x2="64" y2="128" strokeLinecap="round" strokeLinejoin="round" strokeWidth="24"/>
<line
x1="60.1"
y1="60.1"
x2="82.7"
y2="82.7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="24"
/>
</svg>
{!noText && <span className="text-xs font-medium text-gray-500">Loading...</span>}
</div>
);
}
5. Erstellen Sie eine neue ui/NavLink/index.ts Datei und fügen Sie folgenden Code hinzu:
export * from './NavLink';
6. Erstellen Sie eine neue ui/NavLink/NavLink.tsx Datei und fügen Sie folgenden Code hinzu:
import { PropsWithChildren } from 'react';
import Link from 'next/link';
import { cn } from '@woographql/utils/ui';
export const linkClassName = 'transition-colors group-hover:text-blue-400';
export interface NavLinkProps {
href: string;
className?: string;
shallow?: boolean;
prefetch?: boolean;
}
export function NavLink(props: PropsWithChildren<NavLinkProps>) {
const {
children,
href,
className,
shallow,
prefetch,
} = props;
return (
<Link
className={cn(
className,
linkClassName,
)}
href={href}
shallow={shallow}
prefetch={prefetch}
>
{children}
</Link>
);
}
Installieren und Konfigurieren von GraphQL Code Generator
Schließlich müssen wir GraphQL Code Generator einrichten (Codegen) zum Erzeugen typisierter GraphQL-Operationen. Codegen ist echte Zeitersparnis und ein Muss, wenn man mit vielen Komponenten oder einem großen GraphQL-Schema arbeitet. Je nachdem, wie es konfiguriert ist, generiert es Code, der sich auf den bereitgestellten GraphQL-Endpunkt bezieht. In unserer Anwendung wird es verwendet, um TypeScript-Typen für den GraphQL-Endpunkt, TS-Typen für die GraphQL-Operationen, die wir schreiben, und einen ordentlichen Wrapper zu generieren, der das Ausführen dieser Operationen mit dem graphql-request Bibliothek.
1. Installieren Sie den GraphQL Code Generator zusammen mit einigen erforderlichen Plugins, indem Sie den folgenden Befehl in Ihrem Terminal ausführen:
npm i -D npm-run-all @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request2. Erstellen Sie eine neue codegen.ts Datei im Stammverzeichnis Ihres Projekts und fügen Sie folgenden Code hinzu:
import type { CodegenConfig } from '@graphql-codegen/cli';
import dotenv from 'dotenv-flow';
dotenv.config({ silent: true });
const config: CodegenConfig = {
schema: process.env.GRAPHQL_ENDPOINT,
documents: ['graphql/**/*.graphql'],
verbose: true,
overwrite: true,
generates: {
'graphql/generated.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-graphql-request',
],
config: {
namingConvention: 'keep',
}
}
}
}
export default config;
3. Aktualisierung der scripts Eigentum in Ihrem package.json Datei zum Einschließen von Skripten zum Erzeugen der GraphQL-Operationen und Wrapper:
{
...
"scripts": {
"next:dev": "next dev",
"next:build": "next build",
"start": "next start",
"lint": "next lint",
"codegen:dev": "graphql-codegen --config codegen.ts --watch",
"codegen:build": "graphql-codegen --config codegen.ts",
"dev": "run-p codegen:dev next:dev",
"build": "NODE_ENV=production run-s codegen:build next:build",
},
...
}
4. Erstellen Sie eine neue graphql/index.ts Datei und fügen Sie folgenden Code hinzu:
export * from './generated';
export * from './client';
5. Erstellen Sie eine neue graphql/main.graphql Datei und fügen Sie folgenden Code hinzu:
fragment MenuItemContent on MenuItem {
id
uri
title
label
cssClasses
}
fragment MenuItemRecursive on MenuItem {
...MenuItemContent
childItems {
nodes {
...MenuItemContent
}
}
}
fragment MenuContent on Menu {
id
name
locations
slug
menuItems(first: 20, where: {parentId: 0}) {
nodes {
...MenuItemRecursive
}
}
}
fragment CustomerContent on Customer {
id
sessionToken
firstName
shipping {
postcode
state
city
country
}
}
fragment ProductContentSlice on Product {
id
databaseId
name
slug
type
image {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
... on SimpleProduct {
price
regularPrice
soldIndividually
}
... on VariableProduct {
price
regularPrice
soldIndividually
}
}
fragment ProductVariationContentSlice on ProductVariation {
id
databaseId
name
slug
image {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
price
regularPrice
}
fragment ProductContentSmall on Product {
id
databaseId
slug
name
type
shortDescription(format: RAW)
image {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
productCategories(first: 20) {
nodes {
id
slug
name
}
}
productTags(first: 20) {
nodes {
id
slug
name
}
}
allPaColor(first: 100) {
nodes {
id
slug
name
}
}
... on SimpleProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
soldIndividually
}
... on VariableProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
soldIndividually
}
}
fragment ProductContentFull on Product {
id
databaseId
slug
name
type
description
shortDescription(format: RAW)
image {
id
sourceUrl
altText
}
galleryImages {
nodes {
id
sourceUrl(size:WOOCOMMERCE_THUMBNAIL)
altText
}
}
productTags(first: 20) {
nodes {
id
slug
name
}
}
attributes {
nodes {
id
attributeId
... on LocalProductAttribute {
name
label
options
variation
}
... on GlobalProductAttribute {
name
label
options
variation
slug
terms(first: 100) {
nodes {
id
name
slug
}
}
}
}
}
... on SimpleProduct {
onSale
stockStatus
price
rawPrice: price(format: RAW)
regularPrice
salePrice
stockStatus
stockQuantity
soldIndividually
defaultAttributes(first: 100) {
nodes {
id
attributeId
name
value
label
}
}
}
... on VariableProduct {
onSale
price
rawPrice: price(format: RAW)
regularPrice
salePrice
stockStatus
stockQuantity
soldIndividually
defaultAttributes(first: 100) {
nodes {
id
attributeId
label
name
value
}
}
variations(first: 50) {
nodes {
id
databaseId
name
price
rawPrice: price(format: RAW)
regularPrice
salePrice
onSale
attributes {
nodes {
name
label
value
}
}
image {
id
sourceUrl
altText
}
}
}
}
}
fragment VariationContent on ProductVariation {
id
name
slug
price
regularPrice
salePrice
stockStatus
stockQuantity
onSale
image {
id
sourceUrl
altText
}
}
fragment CartItemContent on CartItem {
key
product {
node {
...ProductContentSlice
}
}
variation {
attributes {
id
label
name
value
}
node {
...ProductVariationContentSlice
}
}
quantity
total
subtotal
subtotalTax
extraData {
key
value
}
}
fragment CartContent on Cart {
contents(first: 100) {
itemCount
nodes {
...CartItemContent
}
}
appliedCoupons {
code
discountAmount
discountTax
}
needsShippingAddress
availableShippingMethods {
packageDetails
supportsShippingCalculator
rates {
id
instanceId
methodId
label
cost
}
}
subtotal
subtotalTax
shippingTax
shippingTotal
total
totalTax
feeTax
feeTotal
discountTax
discountTotal
}
fragment CustomerFields on Customer {
id
databaseId
firstName
lastName
displayName
email
sessionToken
metaData {
key
value
}
}
query GetTopNav {
menu(id: "primary", idType: LOCATION) {
...MenuContent
}
}
query GetProducts($first: Int, $after: String, $where: RootQueryToProductConnectionWhereArgs) {
products(first: $first, after: $after, where: $where) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
...ProductContentSmall
}
}
nodes {
...ProductContentSmall
}
}
}
query GetProduct($id: ID!, $idType: ProductIdTypeEnum) {
product(id: $id, idType: $idType) {
...ProductContentFull
}
}
query GetShopCategories($first: Int, $after: String, $where: RootQueryToProductCategoryConnectionWhereArgs) {
productCategories(first: $first, after: $after, where: $where) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
slug
}
}
nodes {
id
name
slug
}
}
}
query GetShopTags($first: Int, $after: String, $where: RootQueryToProductTagConnectionWhereArgs) {
productTags(first: $first, after: $after, where: $where) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
slug
}
}
nodes {
id
name
slug
}
}
}
query GetShopColors($first: Int, $after: String, $where: RootQueryToPaColorConnectionWhereArgs) {
allPaColor(first: $first, after: $after, where: $where) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
name
slug
}
}
nodes {
id
name
slug
}
}
}
query GetSession {
cart {
...CartContent
}
customer {
...CustomerContent
}
}
mutation AddToCart(
$productId: Int!,
$variationId: Int,
$quantity: Int,
$variation: [ProductAttributeInput],
$extraData: String
) {
addToCart(
input: {
productId: $productId,
variationId: $variationId,
quantity: $quantity,
variation: $variation,
extraData: $extraData
}
) {
cart {
...CartContent
}
cartItem {
...CartItemContent
}
}
}
mutation UpdateCartItemQuantities($items: [CartItemQuantityInput]) {
updateItemQuantities(input: {items: $items}) {
cart {
...CartContent
}
items {
...CartItemContent
}
}
}
mutation RemoveItemsFromCart($keys: [ID], $all: Boolean) {
removeItemsFromCart(input: {keys: $keys, all: $all}) {
cart {
...CartContent
}
cartItems {
...CartItemContent
}
}
}
mutation Login($username: String!, $password: String!) {
login(input: { username: $username, password: $password }) {
authToken
refreshToken
customer {
...CustomerFields
}
}
}
mutation RefreshAuthToken($refreshToken: String!) {
refreshJwtAuthToken(input: { jwtRefreshToken: $refreshToken }) {
authToken
}
}
mutation UpdateSession($input: UpdateSessionInput!) {
updateSession(input: $input) {
session {
id
key
value
}
}
}6. Erstellen Sie eine neue graphql/client.ts Datei und fügen Sie folgenden Code hinzu:
import { GraphQLClient } from 'graphql-request';
import deepmerge from 'deepmerge';
import {
RootQueryToProductConnectionWhereArgs,
Product,
ProductCategory,
PaColor,
getSdk,
RootQueryToProductCategoryConnectionWhereArgs,
RootQueryToPaColorConnectionWhereArgs,
ProductIdTypeEnum,
} from './generated';
let client: GraphQLClient;
export function getClient() {
const endpoint = process.env.GRAPHQL_ENDPOINT
if (!endpoint) {
throw new Error('GRAPHQL_ENDPOINT is not defined')
}
if (!client) {
client = new GraphQLClient(endpoint);
}
return client;
}
export function getClientWithSdk() {
return getSdk(getClient());
}
const initialConnectionResults = {
pageInfo: {
hasNextPage: true,
endCursor: null,
},
edges: [],
nodes: [],
};
export async function fetchProducts(
pageSize: number,
pageLimit = 0,
where?: RootQueryToProductConnectionWhereArgs
) {
try {
const client = getClientWithSdk();
let data = { products: initialConnectionResults }
let after = '';
let count = 0
while(data.products.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
const next = await client.GetProducts({
first: pageSize,
after,
where,
});
data = deepmerge(data, next);
after = next.products?.pageInfo.endCursor || '';
count++;
}
return (data.products.nodes) as Product[];
} catch (err) {
console.error(err || 'Failed to fetch product listing!!!');
}
}
export async function fetchCategories(
pageSize: number,
pageLimit = 0,
where?: RootQueryToProductCategoryConnectionWhereArgs
) {
try {
const client = getClientWithSdk();
let data = { productCategories: initialConnectionResults }
let after = '';
let count = 0
while(data.productCategories.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
const next = await client.GetShopCategories({
first: pageSize,
after,
where,
});
data = deepmerge(data, next);
after = next.productCategories?.pageInfo.endCursor || '';
count++;
}
return (data.productCategories.nodes) as ProductCategory[];
} catch (err) {
console.error(err || 'Failed to fetch product categories!!!');
}
}
export async function fetchColors(
pageSize: number,
pageLimit = 0,
where?: RootQueryToPaColorConnectionWhereArgs
) {
try {
const client = getClientWithSdk();
let data = { allPaColor: initialConnectionResults }
let after = '';
let count = 0
while(data.allPaColor.pageInfo.hasNextPage && (pageLimit === 0 || count < pageLimit)) {
const next = await client.GetShopColors({
first: pageSize,
after,
where
});
data = deepmerge(data, next);
after = next.allPaColor?.pageInfo.endCursor || '';
count++;
}
return (data.allPaColor.nodes) as PaColor[];
} catch (err) {
console.error(err || 'Failed to fetch product color attributes!!!');
}
}
export async function fetchProductBy(slug: string, idType: ProductIdTypeEnum) {
try {
const client = getClientWithSdk();
const data = await client.GetProduct({
id: slug,
idType: idType,
});
if (!data.product) {
throw new Error('Product not found!!!');
}
return data.product as Product;
} catch (err) {
console.error(err || 'Failed to fetch product data!!!');
}
}
Schlussfolgerung
Herzlichen Glückwunsch! Sie haben erfolgreich eine Next.js-Anwendung für die E-Commerce-Entwicklung mit dem GraphQL-Endpunkt aus Ihrem WooCommerce-Shop eingerichtet. Sie haben installiert und konfiguriert shadcn/ui Komponenten, andere UI-Komponenten erstellt und den GraphQL Code Generator installiert und konfiguriert. Ihre Anwendung ist jetzt bereit für die weitere Entwicklung und Integration mit Ihrem WooCommerce-Shop mit GraphQL.

Leave a Reply