🦚

Chrome拡張機能をReactで作ってみた

2024/12/12に公開

はじめに

こんにちは。
師走ですね。12月に入ってから一段と寒さと乾燥がひどくなったように感じます。

厳しい冬の寒さにさらされると、必然、物思いに耽る方も多くなることでしょう。私もその1人です。

私は普段、Javaでバックエンドのロジックを担当しています。逆にフロント側を構築することはありません。
なのでこの厳しい寒さの中、夜、布団に入るとふと思うのです。

「...ああ🥶」
「...フロント、触ってみたいな🙃」
「...Reactって」
「なんだか響きだけでかっこいいな🎵」

ミーハー心のままネットを漁っていたところ、こちらの記事を発見。
https://zenn.dev/alvinvin/books/chrome_extension

なんと!ReactでChromeの拡張機能が作れるそうです。
しかも出来合いのスターターキットがあるではないですか!
https://github.com/sinanbekar/browser-extension-react-typescript-starter

これなら気軽にできそう🎵ということで。
今回はこれらを参考に、Chrome拡張機能を作ってみました。

セットアップ

簡単です。

  • nodeやyarnをイントールして
  • テンプレートをクローンして
  • yarn dev

これだけです。お手軽ですね〜

※詳細な環境構築については参考記事をご覧ください

作ったもの

あとはテンプレートを自分の作りたいものに改造していく作業です。
私はコードレビューなどでよく見る略語(LGTM、IMO…)のリストを表示する機能を実装しました。

開発中、突然イカした略語が飛んでくること、ありませんか?
不意の3文字に目が回ること、ありますよね?
もう大丈夫。これで完璧に対応できます。


プルダウンから選ぶと...


なるほど!(略語のコピーもできます)

実装

主に実装したのはPopup.tsxTranslator.tsx
Popup.tsxに略語の全データを保持しておき、プルダウンで選択されたデータをTranslator.tsxに渡して意味や語源が表示されるようにしました。

Popup.tsx
import { getBucket } from '@extend-chrome/storage';
import { Container, Select } from '@mantine/core';
import { useEffect, useState } from 'react';
import { Translator } from '../app/features/translator/Translator';

interface MyBucket {
  targetLang: string | null;
}

const bucket = getBucket<MyBucket>('my_bucket', 'sync');

const Popup = () => {
  document.body.style.width = '20rem';
  document.body.style.height = '20rem';

  const [lang, setLang] = useState<string | null>(null);

  const translations: Record<string, { original: string; japanese: string }> = {
    Q: { original: 'Question', japanese: '質問' },
    WIP: { original: 'Work In Progress', japanese: '作業中です' },
    FYI: { original: 'For Your Information', japanese: '参考までに' },
    IMO: { original: 'In My Opinion', japanese: '私が思うに、私の意見としては' },
    LGTM: { original: 'Looks Good To Me', japanese: '良いと思います' },
    SSIA: { original: 'Subject Says It All', japanese: '件名の通り' },
    ASAP: { original: 'As Soon As Possible', japanese: 'できるだけ早く' },
    IMHO: { original: 'In My Humble Opinion', japanese: '私のつたない意見では' },
    NITS: { original: 'Nitpick', japanese: '細かい指摘' },
    'TL;DR': { original: 'Too Long; Didn’t Read', japanese: '長文なので要約を載せます' },
    AFAIK: { original: 'As Far As I Know', japanese: '私の知る限りでは' },
    GOTCHA: { original: 'I have got you', japanese: 'わかりました' },
  };

  useEffect(() => {
    (async () => {
      const value = await bucket.get();
      if (value.targetLang) {
        setLang(value.targetLang);
      }
    })();
  }, []);

  const saveLang = (lang: string | null) => {
    bucket.set({ targetLang: lang });
    setLang(lang);
  };

  return (
    <Container p="xl">
      <Select
        label="List of abbreviations"
        value={lang}
        onChange={saveLang}
        data={Object.keys(translations).map((key) => ({ value: key, label: key }))}
        clearable
      />
      {lang && (
        <Translator
          targetText={lang}
          originalText={translations[lang].original}
          japaneseText={translations[lang].japanese}
        />
      )}
    </Container>
  );
};

export default Popup;

Translator.tsx
import { ActionIcon, CopyButton, Tooltip, Text, Box, Divider, Stack } from '@mantine/core';
import { MdDone, MdOutlineContentCopy } from 'react-icons/md';

export interface TranslatorProps {
  targetText: string;
  originalText: string;
  japaneseText: string;
}

export const Translator: React.FC<TranslatorProps> = ({
  targetText,
  originalText,
  japaneseText,
}) => {
  return (
    <Box
      sx={{
        padding: '16px',
        backgroundColor: '#ffffff',
        boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)',
        borderRadius: '12px',
        maxWidth: '400px',
        margin: '0 auto',
      }}
    >
      <Stack spacing="xs">
        <Text weight={700} size="lg" style={{ color: '#343a40' }}>
          語源:
          <Text component="span" color="blue" inherit ml="xs">
            {originalText}
          </Text>
        </Text>
        <Text weight={700} size="lg" style={{ color: '#343a40' }}>
          意味:
          <Text component="span" color="teal" inherit ml="xs">
            {japaneseText}
          </Text>
        </Text>
      </Stack>
      <Divider my="sm" />
      <CopyButton value={targetText}>
        {({ copied, copy }) => (
          <Tooltip label={copied ? 'コピーしました' : 'クリップボードにコピー'} withArrow>
            <ActionIcon
              onClick={copy}
              size="lg"
              variant="filled"
              style={{
                backgroundColor: copied ? '#38c172' : '#f0f0f0',
                color: copied ? '#ffffff' : '#343a40',
                transition: 'background-color 0.3s ease',
              }}
            >
              {copied ? <MdDone /> : <MdOutlineContentCopy />}
            </ActionIcon>
          </Tooltip>
        )}
      </CopyButton>
    </Box>
  );
};

まとめ

出来合いのものがある分、かなりとっつきやすかったです。

  • Reactで手を動かしてみたい方
  • Chrome拡張機能をサッと作ってみたい方

にはおすすめのテンプレートです。

ただその分Reactとは何かはほとんど理解しないまま作業していました。
自学を進めたいと思います。。

GMOメディアテックブログ

Discussion