扩展
Alpine 拥有非常开放的代码库,支持多种方式进行扩展。实际上,Alpine 自身可用的每个指令和魔术属性都使用了这些相同的 API。理论上,你可以使用这些 API 自己重建 Alpine 的所有功能。
生命周期注意事项
在深入探讨每个单独的 API 之前,我们先来谈谈应该在代码库的哪个位置使用这些 API。
由于这些 API 会影响 Alpine 初始化页面的方式,它们必须在 Alpine 下载完成并在页面上可用之后,但在 Alpine 初始化页面本身之前进行注册。
根据你是将 Alpine 导入到打包工具中,还是通过 <script> 标签直接引入,有两种不同的技术。让我们来看看这两种方式:
通过 script 标签
如果你通过 script 标签引入 Alpine,你需要在 alpine:init 事件监听器中注册任何自定义扩展代码。
示例如下:
<html>
<script src="/js/alpine.js" defer></script>
<div x-data x-foo></div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.directive('foo', ...)
})
</script>
</html>
如果你想将扩展代码提取到外部文件中,需要确保该文件的 <script> 标签位于 Alpine 的标签之前,如下所示:
<html>
<script src="/js/foo.js" defer></script>
<script src="/js/alpine.js" defer></script>
<div x-data x-foo></div>
</html>
通过 NPM 模块
如果你将 Alpine 导入到打包工具中,则必须在导入 Alpine 全局对象和调用 Alpine.start() 初始化 Alpine 之间注册任何扩展代码。例如:
import Alpine from 'alpinejs'
Alpine.directive('foo', ...)
window.Alpine = Alpine
window.Alpine.start()
现在我们已经了解了在哪里使用这些扩展 API,接下来让我们更详细地看看如何使用每一个:
自定义指令
Alpine 允许你使用 Alpine.directive() API 注册自己的自定义指令。
方法签名
Alpine.directive('[name]', (el, { value, modifiers, expression }, { Alpine, effect, cleanup }) => {})
| name | 指令的名称。例如,"foo" 将作为 x-foo 使用 |
| el | 添加了该指令的 DOM 元素 |
| value | 如果提供,是指令中冒号后面的部分。例如:x-foo:bar 中的 'bar' |
| modifiers | 指令中以点分隔的尾随附加项的数组。例如:x-foo.baz.lob 中的 ['baz', 'lob'] |
| expression | 指令的属性值部分。例如:x-foo="law" 中的 law |
| Alpine | Alpine 全局对象 |
| effect | 用于创建响应式效果,当该指令从 DOM 中移除时会自动清理 |
| cleanup | 一个函数,你可以向其传递自定义回调,当此指令从 DOM 中移除时,这些回调会被执行 |
简单示例
以下是我们将要创建的一个名为 x-uppercase 的简单指令示例:
Alpine.directive('uppercase', el => {
el.textContent = el.textContent.toUpperCase()
})
<div x-data>
<span x-uppercase>Hello World!</span>
</div>
计算表达式
当注册自定义指令时,你可能需要计算用户提供的 JavaScript 表达式:
例如,假设你想创建一个自定义指令作为 console.log() 的快捷方式。像这样:
<div x-data="{ message: 'Hello World!' }">
<div x-log="message"></div>
</div>
你需要通过使用 x-data 作用域将其作为 JavaScript 表达式来求值,以获取 message 的实际值。
幸运的是,Alpine 通过 evaluate() API 公开了其计算 JavaScript 表达式的系统。示例如下:
Alpine.directive('log', (el, { expression }, { evaluate }) => {
// expression === 'message'
console.log(
evaluate(expression)
)
})
现在,当 Alpine 初始化 <div x-log...> 时,它会获取传递给指令的表达式(本例中为 "message"),并在当前元素的 Alpine 组件作用域上下文中进行求值。
引入响应式
基于之前的 x-log 示例,假设我们希望 x-log 记录 message 的值,并在值发生变化时也进行记录。
给定以下模板:
<div x-data="{ message: 'Hello World!' }">
<div x-log="message"></div>
<button @click="message = 'yolo'">Change</button>
</div>
我们希望初始时记录 "Hello World!",然后在按下 <button> 后记录 "yolo"。
我们可以调整 x-log 的实现,引入两个新 API 来实现这一点:evaluateLater() 和 effect():
Alpine.directive('log', (el, { expression }, { evaluateLater, effect }) => {
let getThingToLog = evaluateLater(expression)
effect(() => {
getThingToLog(thingToLog => {
console.log(thingToLog)
})
})
})
让我们逐行分析上面的代码。
let getThingToLog = evaluateLater(expression)
在这里,我们没有立即计算 message 并获取结果,而是将字符串表达式("message")转换为一个实际的 JavaScript 函数,我们可以随时运行它。如果你需要多次计算一个 JavaScript 表达式,强烈建议首先生成一个 JavaScript 函数,而不是直接调用 evaluate()。原因是将纯字符串解析为 JavaScript 函数的过程开销很大,应该避免不必要的重复。
effect(() => {
...
})
通过向 effect() 传递一个回调函数,我们告诉 Alpine 立即运行该回调,然后跟踪它所使用的任何依赖项(在我们的例子中是 x-data 属性,如 message)。这样,一旦某个依赖项发生变化,该回调将重新运行。这就是我们的"响应式"机制。
你可能从 x-effect 中认识到这个功能。底层是相同的机制。
你可能还会注意到 Alpine.effect() 也存在,并想知道为什么我们不在这里使用它。原因是通过方法参数提供的 effect 函数具有特殊功能:当指令因任何原因从页面中移除时,它会自动清理自己。
例如,如果出于某种原因,带有 x-log 的元素从页面中移除了,那么通过使用 effect() 而不是 Alpine.effect(),当 message 属性发生变化时,其值将不再被记录到控制台。
getThingToLog(thingToLog => {
console.log(thingToLog)
})
现在我们将调用 getThingToLog,如果你还记得,这是字符串表达式 "message" 对应的实际 JavaScript 函数版本。
你可能会期望 getThingToCall() 立即返回结果,但 Alpine 要求你传入一个回调来接收结果。
这样做的原因是为了支持像 await getMessage() 这样的异步表达式。通过传入一个"接收者"回调而不是立即获取结果,你的指令也能处理异步表达式。
清理
假设你需要从自定义指令中注册一个事件监听器。当该指令因任何原因从页面中移除时,你也需要移除该事件监听器。
Alpine 通过在注册自定义指令时提供一个 cleanup 函数来简化这一过程。
示例如下:
Alpine.directive('...', (el, {}, { cleanup }) => {
let handler = () => {}
window.addEventListener('click', handler)
cleanup(() => {
window.removeEventListener('click', handler)
})
})
现在,如果该指令从此元素中移除,或者元素本身被移除,事件监听器也会被移除。
自定义顺序
默认情况下,任何新指令将在大多数标准指令之后运行(x-teleport 除外)。这通常是可以接受的,但有时你可能需要让你的自定义指令在另一个特定指令之前运行。
这可以通过在 Alpine.directive() 后链式调用 .before() 函数来实现,并指定哪个指令需要在你自定义指令之后运行。
Alpine.directive('foo', (el, { value, modifiers, expression }) => {
Alpine.addScopeToNode(el, {foo: 'bar'})
}).before('bind')
<div x-data>
<span x-foo x-bind:foo="foo"></span>
</div>
注意,指令名称必须不包含
x-前缀(或你可能使用的任何其他自定义前缀)。
自定义魔术属性
Alpine 允许你使用 Alpine.magic() 注册自定义的"魔术属性"(或方法)。你注册的任何魔术属性都将以 $ 前缀的形式在你的应用程序的所有 Alpine 代码中使用。
方法签名
Alpine.magic('[name]', (el, { Alpine }) => {})
| name | 魔术属性的名称。例如,"foo" 将作为 $foo 使用 |
| el | 触发该魔术属性的 DOM 元素 |
| Alpine | Alpine 全局对象 |
魔术属性
以下是一个 $now 魔术辅助方法的简单示例,用于在 Alpine 中轻松获取当前时间:
Alpine.magic('now', () => {
return (new Date).toLocaleTimeString()
})
<span x-text="$now"></span>
现在 <span> 标签将包含当前时间,类似于 "12:00:00 PM"。
如你所见,$now 的行为类似于一个静态属性,但在底层实际上是一个 getter,每次访问该属性时都会进行求值。
因此,你可以通过从 getter 中返回一个函数来实现魔术"函数"。
魔术函数
例如,如果我们想创建一个 $clipboard() 魔术函数,接受一个要复制到剪贴板的字符串,可以这样实现:
Alpine.magic('clipboard', () => {
return subject => navigator.clipboard.writeText(subject)
})
<button @click="$clipboard('hello world')">Copy "Hello World"</button>
现在,访问 $clipboard 会返回一个函数本身,我们可以立即调用它并传入参数,就像在模板中看到的 $clipboard('hello world') 那样。
如果你愿意,可以使用更简洁的语法(双箭头函数)从函数中返回一个函数:
Alpine.magic('clipboard', () => subject => {
navigator.clipboard.writeText(subject)
})
编写和分享插件
现在你应该已经看到,在你的应用程序中注册自己的自定义指令和魔术属性是多么友好和简单。但是,如何通过 NPM 包等方式与其他人分享这些功能呢?
你可以从 Alpine 官方的"plugin-blueprint"包快速入门。只需克隆该仓库并运行 npm install && npm run build 即可开始编写插件。
出于演示目的,让我们从头开始创建一个名为 Foo 的模拟 Alpine 插件,它包含一个指令(x-foo)和一个魔术属性($foo)。
我们将从作为简单的 <script> 标签与 Alpine 一起使用开始制作此插件,然后升级为可导入到打包工具中的模块:
Script 引入
让我们先反过来看看这个插件将如何被引入到项目中:
<html>
<script src="/js/foo.js" defer></script>
<script src="/js/alpine.js" defer></script>
<div x-data x-init="$foo()">
<span x-foo="'hello world'">
</div>
</html>
请注意,我们的脚本包含在 Alpine 本身之前。这很重要,否则,当我们的插件加载时,Alpine 可能已经被初始化了。
现在让我们看看 /js/foo.js 的内容:
document.addEventListener('alpine:init', () => {
window.Alpine.directive('foo', ...)
window.Alpine.magic('foo', ...)
})
就是这样!使用 Alpine 通过 script 标签方式编写插件非常简洁。
Bundle 模块
现在假设你想编写一个插件,让用户可以通过 NPM 安装并将其包含在他们的打包工具中。
与前面的示例一样,我们将反过来逐步讲解,从使用此插件的方式开始:
import Alpine from 'alpinejs'
import foo from 'foo'
Alpine.plugin(foo)
window.Alpine = Alpine
window.Alpine.start()
你会注意到这里有一个新的 API:Alpine.plugin()。这是 Alpine 提供的一种便捷方法,用于避免插件的使用者自己注册多个不同的指令和魔术属性。
现在让我们看看插件的源代码以及从 foo 导出什么:
export default function (Alpine) {
Alpine.directive('foo', ...)
Alpine.magic('foo', ...)
}
你会看到 Alpine.plugin 非常简单。它接受一个回调并立即调用它,同时将 Alpine 全局对象作为参数提供给它使用。
然后你可以按照自己的意愿自由扩展 Alpine。