Back

Somnia 组件编写指南

Without further ado
| markdown

提醒:本文档为 AI 生成,如需使用请自行验证其内容。

Somnia 使用 Swup.js 实现无刷新页面切换。Swup 通过 AJAX 加载新页面的 HTML 片段并替换 DOM,
但由于浏览器安全策略,innerHTML 插入的 <script> 标签不会自动执行
导致 shortcode 中的 Alpine.js 组件初始化失败。

本文档提供 9 种解决方案,从简单到高级排列,按场景选择。

目录


问题复现

下面的 shortcode 在常规页面中工作正常,但 Swup 切换后 test() 函数不会执行:

<div x-data="test()" x-text="some"></div>
<script>
function test() {
    return { some: "test" }
}
</script>
html

根因说明

Swup.js 的工作原理是拦截链接点击 → 通过 fetch() 获取新页面 HTML → 提取 #content-wrapper 内的 HTML 字符串 → 通过 innerHTML 替换当前内容。浏览器规范规定 innerHTML 赋值时,其中的 <script> 标签不会被解析和执行。 因此 shortcode 中紧跟在 HTML 后面的 <script> 定义在 Swup 切换后会丢失。

Alpine.js 的 x-data 需要对应的函数或数据对象在初始化时已存在于作用域。函数不存在则组件静默失败。


方法 1:内联到 x-data

将返回值直接写在 x-data 属性中,绕过函数名引用。

<div x-data="{ some: 'test' }" x-text="some"></div>
html

优点: 零额外工作,完全兼容 Swup,不需要 <script> 标签。
缺点: 逻辑复杂时不可维护,无法复用。

适合场景: 极简逻辑(一两个属性、无方法)。


方法 2:函数移至全局文件

将组件函数定义到 assets/js/custom.js,该文件在 Swup 初始化前全局加载,始终可用。

<!-- shortcode 中只需引用函数名 -->
<div x-data="test()" x-text="some"></div>
html
// assets/js/custom.js
function test() {
    return { some: "test" }
}
javascript

优点: 逻辑与视图分离,Swup 兼容,函数可被多个 shortcode 复用。
缺点: 每新增一个 shortcode 都要编辑 custom.js,组件多了难以管理。

适合场景: 项目维护期,组件数量可控,追求简单可靠。


方法 3:事件触发 + 脚本重定位

利用 x-load="event (somnia:moved)" 延迟 Alpine 初始化,在 Swup 切换后将 <script> 移到 <head> 并派发自定义事件通知 Alpine 初始化。

<div x-data="test()" x-text="some" x-load="event (somnia:moved)"></div>
<script>
function test() {
    return { some: "test" }
}
</script>
html

assets/js/custom.jsSomnia.prototype.swupPageInitCustom 中添加:

document.querySelectorAll('#content-wrapper script').forEach(script => {
    const newScript = document.createElement('script');
    newScript.innerHTML = script.innerHTML;
    document.head.appendChild(newScript);
});
document.dispatchEvent(new CustomEvent('somnia:moved', { bubbles: true }));
javascript

优点: shortcode 自包含,无需额外文件。
缺点: 页面切换会堆积重复的 <script> 元素。

适合场景: shortcode 高度自包含,不愿意开额外文件。

Somnia.prototype.swupPageInitCustom 在每次 Swup 页面切换后自动调用。
你也可以是使用 Somnia.prototype.PageInitCustom —— 它在常规页面加载时调用。


方法 4:x-ignore + 相邻脚本替换

在 shortcode 中使用 x-ignore 阻止 Alpine 自动解析,然后通过相邻 <script> 的内容动态设置 x-data 属性,触发 Alpine 重新解析。

<div x-data="test()" x-text="some" x-ignore></div>
<script>
function test() {
    return { some: "test" }
}
</script>
html

custom.jsSomnia.prototype.swupPageInitCustom 中添加:

document.querySelectorAll('div[x-ignore]').forEach(div => {
    const nextEl = div.nextElementSibling;
    if (nextEl && nextEl.tagName.toLowerCase() === 'script') {
        div.setAttribute('x-data', nextEl.innerText);
        nextEl.remove();
        div.removeAttribute('x-ignore');
        div.parentNode.replaceChild(div, div); // 触发 Alpine 重新解析
    }
});
javascript

