😎

Shopifyサンプルアプリ(Node.js版)解説 - ボタンを押すと商品を5個追加

2022/10/27に公開
2

Shopify CLIでインストールされるサンプルアプリ(ボタンを押すと商品を5個追加)のコードを解説します。

プログラムが呼び出されるルート

まずは、どのようなルートを辿ってプログラムが呼び出されるかを書いておきます。

web/index.js
   ↓
web/frontend/index.html
   ↓
web/frontend/index.jsx
   ↓
web/frontend/App.jsx

https://xxx.myshopify.com/admin/apps/アプリ名/
   ↓
web/frontend/index.jsx
   ↓
web/frontend/components/ProductsCard.jsx
   ↓
web/helpers/product-creator.js

サンプルアプリを改造しようと思ったら、

  • web/index.js
  • web/frontend/index.jsx
  • web/frontend/components/ProductsCard.jsx
  • web/helpers/product-creator.js

この4つのファイルを触ればOKです。

「index.js」のコード解説(※111~142行目抜粋)

APIへfetch(リクエスト)されたときのルート分岐を処理しています。

\web\index.js
  // 商品数の情報にAPIリクエストがあったときの処理
  app.get("/api/products/count", async (req, res) => {
    const session = await Shopify.Utils.loadCurrentSession(
      req,
      res,
      app.get("use-online-tokens")
    );
    const { Product } = await import(
      `@shopify/shopify-api/dist/rest-resources/${Shopify.Context.API_VERSION}/index.js`
    );

    const countData = await Product.count({ session });
    res.status(200).send(countData);
  });

  // 商品追加のAPIリクエストがあったときの処理
  app.get("/api/products/create", async (req, res) => {
    const session = await Shopify.Utils.loadCurrentSession(
      req,
      res,
      app.get("use-online-tokens")
    );
    let status = 200;
    let error = null;

    try {
      await productCreator(session);
    } catch (e) {
      console.log(`Failed to process products/create: ${e.message}`);
      status = 500;
      error = e.message;
    }
    res.status(status).send({ success: status === 200, error });
  });

「frontend/index.jsx」のコード解説

画面に表示させる内容を処理しています。

\web\frontend\pages\index.jsx
// polarisの読み込み(※下記で詳しく解説)
import {
  Card,
  Page,
  Layout,
  TextContainer,
  Image,
  Stack,
  Link,
  Heading,
} from "@shopify/polaris";

// app-bridgeの読み込み(※下記で詳しく解説)
import { TitleBar } from "@shopify/app-bridge-react";

// トロフィー画像の読み込み
import { trophyImage } from "../assets";

// web/frontend/components/ProductsCard.jsxの読み込み
import { ProductsCard } from "../components";

export default function HomePage() {
  return (
    <Page narrowWidth>
      <TitleBar title="App name" primaryAction={null} />
      <Layout>
        <Layout.Section>
          <Card sectioned>
            <Stack
              wrap={false}
              spacing="extraTight"
              distribution="trailing"
              alignment="center"
            >
              <Stack.Item fill>
                <TextContainer spacing="loose">
                  <Heading>Nice work on building a Shopify app 🎉</Heading>
                  <p>
                    Your app is ready to explore! It contains everything you
                    need to get started including the{" "}
                    <Link url="https://polaris.shopify.com/" external>
                      Polaris design system
                    </Link>
                    ,{" "}
                    <Link url="https://shopify.dev/api/admin-graphql" external>
                      Shopify Admin API
                    </Link>
                    , and{" "}
                    <Link
                      url="https://shopify.dev/apps/tools/app-bridge"
                      external
                    >
                      App Bridge
                    </Link>{" "}
                    UI library and components.
                  </p>
                  <p>
                    Ready to go? Start populating your app with some sample
                    products to view and test in your store.{" "}
                  </p>
                  <p>
                    Learn more about building out your app in{" "}
                    <Link
                      url="https://shopify.dev/apps/getting-started/add-functionality"
                      external
                    >
                      this Shopify tutorial
                    </Link>{" "}
                    📚{" "}
                  </p>
                </TextContainer>
              </Stack.Item>
              <Stack.Item>
                <div style={{ padding: "0 20px" }}>
                  <Image
                    source={trophyImage}
                    alt="Nice work on building a Shopify app"
                    width={120}
                  />
                </div>
              </Stack.Item>
            </Stack>
          </Card>
        </Layout.Section>
        <Layout.Section>
	
	 // web/frontend/components/ProductsCard.jsxから
	  // Populate 5 productsボタンと現在の商品数を呼び出し
          <ProductsCard />
	  
        </Layout.Section>
      </Layout>
    </Page>
  );
}

