如何在外部调用 Alpine Js 声明的变量与函数?

2024 年 12 月 12 日
24 次浏览
5080 字数

最近开发博客前端的时候,使用的 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 监听事件的钩子函数逻辑相似。