Morph 插件
Alpine 的 Morph 插件允许你将页面上的一个元素"变形"为提供的 HTML 模板,同时保留"变形"元素中的任何浏览器或 Alpine 状态。
这对于更新来自服务器请求的 HTML 而不会丢失 Alpine 的页面状态非常有用。这样的工具是全栈框架(如 Laravel Livewire 和 Phoenix LiveView)的核心。
理解其用途的最佳方式是通过以下交互式可视化演示。试试吧!
安装
你可以通过 <script> 标签引入或通过 NPM 安装来使用此插件:
通过 CDN
你可以将此插件的 CDN 构建版本作为 <script> 标签引入,只需确保在 Alpine 核心 JS 文件之前引入。
<!-- Alpine 插件 -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/[email protected]/dist/cdn.min.js"></script>
<!-- Alpine 核心 -->
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
通过 NPM
你可以从 NPM 安装 Morph 以在打包中使用:
npm install @alpinejs/morph
然后从你的打包工具中初始化它:
import Alpine from 'alpinejs'
import morph from '@alpinejs/morph'
window.Alpine = Alpine
Alpine.plugin(morph)
...
Alpine.morph()
Alpine.morph(el, newHtml) 允许你基于传入的 HTML 强制对 DOM 节点进行变形。它接受以下参数:
| 参数 | 描述 |
|---|---|
el |
页面上的一个 DOM 元素。 |
newHtml |
用作变形目标模板的 HTML 字符串。 |
options(可选) |
主要用于注入生命周期钩子的选项对象。 |
以下是一个使用 Alpine.morph() 用新 HTML 更新 Alpine 组件的示例:(在实际应用中,这个新 HTML 很可能来自服务器)
<div x-data="{ message: 'Change me, then press the button!' }">
<input type="text" x-model="message">
<span x-text="message"></span>
</div>
<button>Run Morph</button>
<script>
document.querySelector('button').addEventListener('click', () => {
let el = document.querySelector('div')
Alpine.morph(el, `
<div x-data="{ message: 'Change me, then press the button!' }">
<h2>See how new elements have been added</h2>
<input type="text" x-model="message">
<span x-text="message"></span>
<h2>but the state of this component hasn't changed? Magical.</h2>
</div>
`)
})
</script>
生命周期钩子
"Morph" 插件的工作原理是比较两个 DOM 树:实时元素和传入的 HTML。
Morph 同时遍历两棵树,并比较每个节点及其子节点。如果发现差异,它会对当前 DOM 树进行"修补"(更改),以匹配传入的 HTML 树。
虽然默认算法非常强大,但有些情况下你可能希望挂钩到它的生命周期,在变形发生时观察或改变其行为。
在我们深入了解可用的生命周期钩子之前,先列出它们可能接收的所有参数并解释每个参数的作用:
| 参数 | 描述 |
|---|---|
el |
这始终是页面上实际、当前的、将被"修补"(由 Morph 更改)的 DOM 元素。 |
toEl |
这是一个"模板元素"。它是一个临时元素,代表实时 el 将被修补成的样子。它永远不会实际存在于页面上,仅应用于参考目的。 |
childrenOnly() |
这是一个可以在钩子内部调用的函数,告诉 Morph 跳过当前元素,只"修补"它的子元素。 |
skip() |
一个函数,在钩子内调用时将"跳过"当前元素及其子元素的比较/修补。 |
以下是可用的生命周期钩子(作为第三个参数传递给 Alpine.morph(..., options)):
| 选项 | 描述 |
|---|---|
updating(el, toEl, childrenOnly, skip) |
在将 el 与比较对象 toEl 进行修补之前调用。 |
updated(el, toEl) |
在 Morph 修补了 el 之后调用。 |
removing(el, skip) |
在 Morph 从实时 DOM 中移除一个元素之前调用。 |
removed(el) |
在 Morph 从实时 DOM 中移除一个元素之后调用。 |
adding(el, skip) |
在添加新元素之前调用。 |
added(el) |
在向实时 DOM 树添加新元素之后调用。 |
key(el) |
一个可重用的函数,用于确定 Morph 如何在比较/修补之前对树中的元素进行"键控"。更多信息请看这里 |
lookahead |
一个布尔值,告诉 Morph 在其算法中启用一项额外的功能,即"向前看",以确保即将被移除的 DOM 元素应该只是"移动"到后面的兄弟位置。 |
以下代码是所有生命周期钩子的更具体参考:
Alpine.morph(el, newHtml, {
updating(el, toEl, childrenOnly, skip) {
//
},
updated(el, toEl) {
//
},
removing(el, skip) {
//
},
removed(el) {
//
},
adding(el, skip) {
//
},
added(el) {
//
},
key(el) {
// 默认情况下 Alpine 使用 `key=""` HTML 属性。
return el.id
},
lookahead: true, // 默认值:false
})
键
像 Morph 这样的 DOM 差异比较工具会尽力准确地将原始 DOM "变形"为新的 HTML。然而,在某些情况下,无法确定一个元素是应该被修改还是被完全替换。
由于这个限制,Morph 有一个"键"系统,允许开发者"强制"保留某些元素而不是替换它们。
最常见的用例是循环中的同级元素列表。以下是一个有时需要键的原因示例:
<!-- 页面上的"实时" DOM: -->
<ul>
<li>Mark</li>
<li>Tom</li>
<li>Travis</li>
</ul>
<!-- 要"变形为"的新 HTML: -->
<ul>
<li>Travis</li>
<li>Mark</li>
<li>Tom</li>
</ul>
在以上情况下,Morph 无法知道 "Travis" 节点已经在 DOM 树中被移动了。它只会认为 "Mark" 已变为 "Travis",而 "Travis" 已变为 "Tom"。
这不是我们真正想要的,我们希望 Morph 保留原始元素,而不是修改它们,而是在 <ul> 内移动它们。
通过为每个节点添加键,我们可以实现这一点,如下所示:
<!-- 页面上的"实时" DOM: -->
<ul>
<li key="1">Mark</li>
<li key="2">Tom</li>
<li key="3">Travis</li>
</ul>
<!-- 要"变形为"的新 HTML: -->
<ul>
<li key="3">Travis</li>
<li key="1">Mark</li>
<li key="2">Tom</li>
</ul>
现在 <li> 上有了"键",Morph 会在两棵树中匹配它们并相应地移动它们。
你可以使用 key: 配置选项来配置 Morph 将什么视为"键"。更多信息请看这里
Alpine.morphBetween()
Alpine.morphBetween(startMarker, endMarker, newHtml, options) 方法允许你基于传入的 HTML 变形两个标记元素之间的 DOM 节点范围。当你只想更新 DOM 的特定部分而不提供一个单一的根节点时,这很有用。
| 参数 | 描述 |
|---|---|
startMarker |
一个 DOM 节点(通常是注释节点),标记要变形范围的开始 |
endMarker |
一个 DOM 节点(通常是注释节点),标记要变形范围的结束 |
newHtml |
一个 HTML 字符串或 DOM 元素,用于替换标记之间的内容 |
options |
选项对象(与 Alpine.morph() 相同) |