Reactで行うShopify既存テーマの機能拡張。実践的な開発例を解説

こんにちは!

アソビューでフロントエンドエンジニアをしている野口です。
以前ShopifyテーマにReactのコンポーネントを描画する方法の記事を書いていますが、今回は実際に開発を進めてみてReactを使って機能拡張する上で苦労した点を紹介していきます。

はじめに

弊社では、アソビュー!に掲載されている遊びの中から厳選した体験を、家族や友人にプレゼントできるアソビュー!ギフトというサービスも展開しています。
サイト自体は、Shopifyを利用してECサイトを構築しており、テーマはDawnを適宜更新しつつ使っています。

そんなアソビュー!ギフトですが、今月から商品の贈り方に「eギフト」が選べるようになりました!🎉
eギフトの追加により、今までのように商品の配送を待たず、すぐにお相手にギフトが送れるようになっています。

今回は、eギフト機能を追加するにあたり既存のliquiidファイルにReactで機能を追加したため、その際に行った作業や工夫した点を紹介できればと思います。

1. 本番・ステージング環境間のコード管理

対応の背景

Reactの内容ではないのですが、まずコード管理するところで今回かなり悩みました。
アソビュー!ストアでは、ステージング用に別のストアを用意しており、本番・ステージングの二つのストアが用意されています。

今まではそれぞれShopify上でのみテーマファイルを管理していたのですが、

  • Shopifyの管理画面からコード変更がされるため環境間でコードの同期ができていない
  • ステージング確認後、本番用のファイルに同じ修正を再実装しなければならない

の課題があり、今回どちらのストアのコードもGitHubで管理するようにしました。

コード連携

GitHubで管理しているテーマは、Shopifyの管理画面からGUI上の操作で連携することができます。
その際のディレクトリ構造についてはこちらで触れていますので、合わせてご確認いただければと思います。

ただ、ここで問題になるのがストアの環境ごとに管理されているコードです。

具体的にいうと

  • config/settings_data.json、config/settings_schema.jsonなどの設定ファイル
  • 管理画面から登録した画像、商品情報、コレクションの設定など

になります。

商品やコレクションの情報は、本番環境に登録されているデータを環境ごとに手動で再登録する必要があります。アプリを使うことで簡単に行える手段もあるようなのですが、今回はそこまで数も多くなかったため手動で行ないました。

設定ファイルのコードに関しては、これらを共通化してしまうと、ステージング → 本番のリリース時にステージング用の設定が本番に反映されてしまう可能性があります。

ブランチ運用

そのため、以下の運用で対応することにしました。

まず、ステージング反映用のブランチも開発用ブランチも基本的には本番用のブランチからブランチを作成します。
このようにすることでステージングの内容が誤って本番に反映されてしまうという懸念をなくしています。また、ステージング反映用のブランチを定期的に本番用ブランチから作成することで環境間のコード差分を少なくしています。

その前提のもと、ステージング環境用のブランチは以下の手順で作成していきます。

① 本番用のブランチからステージング反映用のブランチを作成
② 以下のファイルをステージング環境のテーマファイルからコピー

- config/setting_data.json
- config/setting_schema.json
- templates/index.json
※ `templates/index.json`にはTOP画面のカルーセル画像などのパス情報があるためコピー  

③ ステージング環境にテーマを反映

開発時は、

① 本番用のブランチから開発ブランチを作成
② 開発完了後、ステージング環境用のブランチにマージ
③ 問題なければ、開発ブランチから本番用のブランチを作成
④ 本番反映ブランチの最新のコードを取り込んだ上で、本番環境にテーマを反映

.
※ production build、staging buildはそれぞれReactで使う環境変数を設定しています

上記のような開発フローをとることで、複数ストア間でのコード管理を行っています。
現在は主に私だけが開発を行なっている状況なので、このフローを他の開発メンバーも運用しやすいようにしていくのが今後の課題になります。

2. 購入完了画面にReactコンポーネントを描画する

Shopifyの購入完了画面を編集するには主に以下の2つの方法があるかと思います。

  1. Shopify Plusプランにしcheckout.liquidを編集する
  2. Shopifyの管理画面にある「注文状況ページ」欄にコードを追加する

今回はReactで実装することもあり後者のやり方にしています。

Reactコンポーネントの描画

管理画面の「注文状況ページ」からコードを追加する場合、好きな場所にエントリーポイントを配置するということは難しいです。
(もちろん特定のclass名を指定することはできるのですが、指定したclass名がどのタイミングで変更されるかこちら側では分からずリスクがあります)
そのため、ルートのエントリーポイントは以下のように用意しつつ、Shopifyが用意しているOrder StatusのJS assetを使って特定箇所にコンポーネントを描画するようにしています。

今回は、対象商品が注文された時のみUIを追加したかったので、条件を満たした時のみ注文内容を表示するためのエントリーポイントをOrderStatusのAPIにわたします。

if (isEGift) {
  window.Shopify.Checkout.OrderStatus.addContentBox(
    '<h2>ご注文内容</h2>',
    '<div id="message-card"></div>',
  )
}

そして、コンポーネント側の実装でOrderStatusに渡したDOMが存在する場合に、注文内容のコンポーネントを描画するようにしています。

const target = document.getElementById('message-card')

if (!target || !target.parentElement) {
  return null
}

return (
  createPortal(
    <OrderConent />,
    target,
  )
)

上記の実装をすることで購入完了画面に任意のコンポーネントを描画することができます。
ただし、Order Statusのassetを使って描画できる箇所は決められているため、任意の場所に表示する時はchekcout.liquidを修正する必要があるかと思います。

修正の際は現在契約しているプランでの修正可能エリアを把握し、デザイナーと調整しながら実装を進めることをお勧めします。

3. Shopifyオプションアプリとの共存

eギフト対象の商品は、詳細画面で贈り方を選択することができます。
その際に、配送ギフトの場合はラッピングや熨斗などのオプション情報を設定することが可能です。
ただし、配送を伴わないeギフトの場合はこれらを情報が非表示にする必要がありました。

オプションアプリ

アソビュー!ギフトではオプションアプリにbold product optionsを利用しています。
これを使うことでエンジニアを介さず、Shopifyの管理画面から任意の商品にオプション情報を追加することができスピード感を持ってエンハンスすることが可能です。

ただ、bold product optionsの場合、オプションアプリ内での項目の動的な出しわけはできるのですが、Shopifyの商品情報として設定したオプションとの出し分けには対応していません。

今回は、贈り方ごとにSKU情報を分けたかったため、

  • 贈り方: Shopifyの商品情報からオプション設定
  • ラッピングや熨斗など: bold product options.

のような分け方をしています。

そのため、贈り方を変更するたびにbold product optionsで設定したオプション情報の表示・非表示の切り替えが必要なり、それをReact側で行なっています。

bold product optionsの出しわけ

贈り方のそれぞれのボタンがクリックされる度に、オプションアプリで描画されたDOMを表示・非表示を切り替えるようにしています。

もちろんですが、ボタンがクリックされた際の操作や今何が選択されているかはReact側で管理していません。そのため、対象のDOMを取得し、クリック時のイベントを設定します。

// ボタンのDOMを取得
const eGiftButton = document.querySelector<HTMLInputElement>(
  'input[name="オプション名"][value="オプションA"]',
)
const deliverButton = document.querySelector<HTMLInputElement>(
  'input[name="オプション名"][value="オプションB"]',
)
const initializeEGiftInput = (
  eGiftButton: HTMLInputElement,
  deliverButton: HTMLInputElement
) => {
  if (eGiftButton.checked) {
    // 初期表示時にオプションAが選択されていた際にオプションアプリを非表示に
    hideBoldOptions()
  }
  listenHowToSendCheckbox(eGiftButton, deliverButton)
}

const listenHowToSendCheckbox = (
  eGiftButton: HTMLInputElement,
  deliverButton: HTMLInputElement
) => {
  const boldOptions = document.querySelector<HTMLDivElement>('.bold_options')

  // オプションアプリを非表示にする
  eGiftButton.addEventListener('click', () => {
    if (!boldOptions) {
      return
    }
    boldOptions.style.display = 'none'
  })

  // オプションアプリを表示する
  deliverButton.addEventListener('click', () => {
    if (!boldOptions) {
      return
    }
    boldOptions.style.display = 'block'
  })
}

const ProductDetailPage = () => {
  useEffect(() => {
    if (!eGiftButton || !deliverButton) {
      return
    }

    initializeEGiftInput(eGiftButton, deliverButton)
  }, [deliverButton, eGiftButton])

  ...
}

オプションアプリの項目がすでに入力されていた際は、贈り方を切り替える際に入力値を初期化する必要があります。
これをしないと、本来設定できないオプションが設定された状態で購入に進んでしまいます。

bold product optionsではLine Item Property(参考)を利用してオプション情報を設定していたので、bold product optionsが出力している要素のうちname属性が properties で始まる入力値を一括でリセットするようにしています。

  eGiftButton.addEventListener('click', () => {
    if (!boldOptions) {
      return
    }
    boldOptions.style.display = 'none'
        
    // bold product optionsで設定された値をリセットする
    const boldInputs = boldOptions.querySelectorAll<HTMLInputElement>('input[name^="properties"]')
    const boldSelects = boldOptions.querySelectorAll<HTMLInputElement>('select[name^="properties"]')
    boldInputs.forEach((input) => {
      input.value = ''
    })
    boldSelects.forEach((input) => {
      input.value = ''
    })
  })

これでShopifyで設定したオプションボタンを切り替えるたびに、bold product optionsで設定したオプションの出し分けをしています。

最後に

今回は、Reactを使ってShopifyの既存テーマを機能拡張する際に必要になった作業や工夫した点について紹介しました。
Reactで作成した独自コンポーネントとオプションアプリなどShopifyのアプリとの組み合わせでは、それぞれが独立して状態管理をしているためスタイルやローディング時の挙動など共通の体験を提供することが難しいと感じます。
また、オプションアプリを追加することで読み込むJSも増えていくと思われ今回の構成だと描画速度やCLSなどを改善しようと思っても限界があります。
前回のブログでも少し触れているのですが、今後機能をさらに拡張していくことを考慮すると、弊社の技術スタックに合わせフロントエンドは全てReactに寄せるのが良さそうと改めて感じているのでこれからさらにアップデートしていきたいです。 また、現在はビルドコマンドの実行をローカルにて手動で行なっている状態なので、こちらもコミット時に自動で行うようにするかCIで対応できるようにしていければと思っています。

アソビューでは一緒に働くメンバーを大募集しています! カジュアル面談もありますので、少しでも興味があればお気軽にご応募いただければと思います!

www.asoview.com