Skip to content

Precognition

简介

Laravel Precognition 允许你预测未来 HTTP 请求的结果。Precognition 的主要用例之一是能够为你的前端 JavaScript 应用程序提供"实时"验证,而无需复制应用程序的后端验证规则。Precognition 特别适合与 Laravel 基于 Inertia 的启动套件配合使用。

当 Laravel 收到"预知请求"时,它将执行路由的所有中间件并解析路由的控制器依赖项,包括验证表单请求 - 但不会实际执行路由的控制器方法。

实时验证

使用 Vue

使用 Laravel Precognition,你可以为用户提供实时验证体验,而无需在前端 Vue 应用程序中复制验证规则。为了说明它是如何工作的,让我们构建一个用于在应用程序中创建新用户的表单。

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

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

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

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

shell
npm install laravel-precognition-vue

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

然后,要启用实时验证,请在每个输入的 change 事件上调用表单的 validate 方法,并提供输入的名称:

vue
<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">姓名</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">电子邮件</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">
            创建用户
        </button>
    </form>
</template>

现在,当用户填写表单时,Precognition 将提供由路由表单请求的验证规则提供支持的实时验证输出。当表单的输入发生变化时,将发送延迟的"预知"验证请求到你的 Laravel 应用程序。你可以通过调用表单的 setValidationTimeout 函数来配置延迟超时:

js
form.setValidationTimeout(3000);

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

html
<div v-if="form.validating">
    验证中...
</div>

任何在验证请求或表单提交期间返回的验证错误都将自动填充表单的 errors 对象:

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

你可以使用表单的 hasErrors 属性确定表单是否有任何错误:

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

你还可以通过将输入的名称传递给表单的 validinvalid 函数,来确定输入是否已通过或未通过验证:

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

</span>

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

</span>

WARNING

只有在输入已更改并收到验证响应后,输入才会显示为有效或无效。

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

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

        form.forgetError('avatar')
    }"
>

正如我们所见,你可以在输入的 change 事件上挂钩并在用户与之交互时验证各个输入;但是,你可能需要验证用户尚未与之交互的输入。这在构建"向导"时很常见,当你希望在移动到下一步之前验证所有可见输入(无论用户是否与之交互)。

要使用 Precognition 执行此操作,你应该将要验证的字段标记为"已触摸",方法是将它们的名称传递给 touch 方法。然后,调用 validate 方法并提供 onSuccessonValidationError 回调:

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

当然,你还可以在表单提交的响应中执行代码。表单的 submit 函数返回一个 Axios 请求承诺。这提供了一种方便的方式来访问响应有效载荷,在成功提交表单时重置表单输入,或处理失败的请求:

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

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

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

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

使用 Vue 和 Inertia

NOTE

如果你想在使用 Vue 和 Inertia 开发 Laravel 应用程序时有一个良好的开始,请考虑使用我们的启动套件之一。Laravel 的启动套件为新的 Laravel 应用程序提供了后端和前端身份验证脚手架。

在使用 Vue 和 Inertia 之前,请务必查看我们的通用文档,了解如何使用 Precognition 与 Vue。当使用 Vue 与 Inertia 时,你需要通过 NPM 安装与 Inertia 兼容的 Precognition 库:

shell
npm install laravel-precognition-vue-inertia

安装后,Precognition 的 useForm 函数将返回一个增强了上述验证功能的 Inertia 表单助手

表单助手的 submit 方法已经简化,不再需要指定 HTTP 方法或 URL。相反,你可以将 Inertia 的访问选项作为第一个也是唯一的参数传递。此外,submit 方法不会返回 Promise,如 Vue 示例中所示。相反,你可以在传递给 submit 方法的访问选项中提供任何 Inertia 支持的事件回调:

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

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

const submit = () => form.submit({
    preserveScroll: true,
    onSuccess: () => form.reset(),
});
</script>

使用 React

使用 Laravel Precognition,你可以为用户提供实时验证体验,而无需在前端 React 应用程序中复制验证规则。为了说明它是如何工作的,让我们构建一个用于在应用程序中创建新用户的表单。

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

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

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

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

shell
npm install laravel-precognition-react

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

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

jsx
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">姓名</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">电子邮件</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}>
                创建用户
            </button>
        </form>
    );
};

现在,当用户填写表单时,Precognition 将提供由路由表单请求的验证规则提供支持的实时验证输出。当表单的输入发生变化时,将发送延迟的"预知"验证请求到你的 Laravel 应用程序。你可以通过调用表单的 setValidationTimeout 函数来配置延迟超时:

js
form.setValidationTimeout(3000);

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

jsx
{form.validating && <div>验证中...</div>}

任何在验证请求或表单提交期间返回的验证错误都将自动填充表单的 errors 对象:

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

你可以使用表单的 hasErrors 属性确定表单是否有任何错误:

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

你还可以通过将输入的名称传递给表单的 validinvalid 函数,来确定输入是否已通过或未通过验证:

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

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

WARNING

只有在输入已更改并收到验证响应后,输入才会显示为有效或无效。

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

