Nuxt Nation カンファレンスが開催されます。11月12~13日にご参加ください。

precognition
nuxt-precognition

Nitro で Laravel Precognition プロトコルを実装する Nuxt モジュール

Nuxt Precognition

npm versionnpm downloadsLicenseNuxt

これは nuxt-laravel-precognition の新しいバージョンです。同じ機能を提供しますが、Laravel に依存しません。

$fetchLaravel のみをサポートする代わりに、基本的な Precognition プロトコルを実装する任意のバックエンドを対象とした、シンプルな Promise で動作します。これらの Promise は、フォーム payload とプロトコル Headers を受け取ります。

interface User = {
  email: string
  password: string
}

const form = useForm(
  (): User => ({ email: '', password: '' }),
  (body, headers) => $fetch('/api/login', { method: 'POST', headers, body })
)

このモジュールにはネイティブの Nitro 統合が付属していますが、他のバックエンドでも動作します。

Lambda のみを使用していますか?Lambda Precognition でカバーされます!!

任意のバリデーションライブラリ (Zod と言った人は誰ですか?) をサーバー側またはクライアント側でサポートします。特定の Error parsers を設定するだけで済みます。

機能

  •  Laravel に準拠
  •  バリデーションライブラリに依存しない
  •  クライアント側とサーバー側のバリデーション
  •  最適な TypeScript サポート
  •  高度なカスタマイズ性

仕組み

すべては errorParsers (エラー payload からバリデーションエラーを読み取るためのユーザー定義関数) を中心に展開します。

type ValidationErrors = Record<string, string | string[]>

interface ValidationErrorsData {
  message: string
  errors: ValidationErrors
}

type ValidationErrorParser = (error: Error) => ValidationErrorsData | undefined | null

これらはグローバル ( Nuxt Plugin 内またはカスタム eventHandler) で、または form インスタンスごとに定義できます。

Zod を使用しているとしましょう。
nuxt プラグインを作成し、「Zod エラーパーサー」を定義するだけです。

// plugins/precognition.ts

export default defineNuxtPlugin(() => {
  const { $precognition } = useNuxtApp()

  $precognition.errorParsers.push(
    (error) => {
      if (error instanceof ZodError) {
        const errors = {} as Record<string, string[]>
        error.errors.forEach((e) => {
          const key = e.path.join('.')
          if (key in errors) {
            errors[key].push(e.message)
            return
          }
          errors[key] = [e.message]
        })
        return { errors, message: 'Validation error' }
      }
      return null
    },
  )
})

これからは、useForm がエラーをキャッチするたびに、パーサーが実行され、バリデーションエラーをキャプチャして割り当てます。

複数のページで同じオプションを再利用する場合は、useForm.create ファクトリ関数を使用して、カスタムコンポーザブルを作成できます。

サーバー側はどうですか

同じ考えで、nitro プラグインを作成します

// server/plugins/precognition.ts

import { ZodError } from 'zod'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', (event) => {
    event.context.$precognition.errorParsers = [
      (error) => {
        if (error instanceof ZodError) {
          const errors: Record<string, string[]> = {}
          error.errors.forEach((e) => {
            const key = e.path.join('.')
            if (key in errors) {
              errors[key].push(e.message)
              return
            }
            errors[key] = [e.message]
          })
          const message = error.errors.at(0)?.message ?? 'Validation error'
          return { errors, message }
        }
      },
    ]
  })
})

すべてのリクエストにフックするのが好きでない場合は、definePrecognitiveEventHandler.create ファクトリ関数を使用して、カスタム eventHandler を作成できます。

definePrecognitiveEventHandleronRequest ハンドラー内でバリデーションロジックを作成します。

// server/api/login.post.ts
import { z } from 'zod'
import { definePrecognitiveEventHandler, readBody } from '#imports'

const loginSchema = z.object({
  email: z.string().email().refine(_email => // Check for email uniqueness
    true, { message: 'Email is already in use' },
  ),
  password: z.string(),
}).refine((_data) => {
  // Check for email and password match
  // ...
  return true
},
{ message: 'invalid credentials', path: ['email'] },
)

export default definePrecognitiveEventHandler({
  async onRequest(event) {
    const body = await readBody(event)
    loginSchema.parse(body)
  },
  handler: () => {
    return {
      status: 200,
      body: {
        message: 'Success',
      },
    }
  },
})

今回は、エラーが NuxtServerValidationError に変換され、nuxt 構成ファイルで事前定義されたパーサーを有効にすると、クライアント側でキャプチャされます。

// nuxt.config.ts

export default defineNuxtConfig({
  modules: ['nuxt-precognitiion'],
  precognition: {
    backendValidation: true,
    enableNuxtClientErrorParser: true,
  }
})

ValidationError をスローするのは、onRequest ハンドラー内のみ ( オブジェクト表記を使用) であることを忘れないでください。.
ベースの handler 内のロジックは、precognitiveRequests 中には処理されません。

  • event.context には、リクエストが予知であるかどうかを示すフラグ ({ precognitive: boolean }) も含まれており、Precognitive ヘッダーの存在を調べています。

予知プロトコル

