升级指南
Livewire v4 引入了多项改进与优化,并尽可能保持向后兼容。本指南将帮助你从 Livewire v3 升级到 v4。
安装
更新你的 composer.json,要求使用 Livewire v4:
composer require livewire/livewire:^4.0更新后,清除应用缓存:
php artisan optimize:clear从 v4.0 升级到 v4.1
wire:model 修饰符行为变更
.blur 和 .change 等修饰符现在会控制客户端状态何时同步,而不再只影响网络请求时机。若你正在使用这些修饰符并希望保留原先行为,请在它们前面加上 .live(例如 wire:model.live.blur)。
以下变更适用于从 v3 升级到 v4。
高影响变更
这些变更最有可能影响你的应用,应仔细核对。
配置文件更新
若干配置键已重命名、重组,或有了新的默认值。请更新你的 config/livewire.php 文件:
已重命名的配置键
布局配置:
// Before (v3)
'layout' => 'components.layouts.app',
// After (v4)
'component_layout' => 'layouts::app',布局现在默认使用 layouts:: 命名空间,指向 resources/views/layouts/app.blade.php。
占位符配置:
// Before (v3)
'lazy_placeholder' => 'livewire.placeholder',
// After (v4)
'component_placeholder' => 'livewire.placeholder',变更的默认值
智能 wire:key 行为:
// Now defaults to true (was false in v3)
'smart_wire_keys' => true,这有助于避免深层嵌套组件上的 wire:key 问题。注意:在循环中你仍需手动添加 wire:key——该设置并不会取消这一要求。
新配置选项
组件位置:
'component_locations' => [
resource_path('views/components'),
resource_path('views/livewire'),
],定义 Livewire 查找单文件与多文件(基于视图)组件的位置。
组件命名空间:
'component_namespaces' => [
'layouts' => resource_path('views/layouts'),
'pages' => resource_path('views/pages'),
],为组织基于视图的组件创建自定义命名空间(例如 <livewire:pages::dashboard />)。
make 命令默认值:
'make_command' => [
'type' => 'sfc', // Options: 'sfc', 'mfc', or 'class'
'emoji' => true, // Whether to use ⚡ emoji prefix
],配置默认组件格式以及是否使用 emoji。将 type 设为 'class' 即可与 v3 行为一致。
CSP 安全模式:
'csp_safe' => false,启用内容安全策略(CSP)模式以避免 unsafe-eval 违规。启用后,Livewire 会使用 Alpine CSP 构建。注意:该模式会限制指令中的复杂 JavaScript 表达式,例如 wire:click="addToCart($event.detail.productId)",以及 window.location 这类全局引用。
路由变更
对于整页组件,推荐的路由写法已变更:
// Before (v3) - still works but not recommended
Route::get('/dashboard', Dashboard::class);
// After (v4) - recommended for all component types
Route::livewire('/dashboard', Dashboard::class);
// For view-based components, you can use the component name
Route::livewire('/dashboard', 'pages::dashboard');现在推荐使用 Route::livewire(),且单文件与多文件组件要作为整页组件正确工作,必须使用该方法。
wire:model 默认忽略子元素事件
在 v3 中,wire:model 会响应从子元素冒泡上来的 input/change 事件。这导致在包含表单输入的容器元素(如模态框或手风琴)上使用 wire:model 时出现意外行为——清空内部输入会冒泡,并可能关闭模态框。
在 v4 中,wire:model 现在只监听直接来自元素自身的事件(等同于 .self 修饰符的行为)。
若你的代码依赖捕获来自子元素的事件,请添加 .deep 修饰符:
<!-- Before (v3) - listened to child events by default -->
<div wire:model="value">
<input type="text">
</div>
<!-- After (v4) - add .deep to restore old behavior -->
<div wire:model.deep="value">
<input type="text">
</div>TIP
多数应用无需改动
该变更主要影响在容器元素上非常规使用 wire:model 的场景。标准表单输入绑定(input、select、textarea)不受影响。
使用 wire:navigate:scroll
在 v3 中若使用 wire:scroll 在 wire:navigate 请求间保留可滚动容器的滚动位置,在 v4 中需改为使用 wire:navigate:scroll:
@persist('sidebar')
<div class="overflow-y-scroll" wire:scroll> <!-- [tl! remove] -->
<div class="overflow-y-scroll" wire:navigate:scroll> <!-- [tl! add] -->
<!-- ... -->
</div>
@endpersist组件标签必须闭合
在 v3 中,即使 Livewire 组件标签未正确闭合也能渲染。在 v4 中,由于新增了 slot 支持,组件标签必须正确闭合——否则 Livewire 会把后续内容当作 slot 内容,组件将无法渲染:
<!-- Before (v3) - unclosed tag -->
<livewire:component-name>
<!-- After (v4) - Self-closing tag -->
<livewire:component-name />中等影响变更
这些变更可能影响应用中使用相关功能的部分。
wire:model 修饰符现在控制客户端同步时机
在 v3 中,.blur 和 .change 等修饰符只控制何时发送网络请求。输入值会在用户输入时立即同步到客户端状态($wire.property)。
在 v4 中,这些修饰符也会控制客户端状态何时同步。这解锁了新的 UI 模式——例如输入框在用户完成输入并按 Enter 或切走焦点之前不会更新。
迁移: 若你正在使用 .blur 或 .change 并希望保留旧行为,请在修饰符前加上 .live:
<!-- v3 -->
<input wire:model.blur="title">
<!-- v4 equivalent -->
<input wire:model.live.blur="title">| v3 语法 | v4 等价写法 |
|---|---|
wire:model.blur | wire:model.live.blur |
wire:model.change | wire:model.live.change |
INFO
.lazy 向后兼容
wire:model.lazy 的行为与 v3 一致——无需迁移。
v4 新特性: 你现在可以延迟客户端更新,且不发送网络请求:
<!-- Only update $wire.width when user tabs away -->
<input wire:model.blur="width">
<!-- Update on Enter key or blur -->
<input wire:model.blur.enter="search">wire:transition 现在使用 View Transitions API
在 v3 中,wire:transition 是 Alpine x-transition 指令的封装,支持 .opacity、.scale、.duration.200ms 和 .origin.top 等修饰符。
在 v4 中,wire:transition 改为使用浏览器原生的 View Transitions API。基本用法仍然有效——元素会平滑淡入淡出——但所有修饰符均已移除。
<!-- This still works in v4 -->
<div wire:transition>...</div>
<!-- These modifiers are no longer supported -->
<div wire:transition.opacity>...</div> <!-- [tl! remove] -->
<div wire:transition.scale.origin.top>...</div> <!-- [tl! remove] -->
<div wire:transition.duration.500ms>...</div> <!-- [tl! remove] -->性能改进
Livewire v4 对请求处理系统做了显著的性能改进:
- 非阻塞轮询:
wire:poll不再阻塞其他请求,也不会被其他请求阻塞 - 并行 live 更新:
wire:model.live请求现在并行执行,输入更快、结果更及时
这些改进会自动生效——你的代码无需改动。
更新钩子合并数组/对象变更
当从前端替换整个数组或对象时(例如 $wire.items = ['new', 'values']),Livewire 现在会发送一次合并后的更新,而不是针对每个索引发送细粒度更新。
之前: 在含 4 项的数组上设置 $wire.items = ['a', 'b'] 会多次触发 updatingItems/updatedItems 钩子——每个索引变更一次,外加 __rm__ 移除。
之后: 同样的操作只会带着完整的新数组值触发一次钩子,与 v2 行为一致。
若你的代码依赖在替换整个数组时触发各个索引的钩子,可能需要调整。单项变更(如 wire:model="items.0")仍会按预期触发细粒度钩子。
方法签名变更
若你在扩展 Livewire 核心功能或直接使用这些方法,请注意以下签名变更:
流式输出:
stream() 方法的参数顺序已变更:
// Before (v3)
$this->stream(to: '#container', content: 'Hello', replace: true);
// After (v4)
$this->stream(content: 'Hello', replace: true, el: '#container');若你使用命名参数(如上所示),请注意 to: 已重命名为 el:。若使用位置参数,则需更新为如下写法:
// Before (v3) - positional parameters
$this->stream('#container', 'Hello');
// After (v4) - positional/named parameters
$this->stream('Hello', el: '#container');组件挂载(内部):
若你在扩展 LivewireManager 或直接调用 mount() 方法:
// Before (v3)
public function mount($name, $params = [], $key = null)
// After (v4)
public function mount($name, $params = [], $key = null, $slots = [])该变更增加了挂载组件时传递 slots 的支持,通常不会影响大多数应用。
低影响变更
这些变更仅影响使用高级功能或自定义的应用。
wire:model 现在支持方括号写法
wire:model 表达式现在支持用方括号写法访问嵌套属性:
<input type="text" wire:model="foo['bar']['baz']">
<input type="text" wire:model="items[0].name">这意味着 wire:model 值中的方括号([ 和 ])现在会被解释为属性访问器。在 v3 中,它们被当作字面字符。若你的属性键包含方括号,请重命名以避免冲突。
Livewire 资源与端点 URL 变更
所有 Livewire URL 现在都会包含由你的 APP_KEY 派生的唯一哈希。前缀从 /livewire/ 变为 /livewire-{hash}/:
# v3 # v4
/livewire/update → /livewire-{hash}/update
/livewire/upload-file → /livewire-{hash}/upload-file
/livewire/livewire.js → /livewire-{hash}/livewire.js若你的防火墙规则、CDN 配置或中间件引用了 /livewire/ 路径,请更新以适配新前缀。
若你在使用 setUpdateRoute,请使用 $path 参数以保留基于哈希的端点:
// Before (v3)
Livewire::setUpdateRoute(function ($handle) {
return Route::post('/livewire/update', $handle);
});
// After (v4)
Livewire::setUpdateRoute(function ($handle, $path) {
return Route::post($path, $handle);
});JavaScript 弃用
已弃用:$wire.$js() 方法
用于定义 JavaScript 动作的 $wire.$js() 方法已弃用:
// Deprecated (v3)
$wire.$js('bookmark', () => {
// Toggle bookmark...
})
// New (v4)
$wire.$js.bookmark = () => {
// Toggle bookmark...
}新语法更简洁、更直观。
已弃用:无前缀的 $js
在脚本中不带 $wire.$js 或 this.$js 前缀而直接使用 $js 的写法已弃用:
// Deprecated (v3)
$js('bookmark', () => {
// Toggle bookmark...
})
// New (v4)
$wire.$js.bookmark = () => {
// Toggle bookmark...
}
// Or
this.$js.bookmark = () => {
// Toggle bookmark...
}TIP
旧语法仍然可用
出于向后兼容,$wire.$js('bookmark', ...) 和 $js('bookmark', ...) 在 v4 中仍可使用,但建议你在方便时迁移到新语法。
已弃用:commit 与 request 钩子
commit 与 request 钩子已弃用,请改用新的拦截器系统,它能提供更细粒度的控制与更好的性能。
TIP
旧钩子仍然可用
出于向后兼容,已弃用的钩子在 v4 中仍可使用,但建议你在方便时迁移到新系统。
从 commit 钩子迁移
旧的 commit 钩子:
// OLD - Deprecated
Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => {
respond(() => {
// Runs after response received but before processing
})
succeed(({ snapshot, effects }) => {
// Runs after successful response
})
fail(() => {
// Runs if request failed
})
})应替换为新的 interceptMessage:
// NEW - Recommended
Livewire.interceptMessage(({ component, message, onFinish, onSuccess, onError, onFailure }) => {
onFinish(() => {
// Equivalent to respond()
})
onSuccess(({ payload }) => {
// Equivalent to succeed()
// Access snapshot via payload.snapshot
// Access effects via payload.effects
})
onError(() => {
// Equivalent to fail() for server errors
})
onFailure(() => {
// Equivalent to fail() for network errors
})
})从 request 钩子迁移
旧的 request 钩子:
// OLD - Deprecated
Livewire.hook('request', ({ url, options, payload, respond, succeed, fail }) => {
respond(({ status, response }) => {
// Runs when response received
})
succeed(({ status, json }) => {
// Runs on successful response
})
fail(({ status, content, preventDefault }) => {
// Runs on failed response
})
})应替换为新的 interceptRequest:
// NEW - Recommended
Livewire.interceptRequest(({ request, onResponse, onSuccess, onError, onFailure }) => {
// Access url via request.uri
// Access options via request.options
// Access payload via request.payload
onResponse(({ response }) => {
// Equivalent to respond()
// Access status via response.status
})
onSuccess(({ response, responseJson }) => {
// Equivalent to succeed()
// Access status via response.status
// Access json via responseJson
})
onError(({ response, responseBody, preventDefault }) => {
// Equivalent to fail() for server errors
// Access status via response.status
// Access content via responseBody
})
onFailure(({ error }) => {
// Equivalent to fail() for network errors
})
})主要差异
- 更细粒度的错误处理:新系统将网络失败(
onFailure)与服务器错误(onError)分开处理 - 更好的生命周期钩子:消息拦截器提供了
onSync、onMorph和onRender等额外钩子 - 取消支持:消息与请求都可以取消/中止
- 组件作用域:可使用
$wire.intercept(...)将消息拦截器限定到特定组件
关于新拦截器系统的完整文档,请参阅 JavaScript 拦截器文档。
升级 Volt
Livewire v4 现已支持单文件组件,其语法与 Volt 基于类的组件相同。这意味着你可以从 Volt 迁移到 Livewire 内置的单文件组件。
更新组件导入
将所有 Livewire\Volt\Component 替换为 Livewire\Component:
// Before (Volt)
use Livewire\Volt\Component;
new class extends Component { ... }
// After (Livewire v4)
use Livewire\Component;
new class extends Component { ... }更新路由定义
在路由文件中将 Volt::route() 替换为 Route::livewire():
// Before (Volt)
use Livewire\Volt\Volt;
Volt::route('/dashboard', 'dashboard');
// After (Livewire v4)
use Illuminate\Support\Facades\Route;
Route::livewire('/dashboard', 'dashboard');更新测试文件
将所有 Livewire\Volt\Volt 替换为 Livewire\Livewire,并将 Volt::test() 改为 Livewire::test():
// Before (Volt)
use Livewire\Volt\Volt;
Volt::test('counter')
// After (Livewire v4)
use Livewire\Livewire;
Livewire::test('counter')移除 Volt 服务提供者
删除 Volt 服务提供者文件:
rm app/Providers/VoltServiceProvider.php然后从 bootstrap/providers.php 的 providers 数组中移除它:
// Before
return [
App\Providers\AppServiceProvider::class,
App\Providers\VoltServiceProvider::class,
];
// After
return [
App\Providers\AppServiceProvider::class,
];移除 Volt 包
卸载 Volt 包:
composer remove livewire/volt安装 Livewire v4
完成上述变更后,安装 Livewire v4。你现有的 Volt 基于类的组件无需修改即可工作,因为它们使用与 Livewire 单文件组件相同的语法。
v4 新特性
Livewire v4 引入了多项强大的新特性,你可以立即开始使用:
组件特性
单文件与多文件组件
v4 在传统基于类的方式之外,引入了新的组件格式。单文件组件将 PHP 与 Blade 合并到一个文件中,多文件组件则将 PHP、Blade、JavaScript 和测试组织在一个目录中。
默认情况下,基于视图的组件文件会以 ⚡ emoji 作为前缀,以便在编辑器和搜索中与普通 Blade 文件区分。可通过 make_command.emoji 配置关闭。
php artisan make:livewire create-post # Single-file (default)
php artisan make:livewire create-post --mfc # Multi-file
php artisan livewire:convert create-post # Convert between formatsSlots 与属性转发
组件现在支持 slots,以及使用 {{ $attributes }} 的自动属性包转发,使组件组合更加灵活。
基于视图组件中的 JavaScript
基于视图的组件现在可以直接包含 <script> 标签,无需 @script 包裹。这些脚本会作为独立的缓存文件提供,以获得更好的性能,并自动绑定 $wire:
<div>
<!-- Your component template -->
</div>
<script>
// $wire is automatically bound as 'this'
this.count++ // Same as $wire.count++
// $wire is still available if preferred
$wire.save()
</script>Islands
Islands 允许你在组件内创建独立更新的隔离区域,从而显著提升性能,而无需创建单独的子组件。
@island(name: 'stats', lazy: true)
<div>{{ $this->expensiveStats }}</div>
@endisland加载改进
延迟加载
除了懒加载(基于视口)外,组件现在还可以延迟到初始页面加载完成后立即加载:
<livewire:revenue defer />#[Defer]
class Revenue extends Component { ... }捆绑加载
控制多个懒加载/延迟加载组件是并行加载还是捆绑在一起加载:
<livewire:revenue lazy.bundle />
<livewire:expenses defer.bundle />#[Lazy(bundle: true)]
class Revenue extends Component { ... }异步动作
使用 .async 修饰符或 #[Async] attribute,可并行运行动作且不阻塞其他请求:
<button wire:click.async="logActivity">Track</button>#[Async]
public function logActivity() { ... }新指令与修饰符
wire:sort - 拖放排序
内置支持可拖放排序的列表:
<ul wire:sort="updateOrder">
@foreach ($items as $item)
<li wire:sort:item="{{ $item->id }}" wire:key="{{ $item->id }}">{{ $item->name }}</li>
@endforeach
</ul>wire:intersect - 视口交叉
当元素进入或离开视口时运行动作,类似于 Alpine 的 x-intersect:
<!-- Basic usage -->
<div wire:intersect="loadMore">...</div>
<!-- With modifiers -->
<div wire:intersect.once="trackView">...</div>
<div wire:intersect:leave="pauseVideo">...</div>
<div wire:intersect.half="loadMore">...</div>
<div wire:intersect.full="startAnimation">...</div>
<div wire:intersect.half.dwell.500ms="trackImpression">...</div>
<!-- With options -->
<div wire:intersect.margin.200px="loadMore">...</div>
<div wire:intersect.threshold.50="trackScroll">...</div>
<div wire:intersect.parent="loadWithinScroller">...</div>可用修饰符:
.once- 只触发一次.dwell.Xms- 要求持续交叉一段时间(默认 250ms).half- 等到一半可见.full- 等到完全可见.threshold.X- 自定义可见百分比(0-100).margin.Xpx或.margin.X%- 交叉边距.parent- 相对于元素的父级观察.renderless- 动作后跳过渲染.async- 并行运行动作.preserve-scroll- 保留页面滚动位置
wire:ref - 元素引用
轻松引用并与模板中的元素交互:
<div wire:ref="modal">
<!-- Modal content -->
</div>
<button wire:click="$js.scrollToModal">Scroll to modal</button>
<script>
this.$js.scrollToModal = () => {
this.$refs.modal.scrollIntoView()
}
</script>.renderless 修饰符
直接在模板中跳过组件重新渲染:
<button wire:click.renderless="trackClick">Track</button>这是 #[Renderless] attribute 的替代方案,适用于不需要更新 UI 的动作。
.preserve-scroll 修饰符
在更新期间保留滚动位置,以避免布局跳动:
<button wire:click.preserve-scroll="loadMore">Load More</button>data-loading 属性
每个触发网络请求的元素都会自动获得 data-loading 属性,便于用 Tailwind 设置加载状态样式:
<button wire:click="save" class="data-loading:opacity-50 data-loading:pointer-events-none">
Save Changes
</button>JavaScript 改进
$errors 魔法属性
从 JavaScript 访问组件的 error bag:
<div wire:show="$errors.has('email')">
<span wire:text="$errors.first('email')"></span>
</div>$intercept 魔法
从 JavaScript 拦截并修改 Livewire 请求:
<script>
this.$intercept('save', ({ ... }) => {
// ...
})
</script>从 JavaScript 定位 Island
直接从模板触发 island 渲染:
<button wire:click="loadMore" wire:island.append="stats">
Load more
</button>获取帮助
若升级过程中遇到问题:
- 查阅[文档](https://livewire.laravel.com)获取详细功能指南
- 访问 [GitHub discussions](https://github.com/livewire/livewire/discussions) 获取社区支持