Somnia 组件编写指南
Without further ado
| markdown
提醒:本文档为 AI 生成,如需使用请自行验证其内容。
Somnia 使用 Swup.js 实现无刷新页面切换。Swup 通过 AJAX 加载新页面的 HTML 片段并替换 DOM,
但由于浏览器安全策略,innerHTML 插入的 <script> 标签不会自动执行,
导致 shortcode 中的 Alpine.js 组件初始化失败。
本文档提供 9 种解决方案,从简单到高级排列,按场景选择。
目录
- 问题复现
- 根因说明
- 方法 1:内联到 x-data
- 方法 2:函数移至全局文件
- 方法 3:事件触发 + 脚本重定位
- 方法 4:x-ignore + 相邻脚本替换
- 方法 5:Async Alpine 异步加载
- 方法 6:Alpine.initTree() 官方 API
- 方法 7:通用脚本执行器
- 方法 8:MutationObserver 自动检测
- 方法 9:自定义 Swup 插件
- 方法选择总表
问题复现
下面的 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.js 的 Somnia.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.js 的 Somnia.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 按需加载。
- 创建独立文件,如
static/js/comment.mjs:
export default function() {
return {
count: 0,
increment() { this.count++ }
}
}javascript
- 在 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 添加:
function reexecuteScripts(container) {
container.querySelectorAll('script').forEach(oldScript => {
const newScript = document.createElement('script');
newScript.textContent = oldScript.textContent;
if (oldScript.type) newScript.type = oldScript.type;
oldScript.parentNode.replaceChild(newScript, oldScript);
});
}
document.addEventListener('swup:contentReplaced', () => {
reexecuteScripts(document.getElementById('content-wrapper'));
});javascript
优点: 通用性强,shortcode 完全不需要改动。
缺点: 副作用风险;大页面多个 <script> 逐一重建有性能开销。
适合场景: 已有大量 legacy shortcode,改造代价高。
方法 8:MutationObserver 自动检测
不依赖 Swup 事件,通过 MutationObserver 监控 DOM 变化,发现新增 [x-data] 时自动初始化。
在 custom.js 添加(全局运行一次):
const observer = new MutationObserver(mutations => {
const pendings = [];
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.hasAttribute('x-data')) pendings.push(node);
else node.querySelectorAll('[x-data]').forEach(el => pendings.push(el));
});
});
if (pendings.length) {
requestAnimationFrame(() => {
pendings.forEach(el => Alpine.initTree(el));
});
}
});
document.addEventListener('DOMContentLoaded', () => {
const wrapper = document.getElementById('content-wrapper');
if (wrapper) observer.observe(wrapper, { childList: true, subtree: true });
});javascript
优点: 完全解耦,不依赖 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-load | 中 | shortcode 自包含 |
| 4 | x-ignore + 脚本替换 | 加 x-ignore | 中 | shortcode 频率高 |
| 5 | Async Alpine | 迁移 .mjs | 低 | 大型独立组件 |
| 6 | Alpine.initTree() | 无需改动 | 低 | 通用首选 |
| 7 | 脚本执行器 | 无需改动 | 低 | Legacy 改造 |
| 8 | MutationObserver | 无需改动 | 中 | 多动态源 |
| 9 | Swup 插件 | 无需改动 | 中 | 大型项目 |
快速建议:
- 新项目 → 方法 6(Alpine.initTree),一行代码解决
- 已有大量 legacy shortcode → 方法 7(脚本执行器),零改动
- 追求极致性能 → 方法 5(Async Alpine)
- 多动态内容源 → 方法 8(MutationObserver)