Skip to content

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。

blade
<div>
    ...
</div>

@script
<script>
    // This Javascript will get executed every time this component is loaded onto the page...
</script>
@endscript

以下是一个更完整的示例,您可以执行类似注册在 Livewire 组件中使用的 JavaScript 操作的操作。

blade
<div>
    <button wire:click="$js.increment">+</button>
</div>

@script
<script>
    $js.increment = () => {
        console.log('increment')
    }
</script>
@endscript

要了解有关 JavaScript 操作的更多信息,请访问操作文档

从脚本中使用 $wire

使用 @script 处理 JavaScript 的另一个有用功能是您可以自动访问 Livewire 组件的 $wire 对象。

以下是使用简单的 setInterval 每 2 秒刷新组件的示例(您可以轻松使用 wire:poll 来实现,但这是演示该点的简单方法):

您可以在 $wire 文档中了解有关 $wire 的更多信息。

blade
@script
<script>
    setInterval(() => {
        $wire.$refresh()
    }, 2000)
</script>
@endscript

评估一次性 JavaScript 表达式

除了指定要在 JavaScript 中评估的整个方法之外,您还可以使用 js() 方法在后端评估较小的单个表达式。

这通常用于在执行服务器端操作后执行某种客户端后续操作。

例如,以下是 post.create 组件的示例,该组件在将帖子保存到数据库后触发客户端警报对话框:

php
<?php // resources/views/components/post/⚡create.blade.php

use Livewire\Component;

new class extends Component
{
    public $title = '';

    public function save()
    {
        // ...

        $this->js("alert('Post saved!')"); // [tl! highlight:6]
    }
};

在将帖子保存到服务器上的数据库后,JavaScript 表达式 alert('Post saved!') 现在将在客户端执行。

您可以在表达式内访问当前组件的 $wire 对象。

加载资源

@script 指令对于每次加载 Livewire 组件时执行一些 JavaScript 很有用,但是,有时您可能希望将整个脚本和样式资源与组件一起加载到页面上。

以下是使用 @assets 加载名为 Pikaday 的日期选择器库并使用 @script 在组件内初始化它的示例:

blade
<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
<script>
    new Pikaday({ field: $wire.$el.querySelector('[data-picker]') });
</script>
@endscript

当此组件加载时,Livewire 将确保在评估 @script 之前在该页面上加载任何 @assets。此外,它将确保提供的 @assets 每页只加载一次,无论此组件有多少个实例,这与 @script 不同,后者将为页面上的每个组件实例评估。

全局 Livewire 事件

Livewire 调度两个有用的浏览器事件,供您从外部脚本注册任何自定义扩展点:

html
<script>
    document.addEventListener('livewire:init', () => {
        // 在 Livewire 加载后但在页面上初始化之前运行...
    })

    document.addEventListener('livewire:initialized', () => {
        // 在 Livewire 完成页面初始化后立即运行...
    })
</script>

INFO

livewire:init 内注册任何自定义指令生命周期钩子通常是有益的,这样它们在 Livewire 开始在页面上初始化之前就可用。

Livewire 全局对象

Livewire 的全局对象是从外部脚本与 Livewire 交互的最佳起点。

您可以从客户端代码内的任何位置访问 window 上的全局 Livewire JavaScript 对象。

livewire:init 事件监听器内使用 window.Livewire 通常很有帮助。

访问组件

您可以使用以下方法访问当前页面上加载的特定 Livewire 组件:

js
// 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()

INFO

这些方法中的每一个都返回一个 $wire 对象,代表 Livewire 中组件的状态。

您可以在 $wire 文档中了解有关这些对象的更多信息。

与事件交互

除了在 PHP 中从各个组件调度和监听事件之外,全局 Livewire 对象还允许您从应用程序中的任何位置与 Livewire 的事件系统交互:

js
// 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 函数。在此函数内循环遍历所有监听器以注销它们并防止任何不必要的积累。

js
Alpine.data('MyComponent', () => ({
    listeners: [],
    init() {
        this.listeners.push(
            Livewire.on('post-created', (options) => {
                // Do something...
            })
        );
    },
    destroy() {
        this.listeners.forEach((listener) => {
            listener();
        });
    }
}));

