⚡️

microCMSの管理画面にポケモンフィールドを作る

2022/05/31に公開

こんにちは。りゅーそうです。

エンジニアの方々の中にはポケモントレーナーを兼務されている方も多いのではないでしょうか?
個人ブログなどで記事を書かれている方は、文章の中でポケモンを紹介したり、唐突にポケモンの画像を掲載したくなることもあるかと思います(なさそう)。

ヘッドレスCMSであるmicroCMSでは自分で作成したWebアプリを使ってフィールドを拡張する「拡張フィールド」という機能があります。
この機能を使えば、ポケモンを管理画面上で自由に追加することが可能です。

実際に今回作成したのは、以下のようなフィールドです。このように自作のフィールドを管理画面に組み込むことができます。
microCMSにポケモン一覧画面が追加されている
もちろんAPIベースのヘッドレスCMSなので、叩いてポケモンのデータを取得することもできます。

microCMSの拡張フィールドとは?

ドキュメント
https://document.microcms.io/manual/field-extension

拡張フィールド(旧iframeフィールド)は、microCMSのフィールドを外部データと連携して拡張する機能です。
具体的には自社データベースと連携したり、外部APIと連携したりが可能です。

https://blog.microcms.io/rename-iframe-to-field-extension/ より)

内部的にはiframeを使用しており、様々な外部データと連携してフィールドを拡張することができます。
管理画面でのフィールドの設定方法などはドキュメントを参照すると良いかと思います。

microCMS-field-extension(SDK)

拡張フィールドを活用するには、microCMSの管理画面とは別に自身で画面を作成する必要があります。
SDKを使用すると、iframe独自の実装を意識せずに実装をすることができるので便利です。
https://github.com/microcmsio/microcms-field-extension

React以外を使って開発する場合→ microcms-field-extension-api を使用
https://github.com/microcmsio/microcms-field-extension/tree/main/packages/api

Reactを使って開発する場合→ microcms-field-extension-react を使用
https://github.com/microcmsio/microcms-field-extension/tree/main/packages/react

今回は microcms-field-extension-react を使って開発しました。
create-react-appやNext.js用のテンプレートもあるので、そちらを使うのも便利だと思います。

このライブラリは useFieldExtension フックを使うことにmicroCMSとのデータ連携を行うことがきます。
第一引数はmicroCMSに送信したいデータ
第二引数には各種設定をすることができます。
設定については、このあたりをみると良いかと思います。

  const { data, sendMessage } = useFieldExtension(pokemonData, {
    origin: process.env.REACT_APP_MICROCMS_ORIGIN,
    height: 500,
  });

PokeAPIについて

ポケモンフィールドを作成するために、今回はPokeAPIを使用しました。
このAPIは有志によって作成されているポケモンの様々なデータを取得することができるAPIです
https://pokeapi.co/

またPokemon databaseを見て取得可能なデータを参照しながら開発すると捗ると思います。
https://pokemondb.net/

拡張フィールドではこのような外部APIと連携してフィールドを作成することができます。もちろんAPIやDBで自作することもできるかと思います。

実装方法

それでは、ポケモンフィールドの作り方を簡単に紹介します。
全容はこちらのリポジトリを参照ください。
https://github.com/YouheiNozaki/pokemon-microcms

使用ライブラリ

前述した Reactmicrocms-field-extensition を使用しました。
環境構築はCRAで行いました。
また、状態管理としてRecoil、PokeAPIを呼び出すのにReact Queryを使用しています。
理由は使ってみたかったというだけです。

ポケモンの詳細データはReact Queryで取得するフックを作成しています。
nameの変更を検知して、RefetchしているのでReact Queryは必要ないような気もしますが、気にしないでください。
エラー状態とローディングの状態を返す実装が簡単に作れるので、それだけで便利です。
(PokeAPIはいくつか対応していないポケモンが紛れているので、エラー処理はちゃんと作りました)

実装(usePokemon.ts)
usePokemon.ts
import { useEffect } from 'react';
import { useQuery } from 'react-query';
import type { Pokemon } from '../types/pokemon';

type UsePokemonReturnValue = {
  pokemonData: Pokemon;
  pokemonIsError: boolean;
  pokemonIsLoading: boolean;
  pokemonIsRefetching: boolean;
  pokemonIsRefetchError: boolean;
};
type UsePokemon = (name: string) => UsePokemonReturnValue;

