Volt
WARNING
在 Livewire v4 中 Volt 是可选的
Livewire v4 开箱即提供单文件组件,多数应用不必使用 Volt。Volt 面向更偏好函数式、闭包语法而非基于类组件的开发者。
Volt 是为 Livewire 精心打造的函数式 API,支持单文件组件,使组件的 PHP 逻辑与 Blade 模板可共存于同一文件。幕后会将函数式 API 编译为 Livewire 类组件,并与同文件中的模板关联。
一个简单的 Volt 组件如下所示:
<?php
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>安装
首先,用 Composer 包管理器将 Volt 安装到项目中:
composer require livewire/volt安装 Volt 后,可执行 volt:install Artisan 命令,将 Volt 的服务提供者文件安装到应用中。该服务提供者指定 Volt 搜索单文件组件的挂载目录:
php artisan volt:install创建组件
可在任意 Volt 挂载目录中放置 .blade.php 文件来创建 Volt 组件。默认情况下,VoltServiceProvider 会挂载 resources/views/livewire 与 resources/views/pages 目录,你也可在 Volt 服务提供者的 boot 方法中自定义这些目录。
为方便起见,可用 make:volt Artisan 命令创建新的 Volt 组件:
php artisan make:volt counter生成组件时加上 --test 指令,还会生成对应的测试文件。若希望相关测试使用 Pest,应使用 --pest 标志:
php artisan make:volt counter --test --pest加上 --class 指令会生成基于类的 Volt 组件。
php artisan make:volt counter --classAPI 风格
借助 Volt 的函数式 API,可通过导入的 Livewire\Volt 函数定义 Livewire 组件逻辑。Volt 再将函数式代码转换并编译为常规 Livewire 类,从而以更少样板代码使用 Livewire 的丰富能力。
Volt 的 API 会自动将所用闭包绑定到底层组件。因此,动作、计算属性或监听器随时可用 $this 变量引用该组件:
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
// ...基于类的 Volt 组件
若既想使用 Volt 的单文件组件能力,又想继续写基于类的组件,同样可以。首先定义一个继承 Livewire\Volt\Component 的匿名类。类内可用传统 Livewire 语法使用全部 Livewire 功能:
<?php
use Livewire\Volt\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
} ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>类属性
与典型 Livewire 组件一样,Volt 组件支持类属性。使用匿名 PHP 类时,类属性应写在 new 关键字之后:
<?php
use Livewire\Attributes\{Layout, Title};
use Livewire\Volt\Component;
new
#[Layout('layouts.guest')]
#[Title('Login')]
class extends Component {
public string $name = '';
// ...提供额外视图数据
使用基于类的 Volt 组件时,渲染的视图即同文件中的模板。若每次渲染都需向视图传递额外数据,可使用 with 方法。这些数据会与组件的公共属性一并传给视图:
<?php
use Livewire\WithPagination;
use Livewire\Volt\Component;
use App\Models\Post;
new class extends Component {
use WithPagination;
public function with(): array
{
return [
'posts' => Post::paginate(10),
];
}
} ?>
<div>
<!-- ... -->
</div>修改视图实例
有时你可能希望直接操作视图实例,例如用翻译字符串设置视图标题。为此,可在组件上定义 rendering 方法:
<?php
use Illuminate\View\View;
use Livewire\Volt\Component;
new class extends Component {
public function rendering(View $view): void
{
$view->title('Create Post');
// ...
}
// ...渲染与挂载组件
与典型 Livewire 组件一样,可用 Livewire 的标签语法或 @livewire Blade 指令渲染 Volt 组件:
<livewire:user-index :users="$users" />要声明组件接受的属性,可使用 state 函数:
use function Livewire\Volt\{state};
state('users');
// ...如有需要,可向 state 函数传入闭包以拦截传给组件的属性,从而与给定值交互并修改:
use function Livewire\Volt\{state};
state(['count' => fn ($users) => count($users)]);可用 mount 函数定义 Livewire 组件的「mount」生命周期钩子。传给组件的参数会注入该方法;mount 钩子所需的其他参数由 Laravel 服务容器解析:
use App\Services\UserCounter;
use function Livewire\Volt\{mount};
mount(function (UserCounter $counter, $users) {
$counter->store('userCount', count($users));
// ...
});整页组件
可选地,可在应用的 routes/web.php 中定义 Volt 路由,将 Volt 组件渲染为整页组件:
use Livewire\Volt\Volt;
Volt::route('/users', 'user-index');默认使用 components.layouts.app 布局渲染组件。可用 layout 函数自定义该布局文件:
use function Livewire\Volt\{layout, state};
state('users');
layout('components.layouts.admin');
// ...也可用 title 函数自定义页面标题:
use function Livewire\Volt\{layout, state, title};
state('users');
layout('components.layouts.admin');
title('Users');
// ...若标题依赖组件状态或外部依赖,可向 title 函数传入闭包:
use function Livewire\Volt\{layout, state, title};
state('users');
layout('components.layouts.admin');
title(fn () => 'Users: ' . $this->users->count());属性
与 Livewire 属性一样,Volt 属性可在视图中便捷访问,并在 Livewire 更新之间持久化。可用 state 函数定义属性:
<?php
use function Livewire\Volt\{state};
state(['count' => 0]);
?>
<div>
{{ $count }}
</div>若 state 属性的初始值依赖外部因素(如数据库查询、模型或容器服务),应将其解析封装在闭包中,以免在真正需要之前就解析该值:
use App\Models\User;
use function Livewire\Volt\{state};
state(['count' => fn () => User::count()]);若 state 属性的初始值通过 Laravel Folio 的路由模型绑定注入,也应封装在闭包中:
use App\Models\User;
use function Livewire\Volt\{state};
state(['user' => fn () => $user]);当然,也可不显式指定初始值来声明属性。此时初始值为 null,或在组件渲染时根据传入的属性设置:
use function Livewire\Volt\{mount, state};
state(['count']);
mount(function ($users) {
$this->count = count($users);
//
});锁定属性
Livewire 允许你「锁定」属性以防客户端修改。在 Volt 中,对要保护的 state 链式调用 locked 方法即可:
state(['id'])->locked();响应式属性
使用嵌套组件时,可能需要把父组件属性传给子组件,并在父组件更新该属性时让子组件自动更新。
在 Volt 中,对希望成为响应式的 state 链式调用 reactive 方法即可:
state(['todos'])->reactive();可绑定属性
若不使用响应式属性,Livewire 还提供可绑定(modelable)功能:可直接在子组件上用 wire:model 在父子组件间共享状态。
在 Volt 中,对希望成为可绑定的 state 链式调用 modelable 方法即可:
state(['form'])->modelable();计算属性
Livewire 也允许定义计算属性,便于懒加载组件所需信息。计算属性的结果会在单次 Livewire 请求生命周期内「记忆化」(缓存在内存中)。
可用 computed 函数定义计算属性。变量名将决定计算属性的名称:
<?php
use App\Models\User;
use function Livewire\Volt\{computed};
$count = computed(function () {
return User::count();
});
?>
<div>
{{ $this->count }}
</div>可在计算属性定义上链式调用 persist,将值持久化到应用缓存:
$count = computed(function () {
return User::count();
})->persist();默认情况下,Livewire 会将计算属性值缓存 3600 秒。可通过向 persist 传入秒数自定义:
$count = computed(function () {
return User::count();
})->persist(seconds: 10);动作
Livewire 动作提供了监听页面交互并调用组件对应方法、从而重新渲染组件的便捷方式。动作通常在用户点击按钮时触发。
用 Volt 定义 Livewire 动作只需定义一个闭包。存放闭包的变量名将决定动作名称:
<?php
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>闭包内 $this 绑定到底层 Livewire 组件,可像典型 Livewire 组件一样访问组件上的其他方法:
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = function () {
$this->dispatch('count-updated');
//
};动作也可接收参数,或从 Laravel 服务容器注入依赖:
use App\Repositories\PostRepository;
use function Livewire\Volt\{state};
state(['postId']);
$delete = function (PostRepository $posts) {
$posts->delete($this->postId);
// ...
};无渲染动作
有时组件声明的动作不会导致已渲染 Blade 模板发生变化。此时可将动作封装在 action 函数中并链式调用 renderless,以跳过 Livewire 生命周期的渲染阶段:
use function Livewire\Volt\{action};
$incrementViewCount = action(fn () => $this->viewCount++)->renderless();受保护的辅助方法
默认情况下,所有 Volt 动作都是「公共」的,可由客户端调用。若要创建仅能从动作内部访问的函数,可使用 protect 函数:
use App\Repositories\PostRepository;
use function Livewire\Volt\{protect, state};
state(['postId']);
$delete = function (PostRepository $posts) {
$this->ensurePostCanBeDeleted();
$posts->delete($this->postId);
// ...
};
$ensurePostCanBeDeleted = protect(function () {
// ...
});表单
Livewire 的表单提供了在单个类中处理表单验证与提交的便捷方式。要在 Volt 组件中使用 Livewire 表单,可使用 form 函数:
<?php
use App\Livewire\Forms\PostForm;
use function Livewire\Volt\{form};
form(PostForm::class);
$save = function () {
$this->form->store();
// ...
};
?>
<form wire:submit="save">
<input type="text" wire:model="form.title">
@error('form.title') <span class="error">{{ $message }}</span> @enderror
<button type="submit">Save</button>
</form>可见,form 函数接受 Livewire 表单类名。定义后,可在组件内通过 $this->form 属性访问该表单。
若希望表单使用不同的属性名,可将名称作为第二个参数传给 form 函数:
form(PostForm::class, 'postForm');
$save = function () {
$this->postForm->store();
// ...
};监听器
Livewire 的全局事件系统支持组件间通信。同一页面上的两个 Livewire 组件可通过事件与监听器通信。使用 Volt 时,可用 on 函数定义监听器:
use function Livewire\Volt\{on};
on(['eventName' => function () {
//
}]);若需为事件监听器指定动态名称(例如基于已认证用户或传给组件的数据),可向 on 传入闭包。该闭包可接收任意组件参数,以及由 Laravel 服务容器解析的额外依赖:
on(fn ($post) => [
'event-'.$post->id => function () {
//
}),
]);为方便起见,定义监听器时也可用「点」记号引用组件数据:
on(['event-{post.id}' => function () {
//
}]);生命周期钩子
Livewire 提供多种生命周期钩子,可在组件生命周期的不同点执行代码。借助 Volt 的便捷 API,可用对应函数定义这些钩子:
use function Livewire\Volt\{boot, booted, ...};
boot(fn () => /* ... */);
booted(fn () => /* ... */);
mount(fn () => /* ... */);
hydrate(fn () => /* ... */);
hydrate(['count' => fn () => /* ... */]);
dehydrate(fn () => /* ... */);
dehydrate(['count' => fn () => /* ... */]);
updating(['count' => fn () => /* ... */]);
updated(['count' => fn () => /* ... */]);懒加载占位符
渲染 Livewire 组件时,可向组件传入 lazy 参数,推迟其加载直至初始页面完全加载。默认情况下,Livewire 会在组件将要加载的位置插入 <div></div> 标签。
若要自定义初始页面加载期间组件占位符中显示的 HTML,可使用 placeholder 函数:
use function Livewire\Volt\{placeholder};
placeholder('<div>Loading...</div>');验证
Livewire 便于使用 Laravel 强大的验证功能。借助 Volt API,可用 rules 函数定义组件验证规则。与传统 Livewire 组件一样,调用 validate 方法时这些规则会应用到组件数据:
<?php
use function Livewire\Volt\{rules};
rules(['name' => 'required|min:6', 'email' => 'required|email']);
$submit = function () {
$this->validate();
// ...
};
?>
<form wire:submit.prevent="submit">
//
</form>若需动态定义规则(例如基于已认证用户或数据库信息),可向 rules 函数传入闭包:
rules(fn () => [
'name' => ['required', 'min:6'],
'email' => ['required', 'email', 'not_in:'.Auth::user()->email]
]);错误消息与属性
要修改验证过程中使用的消息或属性名,可在 rules 定义上链式调用 messages 与 attributes:
use function Livewire\Volt\{rules};
rules(['name' => 'required|min:6', 'email' => 'required|email'])
->messages([
'email.required' => 'The :attribute may not be empty.',
'email.email' => 'The :attribute format is invalid.',
])->attributes([
'email' => 'email address',
]);文件上传
使用 Volt 时,借助 Livewire,上传与存储文件更简单。要在函数式 Volt 组件上引入 Livewire\WithFileUploads trait,可使用 usesFileUploads 函数:
use function Livewire\Volt\{state, usesFileUploads};
usesFileUploads();
state(['photo']);
$save = function () {
$this->validate([
'photo' => 'image|max:1024',
]);
$this->photo->store('photos');
};URL 查询参数
有时在组件状态变化时更新浏览器 URL 查询参数很有用。此时可用 url 方法指示 Livewire 将 URL 查询参数与某段组件状态同步:
<?php
use App\Models\Post;
use function Livewire\Volt\{computed, state};
state(['search'])->url();
$posts = computed(function () {
return Post::where('title', 'like', '%'.$this->search.'%')->get();
});
?>
<div>
<input wire:model.live="search" type="search" placeholder="Search posts by title...">
<h1>Search Results:</h1>
<ul>
@foreach($this->posts as $post)
<li wire:key="{{ $post->id }}">{{ $post->title }}</li>
@endforeach
</ul>
</div>Livewire 支持的其他 URL 查询参数选项(如别名)也可传给 url 方法:
use App\Models\Post;
use function Livewire\Volt\{state};
state(['page' => 1])->url(as: 'p', history: true, keep: true);
// ...分页
Livewire 与 Volt 也完整支持分页。要在函数式 Volt 组件上引入 Livewire\WithPagination trait,可使用 usesPagination 函数:
<?php
use function Livewire\Volt\{with, usesPagination};
usesPagination();
with(fn () => ['posts' => Post::paginate(10)]);
?>
<div>
@foreach ($posts as $post)
//
@endforeach
{{ $posts->links() }}
</div>与 Laravel 一样,Livewire 默认分页视图使用 Tailwind 类做样式。若应用使用 Bootstrap,可在调用 usesPagination 时指定主题以启用 Bootstrap 分页主题:
usesPagination(theme: 'bootstrap');自定义 trait 与接口
要在函数式 Volt 组件上引入任意 trait 或接口,可使用 uses 函数:
use function Livewire\Volt\{uses};
use App\Contracts\Sorting;
use App\Concerns\WithSorting;
uses([Sorting::class, WithSorting::class]);匿名组件
有时你可能想把页面的一小部分转成 Volt 组件,而不抽到单独文件。例如,设想一个返回如下视图的 Laravel 路由:
Route::get('/counter', fn () => view('pages/counter.blade.php'));视图内容是典型的 Blade 模板,包含布局定义与插槽。不过,用 @volt Blade 指令包裹视图的一部分,即可将该片段转为功能完整的 Volt 组件:
<?php
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
?>
<x-app-layout>
<x-slot name="header">
Counter
</x-slot>
@volt('counter')
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
@endvolt
</x-app-layout>向匿名组件传递数据
渲染包含匿名组件的视图时,传给视图的全部数据也对匿名 Volt 组件可用:
use App\Models\User;
Route::get('/counter', fn () => view('users.counter', [
'count' => User::count(),
]));当然,也可将这些数据声明为 Volt 组件上的「state」。从视图代理到组件的数据初始化 state 时,只需声明 state 变量名。Volt 会用代理的视图数据自动水合 state 的默认值:
<?php
use function Livewire\Volt\{state};
state('count');
$increment = function () {
// Store the new count value in the database...
$this->count++;
};
?>
<x-app-layout>
<x-slot name="header">
Initial value: {{ $count }}
</x-slot>
@volt('counter')
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
@endvolt
</x-app-layout>测试组件
要开始测试 Volt 组件,可调用 Volt::test 方法并传入组件名:
use Livewire\Volt\Volt;
it('increments the counter', function () {
Volt::test('counter')
->assertSee('0')
->call('increment')
->assertSee('1');
});测试 Volt 组件时,可使用标准 Livewire 测试 API 提供的全部方法。
若 Volt 组件是嵌套的,可用「点」记号指定要测试的组件:
Volt::test('users.stats')测试包含匿名 Volt 组件的页面时,可用 assertSeeVolt 方法断言该组件已渲染:
$this->get('/users')
->assertSeeVolt('stats');