🎉 Livewire4 正式发布!Livewire 中文文档也正式发布
Livewire 4 正式发布,这是迄今为止最大的一次版本更新。
这次更新的重点不是增加复杂度,而是更好的默认配置、更少的摩擦、更强大的工具。团队花了几个月时间重新思考 Livewire 组件应该是什么样子。
基于视图的组件
Livewire 4 最直观的变化是组件的写法。以前需要在 PHP 类和 Blade 文件之间来回切换,现在可以把所有东西放在一个文件里:
<?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 的话可以关掉。)
对于大型组件,还有一种多文件格式,把相关文件放在同一个目录下:
⚡counter/ ├── counter.php ├── counter.blade.php ├── counter.css (可选) ├── counter.js (可选) └── counter.test.php (可选) 用 --mfc 参数创建多文件组件,随时可以用 php artisan livewire:convert 在两种格式之间转换。
路由
组件引用方式统一了。Livewire 4 引入了 Route::livewire():
// 之前 (v3) - 仍然支持 Route::get('/posts/create', CreatePost::class); // 现在 (v4) Route::livewire('/posts/create', 'pages::post.create'); 新语法用名称而不是类来引用组件,和应用其他地方渲染组件的方式一致。
命名空间
Livewire 现在对应用结构有了自己的约定。默认提供两个命名空间:pages:: 用于页面组件,layouts:: 用于布局——其他组件和 Blade 组件一起放在 resources/views/components 目录下。
Route::livewire('/dashboard', 'pages::dashboard'); 对于模块化应用,可以注册自定义命名空间。把管理后台组件放在 admin:: 下,计费相关的放在 billing:: 下,按你的架构来组织。
脚本和样式
组件的 JavaScript 和 CSS 现在可以和组件放在一起。直接在模板里加 <script> 和 <style> 标签:
<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 的重头戏。它可以在组件内创建独立更新的隔离区域:
<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'),以及追加内容用于无限滚动:
<button wire:click="loadMore" wire:island.append="feed"> Load more </button> 插槽和属性转发
如果你用过 Blade 组件的插槽和属性转发,这里会很熟悉。
插槽让父组件可以向子组件注入内容,同时保持响应式:
<livewire:card :$post> <h2>{{ $post->title }}</h2> <button wire:click="delete({{ $post->id }})">Delete</button> </livewire:card> 插槽内容在父组件的上下文中求值,所以 wire:click="delete" 调用的是父组件的方法。
属性转发可以把 HTML 属性传递下去:
<livewire:post.show :$post class="mt-4" /> <!-- post.show 组件内部 --> <div {{ $attributes }}> ... </div> 拖拽排序
内置拖拽排序,不需要外部库:
<ul wire:sort="reorder"> @foreach ($items as $item) <li wire:key="{{ $item->id }}" wire:sort:item="{{ $item->id }}"> {{ $item->title }} </li> @endforeach </ul> public function reorder($item, $position) { // $item 是 ID,$position 是新的索引 } 动画效果自动处理。用 wire: sort:handle 添加拖拽手柄,用 wire: sort:ignore 防止交互元素触发拖拽,用 wire 在多个列表之间拖拽。
group
平滑过渡
wire:transition 指令使用浏览器的 View Transitions API 添加硬件加速动画:
@if ($showAlertMessage) <div wire:transition> <!-- 消息平滑淡入淡出 --> </div> @endif 对于步骤向导或轮播这种需要方向感的场景,可以指定过渡类型:
#[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,不发网络请求):
<div wire:show="showModal"> <!-- 立即显示/隐藏 --> </div> wire:text 立即更新文本内容:
Likes: <span wire:text="likes"></span> wire:bind 响应式绑定任意 HTML 属性:
<input wire:model="message" wire:bind:class="message.length > 240 && 'text-red-500'"> $dirty 跟踪未保存的更改:
<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 设置加载状态样式,还能定位兄弟、父级或子元素:
<button wire:click="save" class="data-loading:opacity-50"> Save <svg class="not-in-data-loading:hidden">...</svg> </button> 内联占位符
对于懒加载组件和 islands,@placeholder 指令可以在内容旁边直接定义加载状态:
@placeholder <div class="animate-pulse h-32 bg-gray-200 rounded"></div> @endplaceholder <div> <!-- 实际内容加载到这里 --> </div> 不需要单独的占位符视图或方法——骨架屏就在组件里面。
JavaScript 工具
需要用 JavaScript 的时候,Livewire 4 也能配合。
wire:ref 给元素命名以便定位:
<livewire:modal wire:ref="modal" /> $this->dispatch('close')->to(ref: 'modal'); 也可以在组件脚本里访问 refs:
<input wire:ref="search" type="text" /> <script> this.$refs.search.addEventListener('keydown', (e) => { // 处理键盘事件... }) </script> #[Json] 方法直接返回数据给 JavaScript:
#[Json] public function search($query) { return Post::where('title', 'like', "%{$query}%")->get(); } <script> let results = await this.search('livewire') console.log(results) </script> $js actions 只在客户端运行:
<button wire:click="$js.bookmark">Bookmark</button> <script> this.$js.bookmark = () => { this.bookmarked = !this.bookmarked this.save() } </script> 拦截器可以在各个层级钩入请求:
<script> this.intercept('save', ({ onSuccess, onError }) => { onSuccess(() => showToast('Saved!')) onError(() => showToast('Failed to save', 'error')) }) </script> 用全局拦截器处理应用级别的问题,比如会话过期:
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 现在可用:
composer require livewire/livewire:^4.0 原文 Livewire4 正式发布!PHP 也可以无需写一行 Javascript 代码就能实现 Vue 的功能
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu