PHP 组件的未来 Livewire 4

统一 Livewire 生态

Caleb (Livewire 作者)解决 Livewire 生态系统中最紧迫的问题之一:碎片化。由于有三种不同的创建 Livewire 组件的方式(传统方式、Volt 函数式、Volt 类式),社区已经分裂,新手对于"正确"构建组件的方式感到困惑。

"整个社区都分叉了,"Porzio 承认。"有三种不同的方式来制作 livewire 组件。这对新手不好,对老手也不好,每个人都在猜测什么是最好的方式。"

他的解决方案?第四种方式来统一一切。

新的默认选择:单文件组件

在 Livewire 4 中,运行 php artisan make:livewire counter 默认会创建一个单文件、基于类的组件。这不再是 Volt——这就是 Livewire,由自定义解析器提供支持。

php
<?php

use Livewire\Component;

new class extends Component {
    public $count = 1;

    public function increment()
    {
        $this->count++;
    }

    public function decrement()
    {
        $this->count--;
    }
}; ?>

<div>
    <h1>{{ $count }}</h1>
    <button wire:click="increment">+</button>
    <button wire:click="decrement">-</button>
</div>

<script>
this.watch('count', (value) => {
    console.log('Count changed to', value);
});
</script>

单文件组件的新特性

原生 JavaScript 集成

不再需要 @script 指令。只需在文件底部添加 <script> 标签,并使用 this. 而不是 $wire

html
<script>
  this.watch('count', (value) => {
    console.log('Count changed to', value)
  })
</script>

拦截器功能

可以钩入请求生命周期的任何部分:

javascript
this.intercept('increment', ({ proceed, cancel }) => {
  if (confirm('Are you sure?')) {
    proceed()
  } else {
    cancel()
  }
})

多文件组件 两全其美

不是每个人都喜欢单文件组件,Porzio 理解这一点。当你在现有组件上运行 make:livewire 时,你会得到这个选项:

bash
php artisan make:livewire counter --mfc

这会创建一个多文件组件(MFC)结构:

⚡ counter/
├── counter.php
├── counter.blade.php
└── counter.js

PHP 文件是纯 PHP(匿名类),Blade 文件是纯 Blade,JavaScript 文件作为 ES6 模块提供,具有自动代码分割和缓存功能。

新的项目结构规范

Livewire 4 引入了有主见的文件夹结构,以减少决策疲劳:

  • resources/views/components/ - 所有你的组件(Blade 和 Livewire)
  • resources/views/pages/ - 页面级组件
  • resources/views/layouts/ - 布局组件

这些目录会自动创建命名空间,使组件组织更清洁、更可预测。

文件命名:闪电符号争议

这里变得有趣(也有争议)。Porzio 引入了闪电符号表情符号(⚡)作为 Livewire 组件的文件前缀:

⚡counter.livewire.php

虽然承认他可能是唯一喜欢它的人,但 Porzio 认为 Unicode 表情符号在所有文件系统中都有效,并提供即时的视觉识别。在其他框架(SvelteKit、Next.js)甚至编程语言(如 Mojo)中都有先例。

"我们什么时候不再开心了?"Porzio 问道。"反正我们都会失业的...让我们在路上用表情符号开心一下。"

利用 PHP 8.4 的属性钩子

Livewire 4 中最令人兴奋的特性之一是与 PHP 8.4 新的属性钩子深度集成:

使用 Setter 进行验证

php
public int $count {
    set => max(1, $value);
}

这会自动阻止计数低于 1,在许多情况下消除了更新钩子的需要。

使用 Getter 计算属性

php
public int $multiple {
    get => $this->count * 5;
}

在模板中像访问任何其他属性一样访问它:

html
<div>Count: {{ $count }}, Times 5: {{ $multiple }}</div>

使用属性钩子的高级缓存

你甚至可以直接在属性钩子中实现缓存:

php
public string $expensiveData {
    get => cache()->remember("data-{$this->id}", 3600, fn() => $this->fetchExpensiveData());
    set => cache()->put("data-{$this->id}", $value, 3600);
}

不对称可见性

PHP 8.4 还支持不对称属性可见性:

php
public private(set) string $readOnlyProperty;

这用原生 PHP 功能替换了 Livewire 的 #[Locked] 属性。

增强的加载状态

Livewire 4 会自动为任何触发网络请求的元素添加 data-loading 属性:

html
<button wire:click="save" class="data-[loading]:opacity-50">Save</button>

<div class="data-[loading]:block hidden">
  <span>Loading...</span>
</div>

这与 Tailwind CSS 4 的数据属性选择器完美集成。

期待已久的插槽功能

经过多年的请求,Livewire 4 终于支持插槽:

html
<livewire:modal>
  <form wire:submit="save">
    <input wire:model="title" />
    <button type="submit">Save</button>
  </form>