nitro の外部 (AWS Lambda) で独自のバックエンドロジックを定義する必要がある場合は、次の要件リストを尊重してください。

  • 予知リクエストには以下が必要です
    1. 予知ヘッダー { 'Precognitive': 'true' }
  • 特定の変数を検証するには、各キーを ValidateOnly ヘッダー内で、カンマ区切りでドット表記を利用して指定する必要があります { 'Precognition-Validate-Only': 'name,age,address.street,address.number' }
  • フォーム全体を検証するには、ValidateOnly ヘッダーを省略するか、空の文字列として定義する必要があります。
  • 成功したバリデーション応答には以下が必要です
    1. 予知ヘッダー { 'Precognitive': 'true' }
    2. 予知成功ヘッダー { 'Precognition-Success': 'true' }
    3. 予知成功ステータスコード: 204
  • エラーバリデーション応答には以下が必要です
    1. 予知ヘッダー { 'Precognitive': 'true' }
    2. 必要に応じて ValidationOnly ヘッダー { 'Precognition-Validate-Only': 'name,age,address.street,address.number' }
    3. バリデーションエラー ステータスコード: 422
    4. バリデーションエラーとメッセージは、定義したロジックに従って、または標準の errorParsers を使用して解析されます。
      • NuxtErrorParsers: NuxtPrecognitiveErrorResponse: Response & { _data: { data: ValidationErrorsData }}
      • LaravelErrorParsers: LaravelPrecognitiveErrorResponse: Response & { _data: ValidationErrorsData }

クイックセットアップ

ワンコマンドで Nuxt アプリケーションにモジュールをインストールします

npx nuxi module add nuxt-precognition

構成

名前タイプデフォルト説明
validationTimeout数値15002 つの予知バリデーションリクエスト間のデバウンス時間 (ミリ秒単位)。
backendValidationブール値false予知バリデーションを有効にするためのフラグ。
validateFilesブール値false予知リクエストでファイルのバリデーションを有効にするためのフラグ。
enableNuxtClientErrorParserブール値falsenuxtErrorParsers をクライアント側 (form.validate および form.submit 内) で有効にするためのフラグ。
enableLaravelClientErrorParserブール値falselaravelErrorParsers をクライアント側 (form.validate および form.submit 内) で有効にするためのフラグ。
enableLaravelServerErrorParserブール値falselaravelErrorParsers をクライアント側 (definePrecognitiveEventHandler 内) で有効にするためのフラグ。

ステータスハンドラー

公式パッケージと同様に、特定のエラーコードに対して、グローバルまたはインスタンスレベルでカスタムハンドラーを定義できます。

// plugins/precognition.ts

export default defineNuxtPlugin(() => {
  const { $precognition } = useNuxtApp()

  $precognition.statusHandlers = {
    401: async (error, form) => {
      form.error = createError('Unauthorized')
      await navigateTo('/login')
    },
    403: async (error, form) => {
      form.error = createError('Forbidden')
    },
  }
})

以上です!これで Nuxt アプリで Nuxt Precognition を使用できます ✨

Laravel を使用する

  1. このようなプラグインを定義します
// plugins/api.ts

export default defineNuxtPlugin((app) => {
  const { $precognition } = useNuxtApp()
  const token = useCookie('XSRF-TOKEN')

  const api = $fetch.create({
    baseURL: 'https://127.0.0.1',
    credentials: 'include',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    onRequest: ({ options }) => {
      // Setup csrf protection for every requests if available
      if (token.value) {
        const headers = new Headers(options.headers)
        headers.set('X-XSRF-TOKEN', token.value)
        options.headers = headers
      }
    },
    onResponse: (context) => {
      // ensure that all precognitive requests will receive precognitive responses
      $precognition.assertSuccessfulPrecognitiveResponses(context)
    },
  })

  async function fetchSanctumToken() {
    try {
      await api('/sanctum/csrf-cookie')
      token.value = useCookie('XSRF-TOKEN').value

      if (!token.value) {
        throw new Error('Failed to get CSRF token')
      }
    }
    catch (e) {
      console.error(e)
    }
  }

  app.hook('app:mounted', fetchSanctumToken)

  return {
    provide: {
      api,
      sanctum: {
        fetchToken: fetchSanctumToken,
        token,
      },
    },
  }
})
  1. バックエンドバリデーションとネイティブの Laravel エラーパーサーをクライアント側またはサーバー側で有効にします
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-precognition'],
  precognition: {
    backendValidation: true,
    enableLaravelClientErrorParser: true,
  },
  /*
  ...
  */
})

* enableLaravelServerErrorParser を有効にする場合は、enableNuxtClientErrorParser も有効にする必要があります

  1. Laravel Cors 構成ファイルをセットアップします
// config/cors.php

return [

    /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    */

    'paths' => ['*'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [env('FRONTEND_URL', 'https://127.0.0.1:3000')],

    'allowed_headers' => ['*'],

    'exposed_headers' => ['Precognition', 'Precognition-Success'],

    'max_age' => 0,

    'supports_credentials' => true,

];
  1. 必要な場所で Precognition ミドルウェアを有効にします
// routes/api.php

Route::middleware('precognitive')->group(function () {
    Route::apiResource('posts', \App\Http\Controllers\PostController::class);
});

貢献

ローカル開発
# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with the playground
npm run dev

# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release new version
npm run release