# Somnia 组件编写指南


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

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

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

<!--more-->

## 目录

- [问题复现](#问题复现)
- [根因说明](#根因说明)
- [方法 1：内联到 x-data](#方法-1内联到-x-data)
- [方法 2：函数移至全局文件](#方法-2函数移至全局文件)
- [方法 3：事件触发 + 脚本重定位](#方法-3事件触发--脚本重定位)
- [方法 4：x-ignore + 相邻脚本替换](#方法-4x-ignore--相邻脚本替换)
- [方法 5：Async Alpine 异步加载](#方法-5async-alpine-异步加载)
- [方法 6：Alpine.initTree() 官方 API](#方法-6alpineinittree-官方-api)
- [方法 7：通用脚本执行器](#方法-7通用脚本执行器)
- [方法 8：MutationObserver 自动检测](#方法-8mutationobserver-自动检测)
- [方法 9：自定义 Swup 插件](#方法-9自定义-swup-插件)
- [方法选择总表](#方法选择总表)

---

## 问题复现

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

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

## 根因说明

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

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

---

## 方法 1：内联到 x-data

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

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

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

**适合场景：** 极简逻辑（一两个属性、无方法）。

---

## 方法 2：函数移至全局文件

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

```html
<!-- shortcode 中只需引用函数名 -->
<div x-data="test()" x-text="some"></div>
```

```javascript
// assets/js/custom.js
function test() {
    return { some: "test" }
}
```

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

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

---

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

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

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

在 `assets/js/custom.js` 的 `Somnia.prototype.swupPageInitCustom` 中添加：

```javascript
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 }));
```

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

**适合场景：** shortcode 高度自包含，不愿意开额外文件。

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

---

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

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

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

在 `custom.js` 的 `Somnia.prototype.swupPageInitCustom` 中添加：

```javascript
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 重新解析
    }
});
```

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

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

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

---

## 方法 5：Async Alpine 异步加载

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

1. 创建独立文件，如 `static/js/comment.mjs`：

```javascript
export default function() {
    return {
        count: 0,
        increment() { this.count++ }
    }
}
```

2. 在 shortcode 中使用 `x-load` 指定加载来源：

```html
<div x-data="comment" x-load x-text="count"
     @click="increment()"></div>
```

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

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

**适合场景：** 大型独立组件（评论系统、图库）。

---

## 方法 6：Alpine.initTree() 官方 API

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

**只需在 `custom.js` 添加一次代码，无需修改任何 shortcode：**

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

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

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

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

---

## 方法 7：通用脚本执行器

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

**在 `custom.js` 添加：**

```javascript
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'));
});
```

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

**适合场景：** 已有大量 legacy shortcode，改造代价高。

---

## 方法 8：MutationObserver 自动检测

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

**在 `custom.js` 添加（全局运行一次）：**

```javascript
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 });
});
```

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

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

---

## 方法 9：自定义 Swup 插件

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

**创建 `assets/js/swup-plugin-somnia.js`：**

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

在 `head/js.html` 中注册：

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

**优点：** 干净封装，可组合多种策略。
**缺点：** 需要了解 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）
