嵌套组件

您是视觉学习者吗?
通过我们深入的屏幕录制掌握 Livewire
立即观看

Livewire 允许您在父组件内嵌套其他 Livewire 组件。此功能非常强大,因为它允许您在应用程序中共享的 Livewire 组件中重新使用和封装行为。

您可能不需要 Livewire 组件

在将模板的一部分提取到嵌套 Livewire 组件之前,请自问:此组件中的内容是否需要“实时”?如果不是,我们建议您创建一个简单的 Blade 组件。仅当组件受益于 Livewire 的动态特性或有直接性能优势时,才创建 Livewire 组件。

查阅我们的 对 Livewire 组件嵌套的深入技术检查,以获取有关嵌套 Livewire 组件的性能、使用含义和约束的更多信息。

嵌套组件

要在父组件中嵌套 Livewire 组件,只需将其包含在父组件的 Blade 视图中即可。以下是包含嵌套 TodoList 组件的 Dashboard 父组件的示例

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
 
class Dashboard extends Component
{
public function render()
{
return view('livewire.dashboard');
}
}
<div>
<h1>Dashboard</h1>
 
<livewire:todo-list />
</div>

在此页面的初始渲染中,Dashboard 组件将遇到 <livewire:todo-list /> 并就地渲染它。在对 Dashboard 的后续网络请求中,嵌套的 todo-list 组件将跳过渲染,因为它现在是页面上自己的独立组件。有关嵌套和渲染背后的技术概念的更多信息,请参阅我们的文档,了解为什么 嵌套组件是“孤岛”

有关渲染组件的语法,请参阅我们的文档 渲染组件

传递道具给子组件

从父组件向子组件传递数据很简单。事实上,这很像向典型的 Blade 组件 传递道具。

例如,让我们查看一个 TodoList 组件,它将 $todos 集合传递给一个名为 TodoCount 的子组件

<?php
 
namespace App\Livewire;
 
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
 
class TodoList extends Component
{
public function render()
{
return view('livewire.todo-list', [
'todos' => Auth::user()->todos,
]);
}
}
<div>
<livewire:todo-count :todos="$todos" />
 
<!-- ... -->
</div>

正如你所看到的,我们使用语法 :todos="$todos"$todos 传递到 todo-count

现在 $todos 已传递到子组件,你可以通过子组件的 mount() 方法接收该数据

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use App\Models\Todo;
 
class TodoCount extends Component
{
public $todos;
 
public function mount($todos)
{
$this->todos = $todos;
}
 
public function render()
{
return view('livewire.todo-count', [
'count' => $this->todos->count(),
]);
}
}
省略mount() 作为更简洁的替代方法

如果你觉得上述示例中的 mount() 方法像冗余的样板代码,只要属性和参数名称匹配,就可以省略它

public $todos;

传递静态道具

在前面的示例中,我们使用 Livewire 的动态道具语法向子组件传递道具,它支持这样的 PHP 表达式

<livewire:todo-count :todos="$todos" />

但是,有时你可能希望向组件传递一个简单的静态值,例如字符串。在这些情况下,你可以省略语句开头的冒号

<livewire:todo-count :todos="$todos" label="Todo Count:" />

可以通过仅指定键来向组件提供布尔值。例如,要将值为 true$inline 变量传递给组件,我们只需在组件标记上放置 inline

<livewire:todo-count :todos="$todos" inline />

简短的属性语法

在将 PHP 变量传递到组件时,变量名和道具名通常相同。为了避免两次编写名称,Livewire 允许你简单地用冒号为变量添加前缀

-<livewire:todo-count :todos="$todos" />
 
+<livewire:todo-count :$todos />

在循环中渲染子组件

在循环中渲染子组件时,应为每次迭代包含一个唯一的 key 值。

组件键是 Livewire 在后续渲染中跟踪每个组件的方式,特别是如果组件已渲染或页面上已重新排列多个组件时。

你可以通过在子组件上指定 :key 属性来指定组件的键