Shopify独自のコンポーネントについて

「.jsx」の中では、「polaris」や「app-bridge-react」といったコンポーネントが呼び込まれています。

polaris・app-bridge-reactは、Shopify独自のコンポーネントです。

polarisとは?

polarisは、Shopifyストアで使用されているパーツのデザインをそのまま呼び出せるコンポーネントです。

例えば、カードのデザインであれば、<Card>というpolarisタグを使用します。

https://polaris.shopify.com/components/card

カラム数を設定する<Layout>なんてのもあります。

https://polaris.shopify.com/components/layout

polarisを使えば、HTML・CSSを書かずにShopifyと同じデザインに統一できるので、大幅に作業時間を短縮することができます。

app-bridgeとは?

app-bridgeは、Shopifyストアで使用されている機能をそのまま呼び出せるコンポーネントです。

例えば、ポップアップメッセージを表示したり、
https://shopify.dev/apps/tools/app-bridge/actions/toast

タイトルバーを表示したり、
https://shopify.dev/apps/tools/app-bridge/actions/titlebar

app-bridgeを使えば、Shopifyストアで使用されている同じ機能をアプリでも使うことができるので、大幅に作業時間を短縮することができます。

「product-creator.js」のコード解説

Shopiy APIを叩いて、ストアに商品を追加する処理を行っています。

\web\frontend\components\product-creator.js
// ShopifyストアからAPIを使って情報を取得するためのコンポーネントを読み込む
import { Shopify } from "@shopify/shopify-api";

// ADJECTIVESとNOUNSをランダムで組み合わせて商品名にしています。
const ADJECTIVES = [
  "autumn",
  "hidden",
  "bitter",
  "misty",
  "silent",
  "empty",
  "dry",
  "dark",
  "summer",
  "icy",
  "delicate",
  "quiet",
  "white",
  "cool",
  "spring",
  "winter",
  "patient",
  "twilight",
  "dawn",
  "crimson",
  "wispy",
  "weathered",
  "blue",
  "billowing",
  "broken",
  "cold",
  "damp",
  "falling",
  "frosty",
  "green",
  "long",
]

const NOUNS = [
  "waterfall",
  "river",
  "breeze",
  "moon",
  "rain",
  "wind",
  "sea",
  "morning",
  "snow",
  "lake",
  "sunset",
  "pine",
  "shadow",
  "leaf",
  "dawn",
  "glitter",
  "forest",
  "hill",
  "cloud",
  "meadow",
  "sun",
  "glade",
  "bird",
  "brook",
  "butterfly",
  "bush",
  "dew",
  "dust",
  "field",
  "fire",
  "flower",
]

// 追加する商品の数を設定
export const DEFAULT_PRODUCTS_COUNT = 5;

// APIを取得するために必要な情報を入力
const CREATE_PRODUCTS_MUTATION = `
  mutation populateProduct($input: ProductInput!) {
    productCreate(input: $input) {
      product {
        id
      }
    }
  }
`

// productCreator メイン処理
export default async function productCreator(session, count = DEFAULT_PRODUCTS_COUNT) {

 // Graphql APIを使うための準備
  const client = new Shopify.Clients.Graphql(session.shop, session.accessToken);

  try {
  
   // 指定した回数APIを操作する
    for (let i = 0; i < count; i++) {
    
      // APIを操作するために必要な情報を入力
      await client.query({
        data: {
          query: CREATE_PRODUCTS_MUTATION,
          variables: {
            input: {
              title: `${randomTitle()}`,
              variants: [{ price: randomPrice() }],
            },
          },
        },
      });
    }
    
  // エラー処理
  } catch (error) {
    if (error instanceof ShopifyErrors.GraphqlQueryError) {
      throw new Error(`${error.message}\n${JSON.stringify(error.response, null, 2)}`);
    } else {
      throw error;
    }
  }
}

// 商品名の設定
function randomTitle() {
  const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
  const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
  return `${adjective} ${noun}`;
}

// 値段の設定
function randomPrice() {
  return Math.round((Math.random() * 10 + Number.EPSILON) * 100) / 100;
}

APIについて

Shopifyストアから情報を引き出したり、操作するために『API』という機能を利用します。

例えば、「最近の注文を10件取得したい」という場合。

ShopifyのAPIに「orders」「first: 10」というクエリを投げかければ、その情報を返してくれます。

REST APIとGraphQL API

Shopifyでは、RESTとGraphQLの2つのAPIが利用できます。

