😀

Cloudflare R2 + Reactで絵文字ピッカーを自作してnpmパッケージとして公開してみた

2023/09/03に公開

はじめに

こんにちは。今回は絵文字ピッカーを自作してみた記事です。
npm パッケージとして公開しており、誰でも使える OSS になっているので、どうぞ使ってみていただけたら嬉しいです。

↓GitHub
https://github.com/yajihum/microsoft-3d-emoji-picker

作ったものの概要

作ったものは、Microsoft Fluent Emoji の 3D バージョンを使った絵文字ピッカーです。(厳密には絵"文字"を取得できるというのではなく、絵文字の画像を取得できるもの)
↓ こんな感じ
Microsoft Fluent Emojiを使った絵文字ピッカー

Microsoft Fluent Emoji について

Micorsoft の Fluent Emoji とは、Microsoft 社が作った絵文字ライブラリで、以下の GitHub と Figma で OSS として公開されています。
https://github.com/microsoft/fluentui-emoji
https://www.figma.com/community/file/1138254942249677742

Teams のチャットや Microsoft Loop で使える絵文字ピッカーとして上記のライブラリの 3D バージョンが使われています。
この 3D バージョンのが特に可愛くて個人的にお気に入りで、自分もこの絵文字ピッカーを使いたいと思っていたのですが、OSS として使えそうなライブラリが現状なく、「じゃあ自分で作ってみよー」と思って自作してみることにしました。

↓Loop だとこんな感じで使える
Microsoft Loopで使える絵文字ぴっかー

使用技術

  • Microsoft Fluent Emoji
  • Cloudflare R2, Workers, Pages
  • Figma API
  • React
  • Tailwind CSS
  • Rollup
  • Vite

技術構成

技術構成

実装の流れ

Figma での絵文字画像取得

絵文字の画像はFigma上で取得できるため、Figma API を使って半自動で画像ファイルを取得できるようにしました。
Figma API の実装コードはfigma-zipperというライブラリを参考にしました。

以下のような形で、figma のファイル ID(Figma の URL の~/file/xxxxxx/~の xxxxxx の部分)からコンポーネントたちを取得し、そのコンポーネントの情報から名前に"3D"が含むものだけに絞り込んでいます。
ここで、keys.slice(3200, 4000)の部分は、1 つのファイルに対してコンポーネントが多すぎてタイムアウトしてしまうため、3200 番目から 4000 番目のように手動で番号を変えながら半自動で取得できるようにしました。
(取得できちゃえばいいのでコードの綺麗さはあまり意識していません 🙄)

//指定したファイルのコンポーネントを取得する
const getFileComponents = async (fileId) => {
  const file = await client.file(fileId);
  const fileComponents = file.data.components;
  return fileComponents;
};

// 指定したファイルのコンポーネントの画像を取得する
const getFileComponentsImagesURL = async (fileId, format, scale) => {
  const fileComponents = await getFileComponents(fileId);
  console.log(fileComponents);
  const renderFiles = [];

  let keys = Object.keys(fileComponents);
  let extractKeys = keys.slice(3200, 4000);

  let extractObjects = {};
  for (let i = 0; i < extractKeys.length; i++) {
    let key = extractKeys[i];

    if (fileComponents[key].name.includes("3D")) {
      extractObjects[key] = fileComponents[key];
    }
  }

  for (const nodeId in extractObjects) {
    // コンポーネントがSetかどうか(Variantsが設定されているかどうか)チェック
    console.log(nodeId);
    // 画像を取ってくる
    try {
      const response = await client.fileImages(fileId, {
        ids: [nodeId],
        format: format,
        scale: scale,
      });
      const url = response.data.images[nodeId];
      const fileName = `${fileComponents[nodeId].key}.${format}`;
      renderFiles.push({ url: url, fileName: fileName });
    } catch (error) {
      console.error(`Error! ${error}`);
      process.exit(1);
    }
  }
  return renderFiles;
};
Figma API 実装コードの全文
import dotenv from "dotenv";
import * as Figma from "figma-js";

import { saveFileFromUrlToFs } from "./utils/saveFileToFs.mjs";

// .envの設定を読み込む
dotenv.config();

const currentFileId = process.env.FIGMA_FILE_ID;

