简介

Laravel Precognition 可让你预先判断未来 HTTP 请求的执行结果。Precognition 的主要用例之一,是在无需重复应用后端验证规则的前提下,为你的前端 JavaScript 应用提供“实时”验证能力。

当 Laravel 接收到“预判请求”时,它会执行该路由的所有中间件,解析路由控制器的依赖项,包括对 表单请求 进行验证,但不会真正执行路由的控制器方法。

从 Inertia 2.3 起,已内置 Precognition 支持。请参阅 Inertia 表单文档 获取更多信息。较早版本的 Inertia 需要使用 Precognition 0.x。

实时验证

使用 Vue

借助 Laravel Precognition,你无需在前端 Vue 应用中重复编写验证规则,就能为用户提供实时验证体验。为说明其工作原理,我们来构建一个用于在应用中创建新用户的表单。

首先,要为某条路由启用 Precognition,应在路由定义中添加 HandlePrecognitiveRequests 中间件。你还应创建一个 表单请求,用于承载该路由的验证规则:

use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', function (StoreUserRequest $request) {
    // ...
})->middleware([HandlePrecognitiveRequests::class]);

接着,你应通过 NPM 安装适用于 Vue 的 Laravel Precognition 前端辅助工具:

npm install laravel-precognition-vue

安装 Laravel Precognition 包后,现在可以使用 Precognition 的 useForm 函数创建表单对象,传入 HTTP 方法(post)、目标 URL(/users)以及初始表单数据。

随后,为启用实时验证,请在每个输入项的 change 事件上调用表单的 validate 方法,并传入输入项的名称:

<script setup>
import { useForm } from 'laravel-precognition-vue';

const form = useForm('post', '/users', {
    name: '',
    email: '',
});

const submit = () => form.submit();
</script>

<template>
    <form @submit.prevent="submit">
        <label for="name">Name</label>
        <input
            id="name"
            v-model="form.name"
            @change="form.validate('name')"
        />
        <div v-if="form.invalid('name')">
            {{ form.errors.name }}
        </div>

        <label for="email">Email</label>
        <input
            id="email"
            type="email"
            v-model="form.email"
            @change="form.validate('email')"
        />
        <div v-if="form.invalid('email')">
            {{ form.errors.email }}
        </div>

        <button :disabled="form.processing">
            Create User
        </button>
    </form>
</template>

现在,随着用户填写表单,Precognition 会基于路由表单请求中的验证规则,提供实时验证结果。当表单输入项发生变化时,系统会向 Laravel 应用发送一个经过防抖处理的“预判”验证请求。你可以通过调用表单的 setValidationTimeout 函数来配置防抖超时时间:

form.setValidationTimeout(3000);

当验证请求正在进行时,表单的 validating 属性会为 true:

<div v-if="form.validating">
    Validating...
</div>

验证请求或表单提交期间返回的任何验证错误,都会自动写入表单的 errors 对象:

<div v-if="form.invalid('email')">
    {{ form.errors.email }}
</div>

你可以使用表单的 hasErrors 属性来判断表单是否存在错误:

<div v-if="form.hasErrors">
    <!-- ... -->
</div>

你还可以通过将输入项的名称分别传入表单的 valid 和 invalid 函数,判断该输入项是否通过或未通过验证:

<span v-if="form.valid('email')">
    ✅
</span>

<span v-else-if="form.invalid('email')">
    ❌
</span>

只有当某个表单输入项发生变更并收到验证响应后,它才会显示为有效或无效。

如果使用 Precognition 仅验证表单中的部分输入项,手动清除错误会很有用。你可以使用表单的 forgetError 函数来实现这一点:

<input
    id="avatar"
    type="file"
    @change="(e) => {
        form.avatar = e.target.files[0]

        form.forgetError('avatar')
    }"
>

正如我们所见,你可以绑定到输入项的 change 事件,并在用户与其交互时逐个验证输入项;但有时你可能需要验证用户尚未交互过的输入项。这在构建“向导”时很常见,你需要在进入下一步之前验证所有可见输入项,无论用户是否已经与它们交互。

要在 Precognition 中实现这一点,应在调用 validate 方法时,将希望验证的字段名称传入 only 配置项。你可以使用 onSuccess 或 onValidationError 回调来处理验证结果:

<button
    type="button"
    @click="form.validate({
        only: ['name', 'email', 'phone'],
        onSuccess: (response) => nextStep(),
        onValidationError: (response) => /* ... */,
    })"
>Next Step</button>

当然,你也可以根据表单提交的响应来执行相应代码。表单的 submit 函数会返回一个 Axios 请求 Promise。这为你提供了便捷的方式来获取响应数据、在提交成功后重置表单输入,或处理失败的请求:

