Livewire4 正式发布!PHP 也可以无需写一行 Javascript 代码就能实现 Vue 的功能
Livewire 4 正式发布,这是迄今为止最大的一次版本更新。
这次更新的重点不是增加复杂度,而是更好的默认配置、更少的摩擦、更强大的工具。团队花了几个月时间重新思考 Livewire 组件应该是什么样子。
原文 Livewire4 正式发布!PHP 也可以无需写一行 Javascript 代码就能实现 Vue 的功能
基于视图的组件
Livewire 4 最直观的变化是组件的写法。以前需要在 PHP 类和 Blade 文件之间来回切换,现在可以把所有东西放在一个文件里:
php
<?php // resources/views/components/⚡counter.blade.php
use Livewire\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
};
?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
<style>
/* Scoped CSS... */
</style>
<script>
/* Component JavaScript... */
</script>
运行 php artisan make:livewire 时默认就是这种格式。文件名里的闪电符号让 Livewire 组件在文件树里一眼就能认出来,不会和普通 Blade 组件混淆。(不喜欢 emoji 的话可以关掉。)
对于大型组件,还有一种多文件格式,把相关文件放在同一个目录下:
text
⚡counter/
├── counter.php
├── counter.blade.php
├── counter.css (可选)
├── counter.js (可选)
└── counter.test.php (可选)
用 --mfc 参数创建多文件组件,随时可以用 php artisan livewire:convert 在两种格式之间转换。
路由
组件引用方式统一了。Livewire 4 引入了 Route::livewire():
php
// 之前 (v3) - 仍然支持
Route::get('/posts/create', CreatePost::class);
// 现在 (v4)
Route::livewire('/posts/create', 'pages::post.create');
新语法用名称而不是类来引用组件,和应用其他地方渲染组件的方式一致。
命名空间
Livewire 现在对应用结构有了自己的约定。默认提供两个命名空间:pages:: 用于页面组件,layouts:: 用于布局------其他组件和 Blade 组件一起放在 resources/views/components 目录下。
php
Route::livewire('/dashboard', 'pages::dashboard');
对于模块化应用,可以注册自定义命名空间。把管理后台组件放在 admin:: 下,计费相关的放在 billing:: 下,按你的架构来组织。
脚本和样式
组件的 JavaScript 和 CSS 现在可以和组件放在一起。直接在模板里加 <script> 和 <style> 标签:
html
<div>
<h1 class="title">{{ $count }}</h1>
<button wire:click="$js.celebrate">+</button>
</div>
<style>
.title {
color: blue;
font-size: 2rem;
}
</style>
<script>
this.$js.celebrate = () => {
confetti()
}
</script>
样式自动限定在组件范围内------你的 .title 类不会影响页面其他部分。需要全局样式的话,加上 global 属性:<style global>。
脚本里可以用 this 访问组件上下文------相当于你可能用过的 $wire 的别名。
两者都会作为原生 .js/.css 文件发送到浏览器,自动缓存以获得最佳性能。
Islands
Islands 是 Livewire 4 的重头戏。它可以在组件内创建独立更新的隔离区域:
html
<div>
@island
<div>
Revenue: {{ $this->revenue }}
<button wire:click="$refresh">Refresh</button>
</div>
@endisland
<div>
<!-- 这部分在 island 更新时不会重新渲染 -->
Other content...
</div>
</div>
点击"Refresh"时,只有 island 部分重新渲染,其他内容保持不变。以前要实现类似的隔离效果,需要把这部分提取成单独的子组件,还要处理 props 和 events 的传递。
性能提升不只是 DOM 更新层面。当 islands 和计算属性配合使用时,只有该 island 需要的数据才会被获取。如果组件有三个 island,各自引用不同的计算属性,刷新一个 island 只会执行那个 island 的查询。从数据库到渲染 HTML,整个链路的开销都被隔离了。
Islands 支持懒加载(lazy: true)、命名以便跨组件定位(name: 'revenue'),以及追加内容用于无限滚动:
html
<button wire:click="loadMore" wire:island.append="feed">
Load more
</button>
插槽和属性转发
如果你用过 Blade 组件的插槽和属性转发,这里会很熟悉。
插槽让父组件可以向子组件注入内容,同时保持响应式:
html
<livewire:card :$post>
<h2>{{ $post->title }}</h2>
<button wire:click="delete({{ $post->id }})">Delete</button>
</livewire:card>
插槽内容在父组件的上下文中求值,所以 wire:click="delete" 调用的是父组件的方法。
属性转发可以把 HTML 属性传递下去:
html
<livewire:post.show :$post class="mt-4" />
<!-- post.show 组件内部 -->
<div {{ $attributes }}>
...
</div>
拖拽排序
内置拖拽排序,不需要外部库:
html
<ul wire:sort="reorder">
@foreach ($items as $item)
<li wire:key="{{ $item->id }}" wire:sort:item="{{ $item->id }}">
{{ $item->title }}
</li>
@endforeach
</ul>
php
public function reorder($item, $position)
{
// $item 是 ID,$position 是新的索引
}
动画效果自动处理。用 wire:sort:handle 添加拖拽手柄,用 wire:sort:ignore 防止交互元素触发拖拽,用 wire:sort:group 在多个列表之间拖拽。
平滑过渡
wire:transition 指令使用浏览器的 View Transitions API 添加硬件加速动画:
html
@if ($showAlertMessage)
<div wire:transition>
<!-- 消息平滑淡入淡出 -->
</div>
@endif
对于步骤向导或轮播这种需要方向感的场景,可以指定过渡类型:
php
#[Transition(type: 'forward')]
public function next() { $this->step++; }
#[Transition(type: 'backward')]
public function previous() { $this->step--; }
然后用 ::view-transition-old() 和 ::view-transition-new() 伪元素为每个方向自定义 CSS 动画。
乐观更新
让界面响应更即时。这些指令会立即更新页面,不需要等待服务器响应。
wire:show 用 CSS 切换可见性(不移除 DOM,不发网络请求):
html
<div wire:show="showModal">
<!-- 立即显示/隐藏 -->
</div>
wire:text 立即更新文本内容:
html
Likes: <span wire:text="likes"></span>
wire:bind 响应式绑定任意 HTML 属性:
html
<input wire:model="message" wire:bind:class="message.length > 240 && 'text-red-500'">
$dirty 跟踪未保存的更改:
html
<div wire:show="$dirty">You have unsaved changes</div>
<div wire:show="$dirty('title')">Title modified</div>
加载状态
除了 v3 已有的 wire:loading,Livewire 4 会自动给触发网络请求的元素添加 data-loading 属性。
这样可以直接用 CSS 设置加载状态样式,还能定位兄弟、父级或子元素:
html
<button wire:click="save" class="data-loading:opacity-50">
Save <svg class="not-in-data-loading:hidden">...</svg>
</button>
内联占位符
对于懒加载组件和 islands,@placeholder 指令可以在内容旁边直接定义加载状态:
html
@placeholder
<div class="animate-pulse h-32 bg-gray-200 rounded"></div>
@endplaceholder
<div>
<!-- 实际内容加载到这里 -->
</div>
不需要单独的占位符视图或方法------骨架屏就在组件里面。
JavaScript 工具
需要用 JavaScript 的时候,Livewire 4 也能配合。
wire:ref 给元素命名以便定位:
html
<livewire:modal wire:ref="modal" />
php
$this->dispatch('close')->to(ref: 'modal');
也可以在组件脚本里访问 refs:
html
<input wire:ref="search" type="text" />
<script>
this.$refs.search.addEventListener('keydown', (e) => {
// 处理键盘事件...
})
</script>
#[Json] 方法直接返回数据给 JavaScript:
php
#[Json]
public function search($query)
{
return Post::where('title', 'like', "%{$query}%")->get();
}
html
<script>
let results = await this.search('livewire')
console.log(results)
</script>
$js actions 只在客户端运行:
html
<button wire:click="$js.bookmark">Bookmark</button>
<script>
this.$js.bookmark = () => {
this.bookmarked = !this.bookmarked
this.save()
}
</script>
拦截器可以在各个层级钩入请求:
html
<script>
this.intercept('save', ({ onSuccess, onError }) => {
onSuccess(() => showToast('Saved!'))
onError(() => showToast('Failed to save', 'error'))
})
</script>
用全局拦截器处理应用级别的问题,比如会话过期:
javascript
Livewire.interceptRequest(({ onError }) => {
onError(({ response, preventDefault }) => {
if (response.status === 419) {
preventDefault()
if (confirm('Session expired. Refresh?')) {
window.location.reload()
}
}
})
})
升级
Livewire 4 保持了很好的向后兼容性。现有组件可以继续使用------新的单文件格式是新组件的默认选项,但基于类的组件仍然完全支持。
查看升级指南 →
如果想看实际演示,我在 Laracasts 上录了一个新系列,用真实案例深入讲解每个特性。观看 Livewire 4 系列 →
Livewire 4 现在可用:
bash
composer require livewire/livewire:^4.0