<div>
<h1>Todos</h1>
 
@foreach ($todos as $todo)
<livewire:todo-item :$todo :key="$todo->id" />
@endforeach
</div>

如你所见,每个子组件都将有一个唯一的键,该键设置为每个 $todo 的 ID。这确保了如果待办事项重新排序,键将是唯一的并被跟踪。

键不是可选的

如果你使用过 Vue 或 Alpine 等前端框架,你就会熟悉在循环中为嵌套元素添加键。但是,在这些框架中,键不是必需的,这意味着项目将渲染,但重新排序可能无法正确跟踪。然而,Livewire 更依赖于键,如果没有键,它将行为不正确。

响应式属性

Livewire 的新开发者期望属性默认是“响应式”的。换句话说,他们期望当父组件更改传递给子组件的属性值时,子组件将自动更新。但是,默认情况下,Livewire 属性不是响应式的。

在使用 Livewire 时,每个组件都是一个孤岛。这意味着当父组件上触发更新并发出网络请求时,只有父组件的状态才会发送到服务器以重新渲染 - 而不是子组件。这种行为背后的目的是仅在服务器和客户端之间发送最少数量的数据,从而尽可能提高更新的性能。

但是,如果你希望或需要一个属性是响应式的,你可以使用 #[Reactive] 属性参数轻松启用此行为。

例如,以下是父级 TodoList 组件的模板。在内部,它正在渲染一个 TodoCount 组件并传入当前的待办事项列表

<div>
<h1>Todos:</h1>
 
<livewire:todo-count :$todos />
 
<!-- ... -->
</div>

现在,让我们在 TodoCount 组件中将 #[Reactive] 添加到 $todos 属性。一旦我们这样做,在父组件中添加或删除的任何待办事项都将自动触发 TodoCount 组件中的更新

<?php
 
namespace App\Livewire;
 
use Livewire\Attributes\Reactive;
use Livewire\Component;
use App\Models\Todo;
 
class TodoCount extends Component
{
#[Reactive]
public $todos;
 
public function render()
{
return view('livewire.todo-count', [
'count' => $this->todos->count(),
]);
}
}

响应式属性是一项非常强大的功能,它使 Livewire 更类似于 Vue 和 React 等前端组件库。但是,了解此功能的性能影响并仅在对特定场景有意义时添加 #[Reactive] 非常重要。

使用 wire:model 绑定到子数据

在父组件和子组件之间共享状态的另一个强大模式是通过 Livewire 的 Modelable 功能直接在子组件上使用 wire:model

在将输入元素提取到专用 Livewire 组件中同时仍访问其在父组件中的状态时,通常需要此行为。

下面是一个父 TodoList 组件的示例,其中包含一个 $todo 属性,该属性跟踪用户即将添加的当前待办事项

<?php
 
namespace App\Livewire;
 
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Todo;
 
class TodoList extends Component
{
public $todo = '';
 
public function add()
{
Todo::create([
'content' => $this->pull('todo'),
]);
}
 
public function render()
{
return view('livewire.todo-list', [
'todos' => Auth::user()->todos,
]);
}
}

如你所见,在 TodoList 模板中,wire:model 用于将 $todo 属性直接绑定到嵌套的 TodoInput 组件

<div>
<h1>Todos</h1>
 
<livewire:todo-input wire:model="todo" />
 
<button wire:click="add">Add Todo</button>
 
<div>
@foreach ($todos as $todo)
<livewire:todo-item :$todo :key="$todo->id" />
@endforeach
</div>
</div>

Livewire 提供了一个 #[Modelable] 属性,你可以将其添加到任何子组件属性中,以使其可以从父组件进行建模。

下面是 TodoInput 组件,其中 #[Modelable] 属性添加到 $value 属性上方,以向 Livewire 发出信号,如果父组件在组件上声明了 wire:model,则它应绑定到此属性

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use Livewire\Attributes\Modelable;
 