jsx
<input
    id="avatar"
    type="file"
    onChange={(e) => {
        form.setData('avatar', e.target.value);

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

正如我们所见,你可以在输入的 blur 事件上挂钩并在用户与之交互时验证各个输入;但是,你可能需要验证用户尚未与之交互的输入。这在构建"向导"时很常见,当你希望在移动到下一步之前验证所有可见输入(无论用户是否与之交互)。

要使用 Precognition 执行此操作,你应该将要验证的字段标记为"已触摸",方法是将它们的名称传递给 touch 方法。然后,调用 validate 方法并提供 onSuccessonValidationError 回调:

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

当然,你还可以在表单提交的响应中执行代码。表单的 submit 函数返回一个 Axios 请求承诺。这提供了一种方便的方式来访问响应有效载荷,在成功提交表单时重置表单输入,或处理失败的请求:

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

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

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

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

html
<button disabled={form.processing}>
    提交
</button>

使用 React 和 Inertia

NOTE

如果你想在使用 React 和 Inertia 开发 Laravel 应用程序时有一个良好的开始,请考虑使用我们的启动套件之一。Laravel 的启动套件为新的 Laravel 应用程序提供了后端和前端身份验证脚手架。

在使用 React 和 Inertia 之前,请务必查看我们的通用文档,了解如何使用 Precognition 与 React。当使用 React 与 Inertia 时,你需要通过 NPM 安装与 Inertia 兼容的 Precognition 库:

shell
npm install laravel-precognition-react-inertia

安装后,Precognition 的 useForm 函数将返回一个增强了上述验证功能的 Inertia 表单助手

表单助手的 submit 方法已经简化,不再需要指定 HTTP 方法或 URL。相反,你可以将 Inertia 的访问选项作为第一个也是唯一的参数传递。此外,submit 方法不会返回 Promise,如 React 示例中所示。相反,你可以在传递给 submit 方法的访问选项中提供任何 Inertia 支持的事件回调:

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

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

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

    form.submit({
        preserveScroll: true,
        onSuccess: () => form.reset(),
    });
};

使用 Alpine 和 Blade

使用 Laravel Precognition,你可以为用户提供实时验证体验,而无需在前端 Alpine 应用程序中复制验证规则。为了说明它是如何工作的,让我们构建一个用于在应用程序中创建新用户的表单。

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

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

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

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

shell
npm install laravel-precognition-alpine

然后,在你的 resources/js/app.js 文件中注册 Precognition 插件与 Alpine:

js
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 方法,并提供输入的名称:

html
<form x-data="{
    form: $form('post', '/register', {
        name: '',
        email: '',
    }),
}">
    @csrf
    <label for="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">电子邮件</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">
        创建用户
    </button>
</form>

现在,当用户填写表单时,Precognition 将提供由路由表单请求的验证规则提供支持的实时验证输出。当表单的输入发生变化时,将发送延迟的"预知"验证请求到你的 Laravel 应用程序。你可以通过调用表单的 setValidationTimeout 函数来配置延迟超时:

js
form.setValidationTimeout(3000);

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

html
<template x-if="form.validating">
    <div>验证中...</div>
</template>

任何在验证请求或表单提交期间返回的验证错误都将自动填充表单的 errors 对象:

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

你可以使用表单的 hasErrors 属性确定表单是否有任何错误:

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

你还可以通过将输入的名称传递给表单的 validinvalid 函数,来确定输入是否已通过或未通过验证:

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

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

WARNING

只有在输入已更改并收到验证响应后,输入才会显示为有效或无效。

正如我们所见,你可以在输入的 change 事件上挂钩并在用户与之交互时验证各个输入;但是,你可能需要验证用户尚未与之交互的输入。这在构建"向导"时很常见,当你希望在移动到下一步之前验证所有可见输入(无论用户是否与之交互)。

要使用 Precognition 执行此操作,你应该将要验证的字段标记为"已触摸",方法是将它们的名称传递给 touch 方法。然后,调用 validate 方法并提供 onSuccessonValidationError 回调:

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

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

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

重新填充旧表单数据

在上面讨论的用户创建示例中,我们使用 Precognition 进行实时验证;但是,我们执行传统的服务器端表单提交来提交表单。因此,表单应该填充任何"旧"输入和服务器端表单提交返回的验证错误:

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

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

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

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

配置 Axios

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

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

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

或者,如果你已经为应用程序配置了 Axios 实例,你可以告诉 Precognition 改用该实例:

js
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)

WARNING

与 Inertia 兼容的 Precognition 库只会在验证请求中使用配置的 Axios 实例。表单提交将始终由 Inertia 发送。

自定义验证规则

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

例如,在用户创建表单中,我们可能希望仅在最终表单提交时验证密码是否"未受损",而在预知验证请求中,我们只需验证密码是必需的且至少有 8 个字符。使用 isPrecognitive 方法,我们可以自定义表单请求定义的规则:

php
<?php

namespace App\Http\Requests;

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

class StoreUserRequest extends FormRequest
{
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    protected function rules()
    {
        return [
            'password' => [
                'required',
                $this->isPrecognitive()
                    ? Password::min(8)
                    : Password::min(8)->uncompromised(),
            ],
            // ...
        ];
    }
}

处理文件上传

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

由于此行为,你应该确保自定义相应表单请求的验证规则以指定该字段仅在完整表单提交时是必需的:

php
/**
 * Get the validation rules that apply to the request.
 *
 * @return array
 */
protected function rules()
{
    return [
        'avatar' => [
            ...$this->isPrecognitive() ? [] : ['required'],
            'image',
            'mimes:jpg,png',
            'dimensions:ratio=3/2',
        ],
        // ...
    ];
}

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

js
form.validateFiles();

管理副作用

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

例如,你可能有一个中间件,它会增加每个用户与应用程序的总"交互"次数,但你可能不希望预知请求被计为一次交互。为了实现这一点,我们可以在增加交互计数之前检查请求的 isPrecognitive 方法:

php
<?php

namespace App\Http\Middleware;

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

class InteractionMiddleware
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): mixed
    {
        if (! $request->isPrecognitive()) {
            Interaction::incrementFor($request->user());
        }

        return $next($request);
    }
}

测试

如果你想在测试中进行预知请求,Laravel 的 TestCase 包含一个 withPrecognition 助手,它将添加 Precognition 请求头。

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

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

    $response->assertSuccessfulPrecognition();

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

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