跳转至内容

Precognition

简介

Laravel Precognition 允许你预判未来 HTTP 请求的结果。Precognition 的主要用途之一是为前端 JavaScript 应用提供“实时”验证,而无需重复编写后端的验证规则。

当 Laravel 接收到“预判请求”(precognitive request)时,它会执行路由的所有中间件并解析路由的控制器依赖,包括验证 表单请求(form requests),但它实际上不会执行路由的控制器方法。

从 Inertia 2.3 开始,已内置对 Precognition 的支持。请查阅 Inertia 表单文档 以获取更多信息。早期的 Inertia 版本需要使用 Precognition 0.x。

实时验证

使用 Vue

使用 Laravel Precognition,你可以为用户提供实时验证体验,而无需在前端 Vue 应用中重复定义验证规则。为了演示其工作原理,让我们构建一个用于在应用中创建新用户的表单。

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

1use App\Http\Requests\StoreUserRequest;
2use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
3 
4Route::post('/users', function (StoreUserRequest $request) {
5 // ...
6})->middleware([HandlePrecognitiveRequests::class]);

接下来,你需要通过 NPM 安装适用于 Vue 的 Laravel Precognition 前端助手。

1npm install laravel-precognition-vue

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

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

1<script setup>
2import { useForm } from 'laravel-precognition-vue';
3 
4const form = useForm('post', '/users', {
5 name: '',
6 email: '',
7});
8 
9const submit = () => form.submit();
10</script>
11 
12<template>
13 <form @submit.prevent="submit">
14 <label for="name">Name</label>
15 <input
16 id="name"
17 v-model="form.name"
18 @change="form.validate('name')"
19 />
20 <div v-if="form.invalid('name')">
21 {{ form.errors.name }}
22 </div>
23 
24 <label for="email">Email</label>
25 <input
26 id="email"
27 type="email"
28 v-model="form.email"
29 @change="form.validate('email')"
30 />
31 <div v-if="form.invalid('email')">
32 {{ form.errors.email }}
33 </div>
34 
35 <button :disabled="form.processing">
36 Create User
37 </button>
38 </form>
39</template>

现在,当用户填写表单时,Precognition 将利用路由表单请求中的验证规则提供实时验证输出。当表单输入发生变化时,一个防抖后的“预判”验证请求会被发送到你的 Laravel 应用。你可以通过调用表单的 setValidationTimeout 函数来配置防抖超时时间。

1form.setValidationTimeout(3000);

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

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

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

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

你可以使用表单的 hasErrors 属性来确定表单是否存在错误。

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

你还可以通过将输入框名称分别传递给表单的 validinvalid 函数,来确定某个输入框是否通过了验证。

1<span v-if="form.valid('email')">
2
3</span>
4 
5<span v-else-if="form.invalid('email')">
6
7</span>

表单输入框只有在发生变化并接收到验证响应后,才会显示为已通过或未通过验证。

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

1<input
2 id="avatar"
3 type="file"
4 @change="(e) => {
5 form.avatar = e.target.files[0]
6 
7 form.forgetError('avatar')
8 }"
9>

如我们所见,你可以挂载到输入框的 change 事件上,在用户与输入框交互时验证单个字段;然而,有时你可能需要验证用户尚未交互的输入框。这在构建“向导”表单时很常见,你可能希望在进入下一步之前验证所有可见的输入框,无论用户是否与其交互过。

要使用 Precognition 执行此操作,你应该调用 validate 方法,并将你想验证的字段名传递给 only 配置项。你可以使用 onSuccessonValidationError 回调来处理验证结果。

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

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

1const submit = () => form.submit()
2 .then(response => {
3 form.reset();
4 
5 alert('User created.');
6 })
7 .catch(error => {
8 alert('An error occurred.');
9 });

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

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

使用 React

使用 Laravel Precognition,你可以为用户提供实时验证体验,而无需在前端 React 应用中重复定义验证规则。为了演示其工作原理,让我们构建一个用于在应用中创建新用户的表单。

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

1use App\Http\Requests\StoreUserRequest;
2use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
3 
4Route::post('/users', function (StoreUserRequest $request) {
5 // ...
6})->middleware([HandlePrecognitiveRequests::class]);

接下来,你需要通过 NPM 安装适用于 React 的 Laravel Precognition 前端助手。

1npm install laravel-precognition-react

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

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