class TodoInput extends Component
{
#[Modelable]
public $value = '';
 
public function render()
{
return view('livewire.todo-input');
}
}
<div>
<input type="text" wire:model="value" >
</div>

现在,父 TodoList 组件可以将 TodoInput 视为任何其他输入元素,并使用 wire:model 直接绑定到其值。

目前,Livewire 仅支持一个 #[Modelable] 属性,因此只会绑定第一个属性。

监听来自子组件的事件

另一个强大的父子组件通信技术是 Livewire 的事件系统,它允许你在服务器或客户端上分发事件,其他组件可以拦截该事件。

我们关于 Livewire 事件系统的完整文档 提供了有关事件的更多详细信息,但下面我们将讨论使用事件触发父组件中更新的简单示例。

考虑一个具有显示和删除待办事项功能的 TodoList 组件

<?php
 
namespace App\Livewire;
 
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Todo;
 
class TodoList extends Component
{
public function remove($todoId)
{
$todo = Todo::find($todoId);
 
$this->authorize('delete', $todo);
 
$todo->delete();
}
 
public function render()
{
return view('livewire.todo-list', [
'todos' => Auth::user()->todos,
]);
}
}
<div>
@foreach ($todos as $todo)
<livewire:todo-item :$todo :key="$todo->id" />
@endforeach
</div>

要从子 TodoItem 组件内部调用 remove(),你可以通过 #[On] 属性向 TodoList 添加事件监听器

<?php
 
namespace App\Livewire;
 
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use App\Models\Todo;
use Livewire\Attributes\On;
 
class TodoList extends Component
{
#[On('remove-todo')]
public function remove($todoId)
{
$todo = Todo::find($todoId);
 
$this->authorize('delete', $todo);
 
$todo->delete();
}
 
public function render()
{
return view('livewire.todo-list', [
'todos' => Auth::user()->todos,
]);
}
}

一旦该属性被添加到操作中,你就可以从 TodoList 子组件分发 remove-todo 事件

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use App\Models\Todo;
 
class TodoItem extends Component
{
public Todo $todo;
 
public function remove()
{
$this->dispatch('remove-todo', todoId: $this->todo->id);
}
 
public function render()
{
return view('livewire.todo-item');
}
}
<div>
<span>{{ $todo->content }}</span>
 
<button wire:click="remove">Remove</button>
</div>

现在,当在 TodoItem 内部单击“移除”按钮时,父 TodoList 组件将拦截分发事件并执行待办事项移除。

在父组件中移除待办事项后,列表将重新渲染,分发 remove-todo 事件的子组件将从页面中移除。

通过在客户端分发来提高性能

尽管上述示例有效,但执行单个操作需要两次网络请求

  1. 来自 TodoItem 组件的第一个网络请求触发 remove 操作,分发 remove-todo 事件。
  2. 第二个网络请求是在客户端分发 remove-todo 事件之后,由 TodoList 拦截以调用其 remove 操作。

你可以通过直接在客户端分发 remove-todo 事件来完全避免第一个请求。下面是一个更新的 TodoItem 组件,它在分发 remove-todo 事件时不会触发网络请求

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use App\Models\Todo;
 
class TodoItem extends Component
{
public Todo $todo;
 
public function render()
{
return view('livewire.todo-item');
}
}
<div>
<span>{{ $todo->content }}</span>
 
<button wire:click="$dispatch('remove-todo', { todoId: {{ $todo->id }} })">Remove</button>
</div>

根据经验,尽可能始终优先考虑在客户端分发。

直接从子组件访问父组件

事件通信添加了一层间接性。父组件可以监听子组件从未分发的事件,而子组件可以分发父组件从未拦截的事件。

这种间接性有时是可取的;然而,在其他情况下,你可能更愿意直接从子组件访问父组件。

Livewire 允许你通过向 Blade 模板提供一个神奇的 $parent 变量来实现这一点,你可以使用该变量直接从子组件访问操作和属性。下面是上述 TodoItem 模板,它被重写为通过神奇的 $parent 变量直接在父组件上调用 remove() 操作

