Alpine
AlpineJS 是一个轻量级 JavaScript 库,便于为网页添加客户端交互。它最初是为配合 Livewire 这类工具而生:在应用各处点缀交互时,更偏 JavaScript 的工具会很有帮助。
Livewire 开箱即带 Alpine,无需在项目中单独安装。
学习 AlpineJS 的最佳去处是 Alpine 文档。
一个基础 Alpine 组件
为给后文打基础,这里给出一个最简单也最能说明问题的 Alpine 组件示例:一个小「计数器」,在页面上显示数字,并允许用户点击按钮递增该数字:
<!-- 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 模型的详情。默认只显示文章标题:
<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 属性
下面是创建文章表单里一个简单的「字符计数」工具示例。用户输入时,会即时显示文章内容里有多少字符:
<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」字段。
<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」、焦点从当前元素移到页面下一个元素时由浏览器派发:
<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() 方法:
public function deletePost($postId)
{
$post = Post::find($postId);
// Authorize user can delete...
auth()->user()->can('update', $post);
$post->delete();
}现在可以从 Alpine 向 deletePost() 方法传入 $postId 参数,如下所示:
<button type="button" x-on:click="$wire.deletePost(1)">一般来说,$postId 这类值会在 Blade 里生成。下面示例用 Blade 决定 Alpine 传给 deletePost() 的 $postId:
@foreach ($posts as $post)
<button type="button" wire:key="{{ $post->id }}" x-on:click="$wire.deletePost({{ $post->id }})">
Delete "{{ $post->title }}"
</button>
@endforeach若页面上有三篇文章,上述 Blade 模板在浏览器中大致会渲染成:
<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 那样直接渲染下面这样,就会出问题:
<!-- Warning: this is an example of problematic code... -->
<button
type="button"
x-on:click="$wire.deletePost({{ $post->uuid }})"
>上述 Blade 模板会在 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 表达式外加上引号,如下所示:
<button
type="button"
x-on:click="$wire.deletePost('{{ $post->uuid }}')"
>现在上述模板会正确渲染,一切都会按预期工作:
<button
type="button"
x-on:click="$wire.deletePost('93c7b04c-c9a4-4524-aa7d-39196011b81a')"
>刷新组件
你可以用 $wire.$refresh() 轻松刷新 Livewire 组件(触发网络往返以重新渲染组件的 Blade 视图):
<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():
<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 可立即同步:
<div x-data="{ open: $wire.entangle('showDropdown').live }">使用 @js 指令
若需要直接输出 PHP 数据供 Alpine 使用,可以使用 @js 指令。
<div x-data="{ posts: @js($posts) }">
...
</div>在 JavaScript 构建中手动打包 Alpine
默认情况下,Livewire 与 Alpine 的 JavaScript 会自动注入到每个 Livewire 页面。
这对较简单的配置很理想;不过,你可能希望把自己的 Alpine 组件、store 和插件纳入项目。
通过自己的 JavaScript 包在页面上引入 Livewire 和 Alpine 很直接。
首先,必须在布局文件中加入 @livewireScriptConfig 指令,如下所示:
<html>
<head>
<!-- ... -->
@livewireStyles
@vite(['resources/js/app.js'])
</head>
<body>
{{ $slot }}
@livewireScriptConfig <!-- [tl! highlight] -->
</body>
</html>这样 Livewire 就能向你的打包产物提供应用正常运行所需的某些配置。
现在可以在 resources/js/app.js 中这样导入 Livewire 和 Alpine:
import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
// Register any Alpine directives, components, or plugins here...
Livewire.start()下面示例在应用中注册一个名为「x-clipboard」的自定义 Alpine 指令:
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 派发与监听事件