使用生命周期钩子

Livewire 允许您使用 Livewire.hook() 钩入其全局生命周期的各个部分:

js
// 注册一个回调以在给定的内部 Livewire 钩子上执行...
Livewire.hook('component.init', ({ component, cleanup }) => {
    // ...
})

有关 Livewire 的 JavaScript 钩子的更多信息可以在下面找到

注册自定义指令

Livewire 允许您使用 Livewire.directive() 注册自定义指令。

以下是自定义 wire:confirm 指令的示例,该指令使用 JavaScript 的 confirm() 对话框在将操作发送到服务器之前确认或取消操作:

html
<button wire:confirm="Are you sure?" wire:click="delete">Delete post</button>

以下是使用 Livewire.directive() 实现 wire:confirm 的方法:

js
Livewire.directive('confirm', ({ el, directive, component, cleanup }) => {
    let content =  directive.expression

    // "directive" 对象使您可以访问已解析的指令。
    // 例如,以下是它对于 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 })

    // 在 Livewire 组件在页面仍处于活动状态时从 DOM 中删除的情况下,
    // 在 `cleanup()` 内注册任何清理代码。
    cleanup(() => {
        el.removeEventListener('click', onClick)
    })
})

对象模式

在扩展 Livewire 的 JavaScript 系统时,了解您可能遇到的不同对象非常重要。

以下是 Livewire 每个相关内部属性的详尽参考。

提醒一下,普通 Livewire 用户可能永远不会与这些交互。这些对象中的大多数都可用于 Livewire 的内部系统或高级用户。

$wire 对象

给定以下通用 Counter 组件:

php
<?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 表示:

js
let $wire = {
    // 所有组件公共属性都可以直接在 $wire 上访问...
    count: 0,

    // 所有公共方法都公开并可在 $wire 上调用...
    increment() { ... },

    // 访问父组件的 `$wire` 对象(如果存在)...
    $parent,

    // 访问 Livewire 组件的根 DOM 元素...
    $el,

    // 访问当前 Livewire 组件的 ID...
    $id,

    // 按名称获取属性的值...
    // 用法:$wire.$get('count')
    $get(name) { ... },

    // 按名称在组件上设置属性...
    // 用法:$wire.$set('count', 5)
    $set(name, value, live = true) { ... },

    // 切换布尔属性的值...
    $toggle(name, live = true) { ... },

    // 调用方法...
    // 用法:$wire.$call('increment')
    $call(method, ...params) { ... },

    // 定义 JavaScript 操作...
    // 用法:$wire.$js('increment', () => { ... })
    // 用法:$wire.$js.increment = () => { ... }
    $js(name, callback) { ... },

    // 将 Livewire 属性的值与不同的、
    // 任意的 Alpine 属性纠缠在一起...
    // 用法:<div x-data="{ count: $wire.$entangle('count') }">
    $entangle(name, live = false) { ... },

    // 监视属性值的更改...
    // 用法:Alpine.$watch('count', (value, old) => { ... })
    $watch(name, callback) { ... },

    // 通过向服务器发送提交来刷新组件
    // 以重新渲染 HTML 并将其交换到页面中...
    $refresh() { ... },

    // 与上面的 `$refresh` 相同。只是一个更技术性的名称...
    $commit() { ... },

    // 监听从此组件或其子组件调度的事件...
    // 用法:$wire.$on('post-created', () => { ... })
    $on(event, callback) { ... },

    // 监听从此组件或请求触发的生命周期钩子...
    // 用法:$wire.$hook('commit', () => { ... })
    $hook(name, callback) { ... },

    // 从此组件调度事件...
    // 用法:$wire.$dispatch('post-created', { postId: 2 })
    $dispatch(event, params = {}) { ... },

    // 将事件调度到另一个组件...
    // 用法:$wire.$dispatchTo('dashboard', 'post-created', { postId: 2 })
    $dispatchTo(otherComponentName, event, params = {}) { ... },

    // 将事件调度到此组件而不是其他组件...
    $dispatchSelf(event, params = {}) { ... },

    // 直接将文件上传到组件的 JS API
    // 而不是通过 `wire:model`...
    $upload(
        name, // 属性名称
        file, // File JavaScript 对象
        finish = () => { ... }, // 上传完成时运行...
        error = () => { ... }, // 如果上传过程中触发错误则运行...
        progress = (event) => { // 随着上传进度运行...
            event.detail.progress // 从 1-100 的整数...
        },
    ) { ... },

    // 同时上传多个文件的 API...
    $uploadMultiple(name, files, finish, error, progress) { },

    // 在临时上传但未保存后删除上传...
    $removeUpload(name, tmpFilename, finish, error) { ... },

    // 为此组件实例注册拦截器
    // 用法:$wire.intercept(({ onSend, onSuccess }) => { ... })
    // 或限定到特定操作:$wire.intercept('save', ({ onSuccess }) => { ... })
    intercept(methodOrCallback, callback) { ... },

    // 检索底层的 "component" 对象...
    __instance() { ... },
}

