Skip to content
全部文档

Alpine

AlpineJS 是一个轻量级 JavaScript 库,便于为网页添加客户端交互。它最初是为配合 Livewire 这类工具而生:在应用各处点缀交互时,更偏 JavaScript 的工具会很有帮助。

Livewire 开箱即带 Alpine,无需在项目中单独安装。

学习 AlpineJS 的最佳去处是 Alpine 文档

一个基础 Alpine 组件

为给后文打基础,这里给出一个最简单也最能说明问题的 Alpine 组件示例:一个小「计数器」,在页面上显示数字,并允许用户点击按钮递增该数字:

html
<!-- Declare a JavaScript object of data... -->
<div x-data="{ count: 0 }">
    <!-- Render the current "count" value inside an element... -->
    <h2 x-text="count"></h2>

    <!-- Increment the "count" value by "1" when a click event is dispatched... -->
    <button x-on:click="count++">+</button>
</div>

上面的 Alpine 组件可以无缝用在应用中的任意 Livewire 组件里。Livewire 会负责在 Livewire 组件更新时保持 Alpine 的状态。本质上,你可以像在非 Livewire 环境中一样,在 Livewire 里自由使用 Alpine 组件。

在 Livewire 中使用 Alpine

下面看一个更贴近实际的例子:在 Livewire 组件里使用 Alpine 组件。

下面是一个简单的 Livewire 组件,展示数据库中某篇 post 模型的详情。默认只显示文章标题:

html
<div>
    <h1>{{ $post->title }}</h1>

    <div x-data="{ expanded: false }">
        <button type="button" x-on:click="expanded = ! expanded">
            <span x-show="! expanded">Show post content...</span>
            <span x-show="expanded">Hide post content...</span>
        </button>

        <div x-show="expanded">
            {{ $post->content }}
        </div>
    </div>
</div>

借助 Alpine,可以先隐藏文章内容,直到用户按下「Show post content...」按钮。此时 Alpine 的 expanded 属性会设为 true,内容就会显示出来,因为 x-show="expanded" 把内容的可见性交给了 Alpine 控制。

这正是 Alpine 的用武之地:为应用增加交互,而又不必触发 Livewire 的服务器往返。

$wire 从 Alpine 控制 Livewire

作为 Livewire 开发者,最强大的能力之一就是 $wire$wire 是一个魔法对象,对所有用在 Livewire 内部的 Alpine 组件都可用。

可以把 $wire 看成从 JavaScript 通往 PHP 的网关。它让你访问和修改 Livewire 组件属性、调用 Livewire 组件方法,以及做更多事情——全部都可以在 AlpineJS 里完成。

访问 Livewire 属性

下面是创建文章表单里一个简单的「字符计数」工具示例。用户输入时,会即时显示文章内容里有多少字符:

html
<form wire:submit="save">
    <!-- ... -->

    <input wire:model="content" type="text">

    <small>
        Character count: <span x-text="$wire.content.length"></span> <!-- [tl! highlight] -->
    </small>

    <button type="submit">Save</button>
</form>

如你所见,上例用 x-text 让 Alpine 控制 <span> 元素的文本内容。x-text 可接受任意 JavaScript 表达式,并在依赖更新时自动响应。因为我们用 $wire.content 访问 $content 的值,每当 Livewire 更新 $wire.content(本例中由 wire:model="content" 触发)时,Alpine 都会自动更新文本内容。

修改 Livewire 属性

下面示例在 Alpine 里用 $wire 清空创建文章表单的「title」字段。

html
<form wire:submit="save">
    <input wire:model="title" type="text">

    <button type="button" x-on:click="$wire.title = ''">Clear</button> <!-- [tl! highlight] -->

    <!-- ... -->

    <button type="submit">Save</button>
</form>

用户填写上述 Livewire 表单时,可以按「Clear」,标题字段会被清空,且不会由 Livewire 发出网络请求。交互是「即时」的。

下面简要说明其工作原理:

  • `x-on:click` 告诉 Alpine 监听按钮元素上的点击
  • 点击时,Alpine 运行提供的 JS 表达式:`$wire.title = ''`
  • 因为 `$wire` 是代表 Livewire 组件的魔法对象,组件的所有属性都可以直接从 JavaScript 访问或修改
  • `$wire.title = ''` 会把 Livewire 组件中 `$title` 的值设为空字符串
  • 诸如 `wire:model` 等 Livewire 工具会立即响应此变更,全程无需服务器往返
  • 在下一次 Livewire 网络请求时,后端的 `$title` 属性会被更新为空字符串

调用 Livewire 方法

Alpine 也可以轻松调用任意 Livewire 方法/action:直接在 $wire 上调用即可。

下面示例用 Alpine 监听输入框的「blur」事件并触发表单保存。「blur」事件会在用户按「tab」、焦点从当前元素移到页面下一个元素时由浏览器派发:

html
<form wire:submit="save">
    <input wire:model="title" type="text" x-on:blur="$wire.save()">  <!-- [tl! highlight] -->

    <!-- ... -->

    <button type="submit">Save</button>
</form>

通常这种场景你会直接用 wire:model.live.blur="title",不过用 Alpine 实现同样效果,有助于演示说明。

传递参数

你也可以向 Livewire 方法传参:直接传给 $wire 的方法调用即可。

假设组件有如下 deletePost() 方法:

php
public function deletePost($postId)
{
    $post = Post::find($postId);

    // Authorize user can delete...
    auth()->user()->can('update', $post);

    $post->delete();
}