replaceChild(div, div) 用自身替换自身,不改变 DOM 结构,但会触发 Alpine 的 MutationObserver 回调,重新解析该元素上的 x-data 指令。

优点: shortcode 基本自包含;脚本可以是函数也可以是纯对象;无重复堆积。
缺点: 需要注入一次脚本处理逻辑。

适合场景: shortcode 自包含且使用频繁。


方法 5:Async Alpine 异步加载

将组件函数迁移到独立 .mjs 文件,通过 Async Alpine 按需加载。

  1. 创建独立文件,如 static/js/comment.mjs
export default function() {
    return {
        count: 0,
        increment() { this.count++ }
    }
}
javascript
  1. 在 shortcode 中使用 x-load 指定加载来源:
<div x-data="comment" x-load x-text="count"
     @click="increment()"></div>
html

优点: 真正的代码分割,按需加载,性能最佳。
缺点: 需要了解 Async Alpine 的加载协议。

文档:https://async-alpine.dev/docs/

适合场景: 大型独立组件(评论系统、图库)。


方法 6:Alpine.initTree() 官方 API

Alpine.js v3 提供了 Alpine.initTree(el),可手动初始化指定 DOM 子树。
在 Swup 切换完成后调用,新内容中的 x-data 自动生效。

只需在 custom.js 添加一次代码,无需修改任何 shortcode:

document.addEventListener('swup:contentReplaced', () => {
    Alpine.initTree(document.getElementById('content-wrapper'));
});
javascript

initTree 遍历 DOM 树,找到含 x-data 的未初始化元素并初始化。Alpine 内部维护初始化标记,已初始化的元素不会重复处理。

优点: 零侵入,Alpine 官方 API,无兼容性风险。
缺点: 函数仍需全局定义,局部变量 <script> 仍不工作。

适合场景: 项目中 shortcode 多、不想逐个改造。通用首选。


方法 7:通用脚本执行器

Swup 切换后找到新 DOM 中的 <script> 标签,用 createElement 重建替换,强制浏览器执行。

custom.js 添加:

优点: 通用性强,shortcode 完全不需要改动。
缺点: 副作用风险;大页面多个 <script> 逐一重建有性能开销。

适合场景: 已有大量 legacy shortcode,改造代价高。


方法 8:MutationObserver 自动检测

不依赖 Swup 事件,通过 MutationObserver 监控 DOM 变化,发现新增 [x-data] 时自动初始化。

custom.js 添加(全局运行一次):

优点: 完全解耦,不依赖 Swup 事件,覆盖所有动态内容变化。
缺点: MutationObserver 持续运行有轻微性能开销。

适合场景: 页面除了 Swup 切换,还有其他动态内容加载。


方法 9:自定义 Swup 插件

将脚本处理逻辑封装为 Swup 插件。

创建 assets/js/swup-plugin-somnia.js

class SomniaPlugin {
    name = 'SomniaPlugin';
    exec() {
        Alpine.initTree(document.getElementById('content-wrapper'));
        document.dispatchEvent(new CustomEvent('somnia:content-updated'));
    }
}
javascript

head/js.html 中注册:

const swup = new Swup({
    plugins: [new SomniaPlugin(), SwupScrollPlugin, SwupPreloadPlugin]
});
javascript

优点: 干净封装,可组合多种策略。
缺点: 需要了解 Swup 插件 API。

适合场景: 多人协作的大型项目。


方法选择总表

#方法Shortcode 改动侵入性适合场景
1内联 x-data需改造极简逻辑
2函数放 custom.js需改造项目维护期
3事件触发 + 重定位x-loadshortcode 自包含
4x-ignore + 脚本替换x-ignoreshortcode 频率高
5Async Alpine迁移 .mjs大型独立组件
6Alpine.initTree()无需改动通用首选
7脚本执行器无需改动Legacy 改造
8MutationObserver无需改动多动态源
9Swup 插件无需改动大型项目

快速建议:

  • 新项目 → 方法 6(Alpine.initTree),一行代码解决
  • 已有大量 legacy shortcode → 方法 7(脚本执行器),零改动
  • 追求极致性能 → 方法 5(Async Alpine)
  • 多动态内容源 → 方法 8(MutationObserver)

Docs