サクッとECサイトを構築できるReactベースのHydrogen入門　

2022/4/23 修正　デプロイ２−３　workers.js → worker.js

先日、私の所属コミュニティOver40WebClub主催で
タイトルのような勉強会を開催、そこで登壇して報告しました、
勉強中の人が勉強したことを報告する勉強会ドリブン勉強法という形です。
[connpasのページ](https://over40webclub.connpass.com/event/241732/)

[デモサイト](https://sample-hydrogen.horumont.workers.dev/)
[スライド](https://www.canva.com/design/DAE7-Sx4SyA/l6wG0OS-XxMgqVJN8nWTwg/edit?utm_content=DAE7-Sx4SyA&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton)

そのときにつくったデモサイトや資料などを
GitHubのリポジトリでREADMEにまとめました。
[GitHub:サクッとECサイトを構築できるReactベースのHydrogen入門](https://github.com/sonho463/hydrogen-app)

この記事ではその時に報告した内容のうち、
デモでHydrogenアプリを構築したときの内容を記録したいと思います。
まだまだ理解が浅いので、僕の理解で書いているところで間違っているところがあると思いますので、気づいた方、ご指摘をお願いします！

勉強会の開催にあたって、Over40webClubの
[ピータン@pitang1965](https://twitter.com/pitang1965)さんの大きなサポートを頂きました！
[yoko@yokoiwasaki6](https://twitter.com/yokoiwasaki6)さんに司会をしていただきました！
Over40WebClubのみなさまにたくさんのコメント、はげましをいただきました！
ありがとうございました！

**Table of content**
- Hydrogenとはなにか
- Hydrogenのよいところ
- アプリ構築の手順紹介

# Hydrogenとはなにか
Shopifyによる、Shopifyのための、Shopifyのフロントエンドフレームワークです。ShopifyではStorefrontAPIを提供していて、これを利用してヘッドレスコマースサイトが注目されています。GatsbyやNext.jsでは、Shopifyと接続するためのスターターもあります。
- [GatsbyのECスターターページ](https://www.gatsbyjs.com/solutions/shopify/)
- [Next.jsのECスターターページ](https://nextjs.org/commerce)


![スクリーンショット 2022-04-23 17.03.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362843/6159787e-6217-d673-edd4-cd7ca991baac.png)



海外ではヘッドレスコマースがけっこうあるようです。また、おそらく１００円均一のダイソーさんのECサイトもShopifyを使ったヘッドレスコマースだと思われます。

Gridsome
[Victoria Beckham Beauty](https://www.victoriabeckhambeauty.com/)
Next.js
[Koala Easter Sale | Up To 20% Off Sitewide Sale On Now | Koala AU](https://koala.com/en-au)
Nuxt.js
[Reusable Water Bottles | Reusable Coffee Cups | Chilly's](https://www.chillys.com/uk)
[ダイソー](https://jp.daisonet.com/)

HydrogenはShopifyが自らてがけた、フロントエンドフレームワークで、現在はまだ開発途中です。ヘッドレスCMSにおける課題を解決できそうな期待の持てるものだと思いました。
今回アプリの公開にはCLOUDFLAREのWorkersを使用していますが、もうすぐ専用のホスティングサービスとして、Oxygenがリリースされそうです。現在はShopifyの一部のマーチャントで実験的に使用しているということでした。

# Hydrogenのよいところ
Reactの最新機能を先取りしています。ブログで次の３つのポイントが紹介されていました。
## Streamning SSR
SSRとCSRのいいとこどりをしています。
サーバーサイドでのデータフェッチとクライアントサイドでのレンダリングを同時にやってしまおうという機能です。最初のレンダリングにおけるTBTの削減につながっているようです。
## React Server Components
Reactで課題だった、すべてのコンポーネントが出来るまでレンダーされないという課題を解決しています。サーバーサイドとクライアントサイドのコードを明示的にかき分けることで、バンドルサイズの縮減につながっているようです。
## CDN,キャッシュ戦略
静的なページについては、CDNでキャッシュしておいて、高速に表示をしようというものです。基本的にはSSRなので、SSGに劣る部分をこれで補っているのかなと思いました。
キャッシュ戦略では、秒単位でキャッシュするものから年単位でキャッシュするものを明確にかき分けることができるようでした。

# アプリ構築の手順紹介
## プロジェクト開始
1. プロジェクト用フォルダ作成→プロジェクト開始
```shell
yarn create hydrogen-app
```
このコマンドでアプリのコード一式構築。途中プロジェクト名を聞かれるので入力します。
2.  この段階では、デモンストレーション用のshopifyストアのデータに接続しています。まずは、表示をみてみましょう。

```JavaScript
// プロジェクトのフォルダを移動
cd <your-project-name>
// 依存関係をインストール
yarn
// 開発サーバーを起動
yarn dev
```
## Shopifyでのストア構築、設定
1. Shopifyパートナープログラムに登録
登録すると、開発ストアをいくつも構築することができます。ずっと無料です。
2. 左メニューからストア管理　→　ストアを追加するボタン
![](https://storage.googleapis.com/zenn-user-upload/4a45bb5ff545-20220419.png)

3. ストアタイプを選ぶ　→　開発ストア
![](https://storage.googleapis.com/zenn-user-upload/d0b3e5830d94-20220419.png)

その後いろいろ設定をして保存をすると、ストアが立ち上がります。
![](https://storage.googleapis.com/zenn-user-upload/2da59a0c8d66-20220419.png)

shopifyによるホスティングで、すでにストアをインターネット上に公開はされていますが、開発ストアはすべてパスワードで保護されている状態です。
要するに実際の店舗としては使えないということです。実際の案件ではここでストアを構築した後、所有権を移行することで、マーチャントがストアの管理をできるようになります。
4. 商品、コレクションを登録
いくつか商品とコレクションを登録しておくとあとでHydrogenに接続したときに、わかりやすいです。
[商品を登録する](https://help.shopify.com/ja/manual/products/add-update-products)
[コレクションについて](https://help.shopify.com/ja/manual/products/collections)

## ShopifyからAPIを取得できるようにする
ShopifyからAPIを取得できるようにするために、アプリを設定します。
1. ストア用のアプリを開発する
![](https://storage.googleapis.com/zenn-user-upload/bdb03ce87f63-20220419.png)
    
2. カスタムアプリ開発を許可
![](https://storage.googleapis.com/zenn-user-upload/dbb74c39d354-20220419.png)
    
3. カスタムアプリを作成
![](https://storage.googleapis.com/zenn-user-upload/0eebab7ddd68-20220419.png)
    
4. ストアフロントAPIスコープを設定する→アプリをインストールする
![](https://storage.googleapis.com/zenn-user-upload/4d1c69dd5a43-20220419.png)
    
    - ストアフロントで提供しているデータのうち、取得を許可するものを選びます。
    - 今回はすべての項目にチェックを入れます。
        
![](https://storage.googleapis.com/zenn-user-upload/00f957c8983f-20220419.png)

        

- 保存　→　アプリをインストール

5. アクセストークンを取得
    
    下の画面が表示されていないときは「API資格情報」をクリック
![](https://storage.googleapis.com/zenn-user-upload/cd852261bfe9-20220419.png)



## Hydrogen側API設定
Shopify側で設定したデータをもとに、Hydrogenアプリ側でAPIと接続するための設定をします。

必要なデータ

URL　ストアURLの一部

アクセストークン　Shopify側でアプリを作成したときに取得したもの

1. gitignore
    
    まずは、設定情報を書き込むshopifu.configをGit管理でオープンにしてしまわないように、.gitignoreに設定します。
    
    ここまでの操作でGit管理はしていませんが、Git管理は基本になるので、設定しておきます。
    
    .gitignore
    
    ```jsx
    。。。
    # yarn v2
    .yarn/cache
    .yarn/unplugged
    .yarn/build-state.yml
    .yarn/install-state.gz
    .pnp.*
    
    # Vite output
    dist
    
    一番うしろに追加します。
    # storefront API config
    shopify.config.js
    ```
    
2. shopify.config
    1. storeDomain:の値　→　URLの一部
    2. storefrontToken　→　shopifyアプリ作成のときに取得したもの　
    
    ```jsx
    // こちらはHydrogenサンプルストアのデータ、
    export default {
      storeDomain: 'hydrogen-preview.myshopify.com',
      storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
      storefrontApiVersion: '2022-04',
    };
    ```
    
3. これで、自分のお店のデータがAPIを通じて、Hydrogenに渡ってくる設定ができました。開発サーバーで確認してみます。
    
    ```jsx
    yarn dev
    ```
  

## ちょっとカスタマイズ
Reactベースなので、React出来る方はどんどんカスタマイズできます。Hydrogen独自のコンポーネントがたくさんあって、それを使うとカートボタンや商品リストや詳細ページが簡単に作れます。
スタイリングにはTailwindCSSが使われているので、デザインも比較的かんたんにカスタマイズできました。

1. コンタクトページ追加
src/routes/[handle].server.jsxというところが固定ページのテンプレートのようです。
Shopify側でページをWysiwygエディタで作成すると、`/pages/○○`というルーティングで表示されます。コンタクトページも`/pages/contact`で同様に表示されるのですが、フォーム自体は渡ってきませんでした。
そこで、routes直下に`contact.server.jsx`を作成して、そこにフォームを作りました。
フォームデザインは以下のサイトからいただきました。
[Tail-kit Components and templates for Tailwind CSS 2.0](https://www.tailwind-kit.com/components/form)
また、フォーム機能は、[GetForm](https://getform.io/)というサービスを使っています。

```
import Layout from '../components/Layout.server';

export default function Index({country = {isoCode: 'US'}}) {
  return (
    <Layout>
      <form
        action="https://getform.io/f/76aa1111-aaba-491c-be0c-fc47a12142e2"
        className="flex w-full space-x-3"
				method="post"
      >
        <div className="w-full max-w-2xl px-5 py-10 m-auto mt-10 bg-white rounded-lg shadow dark:bg-gray-800">
          <div className="mb-6 text-3xl font-light text-center text-gray-800 dark:text-white">
            Contact us !
          </div>
          <div className="grid max-w-xl grid-cols-2 gap-4 m-auto">
            <div className="col-span-2 lg:col-span-1">
              <div className=" relative ">
                <input
                  type="text"
                  id="contact-form-name"
                  className=" rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
                  placeholder="Name"
                />
              </div>
            </div>
            <div className="col-span-2 lg:col-span-1">
              <div className=" relative ">
                <input
                  type="text"
                  id="contact-form-email"
                  className=" rounded-lg border-transparent flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
                  placeholder="email"
                />
              </div>
            </div>
            <div className="col-span-2">
              <label className="text-gray-700" htmlFor="name">
                <textarea
                  className="flex-1 appearance-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 rounded-lg text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
                  id="comment"
                  placeholder="Enter your comment"
                  name="comment"
                  rows={5}
                  cols={40}
                  defaultValue={'                            '}
                />
              </label>
            </div>
            <div className="col-span-2 text-right">
              <button
                type="submit"
                className="py-2 px-4  bg-indigo-600 hover:bg-indigo-700 focus:ring-indigo-500 focus:ring-offset-indigo-200 text-white w-full transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2  rounded-lg "
              >
                Send
              </button>
            </div>
          </div>
        </div>
      </form>
    </Layout>
  );
}
```

## デプロイ
[公式ドキュメント](https://shopify.dev/custom-storefronts/hydrogen/deployment#deploy-to-cloudflare-workers)を参考に進めました。

1. CLOUDFRAREにアクセスしてWorkersのサービスを作成します。
左メニュー　→　Workers　→　サービスを作成
![](https://storage.googleapis.com/zenn-user-upload/67aad46235a0-20220419.png)

サービス名を設定して「サービスの作成」ボタン
![](https://storage.googleapis.com/zenn-user-upload/b30dbbcf7d2e-20220419.png)


2. 設定ファイル、workerファイル作成
    1. ルートに２つのファイルを新規作成
    2. wrangler.toml （デプロイの設定ファイル）
        
        ```jsx
        name = "PROJECT_NAME" <- ここは、自分の設定したサービス名
        type = "javascript"
        account_id = ""
        workers_dev = true
        route = ""
        zone_id = ""
        compatibility_date = "2022-01-28"
        compatibility_flags = ["streams_enable_constructors"]
        
        [site]
        bucket = "dist/client"
        entry-point = "dist/worker"
        
        [build]
        upload.format = "service-worker"
        command = "yarn && yarn build"
        ```
        
    3. worker.js
        
        ```jsx
        // If the request path matches any of your assets, then use the `getAssetFromKV`
        // function from `@cloudflare/kv-asset-handler` to serve it. Otherwise, call the
        // `handleRequest` function, which is imported from your `App.server.jsx` file,
        // to return a Hydrogen response.
        import {getAssetFromKV} from '@cloudflare/kv-asset-handler';
        import handleRequest from './src/App.server';
        import indexTemplate from './dist/client/index.html?raw';
        
        function isAsset(url) {
          // Update this RE to fit your assets
          return /\.(png|jpe?g|gif|css|js|svg|ico|map)$/i.test(url.pathname);
        }
        
        async function handleAsset(url, event) {
          const response = await getAssetFromKV(event, {});
        
          // Custom cache-control for assets
          if (response.status < 400) {
            const filename = url.pathname.split('/').pop();
        
            const maxAge =
              filename.split('.').length > 2
                ? 31536000 // hashed asset, will never be updated
                : 86400; // favicon and other public assets
        
            response.headers.append('cache-control', `public, max-age=${maxAge}`);
          }
        
          return response;
        }
        
        async function handleEvent(event) {
          try {
            const url = new URL(event.request.url);
        
            if (isAsset(url)) {
              return await handleAsset(url, event);
            }
        
            return await handleRequest(event.request, {
              indexTemplate,
              cache: caches.default,
              context: event,
              // Buyer IP varies by hosting provider and runtime. You should provide this
              // as an argument to the `handleRequest` function for your runtime.
              // Defaults to `x-forwarded-for` header value.
              buyerIpHeader: 'cf-connecting-ip',
            });
          } catch (error) {
            return new Response(error.message || error.toString(), {status: 500});
          }
        }
        
        addEventListener('fetch', (event) => event.respondWith(handleEvent(event)));
        ```
        
3. CloudFlareのKV asset handlerをインストール
    
    ```jsx
    npm install @cloudflare/kv-asset-handler
    ```
    
4. package.json 変更
    
    ```jsx
    // 以下の行を削除
    "build:worker": "cross-env WORKER=true vite build --outDir dist/worker --ssr @shopify/hydrogen/platforms/worker",
    
    // 以下の行を追加
    "build:worker": "cross-env WORKER=true vite build --outDir dist/worker --ssr worker",
    ```
    
5. アカウントIDを設定して、デプロイ
    
    ```jsx
    CF_ACCOUNT_ID=<YOUR_CLOUDFLARE_ACCT_ID> wrangler publish
    ```
    
    アカウントIDの設定は一度しておくと、その後は、wrangler publish だけでデプロイできます


# おわりに
いかがでしたでしょうか？
フレームワークというのはつくづくありがたいものだと思いました。
しかし、こういうものが実務レベルでサワれるようになるにはまだまだです。
出来る日がくるのだろうかと心配になるときもありますが、
とにかくコツコツとぼちぼちと続けていこうと思います。