export const usePokemon: UsePokemon = (name) => {
  const getPokemon = async () => {
    if (!name) {
      return;
    }
    const pokemon = await fetch(
      `https://pokeapi.co/api/v2/pokemon/${name}`,
    ).then((res) => {
      if (!res.ok) {
        throw new Error();
      }
      return res.json();
    });

    return pokemon;
  };

  const {
    data: pokemonData,
    isError: pokemonIsError,
    isLoading: pokemonIsLoading,
    refetch: pokemonRefetch,
    isRefetching: pokemonIsRefetching,
    isRefetchError: pokemonIsRefetchError,
  } = useQuery('pokemon', getPokemon, {
    staleTime: Infinity,
  });

  useEffect(() => {
    if (name) {
      pokemonRefetch();
    }
  }, [name, pokemonRefetch]);

  return {
    pokemonData,
    pokemonIsError,
    pokemonIsLoading,
    pokemonIsRefetching,
    pokemonIsRefetchError,
  };
};

個別にポケモンを呼ぶためのポケモンの name はRecoilで管理しています。
以下のようなフックを作成してどのコンポーネントからも呼び出せるようにしています。

実装(usePokemonState.ts)
usePokemonState.ts
import { useCallback } from 'react';
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';
type Pokemon = {
  name: string;
};

const pokemonState = atom<Pokemon>({
  key: 'pokemonState',
  default: undefined,
});

export const usePokemonState = () => {
  const pokemonValue = useRecoilValue<Pokemon>(pokemonState);

  return { pokemonValue };
};

export const usePokemonMutators = () => {
  const setPokemonValue = useSetRecoilState<Pokemon>(pokemonState);

  const setPokemon = useCallback(
    (pokemon: Pokemon) => {
      setPokemonValue(pokemon);
    },
    [setPokemonValue],
  );

  return { setPokemon };
};

microCMSにデータ送信する

実装の肝になる部分です。

コード全容
import { useFieldExtension } from 'microcms-field-extension-react';

import { Error } from '../ui/Error';
import { Loading } from '../ui/Loading';
import { usePokemonState } from '../../hooks/usePokemonState';
import { usePokemon } from '../../hooks/usePokemon';
import styles from './pokemondetail.module.scss';
import { useEffect, useMemo } from 'react';

export const PokemonDetail = () => {
  const { pokemonValue } = usePokemonState();
  const {
    pokemonData,
    pokemonIsError,
    pokemonIsLoading,
    pokemonIsRefetching,
    pokemonIsRefetchError,
  } = usePokemon(pokemonValue?.name);

  const url = process.env.REACT_APP_MICROCMS_ORIGIN || '';
  const { data, sendMessage } = useFieldExtension(pokemonData, {
    origin: url,
    height: 500,
  });

  useEffect(() => {
    if (pokemonData) {
      sendMessage({
        id: pokemonData.name,
        title: pokemonData.name,
        imageUrl: pokemonData.sprites?.front_default,
        updatedAt: new Date().toISOString(),
        data: {
          id: pokemonData.id,
          name: pokemonData.name,
          height: pokemonData.height,
          weight: pokemonData.weight,
          abilities: pokemonData.abilities,
          stats: pokemonData.stats,
          sprites: pokemonData.sprites,
        },
      });
    }
  }, [pokemonData, sendMessage]);

  const pokemonDetail = useMemo(() => {
    if (pokemonData) {
      return pokemonData;
    }
    return data;
  }, [data, pokemonData]);

  if (pokemonIsLoading || pokemonIsRefetching) {
    return <Loading />;
  }

  if (pokemonIsError || pokemonIsRefetchError) {
    return <Error text="取得できないポケモンです" />;
  }

  return (
    <div className={styles.main}>
      {pokemonDetail ? (
        <div className={styles.pokemondetails}>
          <div className={styles.header}>
            <img
              alt={`${pokemonDetail.name}の画像`}
              src={pokemonDetail.sprites?.front_default}
              width={120}
              height={120}
              className={styles.img}
            />
            <div>
              <p className={styles.id}>No.{pokemonDetail.id}</p>
              <h3 className={styles.name}>{pokemonDetail.name}</h3>
            </div>
          </div>
          <div className={styles.typeWrapper}>
            <p className={styles.typeTitle}>Type</p>
            <ul className={styles.typeList}>
              {pokemonDetail.types?.map((type, i: number) => (
                <li key={i} className={styles.type}>
                  {type.type.name}
                </li>
              ))}
            </ul>
          </div>
          <div className={styles.abilityWrapper}>
            <p className={styles.typeTitle}>Ability</p>
            <ul className={styles.typeList}>
              {pokemonDetail.abilities?.map((ability, i: number) => (
                <li key={i} className={styles.type}>
                  {ability.ability.name}
                </li>
              ))}
            </ul>
          </div>
          <div className={styles.abilityWrapper}>
            <p className={styles.typeTitle}>Weight</p>
            <p className={styles.unit}>
              <span>{pokemonDetail.weight}</span>
              kg
            </p>
            <p className={styles.typeTitle}>Height</p>
            <p className={styles.unit}>
              <span>{pokemonDetail.height}0</span>
              cm
            </p>
          </div>
          <div className={styles.stats}>
            {pokemonDetail.stats?.map((stats, i: number) => (
              <div key={i} className={styles.stat}>
                <p className={styles.typeTitle}>{stats.stat.name}</p>
                <p className={styles.statValue}>{stats.base_stat}</p>
              </div>
            ))}
          </div>
        </div>
      ) : (
        <div className={styles.empty}>
          <p className={styles.emptyText}>
            ポケモンをクリックしてここに追加しよう
          </p>
        </div>
      )}
    </div>
  );
};