您可以在 Livewire 关于从 JavaScript 访问属性的文档中了解有关 $wire 的更多信息。

snapshot 对象

在每个网络请求之间,Livewire 将 PHP 组件序列化为可在 JavaScript 中使用的对象。此快照用于将组件反序列化回 PHP 对象,因此具有内置机制以防止篡改:

js
let snapshot = {
    // 组件的序列化状态(公共属性)...
    data: { count: 0 },

    // 有关组件的长期信息...
    memo: {
        // 组件的唯一 ID...
        id: '0qCY3ri9pzSSMIXPGg8F',

        // 组件的名称。例如 <livewire:[name] />
        name: 'counter',

        // 最初加载组件的网页的 URI、方法和语言环境。
        // 这用于将原始请求中的任何中间件
        // 重新应用到后续的组件更新请求(提交)...
        path: '/',
        method: 'GET',
        locale: 'en',

        // 任何嵌套"子"组件的列表。按内部模板 ID
        // 键入,组件 ID 作为值...
        children: [],

        // 此组件是否"延迟加载"...
        lazyLoaded: false,

        // 最后一次请求期间抛出的任何验证错误列表...
        errors: [],
    },

    // 此快照的安全加密哈希。这样,
    // 如果恶意用户篡改快照
    // 目的是访问服务器上未拥有的资源,
    // 校验和验证将失败并且将抛出错误...
    checksum: '1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7',
}

component 对象

页面上的每个组件都有一个对应的组件对象在幕后跟踪其状态并公开其底层功能。这比 $wire 深一层。它仅用于高级用法。

以下是上述 Counter 组件的实际组件对象,在 JS 注释中描述了相关属性:

js
let component = {
    // 组件的根 HTML 元素...
    el: HTMLElement,

    // 组件的唯一 ID...
    id: '0qCY3ri9pzSSMIXPGg8F',

    // 组件的"名称"(<livewire:[name] />)...
    name: 'counter',

    // 最新的 "effects" 对象。Effects 是来自服务器
    // 往返的"副作用"。这些包括重定向、文件下载等...
    effects: {},

    // 组件最后已知的服务器端状态...
    canonical: { count: 0 },

    // 组件的可变数据对象,表示其
    // 实时客户端状态...
    ephemeral: { count: 0 },

    // `this.ephemeral` 的响应式版本。对此对象的更改
    // 将被 AlpineJS 表达式捕获...
    reactive: Proxy,

    // 通常在 Alpine 表达式中用作 `$wire` 的 Proxy 对象。
    // 这旨在为 Livewire 组件提供友好的 JS 对象接口...
    $wire: Proxy,

    // 任何嵌套"子"组件的列表。按内部模板 ID
    // 键入,组件 ID 作为值...
    children: [],

    // 此组件最后已知的"快照"表示。
    // 快照取自服务器端组件,用于
    // 在后端重新创建 PHP 对象...
    snapshot: {...},

    // 上述快照的未解析版本。这用于在下一次往返时发送回
    // 服务器,因为 JS 解析会干扰 PHP 编码
    // 这通常会导致校验和不匹配。
    snapshotEncoded: '{"data":{"count":0},"memo":{"id":"0qCY3ri9pzSSMIXPGg8F","name":"counter","path":"\/","method":"GET","children":[],"lazyLoaded":true,"errors":[],"locale":"en"},"checksum":"1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7"}',
}