<div>
<span>{{ $todo->content }}</span>
 
<button wire:click="$parent.remove({{ $todo->id }})">Remove</button>
</div>

事件和直接父组件通信是父组件和子组件之间进行通信的一些方法。了解它们的权衡使你能够在特定场景中做出更明智的决策,选择使用哪种模式。

动态子组件

有时,您可能不知道哪个子组件应该在页面上呈现,直到运行时。因此,Livewire 允许您在运行时通过 <livewire:dynamic-component ...> 选择一个子组件,它接收一个 :is 属性

<livewire:dynamic-component :is="$current" />

动态子组件在各种不同的场景中很有用,但下面是一个使用动态组件呈现多步骤表单中不同步骤的示例

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
 
class Steps extends Component
{
public $current = 'step-one';
 
protected $steps = [
'step-one',
'step-two',
'step-three',
];
 
public function next()
{
$currentIndex = array_search($this->current, $this->steps);
 
$this->current = $this->steps[$currentIndex + 1];
}
 
public function render()
{
return view('livewire.todo-list');
}
}
<div>
<livewire:dynamic-component :is="$current" />
 
<button wire:click="next">Next</button>
</div>

现在,如果 Steps 组件的 $current 属性设置为“step-one”,Livewire 将像这样呈现一个名为“step-one”的组件

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
 
class StepOne extends Component
{
public function render()
{
return view('livewire.step-one');
}
}

如果您愿意,可以使用备用语法

<livewire:is :component="$current" />

递归组件

虽然大多数应用程序很少需要,但 Livewire 组件可以递归嵌套,这意味着父组件将自身呈现为其子组件。

想象一个包含 SurveyQuestion 组件的调查,该组件可以附加子问题

<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use App\Models\Question;
 
class SurveyQuestion extends Component
{
public Question $question;
 
public function render()
{
return view('livewire.survey-question', [
'subQuestions' => $this->question->subQuestions,
]);
}
}
<div>
Question: {{ $question->content }}
 
@foreach ($subQuestions as $subQuestion)
<livewire:survey-question :question="$subQuestion" :key="$subQuestion->id" />
@endforeach
</div>

当然,递归的标准规则适用于递归组件。最重要的是,您应该在模板中编写逻辑以确保模板不会无限递归。在上面的示例中,如果 $subQuestion 将原始问题包含为其自己的 $subQuestion,则会出现无限循环。

强制子组件重新呈现

在幕后,Livewire 为其模板中的每个嵌套 Livewire 组件生成一个键。

例如,考虑以下嵌套的 todo-count 组件

<div>
<livewire:todo-count :$todos />
</div>

Livewire 在内部将一个随机字符串键附加到组件,如下所示

<div>
<livewire:todo-count :$todos key="lska" />
</div>

当父组件正在呈现并遇到如上所示的子组件时,它会将键存储在附加到父组件的子组件列表中

'children' => ['lska'],

Livewire 在后续渲染中使用此列表作为参考,以检测子组件是否已在先前的请求中渲染过。如果已渲染过,则跳过该组件。记住,嵌套组件是孤立的。但是,如果子键不在列表中,则表示尚未渲染,Livewire 将创建组件的新实例并就地渲染它。

这些细微差别都是幕后行为,大多数用户无需了解;但是,在子级上设置键的概念是控制子级渲染的有力工具。

利用此知识,如果你想强制组件重新渲染,你可以简单地更改其键。

下面是一个示例,其中如果传递给组件的 todo 发生更改,我们可能希望销毁并重新初始化 todo-count 组件

<div>
<livewire:todo-count :todos="$todos" :key="$todos->pluck('id')->join('-')" />
</div>

如上所示,我们根据 $todos 的内容生成一个动态 :key 字符串。这样,todo-count 组件将正常渲染和存在,直到 $todos 本身发生更改。那时,组件将完全从头开始重新初始化,旧组件将被丢弃。