Skip to content

嵌套组件

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

你可能不需要 Livewire 组件

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

考虑使用岛屿进行隔离更新

如果你想将重新渲染隔离到组件的特定区域,而不需要创建单独子组件的开销,可以考虑使用岛屿。岛屿让你在单个组件内创建独立更新的区域,而无需管理 props、事件或子组件通信。

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

嵌套组件

要在父组件内嵌套 Livewire 组件,只需在父组件的 Blade 视图中包含它。以下是包含嵌套 todos 组件的 dashboard 父组件示例:

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

use Livewire\Component;

new class extends Component {
    //
};
?>

<div>
    <h1>Dashboard</h1>

    <livewire:todos />
</div>

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

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

向子组件传递 props

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

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

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;

传递静态 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" />

<livewire:todo-count :$todos />

在循环中渲染子组件

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

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

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

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

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

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

键不是可选的

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

响应式 props

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

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

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

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

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

    <livewire:todo-count :$todos />

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

现在让我们在 todo-count 组件的 $todos prop 上添加 #[Reactive]。一旦这样做,父组件内添加或删除的任何待办事项将自动触发 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]
    public $todos;

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

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

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

岛屿可以消除响应式 props 的需求

如果你发现自己主要是为了隔离更新而创建子组件并使用 #[Reactive] 来保持它们同步,可以考虑使用岛屿。岛屿在单个组件内提供隔离的重新渲染,而不需要响应式 props 或子组件通信。

使用 wire:model 绑定到子组件数据

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

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

以下是包含跟踪用户即将添加的当前待办事项的 $todo 属性的父 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 $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" />

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

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

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

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

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

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

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

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

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

WARNING

目前 Livewire 只支持单个 #[Modelable] 属性,所以只有第一个会被绑定。

插槽

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

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

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 :wire: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 :wire: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>

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

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

    <div class="actions">
        {{ $slots['actions'] }}
    </div>

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

检查插槽是否已提供

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

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

    @if ($slots->has('actions'))
        <div class="actions">
            {{ $slots['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 组件:

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 :wire: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')]
    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 :wire: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);
    }
};
?>

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

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

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

待办事项在父组件中被删除后,列表将重新渲染,分发 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" :wire: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" :wire: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" :wire: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 wire:key="lska" />
</div>

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

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

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

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

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

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

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

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

另请参阅

  • 事件 — 嵌套组件之间的通信
  • 组件 — 了解渲染和组织组件
  • 岛屿 — 隔离更新的嵌套替代方案
  • 理解嵌套 — 深入了解嵌套性能和行为
  • Reactive 属性 — 在嵌套组件中使 props 响应式