现在可以从 Alpine 向 deletePost() 方法传入 $postId 参数,如下所示:

html
<button type="button" x-on:click="$wire.deletePost(1)">

一般来说,$postId 这类值会在 Blade 里生成。下面示例用 Blade 决定 Alpine 传给 deletePost()$postId

html
@foreach ($posts as $post)
    <button type="button" wire:key="{{ $post->id }}" x-on:click="$wire.deletePost({{ $post->id }})">
        Delete "{{ $post->title }}"
    </button>
@endforeach

若页面上有三篇文章,上述 Blade 模板在浏览器中大致会渲染成:

html
<button type="button" x-on:click="$wire.deletePost(1)">
    Delete "The power of walking"
</button>

<button type="button" x-on:click="$wire.deletePost(2)">
    Delete "How to record a song"
</button>

<button type="button" x-on:click="$wire.deletePost(3)">
    Delete "Teach what you learn"
</button>

如你所见,我们用 Blade 把不同的文章 ID 渲染进了 Alpine 的 x-on:click 表达式。

Blade 参数的「易错点」

这是一种非常强大的技巧,但阅读 Blade 模板时可能令人困惑。乍看之下很难分清哪些是 Blade、哪些是 Alpine。因此,检查页面上实际渲染的 HTML、确认是否符合预期会很有帮助。

下面是一个常让人困惑的例子:

假设你的 Post 模型不用整数 ID,而是用 UUID 做索引(ID 是整数,UUID 是一长串字符)。

如果像处理 ID 那样直接渲染下面这样,就会出问题:

html
<!-- Warning: this is an example of problematic code... -->
<button
    type="button"
    x-on:click="$wire.deletePost({{ $post->uuid }})"
>

上述 Blade 模板会在 HTML 中渲染成:

html
<!-- Warning: this is an example of problematic code... -->
<button
    type="button"
    x-on:click="$wire.deletePost(93c7b04c-c9a4-4524-aa7d-39196011b81a)"
>

注意到 UUID 字符串周围缺少引号了吗?Alpine 求值该表达式时,JavaScript 会抛出错误:「Uncaught SyntaxError: Invalid or unexpected token」。

要修复这一点,需要在 Blade 表达式外加上引号,如下所示:

html
<button
    type="button"
    x-on:click="$wire.deletePost('{{ $post->uuid }}')"
>

现在上述模板会正确渲染,一切都会按预期工作:

html
<button
    type="button"
    x-on:click="$wire.deletePost('93c7b04c-c9a4-4524-aa7d-39196011b81a')"
>

刷新组件

你可以用 $wire.$refresh() 轻松刷新 Livewire 组件(触发网络往返以重新渲染组件的 Blade 视图):

html
<button type="button" x-on:click="$wire.$refresh()">

$wire.entangle 共享状态

WARNING

你大概不需要这个

几乎在所有情况下,你都应从 Alpine 用 $wire 直接访问 Livewire 属性,而不是使用 $wire.entangle()。Entangle 会创建重复状态,可能导致可预测性与性能问题。该 API 为向后兼容而保留,但不鼓励在新代码中使用。

不要使用 @@entangle Blade 指令——它已弃用,并在移除 DOM 元素时会出问题。

在少数需要 Alpine 与 Livewire 双向状态同步的场景,可以使用 $wire.entangle()

blade
<div x-data="{ open: $wire.entangle('showDropdown') }">
    <button x-on:click="open = true">Show More...</button>

    <ul x-show="open">
        <li><button wire:click="archive">Archive</button></li>
    </ul>
</div>

默认情况下,变更会延迟到下一次 Livewire 请求。使用 .live 可立即同步:

blade
<div x-data="{ open: $wire.entangle('showDropdown').live }">

使用 @js 指令

若需要直接输出 PHP 数据供 Alpine 使用,可以使用 @js 指令。

blade
<div x-data="{ posts: @js($posts) }">
    ...
</div>

在 JavaScript 构建中手动打包 Alpine

默认情况下,Livewire 与 Alpine 的 JavaScript 会自动注入到每个 Livewire 页面。

这对较简单的配置很理想;不过,你可能希望把自己的 Alpine 组件、store 和插件纳入项目。

通过自己的 JavaScript 包在页面上引入 Livewire 和 Alpine 很直接。

首先,必须在布局文件中加入 @livewireScriptConfig 指令,如下所示:

blade
<html>
<head>
    <!-- ... -->
    @livewireStyles
    @vite(['resources/js/app.js'])
</head>
<body>
    {{ $slot }}

    @livewireScriptConfig <!-- [tl! highlight] -->
</body>
</html>

这样 Livewire 就能向你的打包产物提供应用正常运行所需的某些配置。

现在可以在 resources/js/app.js 中这样导入 Livewire 和 Alpine:

js
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';

// Register any Alpine directives, components, or plugins here...

Livewire.start()

下面示例在应用中注册一个名为「x-clipboard」的自定义 Alpine 指令:

js
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';

Alpine.directive('clipboard', (el) => {
    let text = el.textContent

    el.addEventListener('click', () => {
        navigator.clipboard.writeText(text)
    })
})

Livewire.start()

现在 x-clipboard 指令会对 Livewire 应用中所有 Alpine 组件可用。

另见

  • 属性用 $wire 从 Alpine 访问 Livewire 属性
  • 操作从 Alpine 调用 Livewire 操作
  • JavaScript在组件中执行自定义 JavaScript
  • 事件用 Alpine 派发与监听事件