commit 负载

当在浏览器中对 Livewire 组件执行操作时,会触发网络请求。该网络请求包含一个或多个组件以及服务器的各种指令。在内部,这些组件网络负载被称为"提交"。

选择术语"commit"作为思考 Livewire 前端和后端之间关系的有用方式。组件在前端渲染和操作,直到执行需要将其状态和更新"提交"到后端的操作。

您将从浏览器 DevTools 的网络选项卡中的负载或 Livewire 的 JavaScript 钩子中识别此模式:

js
let commit = {
    // Snapshot 对象...
    snapshot: { ... },

    // 要在服务器上更新的属性的
    // 键值对列表...
    updates: {},

    // 要在服务器端调用的方法(带参数)数组...
    calls: [
        { method: 'increment', params: [] },
    ],
}

JavaScript 钩子

对于高级用户,Livewire 公开了其内部客户端"钩子"系统。您可以使用以下钩子来扩展 Livewire 的功能或获取有关 Livewire 应用程序的更多信息。

组件初始化

每次 Livewire 发现新组件时——无论是在初始页面加载时还是稍后——都会触发 component.init 事件。您可以钩入 component.init 以拦截或初始化与新组件相关的任何内容:

js
Livewire.hook('component.init', ({ component, cleanup }) => {
    //
})

有关更多信息,请查阅 component 对象文档

DOM 元素初始化

除了在初始化新组件时触发事件外,Livewire 还会为给定 Livewire 组件内的每个 DOM 元素触发事件。

这可用于在应用程序中提供自定义 Livewire HTML 属性:

js
Livewire.hook('element.init', ({ component, el }) => {
    //
})

DOM Morph 钩子

在 DOM morphing 阶段——在 Livewire 完成网络往返后发生——Livewire 为每个被修改的元素触发一系列事件。

js
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 组件触发 morphmorphed 事件:

js
Livewire.hook('morph',  ({ el, component }) => {
	// 在 `component` 中的子元素被 morphed 之前运行(不包括部分 morphing)
})

Livewire.hook('morphed',  ({ el, component }) => {
    // 在 `component` 中的所有子元素被 morphed 后运行(不包括部分 morphing)
})

拦截器

寻找旧的 commitrequest 钩子?

这些已被更强大的拦截器系统取代。有关迁移详细信息,请参阅升级指南

Livewire 的拦截器系统提供了对请求生命周期的强大钩子,允许您在各个阶段拦截和操作网络请求。

拦截器系统分为多个层次:

  • 消息拦截器 - 在组件状态更新捆绑到请求之前钩入
  • 请求拦截器 - 钩入对服务器的实际 HTTP 请求
  • 组件拦截器 - 将拦截器限定到特定组件

消息拦截器

消息拦截器允许您在发送到服务器之前钩入各个组件更新的生命周期。"消息"表示单个组件的状态更改和方法调用。

js
Livewire.interceptMessage(({ message, component, onSend, onCancel, onFailure, onError, onSuccess, onFinish, cancel }) => {
    // 在创建组件消息时运行,但在
    // 将其捆绑到请求并发送到服务器之前

    // 如果需要,取消消息
    if (shouldCancel) {
        cancel()

        return
    }

    onSend(({ payload }) => {
        // 在包含此消息的请求发送后立即运行
    })

    onCancel(() => {
        // 如果消息因任何原因被取消则运行
    })

    onFailure(({ error }) => {
        // 当出现网络级错误时运行
    })

    onError(({ response, responseBody, preventDefault }) => {
        // 当服务器返回错误状态(400、500 等)时运行

        if (response.status === 403) {
            // 特别处理授权错误
            preventDefault() // 阻止 Livewire 的默认错误处理

            showCustomAuthDialog()
        }
    })

    onSuccess(({ payload, onSync, onMorph, onRender }) => {
        // 在成功响应后、处理前运行

        onSync(() => {
            // 在服务器数据合并到组件状态后运行
        })

        onMorph(() => {
            // 在 HTML 被 morphed 到 DOM 后运行
        })

        onRender(() => {
            // 在渲染完成后运行(在浏览器 tick 后)
        })
    })

    onFinish(() => {
        // 消息处理完成时始终运行
        // (无论成功、失败还是取消)
    })
})

