如何在外部调用 Alpine Js 声明的变量与函数?
最近开发博客前端的时候,使用的 AlpineJs 来做一些元素事件、状态的管理,由于我之前只有 vue 的相关使用经验,并没有 Alpine 的开发经验,所以时常会陷入一个困惑中——如何在外部的普通 js 环境来调用元素的 Alpine 函数和变量。
需求场景
例如,我在博客左侧的文章目录树中,为了控制目录点击展开,使用了如下的 alpine 实现:
<li x-data="{ showItems : false}">
<div>
<a>
<span th:text="${currentCategoryTree.spec.displayName}"></span>
<span @click.prevent.stop="showItems = !showItems;">
<svg>...按钮图片</svg>
</span>
</a>
<ul x-show=""showItems>
<li> ....</li>
</ul>
</div>
</li>
由于文章的 url 并不包含分类信息,所以为了实现点开不同文章展开对应分类,我需要在外部的 js 中携带分类信息,进而在文章加载的时候使用 js 匹配以初始化相应分类展开。
(其实用 thymeleaf 模板变量传递文章分类信息也能实现初始化文章分类归属信息,但是考虑到后面要使用 pjax,所以使用了 js 变量来管理目录展开状态。)
虽然,使用 Alpine 的响应式变量管理分类列表的展开状态很方便,但接下来我的疑问就是在文章加载后,怎么在其携带的 js 中去根据分类匹配来改变对应分类元素的 showItems
变量——即如何在普通 js 中修改 Alpine 中声明的变量?
问题:如何在普通 js 中修改 Alpine 中声明的变量?
为了解决这个问题,我去查询过其他人的方法,发现有人有类似疑问,他使用的是 elm.__x.$data.alpineRelativeValue
这个 api,通过元素获取到其上声明的响应式变量(__x.data return undefined · alpinejs/alpine · Discussion #1543)。但是,这个 api 已经因为安全问题在 Alpine 3.0 版本弃用。
最后,我声明了一个全局的、响应式的 AlpineJs 变量,它即在 Alpine 中是响应式的,又是全局的普通 Js 变量。这个变量既能被外部调用修改,又能在 Alpine 中响应式管理状态。
window.globalBlog = Alpine.reactive(new GlobalBlog());
class GlobalBlog {
tocHeadings = []
updateTocHeadings() {
this.tocHeadings = this.getHeadingsFromArticle()
}
...
}
为什么会有如此矛盾的实现?
当时只顾着快速开发和实现,没有去认真考虑过这件事——既然我已经使用 Alpine 的响应式变量来管理我的页面状态,为何我还要在普通 Js 中去修改 Alpine 变量? 为何不都使用 Alpine 来调用、修改呢?
其实,当时还是对 Js 触发的事件 window.onload
有路径依赖,觉得页面加载后的相关逻辑就得用普通的 js 事件去监听、调用。于是乎,产生了上面的疑问——如何在普通 Js 中修改调用 Alpine 中的响应式变量进而享受它给页面状态控制带来的便利。
所以,最后我使用了在页面的最后添加一个空 div 元素,在其中的 x-init
中去调用需要刷新页面状态相关的钩子函数,以替代普通的 window.onload 事件监听。同时,这样还带来的另外一个便利,那就是 Pjax 更新部分元素后的回调函数也能在其中处理,无需实现额外逻辑。(这里所指的额外逻辑,就是普通 Js 的实现,详细文章可以看这个大佬的Halo 主题 PJAX 开发实践|Takagi)
综上,可以得出这样一个结论:用 Alpine 的响应式变量管理页面状态,就得使用 Alpine 去管理,无论是变量修改、函数调用、事件监听的处理逻辑。
新问题:如何在 Pjax 的回调中调用 Alpine 的响应式变量?
其实这本来不算个问题,因为我们上面说过,在元素末尾添加空 div 在其中使用 x-init
来调用 Alpine 中的钩子函数,这就已经实现了 Pjax 更新元素后执行相关逻辑的处理。
但是,我们可以思考的更深一点,在普通的 Pjax 实现里,它会在更新元素后触发 pjax:success
事件,可以在其中定义页面加载后需要做的逻辑。所以,这就又回到了那个问题——如何在 Pjax 的回调中调用 Alpine 的响应式变量?这和“如何在普通 js 中修改 Alpine 中声明的变量?”是同一个问题。
那么同样的,为什么又会有如此矛盾的实现呢?还记得我们说过——用 Alpine 的响应式变量管理页面状态,就得使用 Alpine 去管理,无论是变量修改、函数调用、事件监听。 所以,这个问题中的核心矛盾就是我们没有使用 Alpine 去监听 Pjax 对元素替换后所触发的事件。对应的解决方案就是,使用 Alpine 去监听pjax:success
事件,然后在相应回调中做处理。
<html>
<body x-data="app()">
<div>
<h1 x-text="message"></h1>
<a href="new-content.html" data-pjax="#content">Load New Content</a>
</div>
<script>
function app() {
return {
pageInfo: {href, categorySlug, ...},
onceInited: false,
initeOnce() { if onceInited return; ... },
initCommom() { ... },
initSpecial() { ... },
pjaxSuccess() {
this.initOnce();
this.initCommon();
this.initSpecial();
},
init() {
// 监听 pjax:success 事件
$(document).on('pjax:success', (event) => {
// 在 PJAX 加载完毕后更新 Alpine 变量或调用方法
this.pjaxSuccess();
});
}
}
}
// 初始化 PJAX
$(document).pjax('a[data-pjax]', '#content');
</script>
</body>
</html>
补充
就先记录到这,另外再补充一下临时想起来的两个点。
- Alpine.$data(elm)
这个 Api 可以获取原生元素上声明的 alpine 变量,至于以后是否会失效,未知。 - 可以在 Alpine 中处理
window.load
监听的钩子函数,和上面描述的pjax:success
监听事件的钩子函数逻辑相似。