1import { useForm } from 'laravel-precognition-react';
2 
3export default function Form() {
4 const form = useForm('post', '/users', {
5 name: '',
6 email: '',
7 });
8 
9 const submit = (e) => {
10 e.preventDefault();
11 
12 form.submit();
13 };
14 
15 return (
16 <form onSubmit={submit}>
17 <label htmlFor="name">Name</label>
18 <input
19 id="name"
20 value={form.data.name}
21 onChange={(e) => form.setData('name', e.target.value)}
22 onBlur={() => form.validate('name')}
23 />
24 {form.invalid('name') && <div>{form.errors.name}</div>}
25 
26 <label htmlFor="email">Email</label>
27 <input
28 id="email"
29 value={form.data.email}
30 onChange={(e) => form.setData('email', e.target.value)}
31 onBlur={() => form.validate('email')}
32 />
33 {form.invalid('email') && <div>{form.errors.email}</div>}
34 
35 <button disabled={form.processing}>
36 Create User
37 </button>
38 </form>
39 );
40};

现在,当用户填写表单时,Precognition 将利用路由表单请求中的验证规则提供实时验证输出。当表单输入发生变化时,一个防抖后的“预判”验证请求会被发送到你的 Laravel 应用。你可以通过调用表单的 setValidationTimeout 函数来配置防抖超时时间。

1form.setValidationTimeout(3000);

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

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

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

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

你可以使用表单的 hasErrors 属性来确定表单是否存在错误。

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

你还可以通过将输入框名称分别传递给表单的 validinvalid 函数,来确定某个输入框是否通过了验证。

1{form.valid('email') && <span></span>}
2 
3{form.invalid('email') && <span></span>}

表单输入框只有在发生变化并接收到验证响应后,才会显示为已通过或未通过验证。

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

1<input
2 id="avatar"
3 type="file"
4 onChange={(e) => {
5 form.setData('avatar', e.target.files[0]);
6 
7 form.forgetError('avatar');
8 }}
9>

如我们所见,你可以挂载到输入框的 blur 事件上,在用户与输入框交互时验证单个字段;然而,有时你可能需要验证用户尚未交互的输入框。这在构建“向导”表单时很常见,你可能希望在进入下一步之前验证所有可见的输入框,无论用户是否与其交互过。

要使用 Precognition 执行此操作,你应该调用 validate 方法,并将你想验证的字段名传递给 only 配置项。你可以使用 onSuccessonValidationError 回调来处理验证结果。

1<button
2 type="button"
3 onClick={() => form.validate({
4 only: ['name', 'email', 'phone'],
5 onSuccess: (response) => nextStep(),
6 onValidationError: (response) => /* ... */,
7 })}
8>Next Step</button>

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

1const submit = (e) => {
2 e.preventDefault();
3 
4 form.submit()
5 .then(response => {
6 form.reset();
7 
8 alert('User created.');
9 })
10 .catch(error => {
11 alert('An error occurred.');
12 });
13};

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

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

使用 Alpine 和 Blade

使用 Laravel Precognition,你可以为用户提供实时验证体验,而无需在前端 Alpine 应用中重复定义验证规则。为了演示其工作原理,让我们构建一个用于在应用中创建新用户的表单。

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

1use App\Http\Requests\CreateUserRequest;
2use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
3 
4Route::post('/users', function (CreateUserRequest $request) {
5 // ...
6})->middleware([HandlePrecognitiveRequests::class]);

接下来,你需要通过 NPM 安装适用于 Alpine 的 Laravel Precognition 前端助手。

1npm install laravel-precognition-alpine

然后,在你的 resources/js/app.js 文件中将 Precognition 插件注册到 Alpine 中。

1import Alpine from 'alpinejs';
2import Precognition from 'laravel-precognition-alpine';
3 
4window.Alpine = Alpine;
5 
6Alpine.plugin(Precognition);
7Alpine.start();

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

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

1<form x-data="{
2 form: $form('post', '/register', {
3 name: '',
4 email: '',
5 }),
6}">
7 @csrf
8 <label for="name">Name</label>
9 <input
10 id="name"
11 name="name"
12 x-model="form.name"
13 @change="form.validate('name')"
14 />
15 <template x-if="form.invalid('name')">
16 <div x-text="form.errors.name"></div>
17 </template>
18 
19 <label for="email">Email</label>
20 <input
21 id="email"
22 name="email"
23 x-model="form.email"
24 @change="form.validate('email')"
25 />
26 <template x-if="form.invalid('email')">
27 <div x-text="form.errors.email"></div>
28 </template>
29 
30 <button :disabled="form.processing">
31 Create User
32 </button>
33</form>

现在,当用户填写表单时,Precognition 将利用路由表单请求中的验证规则提供实时验证输出。当表单输入发生变化时,一个防抖后的“预判”验证请求会被发送到你的 Laravel 应用。你可以通过调用表单的 setValidationTimeout 函数来配置防抖超时时间。

1form.setValidationTimeout(3000);

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

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

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

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

你可以使用表单的 hasErrors 属性来确定表单是否存在错误。

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

你还可以通过将输入框名称分别传递给表单的 validinvalid 函数,来确定某个输入框是否通过了验证。

