（2024年7月現在）NuxtでreCAPTCHA v2を実装する方法

# はじめに
Nuxt.js（Vue.js）は便利なFWですが、ライブラリのサポートに難があり、過去動いていたライブラリが動かないことも結構多いです。

特に、reCAPTCHA v2のサポート状況は厳しく、下記のような有様です。

## vue-recaptcha
v3はまだアルファ版でうまく動かないし、かといって最後の安定版であるv2.0.3はVue最新版でnpm installするとコンフリクトエラー発生
（yarnだと依存関係が解消できて利用できるのですが、npmで利用できないライブラリを無理やり入れたくない）

https://github.com/DanSnow/vue-recaptcha

## nuxt-community/recaptcha-module
nuxtコミュニティから提供されている公式モジュールですが、
**2023年の3月**から割と重大なバグが放置されています。
~~諦めてReact使えってことですかね。~~

https://github.com/nuxt-community/recaptcha-module/issues/123

https://github.com/nuxt-community/recaptcha-module

なので自分で実装して解決しました。

# 実装手順
## ①：環境変数設定
reCAPTCHAのサイトキーをNuxt.js内で利用できるよう、設定します。

```ts: nuxt.config.ts
export default defineNuxtConfig({
~~~
    public: {
      recaptchaKey: "ここに.envファイルで定義したサイトキーの環境変数を入れる"
    }
~~~
})
```

## ①：プラグイン作成
`plugin/recaptcha.client.ts`を作成します。

元々は`nuxt.config.ts`でグローバルに`https://www.google.com/recaptcha/api.js`を読み込んでいたのですが、
プラグインに変更したことで、必要な場所のみreCAPTCHAが読み込まれるようになりました。

```ts
import { defineNuxtPlugin } from '#app'

export default defineNuxtPlugin((nuxtApp) => {
  const loadRecaptcha = () => {
    return new Promise<void>((resolve) => {
      if (typeof window !== 'undefined' && window.grecaptcha) {
        resolve()
        return
      }

     const existingScript = document.querySelector('script[src^="https://www.google.com/recaptcha/api.js?render=explicit"]')
      if (existingScript) {
        existingScript.addEventListener('load', () => resolve())
      } else {
        const script = document.createElement('script')
        script.src = 'https://www.google.com/recaptcha/api.js?render=explicit'
        script.async = true
        script.defer = true
        script.onload = () => resolve()
        document.head.appendChild(script)
      }
    })
  }

  nuxtApp.provide('loadRecaptcha', loadRecaptcha)
})

```

### ポイント
複数プロジェクトで利用していてもscriptタグが複数作られないよう、すでに存在しているかどうかを確認しています。
既に存在している場合もNuxtで利用できる状態でない可能性があるため、読み込みを検知し次第`resolve()`を実行しています。

## ②：コンポーネント作成
実際に画面上にreCAPTCHAを表示するためにコンポーネントを作成します。

今回はreCAPTCHAで認証できたか、期限切れかを知りたいため、`onVerified`と`onExpired`の二つの関数を定義しています。
（エラー時のコールバックも作成可能です。詳しくは[こちら](https://cloud.google.com/recaptcha-enterprise/docs/api-ref-checkbox-keys?hl=ja)）

```vue: recaptcha.vue
<template>
  <div>
    <div id="recaptcha" class="g-recaptcha" :data-sitekey="sitekey" />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const { $loadRecaptcha } = useNuxtApp()
const config = useRuntimeConfig()
const sitekey = config.public.recaptchaKey
const isExpired = ref(false)

const emit = defineEmits<{
  (e: 'verified', response: string): void
  (e: 'expired'): void
}>()

const renderRecaptcha = () => {
  if ((window as any).grecaptcha && (window as any).grecaptcha.render) {
    try {
      (window as any).grecaptcha.render('recaptcha', {
        sitekey: sitekey,
        callback: onVerify,
        'expired-callback': onExpired
      })
    } catch (e) {
    }
  } else {
    setTimeout(renderRecaptcha, 100)
  }
}

const onVerify = (response: string) => {
  console.log('Verified:', response)
  isExpired.value = false
  emit('verified', response)
}

const onExpired = () => {
  console.log('reCAPTCHA expired')
  isExpired.value = true
  emit('expired')
}

onMounted(async () => {
  await $loadRecaptcha()
  renderRecaptcha()
})
</script>

```

できた！
![スクリーンショット 2024-07-08 16.04.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/303906/73e1f790-e7a3-ce9b-dc1b-2e13835c2a9d.png)