Nuxt Nation 大会即将到来。加入我们,时间为 11 月 12 日至 13 日。

precognition
nuxt-precognition

在 Nitro 中实现 Laravel 预知协议的 Nuxt 模块

Nuxt 预知

npm versionnpm downloadsLicenseNuxt

这是 nuxt-laravel-precognition 的新版本。它提供相同的功能,但不再依赖于 Laravel。

它不再只支持 $fetchLaravel,而是使用简单的 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 预知

它支持任何验证库(谁说 Zod??)服务器端或客户端。您只需要配置特定的 错误解析器

特性

  •  兼容 Laravel
  •  与验证库无关
  •  客户端和服务器端验证
  •  最佳 TypeScript 支持
  •  高度可定制

工作原理

一切围绕 errorParsers用户定义的函数,用于从 Error 负载读取验证错误)展开

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

interface ValidationErrorsData {
  message: string
  errors: ValidationErrors
}

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

您可以在全局范围内(在 Nuxt 插件 或自定义 事件处理程序 中)或每个 表单 实例中定义它们。

假设您正在使用 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 工厂函数创建自定义事件处理程序。

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,
  }
})

请记住,只在 onRequest 处理程序中抛出 ValidationError(使用 对象表示法.
基本 处理程序 中的任何逻辑都不会在 预知请求 期间处理。

  • 每个 event.context 还包含一个标志({ precognitive: boolean }),指示请求是否是预知的,查看 预知标头 的是否存在。

预知协议

如果您需要在 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数字1500两次预知验证请求之间的去抖时间(毫秒)。
backendValidation布尔值false启用预知验证的标志。
validateFiles布尔值false启用预知请求上文件验证的标志。
enableNuxtClientErrorParser布尔值false启用客户端 nuxtErrorParsers(在 form.validateform.submit 中)的标志。
enableLaravelClientErrorParser布尔值false启用客户端 laravelErrorParsers(在 form.validateform.submit 中)的标志。
enableLaravelServerErrorParser布尔值false启用客户端 laravelErrorParsers(在 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 预知 ✨

使用 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. 在需要的地方启用预知中间件
// 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