const submit = () => form.submit()
    .then(response => {
        form.reset();

        alert('User created.');
    })
    .catch(error => {
        alert('An error occurred.');
    });

你可以通过检查表单的 processing 属性,来判断表单提交请求是否正在进行:

<button :disabled="form.processing">
    Submit
</button>

使用 React

借助 Laravel Precognition,你无需在前端 React 应用中重复编写验证规则,就能为用户提供实时验证体验。为说明其工作原理,我们来构建一个用于在应用中创建新用户的表单。

首先,要为某条路由启用 Precognition,应在路由定义中添加 HandlePrecognitiveRequests 中间件。你还应创建一个 表单请求,用于承载该路由的验证规则:

use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', function (StoreUserRequest $request) {
    // ...
})->middleware([HandlePrecognitiveRequests::class]);

接着,你应通过 NPM 安装适用于 React 的 Laravel Precognition 前端辅助工具:

npm install laravel-precognition-react

安装 Laravel Precognition 包后,现在可以使用 Precognition 的 useForm 函数创建表单对象,传入 HTTP 方法(post)、目标 URL(/users)以及初始表单数据。

要启用实时验证,你应监听每个输入项的 change 和 blur 事件。在 change 事件处理程序中,应使用 setData 函数设置表单数据,并传入输入项名称和新值。然后,在 blur 事件处理程序中调用表单的 validate 方法,并传入输入项的名称:

import { useForm } from 'laravel-precognition-react';

export default function Form() {
    const form = useForm('post', '/users', {
        name: '',
        email: '',
    });

    const submit = (e) => {
        e.preventDefault();

        form.submit();
    };

    return (
        <form onSubmit={submit}>
            <label htmlFor="name">Name</label>
            <input
                id="name"
                value={form.data.name}
                onChange={(e) => form.setData('name', e.target.value)}
                onBlur={() => form.validate('name')}
            />
            {form.invalid('name') && <div>{form.errors.name}</div>}

            <label htmlFor="email">Email</label>
            <input
                id="email"
                value={form.data.email}
                onChange={(e) => form.setData('email', e.target.value)}
                onBlur={() => form.validate('email')}
            />
            {form.invalid('email') && <div>{form.errors.email}</div>}

            <button disabled={form.processing}>
                Create User
            </button>
        </form>
    );
};

现在,随着用户填写表单,Precognition 将根据路由表单请求中的验证规则提供实时验证输出。当表单的输入发生变化时,会向你的 Laravel 应用程序发送一个经过防抖处理的“预判”验证请求。你可以调用表单的 setValidationTimeout 函数来配置防抖超时时间:

form.setValidationTimeout(3000);

当验证请求正在处理时,表单的 validating 属性将为 true:

{form.validating && <div>Validating...</div>}

在验证请求或表单提交过程中返回的任何验证错误,都会自动填充到表单的 errors 对象中:

{form.invalid('email') && <div>{form.errors.email}</div>}

你可以使用表单的 hasErrors 属性来判断表单是否有任何错误:

{form.hasErrors && <div><!-- ... --></div>}

你也可以分别将输入的名称传递给表单的 valid 和 invalid 函数,来判断某个输入是否通过或未通过验证:

{form.valid('email') && <span>✅</span>}

{form.invalid('email') && <span>❌</span>}

表单输入只有在发生变化并收到验证响应后,才会显示为有效或无效。

如果你使用 Precognition 对表单的部分输入进行验证,手动清除错误可能会很有用。你可以使用表单的 forgetError 函数来实现这一点:

<input
    id="avatar"
    type="file"
    onChange={(e) => {
        form.setData('avatar', e.target.files[0]);

        form.forgetError('avatar');
    }}
>

正如我们所见,你可以监听输入的 blur 事件,并在用户与输入交互时验证单个输入;然而,你可能需要验证用户尚未交互的输入。这在构建“向导”时很常见,在这种情况下,你需要在进入下一步之前,验证所有可见输入,无论用户是否与它们进行过交互。

要在 Precognition 中实现这一点,你应该调用 validate 方法,并将要验证的字段名称传递给 only 配置键。你可以使用 onSuccess 或 onValidationError 回调来处理验证结果:

<button
    type="button"
    onClick={() => form.validate({
        only: ['name', 'email', 'phone'],
        onSuccess: (response) => nextStep(),
        onValidationError: (response) => /* ... */,
    })}
>Next Step</button>

当然,你也可以根据表单提交的响应来执行代码。表单的 submit 函数会返回一个 Axios 请求 promise。这为你提供了一种便捷的方式来访问响应数据、在表单提交成功后重置表单输入,或者处理请求失败的情况:

const submit = (e) => {
    e.preventDefault();

    form.submit()
        .then(response => {
            form.reset();

            alert('User created.');
        })
        .catch(error => {
            alert('An error occurred.');
        });
};

