Skip to content

嵌套组件

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

你可能不需要 Livewire 组件

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

有关嵌套 Livewire 组件的性能、使用影响和约束的更多信息,请查阅我们的 Livewire 组件嵌套的深入技术研究

嵌套组件

要在父组件中嵌套 Livewire 组件,只需将其包含在父组件的 Blade 视图中。下面是一个 dashboard 父组件的示例,其中包含一个嵌套的 todos 组件:

php
<?php // resources/views/components/⚡dashboard.blade.php

use Livewire\Component;

new class extends Component
{
    //
};
?>

<div>
    <h1>Dashboard</h1>

    <livewire:todos /> <!-- [tl! highlight] -->
</div>

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

有关渲染组件的语法的更多信息,请查阅我们关于渲染组件的文档。

传递 props 给子组件

从父组件向子组件传递数据很简单。实际上,它非常类似于将 props 传递给典型的 Blade 组件

例如,让我们看一个 todos 组件,它将一个 $todos 集合传递给名为 todo-count 的子组件:

php
<?php // resources/views/components/⚡todos.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;

new class extends Component
{
    #[Computed]
    public function todos()
    {
        return Auth::user()->todos,
    }
};
?>

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

    <!-- ... -->
</div>

如你所见,我们使用语法 :todos="$this->todos"$this->todos 传递给 todo-count

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

php
<?php // resources/views/components/⚡todo-count.blade.php

use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    public $todos;

    public function mount($todos)
    {
        $this->todos = $todos;
    }

    #[Computed]
    public function count()
    {
        return $this->todos->count(),
    }
};
?>

<div>
    Count: {{ $this->count }}
</div>

省略 mount() 作为更短的替代方案

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

php
public $todos; // [tl! highlight]

传递静态 props

在前面的示例中,我们使用 Livewire 的动态 prop 语法将 props 传递给子组件,它支持 PHP 表达式,如下所示:

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

然而,有时你可能想将一个简单的静态值(如字符串)传递给组件。在这些情况下,你可以省略语句开头的冒号:

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

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

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

简写属性语法

当将 PHP 变量传递到组件时,变量名称和 prop 名称通常相同。为了避免写两次名称,Livewire 允许你简单地在变量前加上冒号:

blade
<livewire:todo-count :todos="$todos" /> <!-- [tl! remove] -->

<livewire:todo-count :$todos /> <!-- [tl! add] -->

在循环中渲染子组件

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

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

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

blade
<div>
    <h1>Todos</h1>

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

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

键不是可选的

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

响应式 props

Livewire 新手开发者期望 props 默认是"响应式"的。换句话说,他们期望当父组件更改传递给子组件的 prop 值时,子组件将自动更新。然而,默认情况下,Livewire props 不是响应式的。

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

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

例如,下面是父 todos 组件的模板。在内部,它正在渲染一个 todo-count 组件并传入当前的 todos 列表:

blade
<div>
    <h1>Todos:</h1>

    <livewire:todo-count :$todos />

    <!-- ... -->
</div>

现在让我们在 todo-count 组件中的 $todos prop 上添加 #[Reactive]。一旦我们这样做,在父组件内添加或删除的任何 todos 都将自动触发 todo-count 组件内的更新:

php
<?php // resources/views/components/⚡todo-count.blade.php

use Livewire\Attributes\Reactive;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    #[Reactive] // [tl! highlight]
    public $todos;

    #[Computed]
    public function count()
    {
        return $this->todos->count(),
    }
};
?>

<div>
    Count: {{ $this->count }}
</div>

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

使用 wire:model 绑定子数据

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

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

下面是一个父 todos 组件的示例,它包含一个 $todo 属性,该属性跟踪用户即将添加的当前 todo:

php
<?php // resources/views/components/⚡todos.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    public $todo = '';

    public function add()
    {
        Todo::create([
            'content' => $this->pull('todo'),
        ]);
    }

    #[Computed]
    public function todos()
    {
        return Auth::user()->todos,
    }
};

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

blade
<div>
    <h1>Todos</h1>

    <livewire:todo-input wire:model="todo" /> <!-- [tl! highlight] -->

    <button wire:click="add">Add Todo</button>

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

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

下面是 todo-input 组件,在 $value 属性上方添加了 #[Modelable] 属性,以向 Livewire 信号如果父组件在组件上声明了 wire:model,它应该绑定到此属性:

php
<?php // resources/views/components/⚡todo-input.blade.php

use Livewire\Attributes\Modelable;
use Livewire\Component;

new class extends Component
{
    #[Modelable] // [tl! highlight]
    public $value = '';
};
?>

<div>
    <input type="text" wire:model="value" >
</div>

现在父 todos 组件可以像处理任何其他输入元素一样处理 todo-input,并使用 wire:model 直接绑定到其值。

WARNING

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

插槽

插槽允许你将 Blade 内容从父组件传递到子组件。当子组件需要渲染自己的内容同时也允许父组件在特定位置注入自定义内容时,这很有用。

下面是一个父组件的示例,它渲染了评论列表。每个评论由 Comment 子组件渲染,但父组件通过插槽传入一个"删除"按钮:

php
<?php

use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Post;

new class extends Component
{
    public Post $post;

    #[Computed]
    public function comments()
    {
        return $this->post->comments;
    }

    public function removeComment($id)
    {
        $this->post->comments()->find($id)->delete();
    }
};
?>

<div>
    @foreach ($this->comments as $comment)
        <livewire:comment :$comment :key="$comment->id">
            <button wire:click="removeComment({{ $comment->id }})">
                Remove
            </button>
        </livewire:comment>
    @endforeach
</div>

现在内容已经传递给 Comment 子组件,你可以使用 $slot 变量渲染它:

php
<?php

use Livewire\Component;
use App\Models\Comment;

new class extends Component
{
    public Comment $comment;
};
?>

<div>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>

    {{ $slot }}
</div>

Comment 组件渲染 $slot 时,Livewire 将注入从父组件传递的内容。

重要的是要理解插槽是在父组件的上下文中评估的。这意味着插槽内引用的任何属性或方法都属于父组件,而不是子组件。在上面的示例中,removeComment() 方法在父组件上调用,而不是 Comment 子组件。

命名插槽

除了默认插槽外,你还可以将多个命名插槽传递到子组件。当你想为子组件的多个区域提供内容时,这很有用。

下面是向 Comment 组件传递默认插槽和命名的 actions 插槽的示例:

blade
<div>
    @foreach ($this->comments as $comment)
        <livewire:comment :$comment :key="$comment->id">
            <livewire:slot name="actions">
                <button wire:click="removeComment({{ $comment->id }})">
                    Remove
                </button>
            </livewire:slot>

            <span>Posted on {{ $comment->created_at }}</span>
        </livewire:comment>
    @endforeach
</div>

你可以通过将插槽名称传递给 $slot 变量在子组件中访问命名插槽:

blade
<div>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>

    <div class="actions">
        {{ $slot('actions') }}
    </div>

    <div class="metadata">
        {{ $slot }}
    </div>
</div>

检查插槽是否提供

你可以使用 $slot 变量上的 has() 方法检查父组件是否提供了插槽。当你想根据插槽是否存在有条件地渲染内容时,这很有帮助:

blade
<div>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>

    @if ($slot->has('actions'))
        <div class="actions">
            {{ $slot('actions') }}
        </div>
    @endif

    {{ $slot }}
</div>

转发 HTML 属性

像 Blade 组件一样,Livewire 组件支持使用 $attributes 变量从父组件向子组件转发 HTML 属性。

下面是一个父组件将 class 属性传递给子组件的示例:

blade
<livewire:comment :$comment class="border-b" />

你可以在子组件中使用 $attributes 变量应用这些属性:

blade
<div {{ $attributes->class('bg-white rounded-md') }}>
    <p>{{ $comment->author }}</p>
    <p>{{ $comment->body }}</p>
</div>

与公共属性名称匹配的属性会自动作为 props 传递并从 $attributes 中排除。任何剩余属性(如 classiddata-*)都可以通过 $attributes 获取。

监听来自子组件的事件

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

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

考虑一个具有显示和删除 todos 功能的 todos 组件:

php
<?php // resources/views/components/⚡todos.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    public function remove($todoId)
    {
        $todo = Todo::find($todoId);

        $this->authorize('delete', $todo);

        $todo->delete();
    }

    #[Computed]
    public function todos()
    {
        return Auth::user()->todos,
    }
};
?>

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

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

php
<?php // resources/views/components/⚡todos.blade.php