const client = Figma.Client({
  personalAccessToken: process.env.FIGMA_TOKEN || "",
});

//指定したファイルの名前を取得する
const getFileName = async (fileId) => {
  const file = await client.file(fileId);
  return file.data.name;
};

//指定したファイルのコンポーネントを取得する
const getFileComponents = async (fileId) => {
  const file = await client.file(fileId);
  const fileComponents = file.data.components;
  return fileComponents;
};

// 指定したファイルのコンポーネントの画像を取得する
const getFileComponentsImagesURL = async (fileId, format, scale) => {
  const fileComponents = await getFileComponents(fileId);
  console.log(fileComponents);
  const renderFiles = [];

  let keys = Object.keys(fileComponents);
  let getKyes = keys.slice(3200, 4000);

  let getObjects = {};
  for (let i = 0; i < getKyes.length; i++) {
    let key = getKyes[i];

    if (fileComponents[key].name.includes("3D")) {
      getObjects[key] = fileComponents[key];
    }
  }

  for (const nodeId in first100Objects) {
    // コンポーネントがSetかどうか(Variantsが設定されているかどうか)チェック
    console.log(nodeId);
    // 画像を取ってくる
    try {
      const response = await client.fileImages(fileId, {
        ids: [nodeId],
        format: format,
        scale: scale,
      });
      const url = response.data.images[nodeId];
      const fileName = `${fileComponents[nodeId].key}.${format}`;
      renderFiles.push({ url: url, fileName: fileName });
    } catch (error) {
      console.error(`Error! ${error}`);
      process.exit(1);
    }
  }
  return renderFiles;
};

// 画像をダウンロードして保存する
const saveFilesToFs = async (files, dest) => {
  for (const file of files) {
    const { url, fileName } = file;
    try {
      await saveFileFromUrlToFs(url, dest, fileName);
    } catch (error) {
      throw new Error("Error saving file ${fileName}");
    }
  }
};

// 指定したFigmaファイルからコンポーネントをダウンロードして保存する
export const exportAssets = async (fileId, dest) => {
  const pngFiles = await getFileComponentsImagesURL(fileId, "png", 2);
  console.log(pngFiles);
  await saveFilesToFs(pngFiles, dest);
};

// ZIP!
export const zipAssets = async (fileId, dest) => {
  const thisFileName = await getFileName(currentFileId);
  console.log(`Start download files from Figma file: ${thisFileName}...`);
  await exportAssets(fileId, dest);
  // await zip({
  //     source: `${dest}/*`,
  //     destination: `./${thisFileName}_${dest}.zip`
  // }).then(function () {
  //     console.log(`Zipped your Figma file: ${thisFileName} to ${thisFileName}_${dest}.zip!`);
  // }).catch(function (err) {
  //     console.error(err.stack);
  //     process.exit(1);
  // });
};

zipAssets(currentFileId, "assets");

画像をリネームする

Figma から取得してきた画像の名前はコンポーネントの key になっています。絵文字画像を表示する際に、笑っている顔/悲しんでいる顔のようにある程度グループ化された状態にしないとユーザーが使いにくくなってしまいます。
そのため、表示したい順番(カテゴリーごとに 1 から順に割り振る)にしてソートできるように画像のリネームをする必要がありました。
これが結構大変で、Loop の絵文字ピッカーを参考にして手動で並び替えました。
(もっといい方法が思いついた方がいればぜひコメントで教えていただきたいです 😇)

R2 に画像をアップロードする

絵文字ピッカー上で絵文字画像を表示するために、どこかのサーバー上に画像を置いておく必要があります。
そこで Loop を見に行くと以下のような形で表示しています。

<div
  aria-hidden="true"
  class="___k280nu0 fbsu25e f5mk7bh f3rmtva f1kpvlpi fk4wq3q f1p9o1ba f1sil6mw"
  style="--emoji-background: url(https://cdn.hubblecontent.osi.office.net/emojis/publish/dc2328ad-23c8-4495-ba9c-bf262d65f017/png/thumbnails/50/s1.png); --emoji-animation-frames-count: 1;"
>
  😉
</div>

cdn.hubblecontent.osi.office.netのドメインからわかるように、CDN を使っているようです。
確かに、絵文字は大量になるのでなるべく速く全て表示できるようにするためには CDN を使って工夫する必要がありそうです。