</livewire:modal>

模态组件的工作方式与 Blade 组件完全相同:

html
<!-- modal.blade.php -->
<div class="modal">{{ $slot }}</div>

组件引用进行通信

伴随插槽而来的是新的 wire:ref 系统,用于父子通信:

html
<livewire:modal wire:ref="modal">
  <form wire:submit="save">
    <!-- form content -->
  </form>
</livewire:modal>
php
public function save()
{
    // Save logic...
    $this->dispatch('close')->to(ref: 'modal');
}

当你需要直接的父子通信时,这消除了复杂事件广播模式的需要。

到目前为止,我们介绍了新的渲染引擎和拉取功能。接下来,让我们深入了解性能优化。

性能问题

在深入解决方案之前,Caleb 展示了许多 Laravel 开发者面临但很少测量的关键性能问题。使用简单的基准测试,他展示了 Blade 组件如何成瓶颈的

php
// 简单基准测试:循环中的 25,000 个 Blade 组件
$start = microtime(true);
for ($i = 0; $i < 25000; $i++) {
    // 渲染一个简单的 Blade 组件
}
$end = microtime(true);
echo ($end - $start) * 1000 . " milliseconds";

令人震惊的结果:

  • 首次运行(编译):508 毫秒
  • 第二次运行(缓存):274 毫秒
  • 纯 PHP require:26 毫秒
  • 普通 div + echo:21 毫秒

这揭示了即使是缓存的 Blade 组件也有显著的开销——比普通 PHP 慢约 10 倍。

Blaze - Blade 编译器革命

什么是 Blaze?

Blaze 是一个革命性的 Blade 优化层,使用代码折叠来消除运行时开销。Blaze 不是积极缓存(这会产生缓存管理问题),而是在编译时分析你的 Blade 模板并预渲染静态部分。

php
// Blaze 之前 - 具有框架开销的复杂编译输出
<?php $__env->startComponent('components.card'); ?>
    <?php $__env->slot('title'); ?>Product Name<?php $__env->endSlot(); ?>
    <div class="content"><?php echo e($product->description); ?></div>
<?php echo $__env->renderComponent(); ?>

// Blaze 之后 - 清洁、优化的输出
<div class="card">
    <h3 class="card-title">Product Name</h3>
    <div class="content"><?php echo e($product->description); ?></div>
</div>

代码折叠的工作原理

代码折叠识别模板中在生产环境中永远不会改变的部分:

html
{{-- 这个 Blade 组件 --}}
<x-card class="bg-white shadow-lg">
  <x-slot:title>{{ $title }}</x-slot:title>
  <div class="p-4">{{ $content }}</div>
</x-card>

Blaze 识别出卡片结构、CSS 类和 HTML 元素是静态的。只有 $title$content 变量是动态的。它在编译时预渲染静态部分。

安装和使用

bash
composer require livewire/blaze

就是这样!Blaze 与你现有的 Blade 模板透明地工作。你可以选择使用配置进行配置,但目标是零配置优化。

性能影响

演示中的实际结果:

  • Blaze 之前:1.6 秒内渲染 29,000 个视图
  • Blaze 之后:131 毫秒内渲染 100 个视图

这在保持完全相同的开发者体验的同时,性能提升了 10 倍以上。

Islands 架构

单体组件的问题

传统的 Livewire 组件在任何更新时都会完全重新渲染。考虑这个仪表板:

php
class Dashboard extends Component
{
    public function render()
    {
        return view('dashboard', [
            'analytics' => $this->getAnalytics(), // 快速查询
            'revenue' => $this->getAccountRevenue(), // 慢 - 1 秒查询
            'reports' => $this->getReports(), // 快速查询
        ]);
    }

    public function generateReport()
    {
        // 这个动作强制重新渲染所有内容
        // 包括慢速的收入计算
    }
}

每个动作都会触发昂贵的 getAccountRevenue() 查询,使整个界面变得迟缓。

Islands:手术级组件隔离

Islands 允许你隔离模板中昂贵的部分:

html
<div class="dashboard">
  {{-- 快速部分 - 始终响应 --}}
  <div class="analytics">
    @foreach($analytics as $metric)
    <x-metric :value="$metric->value" :label="$metric->name" />
    @endforeach
  </div>

  {{-- 慢速部分 - 在岛上隔离 --}} @island
  <div class="revenue-chart">
    <x-chart :data="$this->accountRevenue" />
  </div>
  @endisland {{-- 快速部分 - 始终响应 --}}
  <div class="reports">
    <button wire:click="generateReport">Generate Report</button>
    <button wire:click="downloadReport">Download</button>
  </div>
</div>

Island 的好处

  • 隔离:岛外的动作不会触发岛重新渲染
  • 性能:只有岛内容才会为岛特定的更新而处理
  • 响应性:你的界面其余部分保持快速