肝になるのは以下の部分です。

  const url = process.env.REACT_APP_MICROCMS_ORIGIN || '';
  const { data, sendMessage } = useFieldExtension(pokemonData, {
    origin: url,
    height: 500,
  });

  useEffect(() => {
    if (pokemonData) {
      sendMessage({
        id: pokemonData.name,
        title: pokemonData.name,
        imageUrl: pokemonData.sprites?.front_default,
        updatedAt: new Date(),
        data: {
          id: pokemonData.id,
          name: pokemonData.name,
          height: pokemonData.height,
          weight: pokemonData.weight,
          abilities: pokemonData.abilities,
          stats: pokemonData.stats,
          sprites: pokemonData.sprites,
        },
      });
    }
  }, [pokemonData, sendMessage]);

microCMSに送信したいデータは sendMessage を使って送信します。
このようなデータを送信すると、microCMS側では以下のようなレスポンスを取得することができるようになります。

ピカチュウのレスポンスの例(一部)
{
  "id": "9qn7nevtcx6",
  "createdAt": "2022-05-21T13:02:54.389Z",
  "updatedAt": "2022-05-29T16:35:43.591Z",
  "publishedAt": "2022-05-21T13:02:54.389Z",
  "revisedAt": "2022-05-29T16:35:43.591Z",
  "title": "ピカチュウの紹介をします",
  "body": "<p>サトシととても仲が良いよ。<br>かわいいよ。</p>",
  // ここから拡張フィールドで外部から取得したデータ
  "pokemon": {
    "abilities": [
      {
        "is_hidden": false,
        "ability": {
          "name": "static",
          "url": "https://pokeapi.co/api/v2/ability/9/"
        },
        "slot": 1
      },
    ],
    "name": "pikachu",
    "weight": 60,
    "id": 25,
    "sprites": {
      "front_female": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/female/25.png",
      "back_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/back/shiny/25.png",
      "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/25.png"
    },
    "height": 4
  },
  "day": "2022-05-29T15:00:00.000Z"
}

idなど送信することで、microCMSのコンテンツ一覧画面にポケモンが並んでいい感じになります。
PokeAPIのデータに加えて、別のフィールドを作成してコンテンツを自分で追加できるのも嬉しいですね。
microCMS一覧画面

最後に

拡張フィールドは個人開発はなかなかハードルが高いし....サクッといろんな技術を試したいという時に、お手軽に作れて良いと思います。
microCMSの管理画面をポケモンまみれにしてみてください!

実装の詳細は要望があれば(コードをもう少しまともなものにできたらしつつ)、別途紹介できればと思います。

Discussion