你可以通过检查表单的 processing 属性,来确定表单提交请求是否正在处理中:

<button disabled={form.processing}>
    Submit
</button>

使用 Alpine 和 Blade

使用 Laravel Precognition,你可以在不将验证规则重复编写到前端 Alpine 应用程序的情况下,为用户提供实时验证体验。为了说明其工作原理,让我们在应用程序中构建一个用于创建新用户的表单。

首先,要为路由启用 Precognition,应将 HandlePrecognitiveRequests 中间件添加到路由定义中。你还应该创建一个表单请求来存放路由的验证规则:

use App\Http\Requests\CreateUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;

Route::post('/users', function (CreateUserRequest $request) {
    // ...
})->middleware([HandlePrecognitiveRequests::class]);

接下来,你应该通过 NPM 安装 Laravel Precognition 针对 Alpine 的前端辅助工具:

npm install laravel-precognition-alpine

然后,在 resources/js/app.js 文件中,使用 Alpine 注册 Precognition 插件:

import Alpine from 'alpinejs';
import Precognition from 'laravel-precognition-alpine';

window.Alpine = Alpine;

Alpine.plugin(Precognition);
Alpine.start();

安装并注册 Laravel Precognition 包后,你现在可以使用 Precognition 的 $form “魔术方法”创建表单对象,提供 HTTP 方法(post)、目标 URL(/users)以及初始表单数据。

为了启用实时验证,你应该将表单数据绑定到相关输入,并监听每个输入的 change 事件。在 change 事件处理函数中,你应该调用表单的 validate 方法,并提供输入的名称:

<form x-data="{
    form: $form('post', '/register', {
        name: '',
        email: '',
    }),
}">
    @csrf
    <label for="name">Name</label>
    <input
        id="name"
        name="name"
        x-model="form.name"
        @change="form.validate('name')"
    />
    <template x-if="form.invalid('name')">
        <div x-text="form.errors.name"></div>
    </template>

    <label for="email">Email</label>
    <input
        id="email"
        name="email"
        x-model="form.email"
        @change="form.validate('email')"
    />
    <template x-if="form.invalid('email')">
        <div x-text="form.errors.email"></div>
    </template>

    <button :disabled="form.processing">
        Create User
    </button>
</form>

现在,随着用户填写表单,Precognition 将根据路由表单请求中的验证规则提供实时验证输出。当表单的输入发生变化时,会向你的 Laravel 应用程序发送一个经过防抖处理的“预判”验证请求。你可以调用表单的 setValidationTimeout 函数来配置防抖超时时间:

form.setValidationTimeout(3000);

当验证请求正在处理时,表单的 validating 属性将为 true:

<template x-if="form.validating">
    <div>Validating...</div>
</template>

在验证请求或表单提交过程中返回的任何验证错误,都会自动填充到表单的 errors 对象中:

<template x-if="form.invalid('email')">
    <div x-text="form.errors.email"></div>
</template>

你可以使用表单的 hasErrors 属性来判断表单是否有任何错误:

<template x-if="form.hasErrors">
    <div><!-- ... --></div>
</template>

你也可以分别将输入的名称传递给表单的 valid 和 invalid 函数,来判断某个输入是否通过或未通过验证:

<template x-if="form.valid('email')">
    <span>✅</span>
</template>

<template x-if="form.invalid('email')">
    <span>❌</span>
</template>

表单输入只有在发生变化并收到验证响应后,才会显示为有效或无效。

正如我们所见,你可以监听输入框的 change 事件,并在用户与之交互时验证各个输入项;不过,你可能需要对用户尚未交互的输入项进行验证。在构建“向导”式表单时,这种情况很常见,即在进入下一步之前,你需要验证所有可见的输入项,无论用户是否与之进行过交互。

要使用 Precognition 实现这一点,你应该调用 validate 方法,并将需要验证的字段名传入 only 配置键中。你可以通过 onSuccess 或 onValidationError 回调来处理验证结果:

<button
    type="button"
    @click="form.validate({
        only: ['name', 'email', 'phone'],
        onSuccess: (response) => nextStep(),
        onValidationError: (response) => /* ... */,
    })"
>下一步</button>

你可以通过检查表单的 processing 属性,来判断表单提交请求是否正在进行中:

<button :disabled="form.processing">
    提交
</button>

重新填充旧表单数据

在上面讨论的用户创建示例中,我们使用 Precognition 执行实时验证;然而,我们采用传统的服务端表单提交方式来提交表单。因此,表单应当使用服务端表单提交返回的任何“旧”输入数据和验证错误进行填充:

<form x-data="{
    form: $form('post', '/register', {
        name: '{{ old('name') }}',
        email: '{{ old('email') }}',
    }).setErrors({{ Js::from($errors->messages()) }}),
}">