use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    #[On('remove-todo')] // [tl! highlight]
    public function remove($todoId)
    {
        $todo = Todo::find($todoId);

        $this->authorize('delete', $todo);

        $todo->delete();
    }

    #[Computed]
    public function todos()
    {
        return Auth::user()->todos,
    }
};
?>

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

一旦将属性添加到操作,你可以从 todo-item 子组件派发 remove-todo 事件:

php
<?php // resources/views/components/⚡todo-item.blade.php

use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    public Todo $todo;

    public function remove()
    {
        $this->dispatch('remove-todo', todoId: $this->todo->id); // [tl! highlight]
    }
};
?>

<div>
    <span>{{ $todo->content }}</span>

    <button wire:click="remove">Remove</button>
</div>

现在,当在 todo-item 内部点击"删除"按钮时,父 todos 组件将拦截派发的事件并执行 todo 删除。

在父组件中删除 todo 后,列表将被重新渲染,并且派发 remove-todo 事件的子组件将从页面中删除。

通过客户端派发提高性能

尽管上面的示例有效,但它需要两个网络请求来执行单个操作:

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

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

php
<?php // resources/views/components/⚡todo-item.blade.php

use Livewire\Component;
use App\Models\Todo;

new class extends Component
{
    public Todo $todo;
};
?>

<div>
    <span>{{ $todo->content }}</span>

    <button wire:click="$dispatch('remove-todo', { todoId: {{ $todo->id }} })">Remove</button>
</div>

作为经验法则,在可能的情况下始终优先使用客户端派发。

从子组件直接访问父组件

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

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

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

blade
<div>
    <span>{{ $todo->content }}</span>

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

事件和直接父组件通信是在父子组件之间来回通信的几种方式。理解它们的权衡使你能够在特定场景中就使用哪种模式做出更明智的决定。

动态子组件

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

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

动态子组件在各种不同的场景中都很有用,但下面是使用动态组件在多步骤表单中渲染不同步骤的示例:

php
<?php // resources/views/components/⚡steps.blade.php

use Livewire\Component;

new class 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];
    }
};
?>

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

    <button wire:click="next">Next</button>
</div>

现在,如果 steps 组件的 $current prop 设置为 "step-one",Livewire 将渲染一个名为 "step-one" 的组件,如下所示:

php
<?php // resources/views/components/⚡step-one.blade.php

use Livewire\Component;

new class extends Component
{
    //
};
?>

<div>
    Step One Content
</div>

如果你愿意,可以使用替代语法:

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

WARNING

不要忘记为每个子组件分配一个唯一的键。尽管 Livewire 自动为 <livewire:dynamic-child /><livewire:is /> 生成键,但相同的键将应用于_所有_子组件,这意味着后续渲染将被跳过。

有关键如何影响组件渲染的更深入理解,请参阅强制子组件重新渲染

递归组件

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

想象一个调查,其中包含一个 survey-question 组件,该组件可以附加子问题:

php
<?php // resources/views/components/⚡survey-question.blade.php

use Livewire\Attributes\Computed;
use Livewire\Component;
use App\Models\Question;

new class extends Component
{
    public Question $question;

    #[Computed]
    public function subQuestions()
    {
        return $this->question->subQuestions,
    }
};
?>

<div>
    Question: {{ $question->content }}

    @foreach ($this->subQuestions as $subQuestion)
        <livewire:survey-question :question="$subQuestion" :key="$subQuestion->id" />
    @endforeach
</div>

WARNING

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

强制子组件重新渲染

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

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

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

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

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

当父组件正在渲染并遇到上面这样的子组件时,它将键存储在附加到父组件的子组件列表中:

php
'children' => ['lska'],

Livewire 使用此列表在后续渲染时作为参考,以检测子组件是否已在上一次请求中被渲染。如果已经被渲染,则跳过该组件。记住,嵌套组件是孤岛。然而,如果子键不在列表中,这意味着它尚未被渲染,Livewire 将创建组件的新实例并在原地渲染它。

这些细微差别都是大多数用户不需要了解的幕后行为;然而,在子组件上设置键的概念是控制子组件渲染的强大工具。

使用这些知识,如果你想强制组件重新渲染,只需更改其键即可。

下面是一个示例,如果传递给组件的 $todos 被更改,我们可能想要销毁并重新初始化 todo-count 组件:

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

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