1<template x-if="form.valid('email')">
2 <span></span>
3</template>
4 
5<template x-if="form.invalid('email')">
6 <span></span>
7</template>

表单输入框只有在发生变化并接收到验证响应后,才会显示为已通过或未通过验证。

如我们所见,你可以挂载到输入框的 change 事件上,在用户与输入框交互时验证单个字段;然而,有时你可能需要验证用户尚未交互的输入框。这在构建“向导”表单时很常见,你可能希望在进入下一步之前验证所有可见的输入框,无论用户是否与其交互过。

要使用 Precognition 执行此操作,你应该调用 validate 方法,并将你想验证的字段名传递给 only 配置项。你可以使用 onSuccessonValidationError 回调来处理验证结果。

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

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

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

回填旧表单数据

在上述用户创建示例中,我们使用 Precognition 执行实时验证;但我们仍然在执行传统的服务器端表单提交。因此,表单应该能够回填任何来自服务器端表单提交返回的“旧”输入和验证错误。

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

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

1<form
2 x-data="{
3 form: $form('post', '/register', {
4 name: '',
5 email: '',
6 }),
7 submit() {
8 this.form.submit()
9 .then(response => {
10 this.form.reset();
11 
12 alert('User created.')
13 })
14 .catch(error => {
15 alert('An error occurred.');
16 });
17 },
18 }"
19 @submit.prevent="submit"
20>

配置 Axios

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

1import { client } from 'laravel-precognition-vue';
2 
3client.axios().defaults.headers.common['Authorization'] = authToken;

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

1import Axios from 'axios';
2import { client } from 'laravel-precognition-vue';
3 
4window.axios = Axios.create()
5window.axios.defaults.headers.common['Authorization'] = authToken;
6 
7client.use(window.axios)

验证数组

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

1// Validate email for all users in an array...
2form.validate('users.*.email');
3 
4// Validate all fields in a profile object...
5form.validate('profile.*');
6 
7// Validate all fields for all users...
8form.validate('users.*.*');

自定义验证规则

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

例如,在用户创建表单中,我们可能希望仅在最终表单提交时验证密码是否“未泄露”。对于预判验证请求,我们仅验证密码是否为必填项且长度至少为 8 位。使用 isPrecognitive 方法,我们可以自定义表单请求中定义的规则。

1<?php
2 
3namespace App\Http\Requests;
4 
5use Illuminate\Foundation\Http\FormRequest;
6use Illuminate\Validation\Rules\Password;
7 
8class StoreUserRequest extends FormRequest
9{
10 /**
11 * Get the validation rules that apply to the request.
12 *
13 * @return array
14 */
15 protected function rules()
16 {
17 return [
18 'password' => [
19 'required',
20 $this->isPrecognitive()
21 ? Password::min(8)
22 : Password::min(8)->uncompromised(),
23 ],
24 // ...
25 ];
26 }
27}

处理文件上传

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

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

1/**
2 * Get the validation rules that apply to the request.
3 *
4 * @return array
5 */
6protected function rules()
7{
8 return [
9 'avatar' => [
10 ...$this->isPrecognitive() ? [] : ['required'],
11 'image',
12 'mimes:jpg,png',
13 'dimensions:ratio=3/2',
14 ],
15 // ...
16 ];
17}

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

1form.validateFiles();

管理副作用

当向路由添加 HandlePrecognitiveRequests 中间件时,你应该考虑在*其他*中间件中是否有需要跳过的副作用。

例如,你可能有一个中间件用于记录用户与应用的“交互”总次数,但你可能不希望预判请求被计入交互次数。为了实现这一点,可以在增加交互次数之前检查请求的 isPrecognitive 方法。

1<?php
2 
3namespace App\Http\Middleware;
4 
5use App\Facades\Interaction;
6use Closure;
7use Illuminate\Http\Request;
8 
9class InteractionMiddleware
10{
11 /**
12 * Handle an incoming request.
13 */
14 public function handle(Request $request, Closure $next): mixed
15 {
16 if (! $request->isPrecognitive()) {
17 Interaction::incrementFor($request->user());
18 }
19 
20 return $next($request);
21 }
22}

测试

如果你想在测试中发送预判请求,Laravel 的 TestCase 包含一个 withPrecognition 辅助函数,它会自动添加 Precognition 请求头。

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

1it('validates registration form with precognition', function () {
2 $response = $this->withPrecognition()
3 ->post('/register', [
4 'name' => 'Taylor Otwell',
5 ]);
6 
7 $response->assertSuccessfulPrecognition();
8 
9 expect(User::count())->toBe(0);
10});
1public function test_it_validates_registration_form_with_precognition()
2{
3 $response = $this->withPrecognition()
4 ->post('/register', [
5 'name' => 'Taylor Otwell',
6 ]);
7 
8 $response->assertSuccessfulPrecognition();
9 $this->assertSame(0, User::count());
10}