そこで CDN といえば Cloudflare のサービスを使えば無料で利用できそうなので、画像などのファイルやオブジェクトをサーバー上に格納できるサービスである Cloudflare R2 を使うことにしました。
これはエグレス(下り料金)が無料なので、GET は制限なく無料で利用できるのも最高です!

Wokers 経由で画像をアップロードすることもできのですが、R2 のダッシュボード上でやりました。
詳しくは以下を参考にしました。
https://zenn.dev/chimame/articles/0b28972e5ded3a

画像にアクセスする際のドメインは色々なパターンが利用できるのですが、今回は自分で取得していたドメインを使ってcdn.emoji.yajium.dayでアクセスできるようにしました。

Workers 経由で R2 のオブジェクトにアクセスする

R2 上にある画像オブジェクトにアクセスするために Workers を使いました。
以下のように GET アクセス用のコードを追加しています。
https://github.com/yajihum/microsoft-3d-emoji-picker/blob/main/workers/src/worker.ts#L16-L29

以下を参考にして作成しています。
https://developers.cloudflare.com/r2/api/workers/workers-api-reference/

絵文字ピッカーのutil.tsで EmojiList を取得する際に、Workers のデプロイ先から fetch して R2 に格納されている画像の名前を取得できるようにしています。
https://github.com/yajihum/microsoft-3d-emoji-picker/blob/main/packages/src/util.ts#L8-L23

ビルドする

そんなこんなで React を使って絵文字ピッカーコンポーネントが作成できたら、npm パッケージとして公開できるようにビルドする必要があります。
今回ビルドツールにはRollupを使用しました。

ビルドする際の input として参照する際にsrc/index.tsを指定しています。
このファイルにはユーザーが絵文字ピッカーコンポーネントを利用する際にインポートできるものをエクスポートしておきます。

src/index.ts
export { Picker } from "./components/picker";
export type { EmojiType } from "./type";
rollup.config.mjs の中身
tsconfig.json の中身

以下を参考に少し変えています。
https://zenn.dev/shimabukuromeg/articles/f48e5905e95460

また、今回絵文字ピッカーコンポーネントに Tailwind CSS を使ってスタイルを当てているのですが、そのままビルドしただけではユーザーがパッケージを使う際に Tailwind CSS もインストールした状態でないと利用できなくなってしまうため、ビルド時に CSS ファイルも出力できるようにしました。
postcss-cliautoprefixerをインストールして、package.json に以下のスクリプトを追加して CSS もビルドできるようにします。
https://github.com/yajihum/microsoft-3d-emoji-picker/blob/main/packages/package.json#L24-L28

npm にパッケージを公開する

package.json に公開するファイルたちを指定して、依存パッケージの記載やその他情報を記載します。

package.json の中身

npm publishで公開完了です!

デモサイトを公開する

GitHub に載せるデモサイトを作成するために、Vite を使用して簡単なアプリケーションを作成します。
以下の vite プロジェクトで管理しています。
https://github.com/yajihum/microsoft-3d-emoji-picker/tree/main/vite

実際に npm に公開したパッケージをインストールしてみます。

npm install ms-3d-emoji-picker

そして、EmojiType と Picker、css のインポートを追加して完了です!
https://github.com/yajihum/microsoft-3d-emoji-picker/blob/main/vite/src/App.tsx#L1-L90

↓ 完成したデモサイト
https://microsoft-3d-emoji-picker.pages.dev/

今後の課題

急いで作ったのもあり、まだまだ改善の余地ありの状態です。以下を主に対応していきたいと思っています。

  • テストコードを書く
  • カテゴリーナビケーション部分の連動(Intersection Observer API)を正しく動作させる
  • i18n の対応
  • ダークモードの対応
  • デザイン修正
    などなど

その他要望がありましたら是非issueを作ってくださると嬉しいです✨

終わりに

絵文字ピッカーを自作して npm パッケージとして公開するまでを紹介しました!
今の所 Microsoft Fluent Emoji の 3D バージョンをピッカーとして利用できるものはないのでぜひ使ってみてもらえると嬉しいです!😊

https://github.com/yajihum/microsoft-3d-emoji-picker

GitHubで編集を提案

Discussion