JavaScript
在 Livewire 组件中使用 JavaScript
Livewire 和 Alpine 提供了大量工具,可直接在 HTML 中构建动态组件;不过有时跳出 HTML、为组件执行普通 JavaScript 会更有帮助。
WARNING
基于类的组件需要 @@script 指令
本页示例使用裸 <script> 标签,适用于单文件和多文件组件。若你使用基于类的组件(Blade 视图与 PHP 类分属不同文件),则必须用 @@script 指令包裹 script 标签:
@@script
<script>
// Your JavaScript here...
</script>
@@endscript这会告诉 Livewire 为基于类的组件正确处理执行时机。
执行脚本
你可以在组件模板中直接添加 <script> 标签,以便在组件加载时执行 JavaScript。
由于这些脚本由 Livewire 处理,它们会在恰当时机执行——页面已加载之后、Livewire 组件渲染之前。这意味着你不再需要把脚本包在 document.addEventListener('...') 里才能正确加载。
这也意味着,懒加载或条件加载的 Livewire 组件在页面初始化之后仍能执行 JavaScript。
<div>
...
</div>
<script>
// This Javascript will get executed every time this component is loaded onto the page...
</script>下面是一个更完整的示例:你可以像这样注册在 Livewire 组件中使用的 JavaScript 动作。
<div>
<button wire:click="$js.increment">+</button>
</div>
<script>
this.$js.increment = () => {
console.log('increment')
}
</script>要了解更多关于 JavaScript 动作的内容,请查阅 actions 文档。
脚本何时运行
组件的脚本会在其标记已进入 DOM、但 Alpine 尚未初始化它时运行。Livewire 会暂停组件树的初始化,直到脚本执行完毕,因此脚本注册的任何内容——Alpine.data() 提供者、$js 动作、自定义指令——都能保证在组件标记中的任何表达式求值之前已经存在:
<div x-data="audioUploader">
<span x-text="message"></span>
</div>
<script>
Alpine.data('audioUploader', () => ({
message: 'Loaded',
}))
</script>这一点在首次页面加载以及组件进入页面的其他所有方式下都成立:懒加载、父组件更新时的动态挂载、island,以及 wire:navigate。
这一时机有几点值得了解的后果:
- `$wire.$el` 和普通 DOM 查询可用——脚本运行时标记已存在。
- 由初始化标记产生的内容——`$refs`、Alpine 组件状态——此时尚不存在。请从 Alpine 组件的 `init()` 钩子中获取,或在 `await $wire.$nextTick()` 之后再取。
- 脚本加载期间,组件标记可见但尚不可交互。这个窗口通常察觉不到;但对较重的模块,你可以给在脚本就绪前不应出现的元素加上 [`x-cloak`](https://alpinejs.dev/directives/cloak)——在组件初始化之前它会被遵守。
- 若脚本加载失败或抛错,Livewire 会记录错误并仍初始化组件,以便页面继续工作。
在脚本中使用 $wire
在组件内添加 <script> 标签时,你会自动获得该 Livewire 组件的 $wire 对象。
下面示例用简单的 setInterval 每 2 秒刷新一次组件(你完全可以用 wire:poll 做到,但这是演示要点的简单方式):
<script>
setInterval(() => {
$wire.$refresh()
}, 2000)
</script>$wire 对象
$wire 对象是你与 Livewire 组件交互的 JavaScript 接口。它提供对组件属性、方法的访问,以及与服务器交互的工具。
在组件脚本中,你可以直接使用 $wire。以下是你最常用的方法:
// Access and modify properties
$wire.count
$wire.count = 5
$wire.$set('count', 5)
// Call component methods
$wire.save()
$wire.delete(postId)
// Refresh the component
$wire.$refresh()
// Dispatch events
$wire.$dispatch('post-created', { postId: 2 })
// Listen for events
$wire.$on('post-created', (event) => {
console.log(event.postId)
})
// Access the root element
$wire.$el.querySelector('.modal')加载资源
组件 <script> 标签适合在每次 Livewire 组件加载时执行少量 JavaScript;不过有时你可能希望连同组件一起,把整段脚本和样式资源加载到页面上。
下面示例用 @assets 加载名为 Pikaday 的日期选择库,并在组件内初始化:
<div>
<input type="text" data-picker>
</div>
@assets
<script src="https://cdn.jsdelivr.net/npm/pikaday/pikaday.js" defer></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/pikaday/css/pikaday.css">
@endassets
<script>
new Pikaday({ field: $wire.$el.querySelector('[data-picker]') });
</script>该组件加载时,Livewire 会确保在执行脚本之前,页面上的所有 @assets 都已加载。此外,无论该组件有多少实例,提供的 @assets 在每个页面只会加载一次;与之不同,组件脚本会对页面上的每个组件实例都执行。
拦截器
可在三个层级拦截 Livewire 请求:action(最细粒度)、message(按组件)、以及 request(HTTP 层)。
// Action interceptors - fire for each action call
$wire.intercept(callback) // All actions on this component
$wire.intercept('save', callback) // Only 'save' action
Livewire.interceptAction(callback) // Global (all components)
// Message interceptors - fire for each component message
$wire.interceptMessage(callback) // Messages from this component
$wire.interceptMessage('save', callback) // Only when message contains 'save'
Livewire.interceptMessage(callback) // Global (all components)
// Request interceptors - fire for each HTTP request
$wire.interceptRequest(callback) // Requests involving this component
$wire.interceptRequest('save', callback) // Only when request contains 'save'
Livewire.interceptRequest(callback) // Global (all requests)所有拦截器都会返回一个取消订阅函数:
let unsubscribe = $wire.intercept(callback)
unsubscribe() // Remove the interceptorAction 拦截器
Action 拦截器粒度最细。它们会在组件的每次方法调用时触发。
$wire.intercept(({ action, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => {
// action.name - Method name ('save', '$refresh', etc.)
// action.params - Method parameters
// action.component - Component instance
// action.cancel() - Cancel this action
onSend(({ call }) => {
// call: { method, params, metadata }
})
onCancel(() => {})
onSuccess((result) => {
// result: Return value from PHP method
})
onError(({ response, body, preventDefault }) => {
preventDefault() // Prevent error modal
})
onFailure(({ error }) => {
// error: Network error
})
onFinish(() => {
// Runs after DOM morph completes (or on error/cancel)
})
})Message 拦截器
Message 拦截器会在每次组件更新时触发。一条 message 包含一个或多个 action。
$wire.interceptMessage(({ message, cancel, onSend, onCancel, onSuccess, onSkipped, onError, onFailure, onStream, onFinish }) => {
// message.component - Component instance
// message.actions - Set of actions in this message
// message.isSkipped() - True if the server skipped this message
// cancel() - Cancel this message
onSend(({ payload }) => {
// payload: { snapshot, updates, calls }
})
onCancel(() => {})
onSuccess(({ payload, onSync, onEffect, onMorph, onMorphed, onRender }) => {
// payload: { snapshot, effects }
onSync(() => {}) // After state synced
onEffect(() => {}) // After effects processed
onMorph(async () => {}) // Advanced: add awaited DOM morph work
onMorphed(() => {}) // After all DOM morph work completes
onRender(() => {}) // In the next animation frame
})
onSkipped(() => {
// Server intentionally skipped this message (e.g. an unchanged
// reactive child). No payload, no morph, no render — but action
// promises still resolve. Use for telemetry or dev tools.
})
onError(({ response, body, preventDefault }) => {
preventDefault() // Prevent error modal
})
onFailure(({ error }) => {})
onStream(async ({ json }) => {
// json: Parsed stream chunk
// Async work is awaited before the next chunk is processed
})
onFinish(() => {
// Runs after DOM morph completes (or on error/cancel/skip)
})
})onMorph 是一个高级钩子,用于提交 Livewire 必须等待的异步 DOM 工作。大多数需要访问更新后 DOM 的代码应使用 onMorphed:它在响应中的组件、island 和 slot morph 全部完成后运行,但早于 action promise 兑现以及 onFinish 执行。
时机
成功请求时的钩子执行顺序:
- `onSuccess` — 收到服务器响应后立即执行
- `onSync` — 状态合并之后
- `onEffect` — effects 处理之后
- `onMorph` — 在被 await 的 DOM morph 阶段期间
- `onMorphed` — 所有 DOM morph 工作完成之后
- `onFinish` — 在 `onMorphed` 之后
- `onRender` — 在下一个 `requestAnimationFrame` 中
对于被跳过的 message(例如未变更的响应式子组件),会触发 onSkipped 而非 onSuccess,然后是 onFinish。由于没有内容可应用,morph/render 钩子都不会触发。
Action 的 promise(.then())与 onFinish 同时兑现(morph 之后,或在跳过时立即兑现)。
Request 拦截器
Request 拦截器会在每次 HTTP 请求时触发。一个 request 可能包含来自多个组件的 message。
$wire.interceptRequest(({ request, onSend, onCancel, onSuccess, onError, onFailure, onResponse, onParsed, onStream, onRedirect, onDump, onFinish }) => {
// request.messages - Set of messages in this request
// request.cancel() - Cancel this request
onSend(({ responsePromise }) => {})
onCancel(() => {})
onResponse(({ response }) => {
// response: Fetch Response (before body read)
})
onParsed(({ response, body }) => {
// body: Response body as string
})
onSuccess(({ response, body, json }) => {})
onError(({ response, body, preventDefault }) => {
preventDefault() // Prevent error modal
})
onFailure(({ error }) => {})
onStream(({ response }) => {})
onRedirect(({ url, preventDefault }) => {
preventDefault() // Prevent redirect
})
onDump(({ html, preventDefault }) => {
preventDefault() // Prevent dump modal
})
onFinish(() => {})
})示例
组件的加载状态:
<script>
$wire.intercept(({ onSend, onFinish }) => {
onSend(() => $wire.$el.classList.add('opacity-50'))
onFinish(() => $wire.$el.classList.remove('opacity-50'))
})
</script>删除前确认:
<script>
$wire.intercept('delete', ({ action }) => {
if (!confirm('Are you sure?')) {
action.cancel()
}
})
</script>全局会话过期处理:
Livewire.interceptRequest(({ onError }) => {
onError(({ response, preventDefault }) => {
if (response.status === 419) {
preventDefault()
if (confirm('Session expired. Refresh?')) {
window.location.reload()
}
}
})
})针对特定 action 的成功通知:
<script>
$wire.intercept('save', ({ onSuccess, onError }) => {
onSuccess(() => showToast('Saved!'))
onError(() => showToast('Failed to save', 'error'))
})
</script>全局 Livewire 事件
Livewire 会派发两个有用的浏览器事件,便于你从外部脚本注册任意自定义扩展点:
<script>
document.addEventListener('livewire:init', () => {
// Runs after Livewire is loaded but before it's initialized
// on the page...
})
document.addEventListener('livewire:initialized', () => {
// Runs immediately after Livewire has finished initializing
// on the page...
})
</script>Livewire 全局对象
Livewire 的全局对象是从外部脚本与 Livewire 交互的最佳起点。
你可以在客户端代码的任意位置,通过 window 访问全局 Livewire JavaScript 对象。
在 livewire:init 事件监听器中使用 window.Livewire 通常很有帮助
访问组件
你可以使用以下方法访问当前页面已加载的特定 Livewire 组件:
// Retrieve the $wire object for the first component on the page...
let component = Livewire.first()
// Retrieve a given component's `$wire` object by its ID...
let component = Livewire.find(id)
// Retrieve an array of component `$wire` objects by name...
let components = Livewire.getByName(name)
// Retrieve $wire objects for every component on the page...
let components = Livewire.all()与事件交互
除了在 PHP 中从各个组件派发和监听事件外,全局 Livewire 对象还允许你在应用的任意位置与 Livewire 的事件系统交互:
// Dispatch an event to any Livewire components listening...
Livewire.dispatch('post-created', { postId: 2 })
// Dispatch an event to a given Livewire component by name...
Livewire.dispatchTo('dashboard', 'post-created', { postId: 2 })
// Listen for events dispatched from Livewire components...
Livewire.on('post-created', ({ postId }) => {
// ...
})在某些场景下,你可能需要注销全局 Livewire 事件。例如,在使用 Alpine 组件和 wire:navigate 时,由于在页面间导航时会调用 init,可能注册多个监听器。为此,请使用 Alpine 会自动调用的 destroy 函数:在该函数中遍历所有监听器并注销它们,以避免不必要的累积。
Alpine.data('MyComponent', () => ({
listeners: [],
init() {
this.listeners.push(
Livewire.on('post-created', (options) => {
// Do something...
})
);
},
destroy() {
this.listeners.forEach((listener) => {
listener();
});
}
}));使用生命周期钩子
Livewire 允许你使用 Livewire.hook() 挂接到其全局生命周期的各个部分:
// Register a callback to execute on a given internal Livewire hook...
Livewire.hook('component.init', ({ component, cleanup }) => {
// ...
})关于 Livewire JavaScript 钩子的更多信息可在下方找到。
注册自定义指令
Livewire 允许你使用 Livewire.directive() 注册自定义指令。
下面是一个自定义 wire:confirm 指令的示例:它用 JavaScript 的 confirm() 对话框,在动作发往服务器之前确认或取消:
<button wire:confirm="Are you sure?" wire:click="delete">Delete post</button>下面是用 Livewire.directive() 实现 wire:confirm 的代码:
Livewire.directive('confirm', ({ el, directive, component, cleanup }) => {
let content = directive.expression
// The "directive" object gives you access to the parsed directive.
// For example, here are its values for: wire:click.prevent="deletePost(1)"
//
// directive.raw = wire:click.prevent
// directive.value = "click"
// directive.modifiers = ['prevent']
// directive.expression = "deletePost(1)"
let onClick = e => {
if (! confirm(content)) {
e.preventDefault()
e.stopImmediatePropagation()
}
}
el.addEventListener('click', onClick, { capture: true })
// Register any cleanup code inside `cleanup()` in the case
// where a Livewire component is removed from the DOM while
// the page is still active.
cleanup(() => {
el.removeEventListener('click', onClick)
})
})JavaScript 钩子
面向高级用户,Livewire 暴露了其内部的客户端「hook」系统。你可以使用下列钩子扩展 Livewire 的功能,或获取关于 Livewire 应用的更多信息。
组件初始化
每当 Livewire 发现一个新组件——无论是在首次页面加载还是之后——都会触发 component.init 事件。你可以挂接到 component.init,以拦截或初始化与该新组件相关的任何内容:
Livewire.hook('component.init', ({ component, cleanup }) => {
//
})对于依赖 Livewire 初始 effects 的高级集成——例如事件监听器、脚本或服务器派发的 JavaScript——请使用 component.initialized。它在这些 effects 处理完毕之后运行,但早于 Livewire 继续初始化组件的后代元素:
Livewire.hook('component.initialized', ({ component }) => {
//
})更多信息请参阅 component 对象文档。
DOM 元素初始化
除了在新组件初始化时触发事件外,Livewire 还会为给定 Livewire 组件内的每个 DOM 元素触发事件。
这可用于在应用中提供自定义的 Livewire HTML 属性:
Livewire.hook('element.init', ({ component, el }) => {
//
})DOM Morph 钩子
在 DOM morph 阶段——发生在 Livewire 完成一次网络往返之后——Livewire 会对每个被变更的元素触发一系列事件。
Livewire.hook('morph.updating', ({ el, component, toEl, skip, childrenOnly }) => {
//
})
Livewire.hook('morph.updated', ({ el, component }) => {
//
})
Livewire.hook('morph.removing', ({ el, component, skip }) => {
//
})
Livewire.hook('morph.removed', ({ el, component }) => {
//
})
Livewire.hook('morph.adding', ({ el, component }) => {
//
})
Livewire.hook('morph.added', ({ el }) => {
//
})除了按元素触发的事件外,每个 Livewire 组件还会触发 morph 和 morphed 事件:
Livewire.hook('morph', ({ el, component }) => {
// Runs just before the child elements in `component` are morphed (exluding partial morphing)
})
Livewire.hook('morphed', ({ el, component }) => {
// Runs after all child elements in `component` are morphed (excluding partial morphing)
})服务端 JavaScript 求值
除了直接在组件中执行 JavaScript,你还可以使用 js() 方法从服务端 PHP 代码求值 JavaScript 表达式。
这通常适用于在服务端动作执行后,再做某种客户端后续操作。
例如,下面的 post.create 组件会在文章保存到数据库后,触发客户端 alert 对话框:
<?php // resources/views/components/post/⚡create.blade.php
use Livewire\Component;
new class extends Component {
public $title = '';
public function save()
{
// Save post to database...
$this->js("alert('Post saved!')");
}
};JavaScript 表达式 alert('Post saved!') 会在服务端将文章保存到数据库、且响应的 DOM morph 完成之后,在客户端执行。
你可以在表达式中访问当前组件的 $wire 对象:
$this->js('$wire.$refresh()');
$this->js('$wire.$dispatch("post-created", { id: ' . $post->id . ' })');常见模式
以下是在实际应用中将 JavaScript 与 Livewire 结合使用的一些常见模式。
集成第三方库
许多 JavaScript 库需要在元素加入页面时初始化。使用组件脚本在组件加载时初始化库:
<div>
<div id="map" style="height: 400px;"></div>
</div>
@assets
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_KEY"></script>
@endassets
<script>
new google.maps.Map($wire.$el.querySelector('#map'), {
center: { lat: {{ $latitude }}, lng: {{ $longitude }} },
zoom: 12
});
</script>与 localStorage 同步
你可以使用 $watch 将组件状态与 localStorage 同步:
<script>
// Load from localStorage on init
if (localStorage.getItem('draft')) {
$wire.content = localStorage.getItem('draft');
}
// Save to localStorage when it changes
$wire.$watch('content', (value) => {
localStorage.setItem('draft', value);
});
</script>使用 @js 指令
若需要直接输出供 JavaScript 使用的 PHP 数据,可以使用 @js 指令。
<script>
let posts = @js($posts)
// "posts" will now be a JavaScript array of post data from PHP.
</script>最佳实践
组件脚本 vs 全局脚本
在以下情况使用组件脚本:
- JavaScript 专属于该组件的功能
- 你需要访问 `$wire` 或组件特定数据
- 代码应在每次组件加载时运行
在以下情况使用全局脚本:
- 注册自定义指令或钩子
- 设置全局事件监听器
- 初始化应用级 JavaScript
避免内存泄漏
在组件脚本中添加事件监听器时,Livewire 会在组件移除时自动清理它们。不过,若你使用全局拦截器或钩子,请确保在适当时机进行清理:
// Component-level - automatically cleaned up ✓
$wire.intercept(({ onSend }) => {
onSend(() => console.log('Sending...'));
});
// Global-level - lives for the entire page lifecycle
Livewire.interceptMessage(({ onSend }) => {
onSend(() => console.log('Sending...'));
});调试技巧
从浏览器控制台访问组件:
// Get first component on page
let $wire = Livewire.first()
// Inspect component state
console.log($wire.count)
// Call methods
$wire.increment()监控所有请求:
Livewire.interceptRequest(({ onSend }) => {
onSend(() => {
console.log('Request sent:', Date.now());
});
});查看组件快照:
let component = Livewire.first().__instance()
console.log(component.snapshot)性能考量
- 对不应被 Livewire DOM morph 触及的元素使用 `wire:ignore`
- 用 `wire:model.debounce` 或 JavaScript 防抖来节流昂贵操作
- 对非立即可见的组件使用懒加载(`lazy` 参数)
- 对需要独立更新的隔离区域,考虑使用 island
另见
- 样式 — 为组件添加作用域 CSS
- Alpine — 用 Alpine 做客户端交互
- 操作 — 在组件中创建 JavaScript 动作
- 属性 — 用 $wire 从 JavaScript 访问属性
- 事件 — 在 JavaScript 中派发与监听事件
参考
扩展 Livewire 的 JavaScript 系统时,理解你可能遇到的不同对象很重要。
以下是 Livewire 各相关内部属性的详尽参考。
提醒一下,普通 Livewire 用户可能永远不会与这些对象交互。它们大多供 Livewire 内部系统或高级用户使用。
$wire 对象
以下面这个通用的 Counter 组件为例:
<?php
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public $count = 1;
public function increment()
{
$this->count++;
}
public function render()
{
return view('livewire.counter');
}
}Livewire 以常被称为 $wire 的对象形式,暴露服务端组件的 JavaScript 表示:
let $wire = {
// All component public properties are directly accessible on $wire...
count: 0,
// All public methods are exposed and callable on $wire...
increment() { ... },
// Access the `$wire` object of the parent component if one exists...
$parent,
// Access the root DOM element of the Livewire component...
$el,
// Access the ID of the current Livewire component...
$id,
// Get the value of a property by name...
// Usage: $wire.$get('count')
$get(name) { ... },
// Set a property on the component by name...
// Usage: $wire.$set('count', 5)
$set(name, value, live = true) { ... },
// Toggle the value of a boolean property...
$toggle(name, live = true) { ... },
// Call the method...
// Usage: $wire.$call('increment')
$call(method, ...params) { ... },
// Define a JavaScript action...
// Usage: $wire.$js('increment', () => { ... })
// Usage: $wire.$js.increment = () => { ... }
$js(name, callback) { ... },
// [DEPRECATED] Entangle - You probably don't need this.
// Use $wire directly to access properties instead.
// Usage: <div x-data="{ count: $wire.$entangle('count') }">
$entangle(name, live = false) { ... },
// Watch the value of a property for changes...
// Usage: Alpine.$watch('count', (value, old) => { ... })
$watch(name, callback) { ... },
// Scope the next action to a named island...
// Returns $wire so you can chain any method call.
// The chained action will only re-render the named island.
// Usage: $wire.$island('revenue').$refresh()
// Usage: $wire.$island('feed', { mode: 'append' }).loadMore()
$island(name, options = {}) { ... },
// Refresh a component by sending a message to the server
// to re-render the HTML and swap it into the page...
$refresh() { ... },
// Identical to the above `$refresh`. Just a more technical name...
$commit() { ... }, // Alias for $refresh()
// Listen for a an event dispatched from this component or its children...
// Usage: $wire.$on('post-created', () => { ... })
$on(event, callback) { ... },
// Listen for a lifecycle hook triggered from this component or the request...
// Usage: $wire.$hook('message.sent', () => { ... })
$hook(name, callback) { ... },
// Dispatch an event from this component...
// Usage: $wire.$dispatch('post-created', { postId: 2 })
$dispatch(event, params = {}) { ... },
// Dispatch an event onto another component...
// Usage: $wire.$dispatchTo('dashboard', 'post-created', { postId: 2 })
$dispatchTo(otherComponentName, event, params = {}) { ... },
// Dispatch an event onto this component and no others...
$dispatchSelf(event, params = {}) { ... },
// A JS API to upload a file directly to component
// rather than through `wire:model`...
$upload(
name, // The property name
file, // The File JavaScript object
finish = () => { ... }, // Runs when the upload is finished...
error = () => { ... }, // Runs if an error is triggered mid-upload...
progress = (event) => { // Runs as the upload progresses...
event.detail.progress // An integer from 1-100...
},
) { ... },
// API to upload multiple files at the same time...
$uploadMultiple(name, files, finish, error, progress) { },
// Remove an upload after it's been temporarily uploaded but not saved...
$removeUpload(name, tmpFilename, finish, error) { ... },
// Register an action interceptor for this component instance
// Usage: $wire.intercept(({ action, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => { ... })
// Or scope to specific action: $wire.intercept('save', ({ action, onSuccess }) => { ... })
intercept(actionOrCallback, callback) { ... },
// Alias for intercept
interceptAction(actionOrCallback, callback) { ... },
// Register a message interceptor for this component instance
// Usage: $wire.interceptMessage(({ message, cancel, onSend, onCancel, onSuccess, onSkipped, onError, onFailure, onFinish }) => { ... })
// Or scope to specific action: $wire.interceptMessage('save', callback)
interceptMessage(actionOrCallback, callback) { ... },
// Register a request interceptor for this component instance
// Usage: $wire.interceptRequest(({ request, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => { ... })
// Or scope to specific action: $wire.interceptRequest('save', callback)
interceptRequest(actionOrCallback, callback) { ... },
// Retrieve the underlying "component" object...
__instance() { ... },
}你可以在 Livewire 关于从 JavaScript 访问属性的文档中了解更多关于 $wire 的内容。
snapshot 对象
在每次网络请求之间,Livewire 会将 PHP 组件序列化为可在 JavaScript 中消费的对象。该快照用于将组件反序列化回 PHP 对象,因此内置了防篡改机制:
let snapshot = {
// The serialized state of the component (public properties)...
data: { count: 0 },
// Long-standing information about the component...
memo: {
// The component's unique ID...
id: '0qCY3ri9pzSSMIXPGg8F',
// The component's name. Ex. <livewire:[name] />
name: 'counter',
// The URI, method, and locale of the web page that the
// component was originally loaded on. This is used
// to re-apply any middleware from the original request
// to subsequent component update requests (messages)...
path: '/',
method: 'GET',
locale: 'en',
// A list of any nested "child" components. Keyed by
// internal template ID with the component ID as the values...
children: [],
// Whether or not this component was "lazy loaded"...
lazyLoaded: false,
// A list of any validation errors thrown during the
// last request...
errors: [],
},
// A securely encrypted hash of this snapshot. This way,
// if a malicious user tampers with the snapshot with
// the goal of accessing un-owned resources on the server,
// the checksum validation will fail and an error will
// be thrown...
checksum: '1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7',
}component 对象
页面上的每个组件在幕后都有一个对应的 component 对象,用于跟踪其状态并暴露底层功能。这比 $wire 更深一层,仅面向高级用法。
下面是上述 Counter 组件的实际 component 对象,相关属性的说明写在 JS 注释中:
let component = {
// The root HTML element of the component...
el: HTMLElement,
// The unique ID of the component...
id: '0qCY3ri9pzSSMIXPGg8F',
// The component's "name" (<livewire:[name] />)...
name: 'counter',
// The latest "effects" object. Effects are "side-effects" from server
// round-trips. These include redirects, file downloads, etc...
effects: {},
// The component's last-known server-side state...
canonical: { count: 0 },
// The component's mutable data object representing its
// live client-side state...
ephemeral: { count: 0 },
// A reactive version of `this.ephemeral`. Changes to
// this object will be picked up by AlpineJS expressions...
reactive: Proxy,
// A Proxy object that is typically used inside Alpine
// expressions as `$wire`. This is meant to provide a
// friendly JS object interface for Livewire components...
$wire: Proxy,
// A list of any nested "child" components. Keyed by
// internal template ID with the component ID as the values...
children: [],
// The last-known "snapshot" representation of this component.
// Snapshots are taken from the server-side component and used
// to re-create the PHP object on the backend...
snapshot: {...},
// The un-parsed version of the above snapshot. This is used to send back to the
// server on the next roundtrip because JS parsing messes with PHP encoding
// which often results in checksum mis-matches.
snapshotEncoded: '{"data":{"count":0},"memo":{"id":"0qCY3ri9pzSSMIXPGg8F","name":"counter","path":"\/","method":"GET","children":[],"lazyLoaded":true,"errors":[],"locale":"en"},"checksum":"1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7"}',
}message 载荷
在浏览器中对 Livewire 组件执行动作时,会触发一次网络请求。该请求包含一个或多个组件以及对服务器的各种指令。在内部,这些组件网络载荷被称为「messages」。
「message」表示组件需要更新时从前端发往后端的数据。组件在前端渲染与操作,直到执行了需要将状态与更新以 message 形式发往后端的动作。
你会在浏览器开发者工具的网络面板载荷中,或在 Livewire 的 JavaScript 钩子里认出这一结构:
let message = {
// Snapshot object...
snapshot: { ... },
// A key-value pair list of properties
// to update on the server...
updates: {},
// An array of methods (with parameters) to call server-side...
calls: [
{ method: 'increment', params: [] },
],
}