Islands 之前:

  • 生成报告:1+ 秒(重新渲染所有内容包括慢速收入)
  • 下载:1+ 秒(同样的问题)

Islands 之后:

  • 生成报告:即时(跳过岛)
  • 下载:即时(跳过岛)

使用 Islands 进行懒加载

Islands 支持带有优雅占位符处理的懒加载:

html
@island(lazy: true) @placeholder
<x-revenue-placeholder />
@endplaceholder

<div class="revenue-section">
  <x-chart :data="$this->expensiveRevenueData" />
</div>
@endisland

用户体验:

  • 页面立即加载占位符
  • 收入部分异步加载
  • 从骨架到真实数据的平滑过渡

高级 Island 功能

具有远程目标的命名 Islands

html
@island('reports') @foreach($reports as $report)
<div class="report-item">{{ $report->title }}</div>
@endforeach @endisland {{-- 这个按钮在岛外但目标是它 --}}
<button wire:island="reports" wire:click="loadMoreReports">Load More</button>

灵活的渲染模式

html
{{-- 默认:替换岛内容 --}} @island('reports')
<!-- content -->
@endisland {{-- 追加模式:将新内容添加到现有内容 --}}
<button wire:island="reports" wire:click="loadMore" wire:render="append">Load More</button>

{{-- 前置模式:将内容添加到开头 --}}
<button wire:island="chat" wire:click="loadOlderMessages" wire:render="prepend">Load Older Messages</button>

使用交集观察器的无限滚动

终极现代 UX 模式:

html
@island('posts', render: 'append') @foreach($posts as $post)
<article class="post">{{ $post->content }}</article>
@endforeach @endisland {{-- 不可见触发元素 --}}
<div wire:island="posts" wire:intersect="$paginator.nextPage()" class="h-4"></div>

这创建了真正的无限滚动:

  • ✅ 只在需要时获取新数据
  • ✅ 只渲染新内容
  • ✅ 保留滚动位置
  • ✅ 最少的 DOM 操作
  • ✅ 不需要 JavaScript

带轮询的 Islands

特定部分的实时更新:

html
@island(poll: '5s')
<div class="live-metrics">
  <span>Active Users: {{ $activeUsers }}</span>
  <span>Revenue Today: ${{ number_format($todayRevenue) }}</span>
</div>
@endisland

只有岛进行轮询——你页面的其余部分保持不变。

实际性能结果

完整解决方案

结合所有三个优化(新渲染引擎 + Blaze + Islands):

html
{{-- 一个复杂的仪表板,立即加载并保持响应 --}}
<div class="dashboard">
  {{-- 使用 Blaze 优化的快速部分 --}}
  <x-metric-grid :metrics="$quickMetrics" />

  {{-- 昂贵的部分隔离和懒加载 --}} @island('revenue', lazy: true) @placeholder
  <x-revenue-skeleton />
  @endplaceholder

  <x-revenue-chart :data="$expensiveRevenueData" />
  @endisland {{-- 带无限滚动的交互部分 --}} @island('reports', render: 'append') @foreach($reports as $report)
  <x-report-card :report="$report" />
  @endforeach @endisland

  <div wire:island="reports" wire:intersect="$paginator.nextPage()"></div>
</div>

性能特征:

  • 初始加载:即时(懒岛 + Blaze 优化)
  • 用户交互:始终响应(隔离岛)
  • 实时更新:手术精度(目标轮询)
  • 无限滚动:平滑且高性能(追加渲染)

开发者体验:简单而强大

理念

Livewire v4 的性能功能遵循一个关键原则:以最小的复杂性获得最大的性能。

html
{{-- 这就是无限滚动所需的全部 --}} @island('posts', render: 'append') @foreach($posts as $post)
<x-post :post="$post" />
@endforeach @endisland

<div wire:island="posts" wire:intersect="$paginator.nextPage()"></div>

与需要以下内容的传统 JavaScript 方法相比:

  • 复杂的状态管理
  • 手动 DOM 操作
  • 滚动位置跟踪
  • 加载状态处理
  • 错误边界管理

学习曲线

Islands 的美妙之处在于它们的可组合性。学习基本原语:

html
@island
<!-- expensive content -->
@endisland

然后与修饰符结合:

  • lazy: true - 懒加载
  • poll: '5s' - 实时更新
  • render: 'append' - 追加渲染
  • wire:intersect - 交集触发器

结论

Livewire v4 代表了 Laravel 应用程序的巨大飞跃。对于 Laravel 开发者来说,这意味着你可以构建复杂的、交互式的应用程序,而不牺牲性能或开发者体验。在"快速"和"功能丰富"之间选择的时代已经结束。

JaguarJack
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接