どちらを使うべきか?

RESTでしか取得できない情報もあるようですが、GraphQLの方が性能的には優れています。

https://ichyaku.com/shopify-graphql-rest/

とりあえず、Shopiストアの管理画面で閲覧できる情報(Admin API)を取得したい場合は、GraphQLでコードを書いておけば間違いありません。

以下、注文リストを取得する場合の参考ページを置いておきます(英文)↓

【GraphQL】
https://shopify.dev/api/admin-graphql/2022-10/queries/orders#examples-Get_the_first_10_orders_with_authorized_payments

【REST】
https://shopify.dev/api/admin-rest/2022-10/resources/order#get-orders?since-id=123

「ProductsCard.jsx」のコード解説

Populate 5 productsボタンが押されたあとにtoast(ポップアップメッセージ)を表示したり、現在の商品数の取得・表示処理を行っています。

\web\helpers\ProductsCard.jsx
// reactライブラリからuseStateモジュールを読み込む
import { useState } from "react";

// polarisの読み込み
import {
  Card,
  Heading,
  TextContainer,
  DisplayText,
  TextStyle,
} from "@shopify/polaris";

// app-bridgeからtoast(ポップアップメッセージ)の読み込み
import { Toast } from "@shopify/app-bridge-react";

// API通信を行うためのモジュールを読み込む
import { useAppQuery, useAuthenticatedFetch } from "../hooks";

// ProductsCard メイン処理
export function ProductsCard() {

  // toastPropsに設定する初期値:null
  const emptyToastProps = { content: null };
  
  // const [状態変数, 状態を変更するための関数] = useState(状態の初期値);
  const [isLoading, setIsLoading] = useState(true);
  const [toastProps, setToastProps] = useState(emptyToastProps);
  
  // OAuth(認証処理)
  const fetch = useAuthenticatedFetch();

 // Rest APIで商品数を取得
  const {
    data, // APIから取得したデータが入る
    refetch: refetchProductCount, // データの再取得
    isLoading: isLoadingCount, // 初期ロードの状態(取得完了:false)
    isRefetching: isRefetchingCount, // 再取得中の状態(進行中:True)
  } = useAppQuery({
    url: "/api/products/count",
    reactQueryOptions: {
      onSuccess: () => {
      // ローディングマークを非表示
        setIsLoading(false);
      },
    },
  });

  // ポップアップメッセージの表示処理
  const toastMarkup = toastProps.content && !isRefetchingCount && (
    <Toast {...toastProps} onDismiss={() => setToastProps(emptyToastProps)} />
  );

 // Populate 5 productsボタンがクリックされると実行される処理
  const handlePopulate = async () => {
  
   // ローディングマークを表示
    setIsLoading(true);
    
    // APIにリクエスト
    const response = await fetch("/api/products/create");

    // 商品追加の成功がAPIから帰ってきたとき
    if (response.ok) {
    
     // 商品数表示の更新
      await refetchProductCount();
      
      // ポップアップメッセージ
      setToastProps({ content: "5 products created!" });
      
    // 失敗した時
    } else {
    
     // ローディングマークを非表示
      setIsLoading(false);
      
      // ポップアップメッセージ
      setToastProps({
        content: "There was an error creating products",
        error: true,
      });
    }
  };

  return (
    <>
      {toastMarkup}
      // Populate 5 productsボタンをクリックすると、
      // handlePopulate関数が実行される
      // isLoadingがTrueなら、ローディングマークを表示
      <Card
        title="Product Counter"
        sectioned
        primaryFooterAction={{
          content: "Populate 5 products",
          onAction: handlePopulate,
          loading: isLoading,
        }}
      >
        <TextContainer spacing="loose">
          <p>
            Sample products are created with a default title and price. You can
            remove them at any time.
          </p>
          <Heading element="h4">
            TOTAL PRODUCTS
            <DisplayText size="medium">
              <TextStyle variation="strong">
	      
	       // isLoadingCountがfalseの場合(データの受信が完了している場合)、
	        // 商品数を表示します。
                {isLoadingCount ? "-" : data.count}
		
              </TextStyle>
            </DisplayText>
          </Heading>
        </TextContainer>
      </Card>
    </>
  );
}

Discussion

hidmhidm

情報の少ない中、貴重な解説ありがとうございます!
一点、後半解説中の "ProductsCard.jsx" と "product-creator.js" のファイル名が逆になっているようですよ?!
ご確認を。

コダマコダマ

本当ですね、ご指摘ありがとうございます。
修正しました。