实际示例:加载状态

以下是如何为特定组件实现自定义加载指示器:

js
Livewire.interceptMessage(({ component, onSend, onFinish }) => {
    onSend(() => {
        component.el.classList.add('is-loading')
    })

    onFinish(() => {
        component.el.classList.remove('is-loading')
    })
})

请求拦截器

请求拦截器在 HTTP 级别运行,允许您拦截可能包含多个组件消息的实际网络请求。这对于实现请求排队、重试逻辑或全局错误处理等功能很有用。

js
Livewire.interceptRequest(({ request, onSend, onAbort, onFailure, onResponse, onParsed, onError, onRedirect, onDump, onSuccess, abort }) => {
    // Runs when a request is created but before it's sent

    // Abort the request if needed
    if (shouldAbort) {
        abort()

        return
    }

    onSend(({ responsePromise }) => {
        // 在调用 fetch() 后立即运行
    })

    onAbort(() => {
        // 如果请求被中止则运行
    })

    onFailure(({ error }) => {
        // 在网络级故障时运行
    })

    onResponse(({ response }) => {
        // 当收到任何响应时运行
    })

    onParsed(({ response, responseBody }) => {
        // 在响应体被解析后运行
    })

    onError(({ response, responseBody, preventDefault }) => {
        // 在错误状态码(400、500 等)时运行

        if (response.status === 419) {
            // 自定义会话过期处理
            preventDefault()

            handleSessionExpired()
        }
    })

    onRedirect(({ url, preventDefault }) => {
        // 当响应触发重定向时运行

        // 可选择阻止重定向
        if (shouldPreventRedirect) {
            preventDefault()
        }
    })

    onDump(({ content, preventDefault }) => {
        // 当响应包含调试转储内容时运行

        // 可选择阻止转储模态框
        preventDefault()

        showCustomDumpViewer(content)
    })

    onSuccess(({ response, responseBody, responseJson }) => {
        // 在处理前成功响应时运行
    })
})

实际示例:全局错误处理

为特定状态码实现自定义错误处理:

js
Livewire.interceptRequest(({ onError }) => {
    onError(({ response, preventDefault }) => {
        if (response.status === 419) {
            // 会话过期
            preventDefault()

            if (confirm('您的会话已过期。刷新页面?')) {
                window.location.reload()
            }
        }

        if (response.status === 403) {
            // 禁止访问
            preventDefault()

            alert('您没有权限执行此操作')
        }
    })
})

组件拦截器

组件拦截器允许您注册仅适用于特定组件实例的拦截器。这对于组件特定行为很有用,而不会影响全局作用域。

blade
@script
<script>
    // 此拦截器仅影响此组件实例
    $wire.intercept(({ onSend, onSuccess }) => {
        onSend(() => {
            $wire.$el.style.opacity = '0.5'
        })

        onSuccess(() => {
            $wire.$el.style.opacity = '1'
        })
    })
</script>
@endscript

您还可以通过将方法名称作为第一个参数传递来将拦截器限定到特定操作:

blade
@script
<script>
    // 此拦截器仅在调用 $refresh 时运行
    $wire.intercept('$refresh', ({ onSend, onSuccess }) => {
        onSend(() => {
            // 刷新操作的自定义加载状态
            $wire.$el.classList.add('refreshing')
        })

        onSuccess(() => {
            $wire.$el.classList.remove('refreshing')
        })
    })

    // 此拦截器仅在调用 save 方法时运行
    $wire.intercept('save', ({ onSuccess, onError }) => {
        onSuccess(() => {
            // 为保存操作显示成功消息
            showNotification('保存成功!')
        })

        onError(() => {
            // 为保存操作显示错误消息
            showNotification('保存失败!', 'error')
        })
    })
</script>
@endscript

当您希望同一组件内的不同操作具有不同行为时,这特别有用。