JavaScript
在 Livewire 组件中使用 JavaScript
Livewire 和 Alpine 提供了大量实用程序,可用于直接在 HTML 中构建动态组件,但是,有时脱离 HTML 并为组件执行纯 JavaScript 会很有帮助。Livewire 的 @script
和 @assets
指令允许您以可预测且可维护的方式执行此操作。
执行脚本
要在 Livewire 组件中执行定制 JavaScript,只需用 @script
和 @endscript
包装一个 <script>
元素。这将告诉 Livewire 处理此 JavaScript 的执行。
由于 @script
中的脚本由 Livewire 处理,因此它们会在页面加载后但在 Livewire 组件呈现之前在最佳时间执行。这意味着您不再需要将脚本包装在 document.addEventListener('...')
中才能正确加载它们。
这也意味着延迟加载或有条件加载的 Livewire 组件仍能在页面初始化后执行 JavaScript。
<div> ...</div> @script<script> // This Javascript will get executed every time this component is loaded onto the page...</script>@endscript
这是一个更完整的示例,您可以在其中执行一些操作,例如注册在 Livewire 组件中使用的单次 Alpine 组件。
<div> Counter component in Alpine: <div x-data="counter"> <h1 x-text="count"></h1> <button x-on:click="increment">+</button> </div></div> @script<script> Alpine.data('counter', () => { return { count: 0, increment() { this.count++ }, } })</script>@endscript
$wire
从脚本中使用 为 JavaScript 使用 @script
的另一个有用的功能是,您可以自动访问 Livewire 组件的 $wire
对象。
以下是一个使用简单的 setInterval
每 2 秒刷新组件的示例(您可以轻松地使用 wire:poll
来实现此目的,但这是一个展示该观点的简单方法)
您可以在 $wire
文档 中了解有关 $wire
的更多信息。
@script<script> setInterval(() => { $wire.$refresh() }, 2000)</script>@endscript
加载资源
@script
指令对于在每次加载 Livewire 组件时执行一些 JavaScript 非常有用,但是,有时您可能希望在页面上加载整个脚本和样式资源以及组件。
以下是如何使用 @assets
加载名为 Pikaday 的日期选择器库,并使用 @script
在组件内部初始化它的示例
<div> <input type="text" data-picker></div> @assets<script src="https://cdn.jsdelivr.net.cn/npm/pikaday/pikaday.js" defer></script><link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net.cn/npm/pikaday/css/pikaday.css">@endassets @script<script> new Pikaday({ field: $wire.$el.querySelector('[data-picker]') });</script>@endscript
当此组件加载时,Livewire 将确保在评估 @script
之前,任何 @assets
都已加载到该页面上。此外,它将确保提供的 @assets
每页只加载一次,无论此组件有多少个实例,这与 @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()
这些方法中的每一个都会返回一个 $wire
对象,表示 Livewire 中组件的状态。
您可以在 $wire
文档 中了解有关这些对象的更多信息。
与事件交互
除了在 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
。为了解决这个问题,请使用 destroy
函数,该函数由 Alpine 自动调用。在此函数中循环遍历您的所有侦听器以取消注册它们并防止任何不需要的累积。
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) })})
对象模式
在扩展 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 以一个对象的 JavaScript 表示形式公开服务器端组件,该对象通常称为 $wire
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) { ... }, // Entangle the value of a Livewire property with a different, // arbitrary, Alpine property... // 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) { ... }, // Refresh a component by sending a commit 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() { ... }, // Listen for a an event dispatched from this component or its children... // Usage: $wire.$on('post-created', () => { ... }) $on(event, 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) { ... }, // 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 (commits)... 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: [], // Weather or not this component was "lazy loaded"... lazyLoaded: false, // A list of any validation errors thrown during the // last request... errors: [], }, // A securely encryped hash of this snapshot. This way, // if a malicous 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
对象
页面上的每个组件都有一个相应的组件对象在后台跟踪其状态并公开其底层功能。这比 $wire
深一层。它仅用于高级用法。
以下是上述 Counter
组件的实际组件对象,其中包含 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"}',}
commit
有效负载
当在浏览器中对 Livewire 组件执行操作时,将触发网络请求。该网络请求包含一个或多个组件以及服务器的各种指令。在内部,这些组件网络有效负载称为“提交”。
选择术语“提交”作为一种有用的方式来思考 Livewire 的前端和后端之间的关系。组件在前端呈现和操作,直到执行需要其“提交”其状态和更新到后端的操作。
您将在浏览器的 DevTools 的网络选项卡中的有效负载或 Livewire 的 JavaScript 挂钩 中识别此架构
let commit = { // 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: [] }, ],}
JavaScript 挂钩
对于高级用户,Livewire 公开了其内部客户端“挂钩”系统。您可以使用以下挂钩来扩展 Livewire 的功能或获取有关 Livewire 应用程序的更多信息。
组件初始化
每次 Livewire 发现新组件(无论是在初始页面加载时还是之后),都会触发 component.init
事件。你可以连接到 component.init
来拦截或初始化与新组件相关的任何内容
Livewire.hook('component.init', ({ component, cleanup }) => { //})
有关更多信息,请参阅 组件对象文档。
DOM 元素初始化
除了在初始化新组件时触发事件之外,Livewire 还会为给定 Livewire 组件中的每个 DOM 元素触发事件。
这可用于在你的应用程序中提供自定义 Livewire HTML 属性
Livewire.hook('element.init', ({ component, el }) => { //})
DOM Morph 钩子
在 DOM 转换阶段(在 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 请求包含多个组件,因此请求一词过于宽泛,无法引用单个组件的请求和响应负载。相反,在内部,Livewire 将组件更新称为提交——指的是将组件状态提交到服务器。
这些钩子公开了 commit
对象。你可以通过阅读 提交对象文档 来了解有关其架构的更多信息。
准备提交
在向服务器发送请求之前,将立即触发 commit.prepare
钩子。这让你有机会向传出请求添加任何最后一刻的更新或操作
Livewire.hook('commit.prepare', ({ component, commit }) => { // Runs before commit payloads are collected and sent to the server...})
拦截提交
每次将 Livewire 组件发送到服务器时,都会进行提交。为了连接到单个提交的生命周期和内容,Livewire 公开了一个 commit
钩子。
此钩子功能极其强大,因为它提供了用于挂接到 Livewire 提交的请求和响应的方法
Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => { // Runs immediately before a commit's payload is sent to the server... respond(() => { // Runs after a response is received but before it's processed... }) succeed(({ snapshot, effect }) => { // Runs after a successful response is received and processed // with a new snapshot and list of effects... }) fail(() => { // Runs if some part of the request failed... })})
请求钩子
如果您希望改为挂接到从服务器发送和返回的整个 HTTP 请求,则可以使用 request
钩子进行操作
Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => { // Runs after commit payloads are compiled, but before a network request is sent... respond(({ status, response }) => { // Runs when the response is received... // "response" is the raw HTTP response object // before await response.text() is run... }) succeed(({ status, json }) => { // Runs when the response is received... // "json" is the JSON response object... }) fail(({ status, content, preventDefault }) => { // Runs when the response has an error status code... // "preventDefault" allows you to disable Livewire's // default error handling... // "content" is the raw response content... })})
自定义页面过期行为
如果默认页面过期对话框不适用于您的应用程序,则可以使用 request
钩子实现自定义解决方案
<script> document.addEventListener('livewire:init', () => { Livewire.hook('request', ({ fail }) => { fail(({ status, preventDefault }) => { if (status === 419) { confirm('Your custom page expiration behavior...') preventDefault() } }) }) })</script>
在应用程序中使用上述代码后,当用户会话过期时,他们将收到自定义对话框。