或者,如果你想通过 XHR 提交表单,可以使用表单的 submit 函数,该函数会返回一个 Axios 请求 Promise:

<form
    x-data="{
        form: $form('post', '/register', {
            name: '',
            email: '',
        }),
        submit() {
            this.form.submit()
                .then(response => {
                    this.form.reset();

                    alert('用户已创建。')
                })
                .catch(error => {
                    alert('发生错误。');
                });
        },
    }"
    @submit.prevent="submit"
>

配置 Axios

Precognition 验证库使用 Axios HTTP 客户端向你的应用后端发送请求。为方便起见,Axios 实例可以根据应用需求进行自定义。例如,在使用 laravel-precognition-vue 库时,你可以在应用的 resources/js/app.js 文件中为每个发出的请求添加额外的请求头:

import { client } from 'laravel-precognition-vue';

client.axios().defaults.headers.common['Authorization'] = authToken;

或者,如果你的应用已经配置了 Axios 实例,你可以告知 Precognition 改用该实例:

import Axios from 'axios';
import { client } from 'laravel-precognition-vue';

window.axios = Axios.create()
window.axios.defaults.headers.common['Authorization'] = authToken;

client.use(window.axios)

验证数组

你可以使用通配符来验证数组或嵌套对象中的字段。每个 * 会匹配一个路径片段:

// 验证数组中所有用户的邮箱...
form.validate('users.*.email');

// 验证 profile 对象中的所有字段...
form.validate('profile.*');

// 验证所有用户的所有字段...
form.validate('users.*.*');

自定义验证规则

可以通过使用请求的 isPrecognitive 方法,自定义在预知性请求期间执行的验证规则。

例如,在用户创建表单中,我们可能希望仅在最终提交表单时,才验证密码是否“未泄露”。对于预知性验证请求,我们将仅验证密码为必填项且长度不少于 8 个字符。借助 isPrecognitive 方法,我们可以自定义表单请求中定义的规则:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;

class StoreUserRequest extends FormRequest
{
    /**
     * 获取适用于该请求的验证规则。
     *
     * @return array
     */
    public function rules()
    {
        return [
            'password' => [
                'required',
                $this->isPrecognitive()
                    ? Password::min(8)
                    : Password::min(8)->uncompromised(),
            ],
            // ...
        ];
    }
}

处理文件上传

默认情况下,Laravel Precognition 不会在预知性验证请求期间上传或验证文件。这样可以确保大文件不会被不必要地重复上传。

由于这种行为,你应当确保应用对相应表单请求的验证规则进行自定义,以指定该字段仅在完整提交表单时为必填项:

/**
 * 获取适用于该请求的验证规则。
 *
 * @return array
 */
public function rules()
{
    return [
        'avatar' => [
            ...$this->isPrecognitive() ? [] : ['required'],
            'image',
            'mimes:jpg,png',
            'dimensions:ratio=3/2',
        ],
        // ...
    ];
}

如果你想在每次验证请求中都包含文件,可以在客户端表单实例上调用 validateFiles 函数:

form.validateFiles();

管理副作用

当将 HandlePrecognitiveRequests 中间件添加到路由时,你应当考虑其他中间件中是否存在应在预知性请求期间跳过的副作用。

例如,你可能有一个中间件,用于统计每位用户与应用的总“交互”次数,但你可能不希望将预知性请求计为一次交互。为此,我们可以在增加交互计数前,检查请求的 isPrecognitive 方法:

<?php

namespace App\Http\Middleware;

use App\Facades\Interaction;
use Closure;
use Illuminate\Http\Request;

class InteractionMiddleware
{
    /**
     * 处理传入的请求。
     */
    public function handle(Request $request, Closure $next): mixed
    {
        if (! $request->isPrecognitive()) {
            Interaction::incrementFor($request->user());
        }

        return $next($request);
    }
}

测试

如果你想在测试中发起预知性请求,Laravel 的 TestCase 提供了一个 withPrecognition 辅助方法,该方法会添加 Precognition 请求头。

此外,如果你想断言预知性请求是否成功(例如,是否未返回任何验证错误),可以在响应上使用 assertSuccessfulPrecognition 方法:

it('validates registration form with precognition', function () {
    $response = $this->withPrecognition()
        ->post('/register', [
            'name' => 'Taylor Otwell',
        ]);

    $response->assertSuccessfulPrecognition();

    expect(User::count())->toBe(0);
});
public function test_it_validates_registration_form_with_precognition()
{
    $response = $this->withPrecognition()
        ->post('/register', [
            'name' => 'Taylor Otwell',
        ]);

    $response->assertSuccessfulPrecognition();
    $this->assertSame(0, User::count());
}