Precognition
介绍
Laravel Precognition 让你能够预判未来 HTTP 请求的结果。Precognition 的主要用途之一,是为前端 JavaScript 应用提供「实时」校验,而无需重复编写后端校验规则。Precognition 与 Laravel 基于 Inertia 的起步套件尤其搭配良好。
当 Laravel 收到「预知请求」(precognitive request)时,会执行该路由的全部中间件并解析控制器依赖,包括校验表单请求,但不会真正执行路由的控制器方法。
实时校验
使用 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 安装 Laravel Precognition 的 Vue 前端辅助包:
npm install laravel-precognition-vue安装 Laravel 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>WARNING
表单输入框只有在发生变更且收到校验响应后,才会显示为有效或无效。
使用 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>使用 Vue 与 Inertia
INFO
若希望在用 Vue 与 Inertia 开发 Laravel 应用时抢先起步,可考虑使用我们的起步套件之一。Laravel 起步套件会为新的 Laravel 应用提供前后端认证脚手架。
在 Vue 与 Inertia 中使用 Precognition 之前,请务必先阅读在 Vue 中使用 Precognition的通用文档。将 Vue 与 Inertia 一起使用时,需要通过 NPM 安装与 Inertia 兼容的 Precognition 库:
npm install laravel-precognition-vue-inertia安装后,Precognition 的 useForm 函数将返回一个增强了上文所述校验功能的 Inertia 表单辅助。
表单辅助的 submit 方法已简化,无需再指定 HTTP 方法或 URL。相反,你可将 Inertia 的 visit 选项作为唯一参数传入。此外,与上文 Vue 示例不同,submit 方法不返回 Promise。相反,你可在传给 submit 方法的 visit 选项中提供 Inertia 支持的任意事件回调:
<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 中间件。同时应创建表单请求来承载该路由的校验规则:
use App\Http\Requests\StoreUserRequest;
use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests;
Route::post('/users', function (StoreUserRequest $request) {
// ...
})->middleware([HandlePrecognitiveRequests::class]);接下来,通过 NPM 安装 Laravel Precognition 的 React 前端辅助包:
npm install laravel-precognition-react安装 Laravel 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>}WARNING
表单输入框只有在发生变更且收到校验响应后,才会显示为有效或无效。
使用 Precognition 校验表单的部分输入时,手动清除错误会很有用。可调用表单的 forgetError 函数实现:
<input
id="avatar"
type="file"
onChange={(e) => {
form.setData('avatar', e.target.value);
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>使用 React 与 Inertia
INFO
若希望在用 React 与 Inertia 开发 Laravel 应用时抢先起步,可考虑使用我们的起步套件之一。Laravel 起步套件会为新的 Laravel 应用提供前后端认证脚手架。
在 React 与 Inertia 中使用 Precognition 之前,请务必先阅读在 React 中使用 Precognition的通用文档。将 React 与 Inertia 一起使用时,需要通过 NPM 安装与 Inertia 兼容的 Precognition 库:
npm install laravel-precognition-react-inertia安装后,Precognition 的 useForm 函数将返回一个增强了上文所述校验功能的 Inertia 表单辅助。
表单辅助的 submit 方法已简化,无需再指定 HTTP 方法或 URL。相反,你可将 Inertia 的 visit 选项作为唯一参数传入。此外,与上文 React 示例不同,submit 方法不返回 Promise。相反,你可在传给 submit 方法的 visit 选项中提供 Inertia 支持的任意事件回调:
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 中间件。同时应创建表单请求来承载该路由的校验规则:
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 中将 Precognition 插件注册到 Alpine:
import Alpine from 'alpinejs';
import Precognition from 'laravel-precognition-alpine';
window.Alpine = Alpine;
Alpine.plugin(Precognition);
Alpine.start();安装并注册 Laravel 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>WARNING
表单输入框只有在发生变更且收到校验响应后,才会显示为有效或无效。
如前所述,可监听输入框的 change 事件,在用户交互时逐个校验;但有时需要校验用户尚未交互的输入。这在构建「向导」时很常见——在进入下一步前,无论用户是否已交互,都要校验所有可见输入。
在 Precognition 中,可调用 validate 方法,将要校验的字段名传入 only 配置项。可通过 onSuccess 或 onValidationError 回调处理校验结果:
<button
type="button"
@click="form.validate({
only: ['name', 'email', 'phone'],
onSuccess: (response) => nextStep(),
onValidationError: (response) => /* ... */,
})"
>Next Step</button>可通过表单的 processing 属性判断表单提交请求是否进行中:
<button :disabled="form.processing">
Submit
</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 => {
form.reset();
alert('User created.')
})
.catch(error => {
alert('An error occurred.');
});
},
}"
@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)WARNING
Inertia 风格的 Precognition 库仅会将配置的 Axios 实例用于校验请求。表单提交始终由 Inertia 发送。
自定义校验规则
可通过请求的 isPrecognitive 方法,自定义预知请求期间执行的校验规则。
例如,在用户创建表单中,我们可能只在最终提交时校验密码是否「未泄露」。对于预知校验请求,仅校验密码必填且至少 8 个字符。使用 isPrecognitive 方法可自定义表单请求中的规则:
<?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 在预知校验请求期间不会上传或校验文件,以避免大文件被重复上传。
因此,应确保应用自定义对应表单请求的校验规则,使该字段仅在完整表单提交时必填:
/**
* 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 函数:
form.validateFiles();管理副作用
为路由添加 HandlePrecognitiveRequests 中间件时,应考虑其他中间件中是否有副作用应在预知请求期间跳过。
例如,某中间件会累加用户与应用的「交互」次数,但你可能不希望将预知请求计为一次交互。可在增加交互计数前检查请求的 isPrecognitive 方法:
<?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 方法:
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());
}